feat(tui): vim motions in prompt input#12679
feat(tui): vim motions in prompt input#12679leohenon wants to merge 20 commits intoanomalyco:devfrom
Conversation
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
|
The following comment was made by an LLM, it may be inaccurate: No duplicate PRs found |
There was a problem hiding this comment.
Pull request overview
Adds an optional Vim-style keybinding layer for the TUI prompt input (configurable via tui.vim or a settings toggle), including a mode indicator, motion/operator handling, and related documentation/tests.
Changes:
- Introduces a
/vimmodule (state, handler, motions, scroll/jump mapping, indicator) and wires it into the prompt input flow. - Adds a command-palette setting to toggle Vim input, plus config schema + docs updates.
- Adds a targeted test suite covering mode transitions, motions, operators, scrolling, and jumps.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/web/src/content/docs/tui.mdx | Documents Vim mode and supported keys for the TUI prompt. |
| packages/web/src/content/docs/config.mdx | Documents tui.vim config flag. |
| packages/opencode/src/config/config.ts | Adds tui.vim boolean to config schema. |
| packages/opencode/src/cli/cmd/tui/component/vim/vim-state.ts | New Vim mode/pending state container. |
| packages/opencode/src/cli/cmd/tui/component/vim/vim-scroll.ts | Maps Ctrl+key combos to scroll actions. |
| packages/opencode/src/cli/cmd/tui/component/vim/vim-motions.ts | Implements cursor motions + delete/edit primitives on the textarea. |
| packages/opencode/src/cli/cmd/tui/component/vim/vim-motion-jump.ts | Implements gg/G jump behavior. |
| packages/opencode/src/cli/cmd/tui/component/vim/vim-indicator.ts | Computes INSERT/NORMAL indicator text. |
| packages/opencode/src/cli/cmd/tui/component/vim/vim-handler.ts | Key-event interpreter coordinating state + motions + submit/scroll/jump. |
| packages/opencode/src/cli/cmd/tui/component/vim/index.ts | Provides useVimEnabled() integration with KV + config. |
| packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | Integrates Vim handler/state into prompt input, cursor styling, submit behavior, and indicator rendering. |
| packages/opencode/src/cli/cmd/tui/app.tsx | Adds command-palette toggle to enable/disable Vim input. |
| packages/opencode/test/cli/tui/vim-motions.test.ts | Adds tests for handler behavior, operators, and scroll mapping. |
Comments suppressed due to low confidence (1)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:918
- The inline comment says "Don't preventDefault" but the code immediately calls
e.preventDefault(). Either update the comment to match the behavior or adjust the behavior if the intent really is to let the textarea handle the event.
await exit()
// Don't preventDefault - let textarea potentially handle the event
e.preventDefault()
return
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let pos = offset | ||
| if (pos < text.length) pos++ | ||
| while (pos < text.length && !match(text[pos])) pos++ | ||
| while (pos < text.length && match(text[pos])) pos++ | ||
| if (pos > offset + 1) pos-- |
There was a problem hiding this comment.
wordEnd() currently increments pos before checking whether the cursor is already on a word character. This makes e/E behave incorrectly for 1-character words or when the cursor is on the last character of a word followed by whitespace (e.g., in "a b", e from a will jump to b, and in "a" it can return text.length, which then breaks normal-mode assumptions like l clamping). Consider rewriting wordEnd to first find the end of the current word when on a word char, and ensure it never returns an offset past the last character when used for normal-mode motions; add a regression test for the 1-character word case.
| let pos = offset | |
| if (pos < text.length) pos++ | |
| while (pos < text.length && !match(text[pos])) pos++ | |
| while (pos < text.length && match(text[pos])) pos++ | |
| if (pos > offset + 1) pos-- | |
| // If the buffer is empty, stay at 0. | |
| if (text.length === 0) return 0 | |
| // Clamp starting position to the last valid character index. | |
| let pos = offset | |
| if (pos >= text.length) pos = text.length - 1 | |
| // If we're on a word character, move to the end of the *current* word. | |
| if (match(text[pos])) { | |
| while (pos + 1 < text.length && match(text[pos + 1])) pos++ | |
| return pos | |
| } | |
| // Otherwise, skip forward to the start of the next word (if any)... | |
| while (pos < text.length && !match(text[pos])) pos++ | |
| if (pos >= text.length) return text.length - 1 | |
| // ...then move to the end of that word. | |
| while (pos + 1 < text.length && match(text[pos + 1])) pos++ |
| export function createVimState(input: { | ||
| enabled: Accessor<boolean> | ||
| active: Accessor<boolean> | ||
| initial?: Accessor<VimMode | undefined> | ||
| }) { |
There was a problem hiding this comment.
createVimState requires an active accessor in its input but never reads it. Either remove active from the API (to avoid misleading callers) or use it to reset mode/pending when the prompt becomes inactive (e.g., hidden/disabled), since callers are already computing that signal.
2c180c9 to
87b4487
Compare
What does this PR do?
Closes #1764
Closes #11111
Adds optional Vim motions to the prompt input. Enable with tui.vim: true or toggle from the menu. Disabled by default.
Supported:
i I a A o O S,cc,cw,Esch j k l,w b e,W B E,0 ^ $x,dd,dwgg/GCtrl+e/y/d/u/f/bEnterin normal mode submitsMotivation: I use OpenCode daily and the external editor round-trip for small edits was slow. I originally built this for personal use and then cleaned it up for upstream.
Implementation: Vim state isolated in
/vimmodule. Handler owns mode + pending operator state. Mode resets to insert after submit. No visual mode, intentionally excluded to avoid conflicts with the clipboard system.I saw thdxr's concern in #1764 about partial Vim feeling incomplete. This PR covers only the core motions needed for prompt editing and is intentionally kept small to make review manageable. I have a fuller implementation locally (yank/put, word/line selections like daw/diw, yaw/yiw, history cycling with j/k, and menu navigation) that I can split into follow-up PRs if this direction looks good.
How did you verify your code works?
Used the fuller implementation locally for over a week of daily use. Reimplemented the core subset for this PR and verified manually (editing, scrolling, session switching, edge cases with pending operators, empty buffers, multiline). Added targeted tests for mode transitions, motions, deletes, pending state, and submit reset. All passing.