From 3614e02055015d21d48e99cd9ff172e49a8bd963 Mon Sep 17 00:00:00 2001 From: WayneKent Date: Sat, 27 Jun 2026 22:42:17 +0800 Subject: [PATCH] feat: respects mode-specific keymaps Pass through handleInsertKeybind in insert mode. Route normal mode through processKeystroke for keybind resolution. Fallback to submit when normal mode has no mapping. Document input_submit interaction and add Chinese translation. --- .gitignore | 1 + README.md | 22 +- README.zh.md | 28 +++ docs/configuration.md | 109 +++++----- docs/configuration.zh.md | 428 ++++++++++++++++++++++++++++++++++++++ src/modules/vim/index.tsx | 2 +- src/modules/vim/vimee.ts | 19 +- 7 files changed, 538 insertions(+), 71 deletions(-) create mode 100644 README.zh.md create mode 100644 docs/configuration.zh.md diff --git a/.gitignore b/.gitignore index ade171d..a4bbe6f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ node_modules/ .opencode/ docs/* !docs/configuration.md +!docs/configuration.zh.md assets/*.mp4 assets/screenshot-*.png diff --git a/README.md b/README.md index e273c28..e96d9e1 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,17 @@ opencode plugin opencode-vim@latest --global ## Supported Keys -| Key | Behavior | -| --- | --- | -| ``, `` | Enter normal mode | -| `i`, `a`, `A`, `o`, `O` | Return to insert mode | -| `h`, `j`, `k`, `l`, `w`, `b`, `e`, `$`, `0` | Move through the prompt | -| `x`, `d`, `c`, `y`, `p`, `u`, `` | Edit, yank, paste, undo, redo | -| `v`, `V` | Visual and visual-line selection | -| `3w`, `diw`, `ci"`, `yiq`, `dip`, `yib` | Counts and text objects | -| `` in normal mode | Submit the prompt | -| `/vim` | Toggle Vim mode on or off | +| Key | Behavior | +| ------------------------------------------- | -------------------------------------------- | +| ``, `` | Enter normal mode | +| `i`, `a`, `A`, `o`, `O` | Return to insert mode | +| `h`, `j`, `k`, `l`, `w`, `b`, `e`, `$`, `0` | Move through the prompt | +| `x`, `d`, `c`, `y`, `p`, `u`, `` | Edit, yank, paste, undo, redo | +| `v`, `V` | Visual and visual-line selection | +| `3w`, `diw`, `ci"`, `yiq`, `dip`, `yib` | Counts and text objects | +| `` in normal mode | Submit (default). Configurable via `keymaps` | +| `/vim` | Toggle Vim mode on or off | See [docs/configuration.md](./docs/configuration.md) for configuration options and keymap examples. + +中文文档:[README.zh.md](./README.zh.md) | [docs/configuration.zh.md](./docs/configuration.zh.md) diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 0000000..6c99cbc --- /dev/null +++ b/README.zh.md @@ -0,0 +1,28 @@ +# opencode-vim + +为 OpenCode 输入框添加 Vim 风格的 insert 和 normal 模式编辑。 + +![Demo](./assets/demo2.gif) + +## 安装 + +通过命令行安装: + +```bash +opencode plugin opencode-vim@latest --global +``` + +## 快捷键 + +| 按键 | 说明 | +| ------------------------------------------- | ---------------------------------------- | +| ``, `` | 进入 normal 模式 | +| `i`, `a`, `A`, `o`, `O` | 返回 insert 模式 | +| `h`, `j`, `k`, `l`, `w`, `b`, `e`, `$`, `0` | 移动光标 | +| `x`, `d`, `c`, `y`, `p`, `u`, `` | 编辑、复制、粘贴、撤销、重做 | +| `v`, `V` | 可视模式和可视行模式 | +| `3w`, `diw`, `ci"`, `yiq`, `dip`, `yib` | 计数和文本对象 | +| normal 模式下 `` | 提交 prompt(默认,可通过 keymaps 配置) | +| `/vim` | 开关 Vim 模式 | + +配置文档:[docs/configuration.zh.md](./docs/configuration.zh.md) diff --git a/docs/configuration.md b/docs/configuration.md index 40f1ef3..48875c5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,18 +8,18 @@ Add `opencode-vim` to the `plugin` array in your OpenCode `tui.jsonc` file: ```jsonc { - "$schema": "https://opencode.ai/tui.json", - "plugin": [ - [ - "./plugin/opencode-vim", - { - "autoUpdate": true, - "vim": { - "defaultMode": "insert" - } - } - ] - ] + "$schema": "https://opencode.ai/tui.json", + "plugin": [ + [ + "./plugin/opencode-vim", + { + "autoUpdate": true, + "vim": { + "defaultMode": "insert", + }, + }, + ], + ], } ``` @@ -29,41 +29,41 @@ If your plugin is installed somewhere else, change the plugin path to match your ```jsonc { - "$schema": "https://opencode.ai/tui.json", - "plugin": [ - [ - "./plugin/opencode-vim", - { - "autoUpdate": true, - "vim": { - "defaultMode": "insert", - "keymapTimeout": 500, - "pendingDisplayDelay": 120, - "cursorStyles": { - "insert": { - "style": "line", - "blinking": true - }, - "normal": { - "style": "block", - "blinking": true - } - }, - "debug": false, - "debugPath": "/home/you/.cache/opencode/opencode-vim.log", - "keymaps": { - "insert": { - "kj": "normal" + "$schema": "https://opencode.ai/tui.json", + "plugin": [ + [ + "./plugin/opencode-vim", + { + "autoUpdate": true, + "vim": { + "defaultMode": "insert", + "keymapTimeout": 500, + "pendingDisplayDelay": 120, + "cursorStyles": { + "insert": { + "style": "line", + "blinking": true, + }, + "normal": { + "style": "block", + "blinking": true, + }, + }, + "debug": false, + "debugPath": "/home/you/.cache/opencode/opencode-vim.log", + "keymaps": { + "insert": { + "kj": "normal", + }, + "normal": { + "": "submit", + "Y": "y$", + }, + }, + }, }, - "normal": { - "": "submit", - "Y": "y$" - } - } - } - } - ] - ] + ], + ], } ``` @@ -268,11 +268,14 @@ Each keymap entry maps a key sequence to an action: Supported built-in actions: -- `"normal"` exits insert mode and enters normal mode. -- `"insert"` enters insert mode. -- `"submit"` submits the OpenCode prompt. +| Action | Modes | Effect | +|---|---|---| +| `"submit"` | insert, normal | Submits the prompt | +| `"normal"` | insert only | Exits insert mode, enters normal mode | +| `"insert"` | normal only | Enters insert mode | +| `""` | normal only | Executes the configured Vim key sequence | -Any other action string is treated as a Vim key sequence. For example, this maps `Y` to yank from the cursor to the end of the line: +For example, this maps `Y` to yank from the cursor to the end of the line: ```jsonc "keymaps": { @@ -282,6 +285,10 @@ Any other action string is treated as a Vim key sequence. For example, this maps } ``` +Unlike other keys, `` in normal mode defaults to `"submit"` when no mapping is configured. In insert mode, if `` is unmapped and OpenCode's `input_submit` is not `return`, it inserts a newline instead of submitting. + +> **Note:** When `input_submit` is not `"return"`, make sure to also set `"input_newline": "return"` in the OpenCode `keybinds` config. Without it, `` in insert mode does nothing (neither submits nor inserts a newline). + ## Keymap Syntax Key sequences can contain printable ASCII characters, except literal spaces. Use `` for the space key. @@ -331,7 +338,7 @@ Use `kj` or `jk` to leave insert mode: } ``` -Submit the prompt with Enter in normal mode: +Submit with `` in normal mode is the default — this keymap is optional: ```jsonc "keymaps": { diff --git a/docs/configuration.zh.md b/docs/configuration.zh.md new file mode 100644 index 0000000..8e402b3 --- /dev/null +++ b/docs/configuration.zh.md @@ -0,0 +1,428 @@ +# 配置指南 + +介绍如何在 OpenCode 中启用 `opencode-vim` 并配置 Vim 输入行为。 + +## 基本设置 + +在 `tui.jsonc` 的 `plugin` 数组中添加 `opencode-vim`: + +```jsonc +{ + "$schema": "https://opencode.ai/tui.json", + "plugin": [ + [ + "./plugin/opencode-vim", + { + "autoUpdate": true, + "vim": { + "defaultMode": "insert", + }, + }, + ], + ], +} +``` + +如果插件安装在别处,请修改路径。 + +## 完整示例 + +```jsonc +{ + "$schema": "https://opencode.ai/tui.json", + "plugin": [ + [ + "./plugin/opencode-vim", + { + "autoUpdate": true, + "vim": { + "defaultMode": "insert", + "keymapTimeout": 500, + "pendingDisplayDelay": 120, + "cursorStyles": { + "insert": { + "style": "line", + "blinking": true, + }, + "normal": { + "style": "block", + "blinking": true, + }, + }, + "debug": false, + "debugPath": "/home/you/.cache/opencode/opencode-vim.log", + "keymaps": { + "insert": { + "kj": "normal", + }, + "normal": { + "": "submit", + "Y": "y$", + }, + }, + }, + }, + ], + ], +} +``` + +## 选项 + +### `autoUpdate` + +当有新版本时自动更新 `opencode-vim`。 + +默认: + +```jsonc +"autoUpdate": true +``` + +示例: + +```jsonc +"autoUpdate": false +``` + +### `defaultMode` + +输入框启动时的模式。 + +可选值: + +- `"insert"` +- `"normal"` + +默认: + +```jsonc +"defaultMode": "insert" +``` + +示例: + +```jsonc +"defaultMode": "normal" +``` + +### `keymapTimeout` + +当按键映射只输入了部分时,等待下一个按键的时间,单位毫秒。 + +默认: + +```jsonc +"keymapTimeout": 500 +``` + +示例: + +```jsonc +"keymapTimeout": 250 +``` + +```jsonc +"keymapTimeout": 1000 +``` + +短超时适合快速回退,长超时适合慢速输入多键映射。 + +### `pendingDisplayDelay` + +在状态栏显示待按键序列的延迟,单位毫秒。 + +默认: + +```jsonc +"pendingDisplayDelay": 120 +``` + +示例: + +```jsonc +"pendingDisplayDelay": 0 +``` + +```jsonc +"pendingDisplayDelay": 300 +``` + +只影响显示,不影响按键映射等待时间。 + +### `cursorStyles` + +各模式下的光标样式。 + +可选样式: + +- `"block"` +- `"line"` +- `"underline"` +- `"default"` + +默认: + +```jsonc +"cursorStyles": { + "insert": { + "style": "line", + "blinking": true + }, + "normal": { + "style": "block", + "blinking": true + } +} +``` + +示例: + +```jsonc +"cursorStyles": { + "insert": { + "style": "line", + "blinking": false + }, + "normal": { + "style": "block", + "blinking": false + } +} +``` + +```jsonc +"cursorStyles": { + "insert": { + "style": "underline" + }, + "normal": { + "style": "default" + } +} +``` + +可以只配置某个模式,未设置的部分使用默认值。 + +### `debug` + +启用调试日志。 + +默认: + +```jsonc +"debug": false +``` + +示例: + +```jsonc +"debug": true +``` + +也可以通过环境变量启用: + +```bash +VIM_PROMPT_DEBUG=1 +``` + +### `debugPath` + +调试日志的写入路径。 + +默认: + +```txt +~/.cache/opencode/opencode-vim.log +``` + +示例: + +```jsonc +"debugPath": "/tmp/opencode-vim.log" +``` + +须使用绝对路径,不支持 `~`。 + +### `keymaps` + +自定义 insert 模式和 normal 模式的按键映射。 + +可选模式: + +- `"insert"` +- `"normal"` + +每个映射项将一个按键序列映射到一个动作: + +```jsonc +"keymaps": { + "insert": { + "kj": "normal" + }, + "normal": { + "": "submit" + } +} +``` + +支持的内置动作: + +| 动作 | 可用模式 | 效果 | +| -------------- | -------------- | ---------------------------------- | +| `"submit"` | insert, normal | 提交 prompt | +| `"normal"` | 仅 insert | 退出 insert 模式,进入 normal 模式 | +| `"insert"` | 仅 normal | 进入 insert 模式 | +| `""` | 仅 normal | 执行对应的 Vim 按键序列 | + +例如,将 `Y` 映射为从光标处拉到行尾: + +```jsonc +"keymaps": { + "normal": { + "Y": "y$" + } +} +``` + +与其他按键不同,`` 在 normal 模式下未配置映射时默认提交 prompt。insert 模式下如果 `` 未配置映射,且 OpenCode 的 `input_submit` 不为 `return`,则插入换行而非提交。 + +> **注意:** 当 `input_submit` 不为 `"return"` 时,还需在 OpenCode 的 `keybinds` 中设置 `"input_newline": "return"`。否则 `` 在 insert 模式下既不提交也不换行,相当于无效按键。 + +## 按键映射语法 + +按键序列可以使用可打印 ASCII 字符(不能包含空格,空格用 `` 表示)。 + +示例: + +```jsonc +"x": "d" +"gg": "0" +"Y": "y$" +"\\r": "" +``` + +支持的特殊键: + +- `` +- `` +- `` +- `` +- `` +- `` +- `` 到 `` + +Ctrl 键名称必须用小写,用 `` 而不是 ``。 + +不支持的示例: + +```jsonc +"": "submit" +"": "submit" +"": "k" +"a b": "normal" +``` + +无效的映射会被跳过。如有需要,启用调试日志排查问题。 + +## 按键映射示例 + +使用 `kj` 或 `jk` 退出 insert 模式: + +```jsonc +"keymaps": { + "insert": { + "kj": "normal", + "jk": "normal" + } +} +``` + +normal 模式下使用 `` 提交是默认行为,该映射可省略: + +```jsonc +"keymaps": { + "normal": { + "": "submit" + } +} +``` + +在 insert 模式下用 Ctrl-S 提交: + +```jsonc +"keymaps": { + "insert": { + "": "submit" + } +} +``` + +`Y` 拉取到行尾: + +```jsonc +"keymaps": { + "normal": { + "Y": "y$" + } +} +``` + +`D` 删除到行首: + +```jsonc +"keymaps": { + "normal": { + "D": "d0" + } +} +``` + +`H` 移到行首,`L` 移到行尾: + +```jsonc +"keymaps": { + "normal": { + "H": "0", + "L": "$" + } +} +``` + +用 `q` 从 normal 模式进入 insert 模式: + +```jsonc +"keymaps": { + "normal": { + "q": "insert" + } +} +``` + +使用类似 leader 键的序列: + +```jsonc +"keymaps": { + "normal": { + "\\s": "submit", + "\\r": "" + } +} +``` + +## 故障排查 + +如果映射不生效,请检查: + +- 模式是 `insert` 或 `normal` +- 按键序列不包含空格 +- 特殊键名称准确无误 +- Ctrl 键使用小写字母,如 `` +- 动作字符串不为空 + +如需调试配置问题,启用日志: + +```jsonc +"debug": true, +"debugPath": "/tmp/opencode-vim.log" +``` diff --git a/src/modules/vim/index.tsx b/src/modules/vim/index.tsx index 7c83468..12a9c4e 100644 --- a/src/modules/vim/index.tsx +++ b/src/modules/vim/index.tsx @@ -128,7 +128,7 @@ function readablePending(sequence: string) { function passThroughKey(event: KeyEvent, key: string, mode: string) { if (mode !== "normal") return false - return event.super === true || isArrowKey(key) || key === "" || key === "" + return event.super === true || isArrowKey(key) || key === "" } function preparePassThroughKey(ctx: PromptContext, key: string, mode: string) { diff --git a/src/modules/vim/vimee.ts b/src/modules/vim/vimee.ts index 3771029..64095da 100644 --- a/src/modules/vim/vimee.ts +++ b/src/modules/vim/vimee.ts @@ -38,16 +38,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL let vimeeKey = keyForVimee(event, key) if (!vimeeKey) return false - if (state.mode() === "normal" && key === "") { - ref.submit() - return true - } - if (state.mode() === "insert") { - if (key === "") { - cancelPendingInsert(ctx) - return false - } if (vimeeKey === "Escape") { cancelPendingInsert(ctx, undefined, false) enterNormal(ctx) @@ -99,6 +90,16 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL applyActions(result.actions as HostAction[], ctx, map, clampFinalCursor) if (shouldFlashYank) flashYank(ctx, activeMap, yankAction(result.actions), visualYankRange) syncMode(state, vim.mode) + + if (key === "" && state.mode() === "normal" && result.actions.length === 0) { + const input = focusedInput(ctx) + if (input?.plainText && input.cursorOffset !== undefined) { + input.cursorOffset = Math.min(input.cursorOffset + 1, input.plainText.length) + } + ref.submit() + return true + } + const keybindPending = keybinds?.isPending() ?? false if (wasPending && !keybindPending && pendingBefore && state.mode() === "insert") flushPendingInsert(ctx, pendingBefore, offset) pendingInsert = keybindPending && state.mode() === "insert" ? plainPending(vim.statusMessage) : ""