opencode-vim 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bun.lock CHANGED
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@opencode-ai/plugin": "1.14.44",
16
+ "@types/bun": "^1.3.14",
16
17
  },
17
18
  },
18
19
  },
@@ -119,6 +120,10 @@
119
120
 
120
121
  "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
121
122
 
123
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
124
+
125
+ "@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="],
126
+
122
127
  "@vimee/core": ["@vimee/core@0.3.0", "", {}, "sha512-aC44fw6HhmjrbJMafAk3oUZIDpFbgNebbDsWwMIFD0WwnOUZPETgzU3HYQetmwz0e38JY3I/g8CJXRQjspgJZw=="],
123
128
 
124
129
  "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
@@ -139,6 +144,8 @@
139
144
 
140
145
  "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
141
146
 
147
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
148
+
142
149
  "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="],
143
150
 
144
151
  "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
@@ -273,6 +280,8 @@
273
280
 
274
281
  "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
275
282
 
283
+ "undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="],
284
+
276
285
  "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
277
286
 
278
287
  "uuid": ["uuid@13.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-9ezox2roIft6ExBVTVqibSd5dc5/47Sw/uY6b4SjQUT2TzQ0tltNquWA46y4xPQmdZYqvnio22SgWd41M86+jw=="],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-vim",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "exports": {
5
5
  "./tui": {
6
6
  "import": "./tui.tsx"
@@ -14,6 +14,7 @@
14
14
  "@vimee/core": "^0.3.0"
15
15
  },
16
16
  "devDependencies": {
17
- "@opencode-ai/plugin": "1.14.44"
17
+ "@opencode-ai/plugin": "1.14.44",
18
+ "@types/bun": "^1.3.14"
18
19
  }
19
20
  }
@@ -1,16 +1,24 @@
1
1
  import type { HashtagTriggerMatch, SnippetInfo } from "./types"
2
2
 
3
- const HASHTAG_TRIGGER = /(^|\s)#([^\s#]*)$/
4
-
5
3
  export function findTrailingHashtagTrigger(input: string): HashtagTriggerMatch | undefined {
6
- const match = HASHTAG_TRIGGER.exec(input)
7
- if (!match) return undefined
4
+ let start = -1
5
+ for (let i = input.length - 1; i >= 0; i--) {
6
+ const char = input[i]
7
+ if (char === "#") {
8
+ start = i
9
+ break
10
+ }
11
+ if (/\s/.test(char)) return undefined
12
+ }
13
+
14
+ if (start < 0) return undefined
15
+ if (start > 0 && !/\s/.test(input[start - 1])) return undefined
8
16
 
9
- const query = match[2] ?? ""
17
+ const query = input.slice(start + 1)
10
18
  const token = `#${query}`
11
19
 
12
20
  return {
13
- start: input.length - token.length,
21
+ start,
14
22
  end: input.length,
15
23
  query,
16
24
  token,
@@ -82,11 +82,12 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
82
82
  syncInterval = setInterval(() => {
83
83
  const ref = props.ctx.prompt()
84
84
  if (!ref) {
85
- setInput("")
85
+ if (input()) setInput("")
86
86
  setSyncingPrompt(false)
87
87
  return
88
88
  }
89
- setInput(ref.current.input ?? "")
89
+ const next = ref.current.input ?? ""
90
+ if (next !== input()) setInput(next)
90
91
  }, PROMPT_SYNC_MS)
91
92
 
92
93
  onCleanup(() => {
@@ -75,8 +75,6 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
75
75
 
76
76
  if (passThroughKey(event, key, props.state.mode())) {
77
77
  if (!preparedEvents.delete(event)) preparePassThroughKey(props.ctx, key, props.state.mode())
78
- syncCursorStyle(true)
79
- props.ctx.requestRender()
80
78
  return
81
79
  }
82
80
 
@@ -90,9 +88,9 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
90
88
  if (consumed) {
91
89
  event.preventDefault()
92
90
  event.stopPropagation()
91
+ syncCursorStyle(true)
92
+ props.ctx.requestRender()
93
93
  }
94
- syncCursorStyle(true)
95
- props.ctx.requestRender()
96
94
  })
97
95
 
98
96
  onCleanup(() => {
@@ -10,6 +10,8 @@ import type { createVimState } from "./state"
10
10
 
11
11
  type VimState = ReturnType<typeof createVimState>
12
12
  type HostAction = VimeeAction | { type: "submit" }
13
+ type HostKeybindAction = "normal" | "submit"
14
+ type HostKeybindDefinition = KeybindDefinition & { hostAction?: HostKeybindAction }
13
15
  type HostRange = { start: number; end: number }
14
16
 
15
17
  const YANK_FLASH_MS = 250
@@ -33,11 +35,6 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
33
35
  const ref = ctx.prompt()
34
36
  if (!ref) return false
35
37
 
36
- const input = focusedInput(ctx)
37
- const text = input?.plainText ?? ref.current.input
38
- const offset = clamp(input?.cursorOffset ?? text.length, 0, text.length)
39
- const map = mapForHostText(text, input)
40
- const cursor = hostPosition(map, offset)
41
38
  let vimeeKey = keyForVimee(event, key)
42
39
  if (!vimeeKey) return false
43
40
 
@@ -46,16 +43,29 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
46
43
  return true
47
44
  }
48
45
 
49
- if (state.mode() === "insert" && key === "<CR>") {
50
- cancelPendingInsert(ctx, offset)
51
- return false
52
- }
53
-
54
46
  if (state.mode() === "insert") {
55
- const consumed = handleInsertMode(event, vimeeKey, ctx, map, cursor, offset)
56
- return consumed ?? false
47
+ if (key === "<CR>") {
48
+ cancelPendingInsert(ctx)
49
+ return false
50
+ }
51
+ if (vimeeKey === "Escape") {
52
+ cancelPendingInsert(ctx, undefined, false)
53
+ enterNormal(ctx)
54
+ state.setPending("")
55
+ updateTimeout(ctx)
56
+ log("vimee.key", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
57
+ return true
58
+ }
59
+ return handleInsertKeybind(event, vimeeKey, ctx)
57
60
  }
58
61
 
62
+ const input = focusedInput(ctx)
63
+ const text = input?.plainText ?? ref.current.input
64
+ const offset = clamp(input?.cursorOffset ?? text.length, 0, text.length)
65
+
66
+ const map = mapForHostText(text, input)
67
+ const cursor = hostPosition(map, offset)
68
+
59
69
  const wasPending = keybinds?.isPending() ?? false
60
70
  const pendingBefore = pendingInsert
61
71
  sync(map, cursor)
@@ -172,20 +182,8 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
172
182
  syncVisualSelection(input, currentMap, ctx)
173
183
  }
174
184
 
175
- function handleInsertMode(event: KeyEvent, key: string, ctx: PromptContext, map: PromptMap, cursor: CursorPosition, offset: number) {
176
- if (key === "Escape") {
177
- cancelPendingInsert(ctx, offset, false)
178
- sync(map, cursor)
179
- const result = processKeystroke(key, vim, buffer, event.ctrl, false)
180
- vim = result.newCtx
181
- applyActions(result.actions as HostAction[], ctx, map)
182
- syncMode(state, vim.mode)
183
- state.setPending("")
184
- log("vimee.key", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: result.actions.map((action) => action.type) })
185
- return true
186
- }
187
-
188
- if (!keybinds?.hasKeybinds("insert") && !keybinds?.isPending()) return undefined
185
+ function handleInsertKeybind(event: KeyEvent, key: string, ctx: PromptContext) {
186
+ if (!keybinds?.hasKeybinds("insert") && !keybinds?.isPending()) return false
189
187
 
190
188
  const wasPending = keybinds.isPending()
191
189
  const pendingBefore = pendingInsert
@@ -198,25 +196,67 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
198
196
  updateTimeout(ctx)
199
197
  return true
200
198
  case "matched": {
201
- sync(map, cursor)
202
- const actions = applyKeybind(resolved.definition, map)
203
- applyActions(actions, ctx, map)
204
- syncMode(state, vim.mode)
199
+ if (applyInsertKeybind(resolved.definition, ctx)) {
200
+ pendingInsert = ""
201
+ state.setPending("")
202
+ updateTimeout(ctx)
203
+ log("vimee.keybind", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
204
+ return true
205
+ }
206
+
207
+ if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore)
205
208
  pendingInsert = ""
206
209
  state.setPending("")
207
210
  updateTimeout(ctx)
208
- log("vimee.keybind", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: actions.map((action) => action.type) })
209
- return true
211
+ log("vimee.keybind.unsupported", { key })
212
+ return false
210
213
  }
211
214
  case "none":
212
- if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore, offset)
215
+ if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore)
213
216
  pendingInsert = ""
214
217
  state.setPending("")
215
218
  updateTimeout(ctx)
216
- return wasPending ? false : undefined
219
+ return false
220
+ }
221
+ }
222
+
223
+ function applyInsertKeybind(definition: KeybindDefinition, ctx: PromptContext) {
224
+ switch (insertHostAction(definition)) {
225
+ case "normal":
226
+ enterNormal(ctx)
227
+ return true
228
+ case "submit":
229
+ ctx.prompt()?.submit()
230
+ return true
231
+ default:
232
+ return false
217
233
  }
218
234
  }
219
235
 
236
+ function enterNormal(ctx: PromptContext) {
237
+ const ref = ctx.prompt()
238
+ const input = focusedInput(ctx)
239
+ const text = input?.plainText ?? ref?.current.input ?? ""
240
+ const offset = clamp(input?.cursorOffset ?? text.length, 0, text.length)
241
+
242
+ if (input && text.length > 0) {
243
+ input.cursorOffset = Math.max(0, offset - 1)
244
+ clampNormalCursor(input)
245
+ }
246
+
247
+ vim = { ...resetContext(vim), mode: "normal", statusMessage: "" }
248
+ nativeInsertUndoSaved = false
249
+ syncMode(state, "normal")
250
+ }
251
+
252
+ function insertHostAction(definition: KeybindDefinition): HostKeybindAction | undefined {
253
+ const action = (definition as HostKeybindDefinition).hostAction
254
+ if (action) return action
255
+ if ("execute" in definition) return undefined
256
+ if (definition.keys === "<Esc>" || definition.keys === "<C-[>" || definition.keys === "Escape") return "normal"
257
+ return undefined
258
+ }
259
+
220
260
  function applyKeybind(definition: KeybindDefinition, map: PromptMap) {
221
261
  if ("execute" in definition) {
222
262
  const actions = definition.execute(vim, buffer) as HostAction[]
@@ -299,12 +339,8 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
299
339
  const currentOffset = input?.cursorOffset ?? insertAt
300
340
  const next = text.slice(0, insertAt) + value + text.slice(insertAt)
301
341
  setInput(ref, next)
302
- activeMap = createPromptMap(next, input)
303
- rememberMap(activeMap)
304
- buffer.replaceContent(activeMap.vimText)
305
342
  const nextOffset = currentOffset >= insertAt ? currentOffset + value.length : currentOffset
306
343
  if (input) input.cursorOffset = nextOffset
307
- vim = { ...vim, cursor: hostPosition(activeMap, nextOffset) }
308
344
  }
309
345
 
310
346
  function cursorOffset(map: PromptMap, position: CursorPosition) {
@@ -553,14 +589,14 @@ function createKeybinds(config: VimConfig, log: VimLog): KeybindMap | undefined
553
589
  return count > 0 ? map : undefined
554
590
  }
555
591
 
556
- function keybindAction(action: string): KeybindDefinition {
592
+ function keybindAction(action: string): HostKeybindDefinition {
557
593
  switch (action) {
558
594
  case "normal":
559
- return { keys: "<Esc>" }
595
+ return { keys: "<Esc>", hostAction: "normal" }
560
596
  case "insert":
561
597
  return { keys: "i" }
562
598
  case "submit":
563
- return { execute: () => [{ type: "submit" } as unknown as VimeeAction] }
599
+ return { execute: () => [{ type: "submit" } as unknown as VimeeAction], hostAction: "submit" }
564
600
  default:
565
601
  return { keys: action }
566
602
  }
@@ -2,24 +2,37 @@
2
2
  import { onCleanup } from "solid-js"
3
3
  import type { JSX } from "@opentui/solid"
4
4
  import { HostPrompt } from "./host"
5
- import { sortModules } from "./modules"
6
- import { createPromptRef } from "./ref"
7
- import type { PromptRootProps } from "./types"
5
+ import { notifyPromptRef, sortModules } from "./modules"
6
+ import type { PromptContext, PromptRootProps } from "./types"
8
7
 
9
8
  export function PromptRoot(props: PromptRootProps) {
10
9
  const modules = sortModules(props.modules)
11
10
  const api = props.api
12
- const ctx = createPromptRef(modules, {
11
+ let current: ReturnType<PromptContext["prompt"]>
12
+ const ctx: PromptContext = {
13
13
  api,
14
14
  slot: props.slot,
15
15
  kind: props.kind,
16
- sessionID: props.sessionID,
17
- workspaceID: props.workspaceID,
18
- visible: props.visible,
19
- disabled: props.disabled,
16
+ get sessionID() {
17
+ return props.sessionID
18
+ },
19
+ get workspaceID() {
20
+ return props.workspaceID
21
+ },
22
+ get visible() {
23
+ return props.visible
24
+ },
25
+ get disabled() {
26
+ return props.disabled
27
+ },
28
+ prompt: () => current,
29
+ setPromptRef(ref) {
30
+ current = ref
31
+ notifyPromptRef(modules, ref, ctx)
32
+ },
20
33
  submitHost: () => props.onSubmit?.(),
21
34
  requestRender: () => api.renderer.requestRender(),
22
- })
35
+ }
23
36
 
24
37
  const cleanups = modules.flatMap((module) => {
25
38
  const cleanup = module.setup?.(ctx)
package/src/prompt/ref.ts DELETED
@@ -1,17 +0,0 @@
1
- import type { TuiPromptRef } from "@opencode-ai/plugin/tui"
2
- import type { PromptContext, PromptModule } from "./types"
3
- import { notifyPromptRef } from "./modules"
4
-
5
- export function createPromptRef(modules: PromptModule[], ctx: Omit<PromptContext, "prompt" | "setPromptRef">) {
6
- let current: TuiPromptRef | undefined
7
- const context = {
8
- ...ctx,
9
- prompt: () => current,
10
- setPromptRef(ref: TuiPromptRef | undefined) {
11
- current = ref
12
- notifyPromptRef(modules, ref, context)
13
- },
14
- } satisfies PromptContext
15
-
16
- return context
17
- }