export type Undoable = { loc: L; value_initial: T; value_updated: T; }; export class UndoStack> { /** * 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(invert_diff(this._diffs[this._cursor + 1])); } } 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. */ export function invert_diff>( diff: T[], ): T[] { return diff.map((undoable) => ({ loc: undoable.loc, value_initial: undoable.value_updated, value_updated: undoable.value_initial, } as T)); }