From 329d3a0757aa02deaebaf54d88d4537eb8fe816d Mon Sep 17 00:00:00 2001 From: leohenon <77656081+lhenon999@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:12:46 +0800 Subject: [PATCH 1/2] fix: move cursor left on escape from insert mode --- .../cli/cmd/tui/component/vim/vim-handler.ts | 3 ++ .../opencode/test/cli/tui/vim-motions.test.ts | 44 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/vim/vim-handler.ts b/packages/opencode/src/cli/cmd/tui/component/vim/vim-handler.ts index 324ba479c45b..35ebf4723966 100644 --- a/packages/opencode/src/cli/cmd/tui/component/vim/vim-handler.ts +++ b/packages/opencode/src/cli/cmd/tui/component/vim/vim-handler.ts @@ -1065,6 +1065,9 @@ export function createVimHandler(input: { if (event.name !== "escape") return false input.state.setMode("normal") input.state.commitEdit(snapshot()) + if (input.textarea().cursorOffset > 0 && input.textarea().plainText[input.textarea().cursorOffset - 1] !== "\n") { + input.textarea().cursorOffset = input.textarea().cursorOffset - 1 + } event.preventDefault() return true } diff --git a/packages/opencode/test/cli/tui/vim-motions.test.ts b/packages/opencode/test/cli/tui/vim-motions.test.ts index bbabd338dfb9..5944cf7b58c8 100644 --- a/packages/opencode/test/cli/tui/vim-motions.test.ts +++ b/packages/opencode/test/cli/tui/vim-motions.test.ts @@ -878,6 +878,7 @@ describe("vim motion handler", () => { test("insert mode only handles escape", () => { const ctx = createHandler("abc", { mode: "insert" }) + ctx.textarea.cursorOffset = 2 const w = createEvent("w") expect(ctx.handler.handleKey(w.event)).toBe(false) @@ -887,6 +888,47 @@ describe("vim motion handler", () => { expect(ctx.handler.handleKey(esc.event)).toBe(true) expect(esc.prevented()).toBe(true) expect(ctx.state.mode()).toBe("normal") + expect(ctx.textarea.cursorOffset).toBe(1) + }) + + test("escape from insert mode moves cursor back like vim", () => { + const ctx = createHandler("abcd") + ctx.textarea.cursorOffset = 1 + + ctx.handler.handleKey(createEvent("i").event) + ctx.textarea.insertText("X") + ctx.textarea.insertText("Y") + ctx.handler.handleKey(createEvent("escape").event) + + expect(ctx.textarea.plainText).toBe("aXYbcd") + expect(ctx.textarea.cursorOffset).toBe(2) + expect(ctx.state.mode()).toBe("normal") + }) + + test("escape from insert mode stays on a new empty line", () => { + const ctx = createHandler("abc") + ctx.textarea.cursorOffset = 1 + + ctx.handler.handleKey(createEvent("o").event) + ctx.handler.handleKey(createEvent("escape").event) + + expect(ctx.textarea.plainText).toBe("abc\n") + expect(ctx.textarea.cursorOffset).toBe(4) + expect(ctx.state.mode()).toBe("normal") + }) + + test("db after insert escape keeps the character under cursor like vim", () => { + const ctx = createHandler("") + + ctx.handler.handleKey(createEvent("i").event) + ctx.textarea.insertText("word") + ctx.handler.handleKey(createEvent("escape").event) + expect(ctx.textarea.cursorOffset).toBe(3) + + ctx.handler.handleKey(createEvent("d").event) + ctx.handler.handleKey(createEvent("b").event) + expect(ctx.textarea.plainText).toBe("d") + expect(ctx.textarea.cursorOffset).toBe(0) }) test("R enters replace mode and escape exits", () => { @@ -2499,7 +2541,7 @@ describe("vim undo redo", () => { ctx.handler.handleKey(createEvent("u").event) expect(ctx.textarea.plainText).toBe("hello") - expect(ctx.textarea.cursorOffset).toBe(5) + expect(ctx.textarea.cursorOffset).toBe(4) }) test("redo is cleared after a new edit", () => { From eaf1145b0eb6b8648d527b23bbf3ee2967e019c6 Mon Sep 17 00:00:00 2001 From: leohenon <77656081+lhenon999@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:27:52 +0800 Subject: [PATCH 2/2] trigger ci