opencode-vim 0.0.22 → 0.0.23
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 +17 -66
- package/assets/vim-in-motion-dialogs.gif +0 -0
- package/docs/configuration.md +50 -400
- package/docs/keymap-actions.md +60 -188
- package/docs/vim-behavior.md +93 -0
- package/package.json +1 -1
- package/src/clipboard.tsx +50 -0
- package/src/modules/vim/actions.ts +1 -4
- package/src/modules/vim/config.ts +12 -0
- package/src/modules/vim/map.ts +13 -121
- package/src/modules/vim/vimee.ts +228 -163
- package/src/session.tsx +291 -0
- package/src/transcript-items.tsx +168 -0
- package/src/transcript.tsx +179 -0
- package/tui.tsx +73 -8
- package/view.tsx +1 -1
package/tui.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { Plugin } from "@opencode/plugin/tui"
|
|
3
|
-
import { InputRenderable,
|
|
4
|
-
import { createEffect, onCleanup } from "solid-js"
|
|
3
|
+
import { InputRenderable, KeyEvent, PasteEvent, type CursorStyleOptions } from "@opentui/core"
|
|
4
|
+
import { createEffect, createSignal, onCleanup } from "solid-js"
|
|
5
5
|
import { applyVimCursorStyle, focusedInput } from "./src/modules/vim/actions"
|
|
6
6
|
import { createVimConfig } from "./src/modules/vim/config"
|
|
7
7
|
import { editInput } from "./src/modules/vim/edit"
|
|
@@ -11,6 +11,8 @@ import { displayToChar } from "./src/modules/vim/map"
|
|
|
11
11
|
import { createVimState } from "./src/modules/vim/state"
|
|
12
12
|
import { createVimeeAdapter } from "./src/modules/vim/vimee"
|
|
13
13
|
import { VimStatus } from "./view"
|
|
14
|
+
import { SESSION_MODE, createSessionMode } from "./src/session"
|
|
15
|
+
import { createVimClipboard } from "./src/clipboard"
|
|
14
16
|
|
|
15
17
|
type Context = Parameters<Parameters<typeof Plugin.define>[0]["setup"]>[0]
|
|
16
18
|
|
|
@@ -25,10 +27,13 @@ function VimHost(props: { context: Context }) {
|
|
|
25
27
|
const config = createVimConfig(props.context.options)
|
|
26
28
|
const normalMappings = Object.keys(config.keymaps.normal ?? {})
|
|
27
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 }
|
|
28
32
|
const state = createVimState(config.defaultMode, log)
|
|
29
|
-
const vimee = createVimeeAdapter(state, config, log)
|
|
33
|
+
const vimee = createVimeeAdapter(state, config, log, clipboardOptions)
|
|
30
34
|
const dialogState = createVimState(config.defaultMode, log)
|
|
31
|
-
const dialogVimee = createVimeeAdapter(dialogState, config, log)
|
|
35
|
+
const dialogVimee = createVimeeAdapter(dialogState, config, log, clipboardOptions)
|
|
36
|
+
const [dialogFocused, setDialogFocused] = createSignal(false)
|
|
32
37
|
const promptVim = { state, vimee }
|
|
33
38
|
const dialogVim = { state: dialogState, vimee: dialogVimee }
|
|
34
39
|
let dialogInput: typeof props.context.renderer.currentFocusedEditor = null
|
|
@@ -38,13 +43,15 @@ function VimHost(props: { context: Context }) {
|
|
|
38
43
|
let cursorMode = ""
|
|
39
44
|
let cursorInput: typeof props.context.renderer.currentFocusedEditor = null
|
|
40
45
|
let originalCursorStyle: CursorStyleOptions | undefined
|
|
46
|
+
const session = createSessionMode(props.context, config, clipboard)
|
|
47
|
+
let pendingKeys: Array<KeyEvent | PasteEvent> | undefined
|
|
41
48
|
|
|
42
49
|
const removeStatus = props.context.ui.slot({
|
|
43
50
|
prepend: "prompt.footer",
|
|
44
51
|
render: (footer) =>
|
|
45
|
-
footer.mode === "normal" ? (
|
|
52
|
+
footer.mode === "normal" ? session.active() && !dialogFocused() ? <session.Status /> : (
|
|
46
53
|
<VimStatus
|
|
47
|
-
mode={state.mode}
|
|
54
|
+
mode={() => dialogFocused() ? dialogState.mode() : state.mode()}
|
|
48
55
|
enabled={enabled}
|
|
49
56
|
theme={compatTheme(props.context)}
|
|
50
57
|
/>
|
|
@@ -67,6 +74,8 @@ function VimHost(props: { context: Context }) {
|
|
|
67
74
|
draft.enabled = next
|
|
68
75
|
})
|
|
69
76
|
if (!next) {
|
|
77
|
+
pendingKeys = undefined
|
|
78
|
+
session.close()
|
|
70
79
|
vimee.suspend()
|
|
71
80
|
dialogVimee.suspend()
|
|
72
81
|
}
|
|
@@ -79,6 +88,13 @@ function VimHost(props: { context: Context }) {
|
|
|
79
88
|
|
|
80
89
|
const onKey = (event: KeyEvent) => {
|
|
81
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
|
|
82
98
|
const kind = inputKind(props.context)
|
|
83
99
|
if (!kind) return
|
|
84
100
|
if (props.context.keymap.pending().length) return
|
|
@@ -86,6 +102,13 @@ function VimHost(props: { context: Context }) {
|
|
|
86
102
|
const key = keyNotation(event as never)
|
|
87
103
|
if (!key) return
|
|
88
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
|
+
}
|
|
89
112
|
if (passThroughKey(event, key, state.mode(), vimee.isPending(), mapped)) return
|
|
90
113
|
if (key === "<Esc>" && state.mode() === "normal" && !vimee.isPending()) return
|
|
91
114
|
|
|
@@ -108,7 +131,29 @@ function VimHost(props: { context: Context }) {
|
|
|
108
131
|
const before = state.mode()
|
|
109
132
|
let consumed = false
|
|
110
133
|
try {
|
|
111
|
-
|
|
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
|
+
}
|
|
112
157
|
} finally {
|
|
113
158
|
if (consumed || state.mode() !== before) {
|
|
114
159
|
event.preventDefault()
|
|
@@ -119,19 +164,37 @@ function VimHost(props: { context: Context }) {
|
|
|
119
164
|
}
|
|
120
165
|
|
|
121
166
|
const onFocus = () => {
|
|
167
|
+
pendingKeys = undefined
|
|
122
168
|
vimee.suspend()
|
|
123
169
|
dialogVimee.suspend()
|
|
124
170
|
syncCursor()
|
|
125
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
|
+
}
|
|
126
178
|
props.context.renderer.keyInput.prependListener("keypress", onKey)
|
|
179
|
+
props.context.renderer.keyInput.prependListener("paste", onPaste)
|
|
127
180
|
props.context.renderer.on("focused_editor", onFocus)
|
|
128
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
|
+
})
|
|
129
188
|
onCleanup(() => {
|
|
189
|
+
pendingKeys = undefined
|
|
130
190
|
removeStatus()
|
|
191
|
+
session.close()
|
|
131
192
|
props.context.renderer.keyInput.off("keypress", onKey)
|
|
193
|
+
props.context.renderer.keyInput.off("paste", onPaste)
|
|
132
194
|
props.context.renderer.off("focused_editor", onFocus)
|
|
133
195
|
vimee.cleanup()
|
|
134
196
|
dialogVimee.cleanup()
|
|
197
|
+
void clipboard.dispose()
|
|
135
198
|
restoreCursor()
|
|
136
199
|
})
|
|
137
200
|
|
|
@@ -140,6 +203,7 @@ function VimHost(props: { context: Context }) {
|
|
|
140
203
|
function syncCursor(force = false) {
|
|
141
204
|
const input = props.context.renderer.currentFocusedEditor
|
|
142
205
|
const kind = inputKind(props.context)
|
|
206
|
+
setDialogFocused(kind === "dialog")
|
|
143
207
|
if (!enabled() || !kind || !input) {
|
|
144
208
|
restoreCursor()
|
|
145
209
|
return
|
|
@@ -148,7 +212,7 @@ function VimHost(props: { context: Context }) {
|
|
|
148
212
|
if (kind === "dialog" && dialogInput !== input) {
|
|
149
213
|
dialogInput = input
|
|
150
214
|
dialogVimee.suspend()
|
|
151
|
-
dialogState.setMode(
|
|
215
|
+
dialogState.setMode(promptVim.state.mode())
|
|
152
216
|
}
|
|
153
217
|
const mode = state.mode()
|
|
154
218
|
vimee.attach(ctx)
|
|
@@ -219,6 +283,7 @@ function createCompatContext(context: Context) {
|
|
|
219
283
|
}
|
|
220
284
|
|
|
221
285
|
function inputKind(context: Context): "prompt" | "dialog" | undefined {
|
|
286
|
+
if (context.keymap.mode.current() === SESSION_MODE) return
|
|
222
287
|
if (!context.renderer.currentFocusedEditor) return
|
|
223
288
|
const commands = context.keymap.commands()
|
|
224
289
|
if (context.keymap.mode.current() === "modal") {
|
package/view.tsx
CHANGED
|
@@ -14,7 +14,7 @@ type VimStatusProps = {
|
|
|
14
14
|
|
|
15
15
|
export function VimStatus(props: VimStatusProps) {
|
|
16
16
|
return (
|
|
17
|
-
<box paddingRight={1} flexDirection="row">
|
|
17
|
+
<box paddingRight={1} flexDirection="row" flexShrink={0}>
|
|
18
18
|
<text fg={props.mode() === "insert" ? props.theme.success : props.theme.warning}>
|
|
19
19
|
{props.enabled() ? modeLabel(props.mode()) : ""}
|
|
20
20
|
</text>
|