From 8af284013582cfb1e244929ad90f3513187e95eb Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Sat, 27 Jun 2026 13:17:46 +1000 Subject: [PATCH] feat: expose canUndo/canRedo on the Edit class --- src/core/edit-session.ts | 10 +++++++ tests/edit-commands.test.ts | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/core/edit-session.ts b/src/core/edit-session.ts index e2d17da..169c851 100644 --- a/src/core/edit-session.ts +++ b/src/core/edit-session.ts @@ -1045,6 +1045,16 @@ export class Edit { } }); } + /** True when there is a command to undo (the history pointer is not before the first command). */ + public get canUndo(): boolean { + return this.commandIndex >= 0; + } + + /** True when there is a command ahead to redo (the history pointer is not at the latest command). */ + public get canRedo(): boolean { + return this.commandIndex < this.commandHistory.length - 1; + } + /** @internal */ public setUpdatedClip(clip: Player, initialClipConfig: ResolvedClip | null = null, finalClipConfig: ResolvedClip | null = null): void { // Find track and clip indices diff --git a/tests/edit-commands.test.ts b/tests/edit-commands.test.ts index c7387fe..e297744 100644 --- a/tests/edit-commands.test.ts +++ b/tests/edit-commands.test.ts @@ -654,6 +654,61 @@ describe("Edit Command History", () => { }); }); + describe("canUndo / canRedo getters", () => { + it("are both false on a freshly loaded edit (load is not a command)", () => { + expect(getCommandState(edit).index).toBe(-1); + expect(edit.canUndo).toBe(false); + expect(edit.canRedo).toBe(false); + }); + + it("canUndo turns true after a command; canRedo stays false", async () => { + await edit.executeEditCommand(new TestCommand()); + expect(edit.canUndo).toBe(true); + expect(edit.canRedo).toBe(false); + }); + + it("undo turns canRedo on and, back at the start, canUndo off", async () => { + await edit.executeEditCommand(new TestCommand()); + await edit.undo(); + expect(edit.canUndo).toBe(false); + expect(edit.canRedo).toBe(true); + }); + + it("redo turns canUndo back on and, at the end, canRedo off", async () => { + await edit.executeEditCommand(new TestCommand()); + await edit.undo(); + await edit.redo(); + expect(edit.canUndo).toBe(true); + expect(edit.canRedo).toBe(false); + }); + + it("both are true mid-stack", async () => { + await edit.executeEditCommand(new TestCommand()); + await edit.executeEditCommand(new TestCommand()); + await edit.undo(); + expect(edit.canUndo).toBe(true); + expect(edit.canRedo).toBe(true); + }); + + it("a new command after undo truncates redo (canRedo false)", async () => { + await edit.executeEditCommand(new TestCommand()); + await edit.executeEditCommand(new TestCommand()); + await edit.undo(); + await edit.executeEditCommand(new TestCommand()); + expect(edit.canUndo).toBe(true); + expect(edit.canRedo).toBe(false); + }); + + it("track the underlying commandIndex bounds", async () => { + await edit.executeEditCommand(new TestCommand()); + await edit.executeEditCommand(new TestCommand()); + await edit.undo(); + const { history, index } = getCommandState(edit); + expect(edit.canUndo).toBe(index >= 0); + expect(edit.canRedo).toBe(index < history.length - 1); + }); + }); + describe("state restoration", () => { it("undo restores previous state via command.undo()", async () => { // Use a command that tracks state changes