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 ebff796899f9..95f17eb5273c 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 @@ -6,7 +6,6 @@ import { vimJump, type VimJump } from "./vim-motion-jump" import { appendAfterCursor, appendLineEnd, - clampCursorToLine, clearSelection, deleteLine, deleteLineEnd, @@ -1316,9 +1315,9 @@ export function createVimHandler(input: { if (input.state.isInsert()) { if (event.name !== "escape") return false - clampCursorToLine(input.textarea()) input.state.setMode("normal") input.state.commitEdit(snapshot()) + moveLeft(input.textarea()) 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 f68181c7b5b3..dbda392caad8 100644 --- a/packages/opencode/test/cli/tui/vim-motions.test.ts +++ b/packages/opencode/test/cli/tui/vim-motions.test.ts @@ -1142,7 +1142,7 @@ describe("vim motion handler", () => { expect(ctx.textarea.cursorOffset).toBe(1) }) - test("escape from insert leaves cursor inside line content alone", () => { + test("escape from empty insert moves cursor back like vim", () => { const ctx = createHandler("abc") ctx.textarea.cursorOffset = 1 expect(ctx.handler.handleKey(createEvent("i").event)).toBe(true) @@ -1150,7 +1150,7 @@ describe("vim motion handler", () => { expect(ctx.handler.handleKey(createEvent("escape").event)).toBe(true) expect(ctx.state.mode()).toBe("normal") - expect(ctx.textarea.cursorOffset).toBe(1) + expect(ctx.textarea.cursorOffset).toBe(0) }) test("o opens line below and enters insert", () => { @@ -1499,6 +1499,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) @@ -1508,6 +1509,60 @@ 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 inserted text at end of line", () => { + const ctx = createHandler("abcd") + ctx.textarea.cursorOffset = 3 + + ctx.handler.handleKey(createEvent("a").event) + ctx.textarea.insertText("X") + ctx.handler.handleKey(createEvent("escape").event) + + expect(ctx.textarea.plainText).toBe("abcdX") + expect(ctx.textarea.cursorOffset).toBe(4) + 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", () => { @@ -4128,7 +4183,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", () => {