2026-02-09 21:39:13 +00:00
|
|
|
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.
|
2026-02-09 22:02:06 +00:00
|
|
|
this._apply_diff(invert_diff(this._diffs[this._cursor + 1]));
|
2026-02-09 21:39:13 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
2026-02-09 22:02:06 +00:00
|
|
|
export function invert_diff<T extends Undoable<unknown, unknown>>(
|
|
|
|
|
diff: T[],
|
|
|
|
|
): T[] {
|
|
|
|
|
return diff.map((undoable) => ({
|
2026-02-09 21:39:13 +00:00
|
|
|
loc: undoable.loc,
|
|
|
|
|
value_initial: undoable.value_updated,
|
|
|
|
|
value_updated: undoable.value_initial,
|
2026-02-09 22:02:06 +00:00
|
|
|
} as T));
|
2026-02-09 21:39:13 +00:00
|
|
|
}
|