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.
Files changed (38) hide show
  1. package/README.md +11 -0
  2. package/dist/tui.js +3326 -0
  3. package/package.json +6 -6
  4. package/src/clipboard.tsx +0 -50
  5. package/src/modules/snippets/index.tsx +0 -17
  6. package/src/modules/snippets/loader.ts +0 -147
  7. package/src/modules/snippets/search.ts +0 -118
  8. package/src/modules/snippets/skill-loader.ts +0 -81
  9. package/src/modules/snippets/state.ts +0 -40
  10. package/src/modules/snippets/trigger.ts +0 -68
  11. package/src/modules/snippets/types.ts +0 -50
  12. package/src/modules/snippets/view.tsx +0 -674
  13. package/src/modules/vim/actions.ts +0 -82
  14. package/src/modules/vim/config.ts +0 -137
  15. package/src/modules/vim/edit.test.ts +0 -51
  16. package/src/modules/vim/edit.ts +0 -33
  17. package/src/modules/vim/graphemes.ts +0 -38
  18. package/src/modules/vim/index.tsx +0 -200
  19. package/src/modules/vim/keys.ts +0 -38
  20. package/src/modules/vim/log.ts +0 -37
  21. package/src/modules/vim/map.test.ts +0 -45
  22. package/src/modules/vim/map.ts +0 -90
  23. package/src/modules/vim/state.test.ts +0 -18
  24. package/src/modules/vim/state.tsx +0 -46
  25. package/src/modules/vim/view.tsx +0 -130
  26. package/src/modules/vim/vimee.test.ts +0 -211
  27. package/src/modules/vim/vimee.ts +0 -1211
  28. package/src/plugin.tsx +0 -116
  29. package/src/prompt/host.tsx +0 -38
  30. package/src/prompt/modules.ts +0 -16
  31. package/src/prompt/root.tsx +0 -69
  32. package/src/prompt/types.ts +0 -42
  33. package/src/session.tsx +0 -291
  34. package/src/transcript-items.tsx +0 -168
  35. package/src/transcript.tsx +0 -179
  36. package/src/update.ts +0 -120
  37. package/tui.tsx +0 -340
  38. 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
- }