opencode-vim 0.0.23 → 0.0.24
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/README.md +11 -0
- package/dist/tui.js +3326 -0
- package/package.json +6 -6
- package/src/clipboard.tsx +0 -50
- package/src/modules/snippets/index.tsx +0 -17
- package/src/modules/snippets/loader.ts +0 -147
- package/src/modules/snippets/search.ts +0 -118
- package/src/modules/snippets/skill-loader.ts +0 -81
- package/src/modules/snippets/state.ts +0 -40
- package/src/modules/snippets/trigger.ts +0 -68
- package/src/modules/snippets/types.ts +0 -50
- package/src/modules/snippets/view.tsx +0 -674
- package/src/modules/vim/actions.ts +0 -82
- package/src/modules/vim/config.ts +0 -137
- package/src/modules/vim/edit.test.ts +0 -51
- package/src/modules/vim/edit.ts +0 -33
- package/src/modules/vim/graphemes.ts +0 -38
- package/src/modules/vim/index.tsx +0 -200
- package/src/modules/vim/keys.ts +0 -38
- package/src/modules/vim/log.ts +0 -37
- package/src/modules/vim/map.test.ts +0 -45
- package/src/modules/vim/map.ts +0 -90
- package/src/modules/vim/state.test.ts +0 -18
- package/src/modules/vim/state.tsx +0 -46
- package/src/modules/vim/view.tsx +0 -130
- package/src/modules/vim/vimee.test.ts +0 -211
- package/src/modules/vim/vimee.ts +0 -1211
- package/src/plugin.tsx +0 -116
- package/src/prompt/host.tsx +0 -38
- package/src/prompt/modules.ts +0 -16
- package/src/prompt/root.tsx +0 -69
- package/src/prompt/types.ts +0 -42
- package/src/session.tsx +0 -291
- package/src/transcript-items.tsx +0 -168
- package/src/transcript.tsx +0 -179
- package/src/update.ts +0 -120
- package/tui.tsx +0 -340
- package/view.tsx +0 -29
package/tui.tsx
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { Plugin } from "@opencode/plugin/tui"
|
|
3
|
-
import { InputRenderable, KeyEvent, PasteEvent, type CursorStyleOptions } from "@opentui/core"
|
|
4
|
-
import { createEffect, createSignal, onCleanup } from "solid-js"
|
|
5
|
-
import { applyVimCursorStyle, focusedInput } from "./src/modules/vim/actions"
|
|
6
|
-
import { createVimConfig } from "./src/modules/vim/config"
|
|
7
|
-
import { editInput } from "./src/modules/vim/edit"
|
|
8
|
-
import { keyNotation } from "./src/modules/vim/keys"
|
|
9
|
-
import { createVimLog } from "./src/modules/vim/log"
|
|
10
|
-
import { displayToChar } from "./src/modules/vim/map"
|
|
11
|
-
import { createVimState } from "./src/modules/vim/state"
|
|
12
|
-
import { createVimeeAdapter } from "./src/modules/vim/vimee"
|
|
13
|
-
import { VimStatus } from "./view"
|
|
14
|
-
import { SESSION_MODE, createSessionMode } from "./src/session"
|
|
15
|
-
import { createVimClipboard } from "./src/clipboard"
|
|
16
|
-
|
|
17
|
-
type Context = Parameters<Parameters<typeof Plugin.define>[0]["setup"]>[0]
|
|
18
|
-
|
|
19
|
-
export default Plugin.define({
|
|
20
|
-
id: "opencode-vim",
|
|
21
|
-
setup(context) {
|
|
22
|
-
context.ui.slot({ append: "app", render: () => <VimHost context={context} /> })
|
|
23
|
-
},
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
function VimHost(props: { context: Context }) {
|
|
27
|
-
const config = createVimConfig(props.context.options)
|
|
28
|
-
const normalMappings = Object.keys(config.keymaps.normal ?? {})
|
|
29
|
-
const log = createVimLog(config)
|
|
30
|
-
const clipboard = createVimClipboard(props.context.renderer)
|
|
31
|
-
const clipboardOptions = { onYank: (text: string) => { void clipboard.write(text) }, readClipboard: clipboard.read }
|
|
32
|
-
const state = createVimState(config.defaultMode, log)
|
|
33
|
-
const vimee = createVimeeAdapter(state, config, log, clipboardOptions)
|
|
34
|
-
const dialogState = createVimState(config.defaultMode, log)
|
|
35
|
-
const dialogVimee = createVimeeAdapter(dialogState, config, log, clipboardOptions)
|
|
36
|
-
const [dialogFocused, setDialogFocused] = createSignal(false)
|
|
37
|
-
const promptVim = { state, vimee }
|
|
38
|
-
const dialogVim = { state: dialogState, vimee: dialogVimee }
|
|
39
|
-
let dialogInput: typeof props.context.renderer.currentFocusedEditor = null
|
|
40
|
-
const [saved, setSaved] = props.context.storage.store("state", { initial: { enabled: true } })
|
|
41
|
-
const enabled = () => saved.enabled
|
|
42
|
-
const ctx = createCompatContext(props.context)
|
|
43
|
-
let cursorMode = ""
|
|
44
|
-
let cursorInput: typeof props.context.renderer.currentFocusedEditor = null
|
|
45
|
-
let originalCursorStyle: CursorStyleOptions | undefined
|
|
46
|
-
const session = createSessionMode(props.context, config, clipboard)
|
|
47
|
-
let pendingKeys: Array<KeyEvent | PasteEvent> | undefined
|
|
48
|
-
|
|
49
|
-
const removeStatus = props.context.ui.slot({
|
|
50
|
-
prepend: "prompt.footer",
|
|
51
|
-
render: (footer) =>
|
|
52
|
-
footer.mode === "normal" ? session.active() && !dialogFocused() ? <session.Status /> : (
|
|
53
|
-
<VimStatus
|
|
54
|
-
mode={() => dialogFocused() ? dialogState.mode() : state.mode()}
|
|
55
|
-
enabled={enabled}
|
|
56
|
-
theme={compatTheme(props.context)}
|
|
57
|
-
/>
|
|
58
|
-
) : null,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
props.context.keymap.layer(() => ({
|
|
62
|
-
mode: "global",
|
|
63
|
-
commands: [
|
|
64
|
-
{
|
|
65
|
-
id: "opencode-vim.toggle",
|
|
66
|
-
title: "Toggle Vim Mode",
|
|
67
|
-
description: "Enable or disable Vim key handling",
|
|
68
|
-
group: "Vim",
|
|
69
|
-
palette: true,
|
|
70
|
-
slash: { name: "vim" },
|
|
71
|
-
run() {
|
|
72
|
-
const next = !enabled()
|
|
73
|
-
void setSaved((draft) => {
|
|
74
|
-
draft.enabled = next
|
|
75
|
-
})
|
|
76
|
-
if (!next) {
|
|
77
|
-
pendingKeys = undefined
|
|
78
|
-
session.close()
|
|
79
|
-
vimee.suspend()
|
|
80
|
-
dialogVimee.suspend()
|
|
81
|
-
}
|
|
82
|
-
props.context.ui.toast.show({ message: `Vim mode ${next ? "enabled" : "disabled"}`, variant: "info" })
|
|
83
|
-
props.context.renderer.requestRender()
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
}))
|
|
88
|
-
|
|
89
|
-
const onKey = (event: KeyEvent) => {
|
|
90
|
-
if (!enabled() || event.defaultPrevented) return
|
|
91
|
-
if (pendingKeys) {
|
|
92
|
-
pendingKeys.push(new KeyEvent(event))
|
|
93
|
-
event.preventDefault()
|
|
94
|
-
event.stopPropagation()
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
if (props.context.keymap.mode.current() === SESSION_MODE) return
|
|
98
|
-
const kind = inputKind(props.context)
|
|
99
|
-
if (!kind) return
|
|
100
|
-
if (props.context.keymap.pending().length) return
|
|
101
|
-
const { state, vimee } = kind === "dialog" ? dialogVim : promptVim
|
|
102
|
-
const key = keyNotation(event as never)
|
|
103
|
-
if (!key) return
|
|
104
|
-
const mapped = normalMappings.some((sequence) => sequence.startsWith(key))
|
|
105
|
-
if (kind === "prompt" && key === config.sessionKey && !mapped && state.mode() === "normal" && !vimee.isPending()) {
|
|
106
|
-
if (session.enter()) {
|
|
107
|
-
event.preventDefault()
|
|
108
|
-
event.stopPropagation()
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (passThroughKey(event, key, state.mode(), vimee.isPending(), mapped)) return
|
|
113
|
-
if (key === "<Esc>" && state.mode() === "normal" && !vimee.isPending()) return
|
|
114
|
-
|
|
115
|
-
if (kind === "dialog" && !vimee.isPending() && !mapped && state.mode() === "normal") {
|
|
116
|
-
if (key === "<Tab>" || key === "<Home>" || key === "<End>" || key === "<PageUp>" || key === "<PageDown>") return
|
|
117
|
-
const command = key === "j" ? "dialog.select.next" : key === "k" ? "dialog.select.prev" : undefined
|
|
118
|
-
if (command && props.context.keymap.commands().some((item) => item.id === command)) {
|
|
119
|
-
event.preventDefault()
|
|
120
|
-
event.stopPropagation()
|
|
121
|
-
props.context.keymap.dispatch(command)
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (kind === "prompt" && !vimee.isPending() && sendCompletionKey(event, props.context, key, state.mode())) {
|
|
127
|
-
syncCursor(true)
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const before = state.mode()
|
|
132
|
-
let consumed = false
|
|
133
|
-
try {
|
|
134
|
-
const result = vimee.handle(event as never, key, ctx as never)
|
|
135
|
-
if (typeof result === "boolean") consumed = result
|
|
136
|
-
else {
|
|
137
|
-
consumed = true
|
|
138
|
-
const queued: Array<KeyEvent | PasteEvent> = []
|
|
139
|
-
pendingKeys = queued
|
|
140
|
-
const input = props.context.renderer.currentFocusedEditor
|
|
141
|
-
void result.then((handled) => {
|
|
142
|
-
if (pendingKeys !== queued) return
|
|
143
|
-
pendingKeys = undefined
|
|
144
|
-
if (!handled) return
|
|
145
|
-
syncCursor(true)
|
|
146
|
-
// Replay through the host too: a queued i may be followed by native typing.
|
|
147
|
-
for (const event of queued) {
|
|
148
|
-
if (!enabled() || props.context.renderer.currentFocusedEditor !== input) break
|
|
149
|
-
if (event instanceof PasteEvent) props.context.renderer.keyInput.emit("paste", event)
|
|
150
|
-
else props.context.renderer.keyInput.emit("keypress", event)
|
|
151
|
-
}
|
|
152
|
-
}).catch((error) => {
|
|
153
|
-
if (pendingKeys === queued) pendingKeys = undefined
|
|
154
|
-
log("vim.clipboard.paste.error", { error: String(error) })
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
} finally {
|
|
158
|
-
if (consumed || state.mode() !== before) {
|
|
159
|
-
event.preventDefault()
|
|
160
|
-
event.stopPropagation()
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (consumed) syncCursor(true)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const onFocus = () => {
|
|
167
|
-
pendingKeys = undefined
|
|
168
|
-
vimee.suspend()
|
|
169
|
-
dialogVimee.suspend()
|
|
170
|
-
syncCursor()
|
|
171
|
-
}
|
|
172
|
-
const onPaste = (event: PasteEvent) => {
|
|
173
|
-
if (!pendingKeys || event.defaultPrevented) return
|
|
174
|
-
pendingKeys.push(new PasteEvent(event.bytes, event.metadata))
|
|
175
|
-
event.preventDefault()
|
|
176
|
-
event.stopPropagation()
|
|
177
|
-
}
|
|
178
|
-
props.context.renderer.keyInput.prependListener("keypress", onKey)
|
|
179
|
-
props.context.renderer.keyInput.prependListener("paste", onPaste)
|
|
180
|
-
props.context.renderer.on("focused_editor", onFocus)
|
|
181
|
-
createEffect(() => syncCursor())
|
|
182
|
-
let route = props.context.ui.router.current()
|
|
183
|
-
createEffect(() => {
|
|
184
|
-
const next = props.context.ui.router.current()
|
|
185
|
-
if (next !== route && pendingKeys) onFocus()
|
|
186
|
-
route = next
|
|
187
|
-
})
|
|
188
|
-
onCleanup(() => {
|
|
189
|
-
pendingKeys = undefined
|
|
190
|
-
removeStatus()
|
|
191
|
-
session.close()
|
|
192
|
-
props.context.renderer.keyInput.off("keypress", onKey)
|
|
193
|
-
props.context.renderer.keyInput.off("paste", onPaste)
|
|
194
|
-
props.context.renderer.off("focused_editor", onFocus)
|
|
195
|
-
vimee.cleanup()
|
|
196
|
-
dialogVimee.cleanup()
|
|
197
|
-
void clipboard.dispose()
|
|
198
|
-
restoreCursor()
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
return null
|
|
202
|
-
|
|
203
|
-
function syncCursor(force = false) {
|
|
204
|
-
const input = props.context.renderer.currentFocusedEditor
|
|
205
|
-
const kind = inputKind(props.context)
|
|
206
|
-
setDialogFocused(kind === "dialog")
|
|
207
|
-
if (!enabled() || !kind || !input) {
|
|
208
|
-
restoreCursor()
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
const { state, vimee } = kind === "dialog" ? dialogVim : promptVim
|
|
212
|
-
if (kind === "dialog" && dialogInput !== input) {
|
|
213
|
-
dialogInput = input
|
|
214
|
-
dialogVimee.suspend()
|
|
215
|
-
dialogState.setMode(promptVim.state.mode())
|
|
216
|
-
}
|
|
217
|
-
const mode = state.mode()
|
|
218
|
-
vimee.attach(ctx)
|
|
219
|
-
const inputChanged = cursorInput !== input
|
|
220
|
-
if (inputChanged) {
|
|
221
|
-
restoreCursor()
|
|
222
|
-
cursorInput = input
|
|
223
|
-
originalCursorStyle = input.cursorStyle
|
|
224
|
-
}
|
|
225
|
-
if (!force && !inputChanged && cursorMode === mode) return
|
|
226
|
-
if (applyVimCursorStyle(ctx as never, config.cursorStyles[mode])) {
|
|
227
|
-
cursorMode = mode
|
|
228
|
-
props.context.renderer.requestRender()
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function restoreCursor() {
|
|
233
|
-
if (cursorInput && !cursorInput.isDestroyed && originalCursorStyle) cursorInput.cursorStyle = originalCursorStyle
|
|
234
|
-
cursorInput = null
|
|
235
|
-
cursorMode = ""
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function createCompatContext(context: Context) {
|
|
240
|
-
const prompt = {
|
|
241
|
-
get current() {
|
|
242
|
-
return { input: focusedInputValue(context), mode: "normal", parts: [] }
|
|
243
|
-
},
|
|
244
|
-
get focused() {
|
|
245
|
-
return inputKind(context) !== undefined
|
|
246
|
-
},
|
|
247
|
-
set(value: { input: string }) {
|
|
248
|
-
const input = context.renderer.currentFocusedEditor
|
|
249
|
-
if (input instanceof InputRenderable) input.value = value.input
|
|
250
|
-
else if (input) editInput(input, value.input)
|
|
251
|
-
},
|
|
252
|
-
submit() {
|
|
253
|
-
if (inputKind(context) === "dialog") {
|
|
254
|
-
const command = context.keymap.commands().some((item) => item.id === "dialog.select.submit")
|
|
255
|
-
? "dialog.select.submit"
|
|
256
|
-
: "dialog.prompt.submit"
|
|
257
|
-
context.keymap.dispatch(command)
|
|
258
|
-
} else {
|
|
259
|
-
context.keymap.dispatch("prompt.submit")
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
blur() {
|
|
263
|
-
context.renderer.currentFocusedEditor?.blur()
|
|
264
|
-
},
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
api: {
|
|
269
|
-
renderer: context.renderer,
|
|
270
|
-
keymap: {
|
|
271
|
-
dispatchCommand(command: string) {
|
|
272
|
-
const ok = context.keymap.commands().some((item) => item.id === command)
|
|
273
|
-
if (ok) context.keymap.dispatch(command)
|
|
274
|
-
return { ok }
|
|
275
|
-
},
|
|
276
|
-
},
|
|
277
|
-
theme: { current: compatTheme(context) },
|
|
278
|
-
},
|
|
279
|
-
kind: context.ui.router.current().type === "session" ? "session" : "home",
|
|
280
|
-
prompt: () => (inputKind(context) ? prompt : undefined),
|
|
281
|
-
requestRender: () => context.renderer.requestRender(),
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function inputKind(context: Context): "prompt" | "dialog" | undefined {
|
|
286
|
-
if (context.keymap.mode.current() === SESSION_MODE) return
|
|
287
|
-
if (!context.renderer.currentFocusedEditor) return
|
|
288
|
-
const commands = context.keymap.commands()
|
|
289
|
-
if (context.keymap.mode.current() === "modal") {
|
|
290
|
-
if (commands.some((item) => item.id === "dialog.select.submit" || item.id === "dialog.prompt.submit")) return "dialog"
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
if (commands.some(
|
|
294
|
-
(item) =>
|
|
295
|
-
item.id === "prompt.submit" || item.id === "prompt.autocomplete.next" || item.id === "prompt.history.previous",
|
|
296
|
-
)) return "prompt"
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function focusedInputValue(context: Context) {
|
|
300
|
-
return context.renderer.currentFocusedEditor?.plainText ?? ""
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function compatTheme(context: Context) {
|
|
304
|
-
return {
|
|
305
|
-
get background() { return context.theme.background.base },
|
|
306
|
-
get info() { return context.theme.text.feedback.info.base },
|
|
307
|
-
get success() { return context.theme.text.feedback.success.base },
|
|
308
|
-
get warning() { return context.theme.text.feedback.warning.base },
|
|
309
|
-
get textMuted() { return context.theme.text.muted },
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function passThroughKey(event: KeyEvent, key: string, mode: string, pending: boolean, mapped: boolean) {
|
|
314
|
-
if (mode !== "normal") return false
|
|
315
|
-
if (mapped) return false
|
|
316
|
-
if (event.ctrl && key !== "<C-r>" && key !== "<C-[>") return true
|
|
317
|
-
return event.super === true || event.meta === true || (isArrowKey(key) && !pending)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function sendCompletionKey(event: KeyEvent, context: Context, key: string, mode: string) {
|
|
321
|
-
if (mode !== "normal") return false
|
|
322
|
-
const command = key === "j" ? "prompt.autocomplete.next" : key === "k" ? "prompt.autocomplete.prev" : undefined
|
|
323
|
-
if (!command || !isCompletionToken(context) || !context.keymap.commands().some((item) => item.id === command)) return false
|
|
324
|
-
event.preventDefault()
|
|
325
|
-
event.stopPropagation()
|
|
326
|
-
context.keymap.dispatch(command)
|
|
327
|
-
return true
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function isCompletionToken(context: Context) {
|
|
331
|
-
const input = context.renderer.currentFocusedEditor
|
|
332
|
-
const text = input?.plainText ?? ""
|
|
333
|
-
const index = displayToChar(text, Math.max(0, input?.cursorOffset ?? 0))
|
|
334
|
-
const before = text.slice(0, Math.min(index + 1, text.length))
|
|
335
|
-
return /^\/\S*$/.test(before) || /(?:^|\s)@\S*$/.test(before)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function isArrowKey(key: string) {
|
|
339
|
-
return key === "<Left>" || key === "<Down>" || key === "<Up>" || key === "<Right>"
|
|
340
|
-
}
|
package/view.tsx
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import type { TextRenderable } from "@opentui/core"
|
|
3
|
-
import type { Accessor } from "solid-js"
|
|
4
|
-
import type { VimMode } from "./src/modules/vim/state"
|
|
5
|
-
|
|
6
|
-
type VimStatusProps = {
|
|
7
|
-
mode: Accessor<VimMode>
|
|
8
|
-
enabled: Accessor<boolean>
|
|
9
|
-
theme: {
|
|
10
|
-
success: TextRenderable["fg"]
|
|
11
|
-
warning: TextRenderable["fg"]
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function VimStatus(props: VimStatusProps) {
|
|
16
|
-
return (
|
|
17
|
-
<box paddingRight={1} flexDirection="row" flexShrink={0}>
|
|
18
|
-
<text fg={props.mode() === "insert" ? props.theme.success : props.theme.warning}>
|
|
19
|
-
{props.enabled() ? modeLabel(props.mode()) : ""}
|
|
20
|
-
</text>
|
|
21
|
-
</box>
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function modeLabel(mode: VimMode) {
|
|
26
|
-
if (mode === "visual") return "VISUAL"
|
|
27
|
-
if (mode === "visual-line") return "VISUAL LINE"
|
|
28
|
-
return mode === "normal" ? "NORMAL" : "INSERT"
|
|
29
|
-
}
|