diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 823ca16bef21..da8380a02304 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -461,6 +461,23 @@ export function Prompt(props: PromptProps) { syncVimRegisterFromClipboard, } = promptVim + function resetVimHistory() { + vimState.resetHistory() + if (vimEnabled() && store.mode === "normal" && vimState.isInsert()) vim.beginInsertEdit() + } + + createEffect((previous) => { + const enabled = vimEnabled() + if (enabled && previous === false) resetVimHistory() + return enabled + }, vimEnabled()) + + createEffect<"normal" | "shell">((previous) => { + const mode = store.mode + if (mode !== previous) resetVimHistory() + return mode + }, store.mode) + createEffect( on( () => props.sessionID, @@ -702,7 +719,7 @@ export function Prompt(props: PromptProps) { }) restoreExtmarksFromParts(updatedNonTextParts) input.cursorOffset = Bun.stringWidth(normalized) - vimState.resetHistory() + resetVimHistory() }, }, { @@ -734,7 +751,7 @@ export function Prompt(props: PromptProps) { parts: [], }) input.gotoBufferEnd() - vimState.resetHistory() + resetVimHistory() }} /> )) @@ -925,7 +942,7 @@ export function Prompt(props: PromptProps) { setStore("prompt", prompt) restoreExtmarksFromParts(prompt.parts) input.gotoBufferEnd() - vimState.resetHistory() + resetVimHistory() }, reset() { input.clear() @@ -935,7 +952,7 @@ export function Prompt(props: PromptProps) { parts: [], }) setStore("extmarkToPartIndex", new Map()) - vimState.resetHistory() + resetVimHistory() }, submit() { void submit() @@ -945,13 +962,13 @@ export function Prompt(props: PromptProps) { onMount(() => { const saved = stashed stashed = undefined - if (store.prompt.input) return - if (saved && saved.prompt.input) { + if (!store.prompt.input && saved?.prompt.input) { input.setText(saved.prompt.input) setStore("prompt", saved.prompt) restoreExtmarksFromParts(saved.prompt.parts) input.cursorOffset = saved.cursor } + if (vimEnabled() && vimState.isInsert()) vim.beginInsertEdit() }) onCleanup(() => { @@ -1136,6 +1153,7 @@ export function Prompt(props: PromptProps) { input.clear() setStore("prompt", { input: "", parts: [] }) setStore("extmarkToPartIndex", new Map()) + resetVimHistory() dialog.clear() }, }, @@ -1151,6 +1169,7 @@ export function Prompt(props: PromptProps) { setStore("prompt", { input: entry.input, parts: entry.parts }) restoreExtmarksFromParts(entry.parts) input.gotoBufferEnd() + resetVimHistory() } dialog.clear() }, @@ -1168,6 +1187,7 @@ export function Prompt(props: PromptProps) { setStore("prompt", { input: entry.input, parts: entry.parts }) restoreExtmarksFromParts(entry.parts) input.gotoBufferEnd() + resetVimHistory() }} /> )) @@ -1271,6 +1291,7 @@ export function Prompt(props: PromptProps) { setStore("mode", item.mode ?? "normal") restoreExtmarksFromParts(item.parts) input.cursorOffset = 0 + resetVimHistory() }, }, ], @@ -1307,6 +1328,7 @@ export function Prompt(props: PromptProps) { setStore("mode", item.mode ?? "normal") restoreExtmarksFromParts(item.parts) input.cursorOffset = input.plainText.length + resetVimHistory() }, }, ], @@ -1532,6 +1554,7 @@ export function Prompt(props: PromptProps) { }, 50) } input.clear() + if (vimEnabled() && vimState.isInsert()) vim.beginInsertEdit() if (finishMoveProgress) move.finishSubmit() return true } @@ -1715,6 +1738,7 @@ export function Prompt(props: PromptProps) { parts: [], }) setStore("extmarkToPartIndex", new Map()) + resetVimHistory() } const dimmed = createMemo(() => leader() || vimState.isCopy()) diff --git a/packages/tui/src/component/prompt/vim.ts b/packages/tui/src/component/prompt/vim.ts index ab779222b9e8..a3ad483fa8e4 100644 --- a/packages/tui/src/component/prompt/vim.ts +++ b/packages/tui/src/component/prompt/vim.ts @@ -50,6 +50,7 @@ export function usePromptVim(opts: { popCopyMode?.() keymap.setData(OPENCODE_VIM_MODE_KEY, undefined) if (vimEnabled()) lastVimMode = vimState.isCopy() ? "normal" : vimState.mode() + vimState.cancelEdit() }) createEffect(() => { @@ -93,6 +94,7 @@ export function usePromptVim(opts: { function enterCopyMode() { const copy = opts.copy() if (!vimEnabled() || !copy) return false + if (vimState.isInsert()) vim.finishInsertEdit() vimState.setMode("copy") copy.enter() const input = opts.textarea() diff --git a/packages/tui/src/component/vim/vim-handler.ts b/packages/tui/src/component/vim/vim-handler.ts index aeccdf5f449c..c72a943287d6 100644 --- a/packages/tui/src/component/vim/vim-handler.ts +++ b/packages/tui/src/component/vim/vim-handler.ts @@ -410,6 +410,13 @@ export function createVimHandler(input: { const edit = repeat.edit const begin = repeat.begin + function finishInsertEdit() { + input.state.setMode("normal") + input.state.commitEdit(snapshot()) + moveLeft(input.textarea()) + repeat.commit(snapshot()) + } + function applyOperatorYank(result: VimOperatorResult) { if (result.register) setRegister(result.register, true) if (result.span && result.span.end > result.span.start) input.flash?.(result.span) @@ -2271,6 +2278,8 @@ export function createVimHandler(input: { } return { + beginInsertEdit: repeat.begin, + finishInsertEdit, handleKey(event: VimEvent) { if (!input.enabled()) return false diff --git a/packages/tui/test/cli/tui/vim-motions.test.ts b/packages/tui/test/cli/tui/vim-motions.test.ts index 822540bd8d94..c63e7ded967f 100644 --- a/packages/tui/test/cli/tui/vim-motions.test.ts +++ b/packages/tui/test/cli/tui/vim-motions.test.ts @@ -8084,6 +8084,50 @@ describe("vim dot repeat", () => { expect(ctx.textarea.insertText).toBe(insertText) }) + test("tracks implicit insert sessions", () => { + const ctx = createHandler("", { mode: "insert" }) + + ctx.handler.beginInsertEdit() + ctx.textarea.insertText("abc") + press(ctx, "escape") + press(ctx, "u") + expect(ctx.textarea.plainText).toBe("") + + press(ctx, ".") + expect(ctx.textarea.plainText).toBe("abc") + }) + + test("restarts implicit insert tracking after history reset", () => { + const ctx = createHandler("draft", { mode: "insert" }) + + ctx.handler.beginInsertEdit() + ctx.textarea.insertText("!") + ctx.state.resetHistory() + ctx.textarea.setText("") + ctx.handler.beginInsertEdit() + ctx.textarea.insertText("abc") + press(ctx, "escape") + press(ctx, "u") + expect(ctx.textarea.plainText).toBe("") + + press(ctx, ".") + expect(ctx.textarea.plainText).toBe("abc") + }) + + test("finishes implicit insert tracking before external mode changes", () => { + const ctx = createHandler("", { mode: "insert" }) + + ctx.handler.beginInsertEdit() + ctx.textarea.insertText("abc") + ctx.handler.finishInsertEdit() + expect(ctx.textarea.cursorOffset).toBe(2) + press(ctx, "u") + expect(ctx.textarea.plainText).toBe("") + + press(ctx, ".") + expect(ctx.textarea.plainText).toBe("abc") + }) + test("dot repeats complex insert sessions from the current text", () => { const ctx = createHandler("abcd") ctx.textarea.cursorOffset = 1