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

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