From 5583c142bb541c541d43b244185334a0280a78e2 Mon Sep 17 00:00:00 2001 From: WayneKent Date: Sun, 28 Jun 2026 14:42:01 +0800 Subject: [PATCH] feat: support command: prefix in keymap actions Allow users to bind arbitrary opencode commands via the `command:` prefix in keymap values, e.g. `"": "command:prompt.history.previous"`. Supported in both normal and insert mode. --- src/modules/vim/vimee.ts | 56 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/modules/vim/vimee.ts b/src/modules/vim/vimee.ts index 3771029..9d8ce07 100644 --- a/src/modules/vim/vimee.ts +++ b/src/modules/vim/vimee.ts @@ -9,9 +9,9 @@ import { createPromptMap, derivePromptMap, hostOffset, hostPosition, type Prompt import type { createVimState } from "./state" type VimState = ReturnType -type HostAction = VimeeAction | { type: "submit" } -type HostKeybindAction = "normal" | "submit" -type HostKeybindDefinition = KeybindDefinition & { hostAction?: HostKeybindAction } +type HostAction = VimeeAction | { type: "submit" } | { type: "command"; command: string } +type HostKeybindAction = "normal" | "submit" | "command" +type HostKeybindDefinition = KeybindDefinition & { hostAction?: HostKeybindAction; command?: string } type HostRange = { start: number; end: number } const YANK_FLASH_MS = 250 @@ -164,6 +164,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL break case "cursor-move": setCursor(input, currentMap, action.position) + syncVisualCursor(input) break case "mode-change": nativeInsertUndoSaved = false @@ -175,6 +176,18 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL case "submit": ref.submit() break + case "command": + if (input) { + const textLen = input.plainText?.length ?? 0 + if (action.command === "prompt.history.previous") { + input.cursorOffset = 0 + } else if (action.command === "prompt.history.next") { + input.cursorOffset = textLen + } + syncVisualCursor(input) + } + ctx.api.keymap.dispatchCommand(action.command) + break } } @@ -228,6 +241,22 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL case "submit": ctx.prompt()?.submit() return true + case "command": + { + const input = focusedInput(ctx) + if (input) { + const textLen = input.plainText?.length ?? 0 + const cmd = (definition as HostKeybindDefinition).command! + if (cmd === "prompt.history.previous") { + input.cursorOffset = 0 + } else if (cmd === "prompt.history.next") { + input.cursorOffset = textLen + } + syncVisualCursor(input) + } + ctx.api.keymap.dispatchCommand((definition as HostKeybindDefinition).command!) + } + return true default: return false } @@ -549,6 +578,19 @@ function clampNormalCursor(input: EditBufferLike) { if (cursor.visualRow === eol.visualRow && (cursor.offset === eol.offset || offset === eol.offset)) input.cursorOffset = Math.max(0, offset - 1) } +function syncVisualCursor(input: EditBufferLike | undefined) { + if (!input?.visualCursor || input.plainText === undefined) return + const text = input.plainText + const offset = input.cursorOffset ?? 0 + const before = text.slice(0, offset) + const row = before.split('\n').length - 1 + const lastNewline = before.lastIndexOf('\n') + const col = offset - lastNewline - 1 + input.visualCursor.visualRow = row + input.visualCursor.visualCol = col + input.visualCursor.offset = offset +} + function endMotionOffset(text: string, offset: number, count: number) { let index = offset for (let step = 0; step < count; step++) { @@ -590,6 +632,14 @@ function createKeybinds(config: VimConfig, log: VimLog): KeybindMap | undefined } function keybindAction(action: string): HostKeybindDefinition { + if (action.startsWith("command:")) { + const command = action.slice(8) + return { + execute: () => [{ type: "command", command } as unknown as VimeeAction], + hostAction: "command", + command, + } + } switch (action) { case "normal": return { keys: "", hostAction: "normal" }