opencode-vim 0.0.11 → 0.0.12

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
@@ -5,13 +5,14 @@
5
5
  "": {
6
6
  "name": "opencode-vim",
7
7
  "dependencies": {
8
- "@opentui/core": "^0.2.1",
9
- "@opentui/solid": "^0.2.1",
8
+ "@opentui/core": "^0.2.6",
9
+ "@opentui/keymap": "^0.2.6",
10
+ "@opentui/solid": "^0.2.6",
10
11
  "@vimee/core": "^0.3.0",
11
12
  "solid-js": "^1.9.12",
12
13
  },
13
14
  "devDependencies": {
14
- "@opencode-ai/plugin": "1.14.30",
15
+ "@opencode-ai/plugin": "1.14.44",
15
16
  },
16
17
  },
17
18
  },
@@ -94,25 +95,27 @@
94
95
 
95
96
  "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
96
97
 
97
- "@opencode-ai/plugin": ["@opencode-ai/plugin@1.14.30", "", { "dependencies": { "@opencode-ai/sdk": "1.14.30", "effect": "4.0.0-beta.57", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.1.105", "@opentui/solid": ">=0.1.105" }, "optionalPeers": ["@opentui/core", "@opentui/solid"] }, "sha512-O1y6qR349R5XJtB76Vx4TO/uvLkqNvdsgxtj2ZpWoygb3rtrkuVMiBsrcL2WfuyyaLJnPmbnkeigcdm42r09Hg=="],
98
+ "@opencode-ai/plugin": ["@opencode-ai/plugin@1.14.44", "", { "dependencies": { "@opencode-ai/sdk": "1.14.44", "effect": "4.0.0-beta.59", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.2.6", "@opentui/keymap": ">=0.2.6", "@opentui/solid": ">=0.2.6" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-7uqSJWaDuP9GNvdUJ57E8AX0WNVP4C8PyCIJ2qvjtucjw6DFT/ErjXDXUrRiqwK3omImXRlTDb32xP9IMfk0dw=="],
98
99
 
99
- "@opencode-ai/sdk": ["@opencode-ai/sdk@1.14.30", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-OgPEDvALekHZIjByo/okJ699aLPn+XtsVxgZxUqE8TlzAG7TtskMGFl0fro8O0T2p+nkOT/LstnKGbECvc0+YA=="],
100
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.14.44", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-hpMCYhwEWk2B1b9THHYy5GRlrCJ9g67pvSyLt9O2b2pYj0HE6v8MeVOhg6ucn2T4QBIoVJ3ZDWnh7ApaeunNWg=="],
100
101
 
101
- "@opentui/core": ["@opentui/core@0.2.1", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.1", "@opentui/core-darwin-x64": "0.2.1", "@opentui/core-linux-arm64": "0.2.1", "@opentui/core-linux-x64": "0.2.1", "@opentui/core-win32-arm64": "0.2.1", "@opentui/core-win32-x64": "0.2.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-tXFD2NeznXzZa02yOsQ/D0o/06GBqVrPR7VdmgqI+ZChcS7NY/QgNQGgAZUditW2vezFcxlgTqoCgQaZFqRfGQ=="],
102
+ "@opentui/core": ["@opentui/core@0.2.6", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.6", "@opentui/core-darwin-x64": "0.2.6", "@opentui/core-linux-arm64": "0.2.6", "@opentui/core-linux-x64": "0.2.6", "@opentui/core-win32-arm64": "0.2.6", "@opentui/core-win32-x64": "0.2.6" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-dBpMaWVM7wtW2/2TlGPrkPjg6gOL3MVU/5XXk+U1LDJB8L4q4NeYWVdzfAVNcEvgmuuCy/cVqdY2D4ei+e7MMg=="],
102
103
 
103
- "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vT8rgeNdkzD84wCcS5zmrSVFgR/SjeVr+2FNM/icTjwKh9Jq3cY6UY7/EFck3PRjrmanNu4zw7W5lFKSpMxnPQ=="],
104
+ "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hR5nsxNj+059utzenTCF0kealUlibON6fLuebFUCGM/5kJnqa+shIh0XbUDFm0+F47vqVUgZufBdUuieQZIbvQ=="],
104
105
 
105
- "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HAH1KeTejs0Z+RChNsNImI1eOlYJabKeAEvLtnn1aiHxvJFpzdAi6d/VrG5WFcxAem6TVRtnKN46gXff26lkig=="],
106
+ "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-pJ/bH4WC/mbBaakM1YdH6TVo67jhy0KPd61bCz97w0I/PJGr8fmNKvhmMt/AwyFgOQi3FYZiEKLMpGdvUcSsrQ=="],
106
107
 
107
- "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-447FI6xolsKbG1eH3hpNGQhQ/8M6FDJIgpiJhX9XXvZGO7vEyN6efyqqIzRF+quafqF69+rT3qK70S1svjwVKA=="],
108
+ "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Pnd3kOxig8ii+/IqYheOPEgferylsQA0L6tKBnHQ9jRlCJOcu0Rv65Jepueh212vevdV9DzPURJnhejG06J6g=="],
108
109
 
109
- "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Xdds+HnAbFiylTdxyYhRxPFPnrHm75KTi7QCA1JHU3lb8Sfwrxv3nlYtFhEV798y4osafsghL20Nq5mqVPjizQ=="],
110
+ "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-458Mx9tBzEPzfft8cSt5ZaIpEepoxBXBOL6AUVmDTKWaZ3uouraPcEKraGAyvOTDQp2XDI3R8c/2GdaR77FaUQ=="],
110
111
 
111
- "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-VEuqX9OSFpw960mibe4OXVmnE43V/VYwB61zoxD5uw7L3jgCiOfrf+W2TuFbcRAgtTD35MypJ53fYxmUcVZc8g=="],
112
+ "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-BDUrdrT1RCcVnQoHJmUut4y811jDBAEtc6GJFB4Gs265Be8SrTjVCus6p2fSQ7j9sZQ1OcjO+5+4NkheSZICDQ=="],
112
113
 
113
- "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ywwRbyjPPEbNe3da+ez4NxIaJWQ79sA2gdepuHKUVdmdxsevdRpAxOHkEpJNtiVvphPc/On9EsP25Hmn5gjYxg=="],
114
+ "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-SUYAzRJ9TSoD2Qt8kn6FJz6dbTrFEPVig5mScB4zFGgGQO/Bbod2/Q31vLS/IQrX+FDb67WaErD+kuMCnMPPLA=="],
114
115
 
115
- "@opentui/solid": ["@opentui/solid@0.2.1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.1", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-/vlW9SmcqkarbMwB4JaIwwS3ovo42DSwAnyM04pjJVhG7DzKKwdUzozQMRgvEDZZ7oGYQRvHyVAfL8GCLh0AZw=="],
116
+ "@opentui/keymap": ["@opentui/keymap@0.2.6", "", { "dependencies": { "@opentui/core": "0.2.6" }, "peerDependencies": { "@opentui/react": "0.2.6", "@opentui/solid": "0.2.6", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-+6OYuedrFCKVo4ryGFNwws++2VOmPcXU3PwpY0mP47gYQY2nvQ+etWIs2Y7r5eMIqUfxVCldkKsrzcEcA4tb/A=="],
117
+
118
+ "@opentui/solid": ["@opentui/solid@0.2.6", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.6", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-2y225WlOGi/fCaajkxBmLyVW8Cr+OmhowHdvrYcz5w2kBD15sKbJLIYu1G9DxceirT1uIyasGy2TGzRRcVkTDg=="],
116
119
 
117
120
  "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
118
121
 
@@ -134,7 +137,7 @@
134
137
 
135
138
  "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
136
139
 
137
- "bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
140
+ "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
138
141
 
139
142
  "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="],
140
143
 
@@ -150,7 +153,7 @@
150
153
 
151
154
  "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
152
155
 
153
- "effect": ["effect@4.0.0-beta.57", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g=="],
156
+ "effect": ["effect@4.0.0-beta.59", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA=="],
154
157
 
155
158
  "electron-to-chromium": ["electron-to-chromium@1.5.345", "", {}, "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg=="],
156
159
 
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "opencode-vim",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "exports": {
5
5
  "./tui": {
6
6
  "import": "./tui.tsx"
7
7
  }
8
8
  },
9
9
  "dependencies": {
10
- "@opentui/core": "^0.2.1",
11
- "@opentui/solid": "^0.2.1",
10
+ "@opentui/core": "^0.2.6",
11
+ "@opentui/keymap": "^0.2.6",
12
+ "@opentui/solid": "^0.2.6",
12
13
  "solid-js": "^1.9.12",
13
14
  "@vimee/core": "^0.3.0"
14
15
  },
15
16
  "devDependencies": {
16
- "@opencode-ai/plugin": "1.14.30"
17
+ "@opencode-ai/plugin": "1.14.44"
17
18
  }
18
19
  }
@@ -3,9 +3,7 @@ import type { PromptModule } from "../../prompt/types"
3
3
  import type { SnippetController } from "./types"
4
4
  import { SnippetAutocomplete } from "./view"
5
5
 
6
- export function createSnippetsModule(): PromptModule {
7
- const controller: SnippetController = {}
8
-
6
+ export function createSnippetsModule(controller: SnippetController): PromptModule {
9
7
  return {
10
8
  id: "snippets",
11
9
  order: -10,
@@ -46,4 +46,5 @@ export type SnippetController = {
46
46
  accept?: () => boolean
47
47
  reload?: () => void
48
48
  insertTrigger?: () => void
49
+ navigate?: (delta: number) => boolean
49
50
  }
@@ -100,6 +100,7 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
100
100
  props.controller.accept = undefined
101
101
  props.controller.reload = undefined
102
102
  props.controller.insertTrigger = undefined
103
+ props.controller.navigate = undefined
103
104
  })
104
105
 
105
106
  const dialogBlockingInput = () => dialogOpen() || dialogHandoffUntil() > Date.now()
@@ -156,6 +157,27 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
156
157
  return item ? chooseItem(item) : false
157
158
  }
158
159
 
160
+ const navigate = (delta: number) => {
161
+ const ref = props.ctx.prompt()
162
+ if (!ref || !ref.focused || dialogBlockingInput()) return false
163
+
164
+ const current = findTrailingHashtagTrigger(ref.current.input)
165
+ if (!current || dismissed() === current.token) return false
166
+
167
+ const value = current.query.trim()
168
+ const total = optionsForQuery(value).length
169
+ if (total === 0 && !canCreateForQuery(current.query)) return false
170
+
171
+ if (pendingPromptSync) clearTimeout(pendingPromptSync)
172
+ pendingPromptSync = undefined
173
+ setInput(ref.current.input)
174
+ setSyncingPrompt(false)
175
+ lockKeyboardSelection()
176
+ setSelected((current) => stepSelection(current, total || 1, delta))
177
+ props.ctx.requestRender()
178
+ return true
179
+ }
180
+
159
181
  const accept = () => {
160
182
  const ref = props.ctx.prompt()
161
183
  if (!ref) return false
@@ -205,6 +227,14 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
205
227
  return false
206
228
  }
207
229
 
230
+ const canAcceptSubmit = (ref: TuiPromptRef) => {
231
+ if (isReloadCommand(ref.current.input)) return true
232
+ if (dialogBlockingInput()) return true
233
+
234
+ const current = findTrailingHashtagTrigger(ref.current.input)
235
+ return !!current && dismissed() !== current.token
236
+ }
237
+
208
238
  props.controller.accept = accept
209
239
  props.controller.reload = () => {
210
240
  const ref = props.ctx.prompt()
@@ -216,6 +246,7 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
216
246
  syncPromptInput(ref, insertSnippetTrigger(ref.current.input))
217
247
  ref.focus()
218
248
  }
249
+ props.controller.navigate = navigate
219
250
 
220
251
  createEffect(() => {
221
252
  const ref = props.ctx.prompt()
@@ -229,40 +260,45 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
229
260
  commandTimer = undefined
230
261
  if (disposed) return
231
262
 
232
- commandDispose = props.ctx.api.command.register(() => [
233
- {
234
- title: "Reload snippets",
235
- value: "snippets.reload",
236
- description: "Reload snippet files from disk",
237
- category: "Prompt",
238
- slash: { name: "snippets:reload" },
239
- onSelect() {
240
- void executeReloadInPrompt(ref)
263
+ commandDispose = props.ctx.api.keymap.registerLayer({
264
+ commands: [
265
+ {
266
+ namespace: "palette",
267
+ name: "snippets.reload",
268
+ title: "Reload snippets",
269
+ desc: "Reload snippet files from disk",
270
+ category: "Prompt",
271
+ slashName: "snippets:reload",
272
+ run() {
273
+ void executeReloadInPrompt(ref)
274
+ },
241
275
  },
242
- },
243
- {
244
- title: "Insert snippet",
245
- value: "snippets.insert",
246
- description: "Insert a snippet trigger into the prompt",
247
- category: "Prompt",
248
- onSelect() {
249
- syncPromptInput(ref, insertSnippetTrigger(ref.current.input))
250
- ref.focus()
276
+ {
277
+ namespace: "palette",
278
+ name: "snippets.insert",
279
+ title: "Insert snippet",
280
+ desc: "Insert a snippet trigger into the prompt",
281
+ category: "Prompt",
282
+ run() {
283
+ syncPromptInput(ref, insertSnippetTrigger(ref.current.input))
284
+ ref.focus()
285
+ },
251
286
  },
252
- },
253
- {
254
- title: "Accept snippet autocomplete",
255
- value: "snippets.accept",
256
- keybind: "input_submit",
257
- category: "Prompt",
258
- hidden: true,
259
- enabled: ref.focused,
260
- onSelect() {
261
- if (accept()) return
262
- ref.submit()
287
+ {
288
+ namespace: "palette",
289
+ name: "snippets.accept",
290
+ title: "Accept snippet autocomplete",
291
+ category: "Prompt",
292
+ hidden: true,
293
+ enabled: () => ref.focused && canAcceptSubmit(ref),
294
+ run() {
295
+ if (accept()) return
296
+ ref.submit()
297
+ },
263
298
  },
264
- },
265
- ])
299
+ ],
300
+ bindings: acceptSnippetBindings(props.ctx.api),
301
+ })
266
302
  }, 0)
267
303
  })
268
304
 
@@ -280,12 +316,7 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
280
316
  if (dialogBlockingInput()) return
281
317
  if (!visible()) return
282
318
 
283
- const total = options().length
284
- const actionable = total > 0 || canCreate()
285
-
286
- if ((name === "up" || name === "down") && actionable) {
287
- lockKeyboardSelection()
288
- setSelected((current) => stepSelection(current, total || 1, name === "up" ? -1 : 1))
319
+ if ((name === "up" || name === "down") && navigate(name === "up" ? -1 : 1)) {
289
320
  event.preventDefault()
290
321
  event.stopPropagation()
291
322
  return
@@ -298,8 +329,8 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
298
329
  return
299
330
  }
300
331
 
301
- if (name === "tab" && actionable) {
302
- if (total > 0) choose()
332
+ if (name === "tab" && (options().length > 0 || canCreate())) {
333
+ if (options().length > 0) choose()
303
334
  else void createSnippetDraft()
304
335
  event.preventDefault()
305
336
  event.stopPropagation()
@@ -559,6 +590,21 @@ function syncPromptInput(prompt: TuiPromptRef, input: string) {
559
590
  prompt.set({ input, mode: prompt.current.mode, parts: [...prompt.current.parts] })
560
591
  }
561
592
 
593
+ function acceptSnippetBindings(api: TuiPluginApi) {
594
+ const command = "snippets.accept"
595
+ const title = "Accept snippet autocomplete"
596
+
597
+ if (!api.tuiConfig.keybinds.has("input.submit")) {
598
+ return [{ key: "input_submit", cmd: command, desc: title }]
599
+ }
600
+
601
+ return api.tuiConfig.keybinds.get("input.submit").map((binding) => ({
602
+ ...binding,
603
+ cmd: command,
604
+ desc: binding.desc ?? title,
605
+ }))
606
+ }
607
+
562
608
  function selectedText(theme: PromptContext["api"]["theme"]["current"]) {
563
609
  if (theme.background.a !== 0) return theme.background
564
610
  const { r, g, b } = theme.primary
@@ -4,6 +4,7 @@ import { useKeyboard } from "@opentui/solid"
4
4
  import { onCleanup } from "solid-js"
5
5
  import type { Accessor } from "solid-js"
6
6
  import type { PromptContext, PromptModule } from "../../prompt/types"
7
+ import type { SnippetController } from "../snippets/types"
7
8
  import { applyVimCursorStyle, focusedInput } from "./actions"
8
9
  import type { VimConfig } from "./config"
9
10
  import { createVimConfig } from "./config"
@@ -14,7 +15,7 @@ import { createVimState } from "./state"
14
15
  import { createVimeeAdapter } from "./vimee"
15
16
  import { VimStatus } from "./view"
16
17
 
17
- export function createVimModule(options?: unknown, enabled: Accessor<boolean> = () => true): PromptModule {
18
+ export function createVimModule(options?: unknown, enabled: Accessor<boolean> = () => true, snippets?: SnippetController): PromptModule {
18
19
  const config = createVimConfig(options)
19
20
  const log = createVimLog(config)
20
21
  const state = createVimState(config.defaultMode, log)
@@ -27,7 +28,7 @@ export function createVimModule(options?: unknown, enabled: Accessor<boolean> =
27
28
  log("module.setup", { kind: ctx.kind, sessionID: ctx.sessionID, workspaceID: ctx.workspaceID })
28
29
  },
29
30
  renderAbove(ctx) {
30
- return <VimKeyboard ctx={ctx} config={config} state={state} enabled={enabled} log={log} />
31
+ return <VimKeyboard ctx={ctx} config={config} state={state} snippets={snippets} enabled={enabled} log={log} />
31
32
  },
32
33
  renderRight(ctx) {
33
34
  return <VimStatus mode={state.mode} pending={() => readablePending(state.pending())} enabled={enabled} theme={ctx.api.theme.current} pendingDisplayDelay={config.pendingDisplayDelay} disabled={ctx.disabled} log={log} requestRender={ctx.requestRender} />
@@ -35,12 +36,19 @@ export function createVimModule(options?: unknown, enabled: Accessor<boolean> =
35
36
  }
36
37
  }
37
38
 
38
- function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: ReturnType<typeof createVimState>; enabled: Accessor<boolean>; log: VimLog }) {
39
+ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: ReturnType<typeof createVimState>; snippets?: SnippetController; enabled: Accessor<boolean>; log: VimLog }) {
39
40
  let cursorStyleMode = ""
40
41
  const vimee = createVimeeAdapter(props.state, props.config, props.log)
42
+ const preparedEvents = new WeakSet<KeyEvent>()
41
43
  props.log("keyboard.mount", { kind: props.ctx.kind })
42
44
 
43
45
  const cursorStyleTimer = setInterval(syncCursorStyle, 50)
46
+ const offKeyIntercept = props.ctx.api.keymap.intercept("key", ({ event }) => {
47
+ if (!props.enabled() || props.ctx.disabled || props.ctx.visible === false) return
48
+ const key = keyNotation(event)
49
+ if (!key || !passThroughKey(event, key, props.state.mode())) return
50
+ if (preparePassThroughKey(props.ctx, key, props.state.mode())) preparedEvents.add(event)
51
+ }, { priority: 100 })
44
52
 
45
53
  useKeyboard((event) => {
46
54
  props.log("keyboard.event", {
@@ -66,13 +74,13 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
66
74
  }
67
75
 
68
76
  if (passThroughKey(event, key, props.state.mode())) {
69
- preparePassThroughKey(props.ctx, key, props.state.mode())
77
+ if (!preparedEvents.delete(event)) preparePassThroughKey(props.ctx, key, props.state.mode())
70
78
  syncCursorStyle(true)
71
79
  props.ctx.requestRender()
72
80
  return
73
81
  }
74
82
 
75
- if (sendNavigationKey(event, props.ctx, key, props.state.mode())) {
83
+ if (sendNavigationKey(event, props.ctx, key, props.state.mode(), props.snippets)) {
76
84
  syncCursorStyle(true)
77
85
  props.ctx.requestRender()
78
86
  return
@@ -89,6 +97,7 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
89
97
 
90
98
  onCleanup(() => {
91
99
  props.log("keyboard.cleanup", { kind: props.ctx.kind })
100
+ offKeyIntercept()
92
101
  vimee.cleanup()
93
102
  clearInterval(cursorStyleTimer)
94
103
  })
@@ -116,14 +125,22 @@ function passThroughKey(event: KeyEvent, key: string, mode: string) {
116
125
  }
117
126
 
118
127
  function preparePassThroughKey(ctx: PromptContext, key: string, mode: string) {
119
- if (mode !== "normal" || key !== "<CR>") return
128
+ if (mode !== "normal" || key !== "<CR>") return false
120
129
  const input = focusedInput(ctx)
121
- if (!input?.plainText || input.cursorOffset === undefined) return
130
+ if (!input?.plainText || input.cursorOffset === undefined) return false
122
131
  input.cursorOffset = Math.min(input.cursorOffset + 1, input.plainText.length)
132
+ return true
123
133
  }
124
134
 
125
- function sendNavigationKey(event: KeyEvent, ctx: PromptContext, key: string, mode: string) {
135
+ function sendNavigationKey(event: KeyEvent, ctx: PromptContext, key: string, mode: string, snippets?: SnippetController) {
126
136
  if (mode !== "normal") return false
137
+ const delta = snippetNavigationDelta(key)
138
+ if (delta !== 0 && snippets?.navigate?.(delta)) {
139
+ event.preventDefault()
140
+ event.stopPropagation()
141
+ return true
142
+ }
143
+
127
144
  const forwarded = commandNavigationKey(ctx, key)
128
145
  if (!forwarded) return false
129
146
 
@@ -133,6 +150,12 @@ function sendNavigationKey(event: KeyEvent, ctx: PromptContext, key: string, mod
133
150
  return true
134
151
  }
135
152
 
153
+ function snippetNavigationDelta(key: string) {
154
+ if (key === "j") return 1
155
+ if (key === "k") return -1
156
+ return 0
157
+ }
158
+
136
159
  function commandNavigationKey(ctx: PromptContext, key: string): ParsedKey | undefined {
137
160
  if (!isPromptFocused(ctx)) return navigationKey(key)
138
161
  if (!isSlashCommandToken(ctx)) return undefined
package/src/plugin.tsx CHANGED
@@ -5,25 +5,31 @@ import { PromptRoot } from "./prompt/root"
5
5
  import type { PromptModule } from "./prompt/types"
6
6
  import { createVimModule } from "./modules/vim"
7
7
  import { createSnippetsModule } from "./modules/snippets"
8
+ import type { SnippetController } from "./modules/snippets/types"
8
9
  import { checkAutoUpdate } from "./update"
9
10
 
10
11
  const tui: TuiPlugin = async (api, options, meta) => {
11
12
  const vimEnabledKey = `${meta.id}:enabled`
12
13
  const [vimEnabled, setVimEnabled] = createSignal(api.kv.get(vimEnabledKey, true) === true)
13
14
 
14
- api.command.register(() => [{
15
- title: vimEnabled() ? "Disable Vim Mode" : "Enable Vim Mode",
16
- value: "opencode-vim.toggle",
17
- description: vimEnabled() ? "Turn off Vim key handling" : "Turn on Vim key handling",
18
- category: "Vim",
19
- slash: { name: "vim" },
20
- onSelect() {
21
- const next = !vimEnabled()
22
- setVimEnabled(next)
23
- api.kv.set(vimEnabledKey, next)
24
- api.ui.toast({ variant: "info", message: `Vim mode ${next ? "enabled" : "disabled"}` })
25
- },
26
- }])
15
+ api.keymap.registerLayer({
16
+ commands: [
17
+ {
18
+ namespace: "palette",
19
+ name: "opencode-vim.toggle",
20
+ title: "Toggle Vim Mode",
21
+ desc: "Enable or disable Vim key handling",
22
+ category: "Vim",
23
+ slashName: "vim",
24
+ run() {
25
+ const next = !vimEnabled()
26
+ setVimEnabled(next)
27
+ api.kv.set(vimEnabledKey, next)
28
+ api.ui.toast({ variant: "info", message: `Vim mode ${next ? "enabled" : "disabled"}` })
29
+ },
30
+ },
31
+ ],
32
+ })
27
33
 
28
34
  if (readAutoUpdate(options)) {
29
35
  let timer: Timer | undefined
@@ -46,8 +52,9 @@ const tui: TuiPlugin = async (api, options, meta) => {
46
52
  if (cached) return cached
47
53
 
48
54
  const modules: PromptModule[] = []
49
- if (hasSnippetsPlugin(api)) modules.push(createSnippetsModule())
50
- modules.push(createVimModule(options, vimEnabled))
55
+ const snippets: SnippetController = {}
56
+ if (hasSnippetsPlugin(api)) modules.push(createSnippetsModule(snippets))
57
+ modules.push(createVimModule(options, vimEnabled, snippets))
51
58
  moduleCache.set(key, modules)
52
59
  return modules
53
60
  }