phonograph/svelte/src/table-viewer.webc/undo-stack.svelte.ts

83 lines
2.5 KiB
TypeScript
Raw Normal View History

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;
}