Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export default [...[].concat(
},
rules: {
...jest.configs['flat/all'].rules,
"no-void": "off",
"no-new": "off",
"no-underscore-dangle": "off",
"no-duplicate-imports": "off",
Expand Down
90 changes: 90 additions & 0 deletions src/api/command/Memento.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* This file is part of Interacto.
* Interacto is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* Interacto is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Interacto. If not, see <https://www.gnu.org/licenses/>.
*/

const INTERACTO_MEMENTO: unique symbol = Symbol("interacto-cmd-memento");

interface MementoMetadata {
[INTERACTO_MEMENTO]?: Map<string, unknown>;
}

/**
* The Interacto decorator to mark one command's property as a property from which a memento must be created.
* Cannot check the type of the property here (requires reflect-metadata we do not want to install).
* @param target - The targeted command.
* @param propertyName - The name of the property the decorator targets.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export function Memento(target: unknown, propertyName: string): void {
if (!(target instanceof Object)) {
// eslint-disable-next-line no-console
console.error("The @Memento decorator currently operates on objects only");
return;
}

// Getting the object cache related to the modifiable symbol
const symbolValues: unknown = (target.constructor as MementoMetadata)[INTERACTO_MEMENTO];
const map = symbolValues instanceof Map ? symbolValues as Map<string, unknown> : new Map<string, unknown>();

// Adding the property in this cache (setting its value to undefined for the moment)
map.set(propertyName, undefined);
(target.constructor as MementoMetadata)[INTERACTO_MEMENTO] = map;
}

/**
* Retrieves the subset of attributes from an {@link Undoable} instance that are
* considered being the memento of the object (i.e., annotated with `@Memento`)
* @param obj - The object from which to extract memento properties.
* @returns Partial<T> - An object (extracted from `obj`) containing only the properties that are
* declared as being the memento.
*/
function getMementoProperties<T extends object>(obj: T): Partial<T> {
const modifiableAttributes: Partial<T> = {};
const mementoProps: unknown = (obj.constructor as MementoMetadata)[INTERACTO_MEMENTO];

if (mementoProps instanceof Map) {
for (const [key, value] of mementoProps.entries()) {
const tkey = key as keyof T;
modifiableAttributes[tkey] = value as T[keyof T];
}
}
return modifiableAttributes;
}

/**
* Retrieves the subset of attributes from an {@link Undoable} instance that are
* considered being the memento of the object (i.e., annotated with `@Memento`)
* @param obj - The object from which to restore the values using the stored memento properties.
*/
export function restoreMementoProperties<T extends object>(obj: T): void {
for (const [propName, propMementoValue] of Object.entries(getMementoProperties(obj))) {
const typedPropName = propName as keyof T;
obj[typedPropName] = propMementoValue as T[keyof T];
}
}

/**
* Retrieves the subset of attributes from an {@link Undoable} instance that are
* considered being the memento of the object (i.e., annotated with `@Memento`)
* @param obj - The object from which to restore the values using the stored memento properties.
*/
export function createMementoProperties<T extends object>(obj: T): void {
const mementoProps: unknown = (obj.constructor as MementoMetadata)[INTERACTO_MEMENTO];

if (mementoProps instanceof Map) {
for (const propName of mementoProps.keys()) {
const typedPropName = propName as keyof T;
mementoProps.set(propName, obj[typedPropName]);
}
}
}
2 changes: 1 addition & 1 deletion src/api/command/ModifiableCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function getModifiableCmdAttributes<T extends Undoable>(obj: T): Partial<
const tkey = key as keyof T;
const type = typeof obj[tkey];

if (type === "string" || type === "number" || type === "boolean") {
if (type === "string" || type === "number" || type === "boolean" || type === "bigint") {
modifiableAttributes[tkey] = obj[tkey];
} else {
// eslint-disable-next-line no-console
Expand Down
2 changes: 1 addition & 1 deletion src/api/command/Selective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function isCmdSelective<T extends object>(obj: T): boolean {
* done using the === operator.
* @returns True if the given object is selective and its selective property has as value the given `value`
*/
export function hasSelectiveValue<V extends object | number | string>(
export function hasSelectiveValue<V extends object | number | string | bigint>(
obj: object, value: V, eqFn: ((v1: V, v2: V) => boolean) = (v1, v2) => v1 === v2): boolean {

const res = getSelectiveValue(obj);
Expand Down
12 changes: 7 additions & 5 deletions src/api/history/LinearHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export abstract class LinearHistory implements LinearHistoryBase {

public abstract clear(): void;

public abstract undo(): void;
public abstract undo(): Promise<void> | void;

public abstract redo(): void;
public abstract redo(): Promise<void> | void;

/**
* @returns The stack of saved undoable objects.
Expand Down Expand Up @@ -95,7 +95,7 @@ export abstract class LinearHistory implements LinearHistoryBase {
* The second one contains the matching commands in the redo stack.
* Each undoable is associated with its index in its undo/redo stack.
*/
public abstract getSelectiveOf<T extends object | string | number> (key: T, eqFn?: (v1: T, v2: T) => boolean):
public abstract getSelectiveOf<T extends object | string | number | bigint> (key: T, eqFn?: (v1: T, v2: T) => boolean):
[undos: Array<[undoable: Undoable, index: number]>, redos: Array<[undoable: Undoable, index: number]>];

/**
Expand All @@ -106,8 +106,9 @@ export abstract class LinearHistory implements LinearHistoryBase {
* Index size()[0] is the most recent undoable elements (corresponds to getLastUndo())
* @param index - The index in the undo stack to undo up to it.
* If not valid, the method does nothing.
* @returns nothing or a promise (a chain of promises in fact) if the undo process is async.
*/
public abstract undoUpTo(index: number): void;
public abstract undoUpTo(index: number): Promise<void> | void;

/**
* Redoes the last undoable objects up to go to the provided index.
Expand All @@ -117,8 +118,9 @@ export abstract class LinearHistory implements LinearHistoryBase {
* Index size()[0] is the most recent undoable elements (corresponds to getLastRedo())
* @param index - The index in the redo stack to redo up to it.
* If not valid, the method does nothing.
* @returns nothing or a promise (a chain of promises in fact) if the undo process is async.
*/
public abstract redoUpTo(index: number): void;
public abstract redoUpTo(index: number): Promise<void> | void;

/**
* Gets all the unique selective objects of this history.
Expand Down
6 changes: 4 additions & 2 deletions src/api/history/LinearHistoryBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import type {Observable} from "rxjs";
export interface LinearHistoryBase {
/**
* Undoes the last undoable object.
* @returns nothing or a promise if the undo process is async.
*/
undo(): void;
undo(): Promise<void> | void;

/**
* Redoes the last undoable object.
* @returns nothing or a promise if the undo process is async.
*/
redo(): void;
redo(): Promise<void> | void;

/**
* Removes all the undoable objects of the collector.
Expand Down
8 changes: 4 additions & 4 deletions src/api/history/TreeHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ export interface TreeHistoryNode {
/**
* Undoes the undoable object of this node.
*/
undo(): void;
undo(): Promise<void> | void;

/**
* Redoes the undoable object of this node.
*/
redo(): void;
redo(): Promise<void> | void;
}

/**
Expand Down Expand Up @@ -215,11 +215,11 @@ export abstract class TreeHistory implements LinearHistoryBase {

public abstract getLastUndoMessage(): string | undefined;

public abstract redo(): void;
public abstract redo(): Promise<void> | void;

public abstract redosObservable(): Observable<Undoable | undefined>;

public abstract undo(): void;
public abstract undo(): Promise<void> | void;

public abstract undosObservable(): Observable<Undoable | undefined>;

Expand Down
4 changes: 2 additions & 2 deletions src/api/history/Undoable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ export interface Undoable {
/**
* Cancels the command.
*/
undo(): void;
undo(): Promise<void> | void;

/**
* Redoes the canceled command.
*/
redo(): void;
redo(): Promise<void> | void;

/**
* @returns The name of the history command.
Expand Down
3 changes: 2 additions & 1 deletion src/impl/binding/BindingImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ implements Binding<C, I, A, D> {

private cancelContinuousWithEffectsCmd(cmd: C): void {
if (isUndoableType(cmd)) {
cmd.undo();
// eslint-disable-next-line no-void
void cmd.undo();
if (this.logCmd) {
this.logger.logCmdMsg("Command undone", cmd.constructor.name);
}
Expand Down
5 changes: 4 additions & 1 deletion src/impl/command/CommandBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

import type {Command, CmdStatus} from "../../api/command/Command";
import {createMementoProperties} from "../../api/command/Memento";

/**
* The base implementation class for coding UI commands.
Expand Down Expand Up @@ -44,7 +45,9 @@ export abstract class CommandBase implements Command {
* This is the goal of the operation that should be overridden.
* This operator is called a single time before the first execution of the command.
*/
protected createMemento(): void {}
protected createMemento(): void {
createMementoProperties(this);
}

public execute(): Promise<boolean> | boolean {
let ok: boolean;
Expand Down
9 changes: 7 additions & 2 deletions src/impl/command/UndoableCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import {CommandBase} from "./CommandBase";
import type {Undoable, UndoableSnapshot} from "../../api/history/Undoable";
import {restoreMementoProperties} from "../../api/command/Memento";

/**
* The base class for undoable UI commands.
Expand Down Expand Up @@ -68,7 +69,11 @@ export abstract class UndoableCommand<T extends UndoableSnapshot = undefined> ex
return this === undoable;
}

public abstract redo(): void;
public redo(): Promise<void> | void {
return this.execution();
}

public abstract undo(): void;
public undo(): Promise<void> | void {
restoreMementoProperties(this);
}
}
4 changes: 2 additions & 2 deletions src/impl/command/library/Redo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class Redo extends CommandBase {
return this.history.getLastRedo() !== undefined;
}

protected execution(): void {
this.history.redo();
protected execution(): Promise<void> | void {
return this.history.redo();
}
}
10 changes: 8 additions & 2 deletions src/impl/command/library/RedoNTimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ export class RedoNTimes extends CommandBase {
return this.history.getRedo().length >= this.numberOfRedos;
}

protected execution(): void {
protected execution(): Promise<void> | void {
let chain = Promise.resolve();
for (let i = 0; i < this.numberOfRedos; i++) {
this.history.redo();
const res = this.history.redo();
if (res instanceof Promise) {
// eslint-disable-next-line @typescript-eslint/promise-function-async
chain = chain.then(() => res);
}
}
return chain;
}
}
7 changes: 4 additions & 3 deletions src/impl/command/library/SetProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@ export class SetProperties<T> extends UndoableCommand {

protected execution(): void {}

public redo(): void {
public override redo(): void {
for (const cmd of this.compositeCmds) {
cmd.redo();
// eslint-disable-next-line no-void
void cmd.redo();
}
}

public undo(): void {
public override undo(): void {
for (const cmd of this.compositeCmds) {
cmd.undo();
}
Expand Down
6 changes: 1 addition & 5 deletions src/impl/command/library/SetProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,7 @@ export class SetProperty<T, S extends keyof T> extends UndoableCommand {
this.obj[this.prop] = this.newvalue;
}

public redo(): void {
this.execution();
}

public undo(): void {
public override undo(): void {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.obj[this.prop] = this.mementoValue!;
}
Expand Down
4 changes: 2 additions & 2 deletions src/impl/command/library/TransferArrayItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ export class TransferArrayItem<T> extends UndoableCommand {
return this.cmdName;
}

public redo(): void {
public override redo(): void {
const elt = this._srcArray[this._srcIndex];
if (elt !== undefined) {
this._srcArray.splice(this._srcIndex, 1);
this._tgtArray.splice(this._tgtIndex, 0, elt);
}
}

public undo(): void {
public override undo(): void {
const elt = this._tgtArray[this._tgtIndex];
if (elt !== undefined) {
this._tgtArray.splice(this._tgtIndex, 1);
Expand Down
4 changes: 2 additions & 2 deletions src/impl/command/library/Undo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class Undo extends CommandBase {
return this.history.getLastUndo() !== undefined;
}

protected execution(): void {
this.history.undo();
protected execution(): Promise<void> | void {
return this.history.undo();
}
}
10 changes: 8 additions & 2 deletions src/impl/command/library/UndoNTimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ export class UndoNTimes extends CommandBase {
return this.history.getUndo().length >= this.numberOfUndos;
}

protected execution(): void {
protected execution(): Promise<void> | void {
let chain = Promise.resolve();
for (let i = 0; i < this.numberOfUndos; i++) {
this.history.undo();
const res = this.history.undo();
if (res instanceof Promise) {
// eslint-disable-next-line @typescript-eslint/promise-function-async
chain = chain.then(() => res);
}
}
return chain;
}
}
Loading
Loading