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 +18 -15
- package/package.json +5 -4
- package/src/modules/snippets/index.tsx +1 -3
- package/src/modules/snippets/types.ts +1 -0
- package/src/modules/snippets/view.tsx +85 -39
- package/src/modules/vim/index.tsx +31 -8
- package/src/plugin.tsx +22 -15
package/bun.lock
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "opencode-vim",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@opentui/core": "^0.2.
|
|
9
|
-
"@opentui/
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
11
|
-
"@opentui/
|
|
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.
|
|
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,
|
|
@@ -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.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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" &&
|
|
302
|
-
if (
|
|
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.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
50
|
-
modules.push(
|
|
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
|
}
|