diff --git a/src/core.ts b/src/core.ts index 9497dd6..5161aea 100644 --- a/src/core.ts +++ b/src/core.ts @@ -37,12 +37,24 @@ export function createHistory(options: HistoryOptions = {}): History { } }; + const undoEntry = (): Entry | undefined => { + if (cursor === 0) return undefined; + // The cursor is kept in [0, length], so cursor - 1 names an undoable entry. + return entries[cursor - 1]!; + }; + + const redoEntry = (): Entry | undefined => { + if (cursor === entries.length) return undefined; + // The cursor is kept in [0, length], so cursor names a redoable entry. + return entries[cursor]!; + }; + const buildMeta = (): HistoryMeta => Object.freeze({ canUndo: cursor > 0, canRedo: cursor < entries.length, - undoLabel: cursor > 0 ? entries[cursor - 1].label : undefined, - redoLabel: cursor < entries.length ? entries[cursor].label : undefined, + undoLabel: undoEntry()?.label, + redoLabel: redoEntry()?.label, position: cursor, length: entries.length, }); @@ -88,11 +100,13 @@ export function createHistory(options: HistoryOptions = {}): History { while (remaining > 0) { if (dir < 0) { if (cursor === 0) break; - runGuarded(entries[cursor - 1].undo); + const entry = undoEntry()!; + runGuarded(entry.undo); cursor -= 1; } else { if (cursor === entries.length) break; - runGuarded(entries[cursor].do); + const entry = redoEntry()!; + runGuarded(entry.do); cursor += 1; } remaining -= 1; @@ -154,7 +168,7 @@ export function createHistory(options: HistoryOptions = {}): History { const checkpoint = (name: string): void => { assertIdle(); - checkpoints.set(name, cursor === 0 ? START : entries[cursor - 1]); + checkpoints.set(name, cursor === 0 ? START : undoEntry()!); }; const hasCheckpoint = (name: string): boolean => checkpoints.has(name); @@ -218,10 +232,10 @@ export function createHistory(options: HistoryOptions = {}): History { return cursor < entries.length; }, get undoLabel() { - return cursor > 0 ? entries[cursor - 1].label : undefined; + return undoEntry()?.label; }, get redoLabel() { - return cursor < entries.length ? entries[cursor].label : undefined; + return redoEntry()?.label; }, get position() { return cursor; diff --git a/src/transaction.ts b/src/transaction.ts index 9a4c0f1..383fa76 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -86,7 +86,9 @@ export function runTransaction( closed = true; try { for (let i = ops.length - 1; i >= 0; i -= 1) { - engine.runGuarded(ops[i].undo); + // The reverse walk starts at the last recorded op and stops before -1. + const op = ops[i]!; + engine.runGuarded(op.undo); } } finally { engine.endMutation(); @@ -106,7 +108,9 @@ export function runTransaction( }, undo() { for (let i = ops.length - 1; i >= 0; i -= 1) { - engine.runGuarded(ops[i].undo); + // The reverse walk starts at the last recorded op and stops before -1. + const op = ops[i]!; + engine.runGuarded(op.undo); } }, }); diff --git a/tsconfig.json b/tsconfig.json index ff4adab..2745bc3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,23 @@ { "compilerOptions": { - "target": "esnext", - "lib": ["es2023"], + "target": "es2024", + "lib": ["es2024"], "moduleDetection": "force", "module": "nodenext", "moduleResolution": "nodenext", - "resolveJsonModule": true, - "types": ["node"], + "types": [], "strict": true, + "noUncheckedIndexedAccess": true, "noUnusedLocals": true, - "declaration": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, "noEmit": true, "allowImportingTsExtensions": true, - "esModuleInterop": true, "isolatedModules": true, "verbatimModuleSyntax": true, "skipLibCheck": true - } + }, + "include": ["src"] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..25cae3c --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + // Keeps editor/raw tsc coverage for Node-run files while tsconfig.json + // remains the shipped-source contract with no Node ambient globals. + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["tests", "vite.config.ts"] +}