Skip to content

Commit ac10101

Browse files
authored
fix: move cursor left on escape from insert mode (#92)
* fix: move cursor left on escape from insert mode * fix: avoid overcorrecting insert escape cursor * test: update empty insert escape cursor expectation --------- Co-authored-by: leohenon <[email protected]>
1 parent 709188a commit ac10101

2 files changed

Lines changed: 59 additions & 5 deletions

File tree

packages/opencode/src/cli/cmd/tui/component/vim/vim-handler.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { vimJump, type VimJump } from "./vim-motion-jump"
66
import {
77
appendAfterCursor,
88
appendLineEnd,
9-
clampCursorToLine,
109
clearSelection,
1110
deleteLine,
1211
deleteLineEnd,
@@ -1316,9 +1315,9 @@ export function createVimHandler(input: {
13161315

13171316
if (input.state.isInsert()) {
13181317
if (event.name !== "escape") return false
1319-
clampCursorToLine(input.textarea())
13201318
input.state.setMode("normal")
13211319
input.state.commitEdit(snapshot())
1320+
moveLeft(input.textarea())
13221321
event.preventDefault()
13231322
return true
13241323
}

packages/opencode/test/cli/tui/vim-motions.test.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,15 +1142,15 @@ describe("vim motion handler", () => {
11421142
expect(ctx.textarea.cursorOffset).toBe(1)
11431143
})
11441144

1145-
test("escape from insert leaves cursor inside line content alone", () => {
1145+
test("escape from empty insert moves cursor back like vim", () => {
11461146
const ctx = createHandler("abc")
11471147
ctx.textarea.cursorOffset = 1
11481148
expect(ctx.handler.handleKey(createEvent("i").event)).toBe(true)
11491149
expect(ctx.state.mode()).toBe("insert")
11501150

11511151
expect(ctx.handler.handleKey(createEvent("escape").event)).toBe(true)
11521152
expect(ctx.state.mode()).toBe("normal")
1153-
expect(ctx.textarea.cursorOffset).toBe(1)
1153+
expect(ctx.textarea.cursorOffset).toBe(0)
11541154
})
11551155

11561156
test("o opens line below and enters insert", () => {
@@ -1499,6 +1499,7 @@ describe("vim motion handler", () => {
14991499

15001500
test("insert mode only handles escape", () => {
15011501
const ctx = createHandler("abc", { mode: "insert" })
1502+
ctx.textarea.cursorOffset = 2
15021503

15031504
const w = createEvent("w")
15041505
expect(ctx.handler.handleKey(w.event)).toBe(false)
@@ -1508,6 +1509,60 @@ describe("vim motion handler", () => {
15081509
expect(ctx.handler.handleKey(esc.event)).toBe(true)
15091510
expect(esc.prevented()).toBe(true)
15101511
expect(ctx.state.mode()).toBe("normal")
1512+
expect(ctx.textarea.cursorOffset).toBe(1)
1513+
})
1514+
1515+
test("escape from insert mode moves cursor back like vim", () => {
1516+
const ctx = createHandler("abcd")
1517+
ctx.textarea.cursorOffset = 1
1518+
1519+
ctx.handler.handleKey(createEvent("i").event)
1520+
ctx.textarea.insertText("X")
1521+
ctx.textarea.insertText("Y")
1522+
ctx.handler.handleKey(createEvent("escape").event)
1523+
1524+
expect(ctx.textarea.plainText).toBe("aXYbcd")
1525+
expect(ctx.textarea.cursorOffset).toBe(2)
1526+
expect(ctx.state.mode()).toBe("normal")
1527+
})
1528+
1529+
test("escape from insert mode stays on inserted text at end of line", () => {
1530+
const ctx = createHandler("abcd")
1531+
ctx.textarea.cursorOffset = 3
1532+
1533+
ctx.handler.handleKey(createEvent("a").event)
1534+
ctx.textarea.insertText("X")
1535+
ctx.handler.handleKey(createEvent("escape").event)
1536+
1537+
expect(ctx.textarea.plainText).toBe("abcdX")
1538+
expect(ctx.textarea.cursorOffset).toBe(4)
1539+
expect(ctx.state.mode()).toBe("normal")
1540+
})
1541+
1542+
test("escape from insert mode stays on a new empty line", () => {
1543+
const ctx = createHandler("abc")
1544+
ctx.textarea.cursorOffset = 1
1545+
1546+
ctx.handler.handleKey(createEvent("o").event)
1547+
ctx.handler.handleKey(createEvent("escape").event)
1548+
1549+
expect(ctx.textarea.plainText).toBe("abc\n")
1550+
expect(ctx.textarea.cursorOffset).toBe(4)
1551+
expect(ctx.state.mode()).toBe("normal")
1552+
})
1553+
1554+
test("db after insert escape keeps the character under cursor like vim", () => {
1555+
const ctx = createHandler("")
1556+
1557+
ctx.handler.handleKey(createEvent("i").event)
1558+
ctx.textarea.insertText("word")
1559+
ctx.handler.handleKey(createEvent("escape").event)
1560+
expect(ctx.textarea.cursorOffset).toBe(3)
1561+
1562+
ctx.handler.handleKey(createEvent("d").event)
1563+
ctx.handler.handleKey(createEvent("b").event)
1564+
expect(ctx.textarea.plainText).toBe("d")
1565+
expect(ctx.textarea.cursorOffset).toBe(0)
15111566
})
15121567

15131568
test("R enters replace mode and escape exits", () => {
@@ -4128,7 +4183,7 @@ describe("vim undo redo", () => {
41284183

41294184
ctx.handler.handleKey(createEvent("u").event)
41304185
expect(ctx.textarea.plainText).toBe("hello")
4131-
expect(ctx.textarea.cursorOffset).toBe(5)
4186+
expect(ctx.textarea.cursorOffset).toBe(4)
41324187
})
41334188

41344189
test("redo is cleared after a new edit", () => {

0 commit comments

Comments
 (0)