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 +9 -0
- package/package.json +3 -2
- package/src/modules/snippets/trigger.ts +14 -6
- package/src/modules/snippets/view.tsx +3 -2
- package/src/modules/vim/index.tsx +2 -4
- package/src/modules/vim/vimee.ts +77 -41
- package/src/prompt/root.tsx +22 -9
- package/src/prompt/ref.ts +0 -17
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.
|
|
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
|
-
|
|
7
|
-
|
|
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 =
|
|
17
|
+
const query = input.slice(start + 1)
|
|
10
18
|
const token = `#${query}`
|
|
11
19
|
|
|
12
20
|
return {
|
|
13
|
-
start
|
|
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
|
-
|
|
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(() => {
|
package/src/modules/vim/vimee.ts
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
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
|
|
176
|
-
if (
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
209
|
-
return
|
|
211
|
+
log("vimee.keybind.unsupported", { key })
|
|
212
|
+
return false
|
|
210
213
|
}
|
|
211
214
|
case "none":
|
|
212
|
-
if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore
|
|
215
|
+
if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore)
|
|
213
216
|
pendingInsert = ""
|
|
214
217
|
state.setPending("")
|
|
215
218
|
updateTimeout(ctx)
|
|
216
|
-
return
|
|
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):
|
|
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
|
}
|
package/src/prompt/root.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
}
|