83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
|
|
export type Undoable<L, T> = {
|
||
|
|
loc: L;
|
||
|
|
value_initial: T;
|
||
|
|
value_updated: T;
|
||
|
|
};
|
||
|
|
|
||
|
|
export class UndoStack<T extends Undoable<unknown, unknown>> {
|
||
|
|
/**
|
||
|
|
* All undo- and redo-able diffs, in order.
|
||
|
|
*/
|
||
|
|
private _diffs: T[][] = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Index of last delta currently represented in application state. Decrements
|
||
|
|
* when undoing; increments when pushing or redoing. -1 when stack is empty.
|
||
|
|
*/
|
||
|
|
private _cursor: number = -1;
|
||
|
|
|
||
|
|
private readonly _apply_diff: (diff: T[]) => unknown;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* NOTE: `apply_diff()` is permitted to mutate the array it receives as a
|
||
|
|
* parameter, but otherwise `UndoStack` expects that the deltas it receives
|
||
|
|
* and provides will not be tampered with. Mutating arrays after providing
|
||
|
|
* them to `.push()` or mutating the individual array items supplied to
|
||
|
|
* `apply_diff()`, for example, is considered undefined behavior.
|
||
|
|
*/
|
||
|
|
constructor(
|
||
|
|
{ apply_diff }: { apply_diff(deltas: T[]): unknown },
|
||
|
|
) {
|
||
|
|
// Always shallow copy array so that callback may perform some mutations.
|
||
|
|
this._apply_diff = (diff: T[]) => apply_diff([...diff]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Pushes a batch of deltas to the end of the undo stack, and clears the redo
|
||
|
|
* stack. `apply_diff()` will be immediately called with a copy of `deltas`.
|
||
|
|
*/
|
||
|
|
push(diff: T[]): undefined {
|
||
|
|
// Clear redo stack. `Array.splice()` does nothing if start index > array
|
||
|
|
// length, and `this._cursor` is always >= -1, so no conditional needed.
|
||
|
|
this._diffs.splice(this._cursor + 1);
|
||
|
|
|
||
|
|
this._diffs.push(diff);
|
||
|
|
this._cursor += 1;
|
||
|
|
|
||
|
|
// Call `_apply_diff()` after shifting cursor, in case it recursively
|
||
|
|
// mutates this UndoStack.
|
||
|
|
this._apply_diff(diff);
|
||
|
|
}
|
||
|
|
|
||
|
|
undo(): undefined {
|
||
|
|
if (this._cursor > -1) {
|
||
|
|
this._cursor -= 1;
|
||
|
|
|
||
|
|
// Call `_apply_diff()` after shifting cursor, in case it recursively
|
||
|
|
// mutates this UndoStack.
|
||
|
|
this._apply_diff(this._diffs[this._cursor + 1].map(invert));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
redo(): undefined {
|
||
|
|
if (this._diffs.length > this._cursor + 1) {
|
||
|
|
this._cursor += 1;
|
||
|
|
|
||
|
|
// Call `_apply_diff()` after shifting cursor, in case it recursively
|
||
|
|
// mutates this UndoStack.
|
||
|
|
this._apply_diff(this._diffs[this._cursor]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns a copy of the parameter with initial and updated values swapped.
|
||
|
|
*/
|
||
|
|
function invert<T extends Undoable<unknown, unknown>>(undoable: T): T {
|
||
|
|
return {
|
||
|
|
loc: undoable.loc,
|
||
|
|
value_initial: undoable.value_updated,
|
||
|
|
value_updated: undoable.value_initial,
|
||
|
|
} as T;
|
||
|
|
}
|