opencode-vim 0.0.16 → 0.0.17
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/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { spawn } from "node:child_process"
|
|
3
3
|
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
|
|
4
|
-
import { useKeyboard } from "@opentui/solid"
|
|
5
4
|
import { RGBA, type ScrollBoxRenderable } from "@opentui/core"
|
|
6
5
|
import type { TuiPluginApi, TuiPromptRef } from "@opencode-ai/plugin/tui"
|
|
7
6
|
import type { PromptContext } from "../../prompt/types"
|
|
@@ -303,7 +302,7 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
|
|
|
303
302
|
}, 0)
|
|
304
303
|
})
|
|
305
304
|
|
|
306
|
-
|
|
305
|
+
const offKeyboard = props.ctx.api.keymap.intercept("key", ({ event }) => {
|
|
307
306
|
const ref = props.ctx.prompt()
|
|
308
307
|
const name = event.name?.toLowerCase()
|
|
309
308
|
|
|
@@ -341,6 +340,8 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
|
|
|
341
340
|
schedulePromptSync()
|
|
342
341
|
})
|
|
343
342
|
|
|
343
|
+
onCleanup(() => offKeyboard())
|
|
344
|
+
|
|
344
345
|
return (
|
|
345
346
|
<Show when={visible()}>
|
|
346
347
|
<box position="absolute" top={-menuHeight()} left={0} right={0} zIndex={100} borderColor={props.ctx.api.theme.current.border} {...INLINE_BORDER}>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { KeyEvent, ParsedKey } from "@opentui/core"
|
|
3
|
-
import { useKeyboard } from "@opentui/solid"
|
|
4
3
|
import { onCleanup } from "solid-js"
|
|
5
4
|
import type { Accessor } from "solid-js"
|
|
6
5
|
import type { PromptContext, PromptModule } from "../../prompt/types"
|
|
@@ -31,7 +30,7 @@ export function createVimModule(options?: unknown, enabled: Accessor<boolean> =
|
|
|
31
30
|
return <VimKeyboard ctx={ctx} config={config} state={state} snippets={snippets} enabled={enabled} log={log} />
|
|
32
31
|
},
|
|
33
32
|
renderRight(ctx) {
|
|
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} />
|
|
33
|
+
return <VimStatus mode={state.mode} pending={() => readablePending(state.pending())} subscribe={state.subscribe} enabled={enabled} theme={ctx.api.theme.current} pendingDisplayDelay={config.pendingDisplayDelay} disabled={ctx.disabled} log={log} requestRender={ctx.requestRender} />
|
|
35
34
|
},
|
|
36
35
|
}
|
|
37
36
|
}
|
|
@@ -50,7 +49,7 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
|
|
|
50
49
|
if (preparePassThroughKey(props.ctx, key, props.state.mode())) preparedEvents.add(event)
|
|
51
50
|
}, { priority: 100 })
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
const offKeyboard = props.ctx.api.keymap.intercept("key", ({ event }) => {
|
|
54
53
|
props.log("keyboard.event", {
|
|
55
54
|
name: event.name,
|
|
56
55
|
ctrl: event.ctrl,
|
|
@@ -96,6 +95,7 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
|
|
|
96
95
|
onCleanup(() => {
|
|
97
96
|
props.log("keyboard.cleanup", { kind: props.ctx.kind })
|
|
98
97
|
offKeyIntercept()
|
|
98
|
+
offKeyboard()
|
|
99
99
|
vimee.cleanup()
|
|
100
100
|
clearInterval(cursorStyleTimer)
|
|
101
101
|
})
|
package/src/modules/vim/state.ts
CHANGED
|
@@ -2,23 +2,37 @@ import { createSignal } from "solid-js"
|
|
|
2
2
|
import type { VimLog } from "./log"
|
|
3
3
|
|
|
4
4
|
export type VimMode = "normal" | "insert" | "visual" | "visual-line"
|
|
5
|
+
export type VimStateListener = () => void
|
|
5
6
|
|
|
6
7
|
export function createVimState(defaultMode: VimMode, log: VimLog = () => {}) {
|
|
7
8
|
const [mode, setMode] = createSignal<VimMode>(defaultMode)
|
|
8
9
|
const [pending, setPending] = createSignal("")
|
|
10
|
+
const listeners = new Set<VimStateListener>()
|
|
9
11
|
|
|
10
12
|
log("state.init", { mode: defaultMode })
|
|
11
13
|
|
|
12
14
|
return {
|
|
13
15
|
mode,
|
|
14
16
|
setMode(next: VimMode) {
|
|
15
|
-
|
|
17
|
+
const changed = mode() !== next
|
|
18
|
+
if (changed) log("state.mode", { from: mode(), to: next })
|
|
16
19
|
setMode(next)
|
|
20
|
+
if (changed) notify()
|
|
17
21
|
},
|
|
18
22
|
pending,
|
|
19
23
|
setPending(next: string) {
|
|
20
|
-
|
|
24
|
+
const changed = pending() !== next
|
|
25
|
+
if (changed) log("state.pending", { from: pending(), to: next })
|
|
21
26
|
setPending(next)
|
|
27
|
+
if (changed) notify()
|
|
22
28
|
},
|
|
29
|
+
subscribe(listener: VimStateListener) {
|
|
30
|
+
listeners.add(listener)
|
|
31
|
+
return () => listeners.delete(listener)
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function notify() {
|
|
36
|
+
for (const listener of listeners) listener()
|
|
23
37
|
}
|
|
24
38
|
}
|
package/src/modules/vim/view.tsx
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import {
|
|
2
|
+
import type { TextRenderable } from "@opentui/core"
|
|
3
|
+
import { onCleanup } from "solid-js"
|
|
3
4
|
import type { Accessor } from "solid-js"
|
|
4
5
|
import type { PromptContext } from "../../prompt/types"
|
|
5
6
|
import type { VimLog } from "./log"
|
|
6
|
-
import type { VimMode } from "./state"
|
|
7
|
+
import type { VimMode, VimStateListener } from "./state"
|
|
7
8
|
|
|
8
9
|
const STATUS_SYNC_MS = 50
|
|
9
10
|
|
|
10
11
|
type VimStatusProps = {
|
|
11
12
|
mode: Accessor<VimMode>
|
|
12
13
|
pending: Accessor<string | undefined>
|
|
14
|
+
subscribe: (listener: VimStateListener) => () => void
|
|
13
15
|
enabled: Accessor<boolean>
|
|
14
16
|
theme: PromptContext["api"]["theme"]["current"]
|
|
15
17
|
pendingDisplayDelay?: number
|
|
@@ -19,45 +21,63 @@ type VimStatusProps = {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function VimStatus(props: VimStatusProps) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
let modeText: TextRenderable | undefined
|
|
25
|
+
let pendingText: TextRenderable | undefined
|
|
26
|
+
let displayedMode = props.mode()
|
|
27
|
+
let displayedPending: string | undefined
|
|
28
|
+
let displayedEnabled = props.enabled()
|
|
25
29
|
let pendingTimer: ReturnType<typeof setTimeout> | undefined
|
|
26
30
|
let scheduledPending: string | undefined
|
|
27
31
|
|
|
28
32
|
const sync = () => {
|
|
29
33
|
const nextMode = props.mode()
|
|
30
34
|
const nextPending = props.pending()
|
|
35
|
+
let changed = false
|
|
31
36
|
|
|
32
|
-
if (
|
|
33
|
-
props.log?.("status.sync", { fromMode:
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
if (displayedMode !== nextMode) {
|
|
38
|
+
props.log?.("status.sync", { fromMode: displayedMode, toMode: nextMode, fromPending: displayedPending, toPending: displayedPending })
|
|
39
|
+
displayedMode = nextMode
|
|
40
|
+
changed = true
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
const nextEnabled = props.enabled()
|
|
39
|
-
if (
|
|
40
|
-
props.log?.("status.enabled", { from:
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
if (displayedEnabled !== nextEnabled) {
|
|
45
|
+
props.log?.("status.enabled", { from: displayedEnabled, to: nextEnabled })
|
|
46
|
+
displayedEnabled = nextEnabled
|
|
47
|
+
changed = true
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
if (changed) updateModeText()
|
|
45
51
|
syncPending(nextPending)
|
|
52
|
+
if (changed) props.requestRender?.()
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
const unsubscribe = props.subscribe(sync)
|
|
48
56
|
const timer = setInterval(sync, STATUS_SYNC_MS)
|
|
49
57
|
onCleanup(() => {
|
|
58
|
+
unsubscribe()
|
|
50
59
|
clearInterval(timer)
|
|
51
60
|
if (pendingTimer) clearTimeout(pendingTimer)
|
|
52
61
|
})
|
|
53
62
|
|
|
54
63
|
return (
|
|
55
64
|
<box paddingLeft={1} paddingRight={1} flexDirection="row">
|
|
56
|
-
{
|
|
57
|
-
{
|
|
65
|
+
<text ref={(ref: TextRenderable) => { pendingText = ref; updatePendingText() }} fg={props.theme.info}>{displayedPending ? `${displayedPending} ` : ""}</text>
|
|
66
|
+
<text ref={(ref: TextRenderable) => { modeText = ref; updateModeText() }} fg={modeColor(displayedMode, displayedEnabled, props.theme, props.disabled)}>{displayedEnabled ? modeLabel(displayedMode) : ""}</text>
|
|
58
67
|
</box>
|
|
59
68
|
)
|
|
60
69
|
|
|
70
|
+
function updateModeText() {
|
|
71
|
+
if (!modeText) return
|
|
72
|
+
modeText.content = displayedEnabled ? modeLabel(displayedMode) : ""
|
|
73
|
+
modeText.fg = modeColor(displayedMode, displayedEnabled, props.theme, props.disabled)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function updatePendingText() {
|
|
77
|
+
if (!pendingText) return
|
|
78
|
+
pendingText.content = displayedEnabled && displayedPending ? `${displayedPending} ` : ""
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
function syncPending(nextPending: string | undefined) {
|
|
62
82
|
if (!nextPending) {
|
|
63
83
|
if (pendingTimer) clearTimeout(pendingTimer)
|
|
@@ -67,7 +87,7 @@ export function VimStatus(props: VimStatusProps) {
|
|
|
67
87
|
return
|
|
68
88
|
}
|
|
69
89
|
|
|
70
|
-
if (
|
|
90
|
+
if (displayedPending === nextPending) return
|
|
71
91
|
if (scheduledPending === nextPending) return
|
|
72
92
|
|
|
73
93
|
if (pendingTimer) clearTimeout(pendingTimer)
|
|
@@ -87,13 +107,20 @@ export function VimStatus(props: VimStatusProps) {
|
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
function setDisplayedPending(nextPending: string | undefined) {
|
|
90
|
-
if (
|
|
91
|
-
props.log?.("status.sync", { fromMode:
|
|
92
|
-
|
|
110
|
+
if (displayedPending === nextPending) return
|
|
111
|
+
props.log?.("status.sync", { fromMode: displayedMode, toMode: displayedMode, fromPending: displayedPending, toPending: nextPending })
|
|
112
|
+
displayedPending = nextPending
|
|
113
|
+
updatePendingText()
|
|
93
114
|
props.requestRender?.()
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
|
|
118
|
+
function modeColor(mode: VimMode, enabled: boolean, theme: PromptContext["api"]["theme"]["current"], disabled?: boolean) {
|
|
119
|
+
if (!enabled) return undefined
|
|
120
|
+
if (disabled) return theme.textMuted
|
|
121
|
+
return mode === "insert" ? theme.success : theme.warning
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
function modeLabel(mode: VimMode) {
|
|
98
125
|
if (mode === "visual") return "VISUAL"
|
|
99
126
|
if (mode === "visual-line") return "VISUAL LINE"
|