opencode-vim 0.0.16 → 0.0.18
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"
|
|
@@ -104,7 +103,7 @@ export function SnippetAutocomplete(props: SnippetAutocompleteProps) {
|
|
|
104
103
|
props.controller.navigate = undefined
|
|
105
104
|
})
|
|
106
105
|
|
|
107
|
-
const dialogBlockingInput = () => dialogOpen() || dialogHandoffUntil() > Date.now()
|
|
106
|
+
const dialogBlockingInput = () => props.ctx.api.ui.dialog.open || dialogOpen() || dialogHandoffUntil() > Date.now()
|
|
108
107
|
const match = createMemo(() => {
|
|
109
108
|
if (props.ctx.disabled || props.ctx.visible === false) return undefined
|
|
110
109
|
return findTrailingHashtagTrigger(input())
|
|
@@ -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
|
}
|
|
@@ -44,13 +43,13 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
|
|
|
44
43
|
|
|
45
44
|
const cursorStyleTimer = setInterval(syncCursorStyle, 50)
|
|
46
45
|
const offKeyIntercept = props.ctx.api.keymap.intercept("key", ({ event }) => {
|
|
47
|
-
if (!
|
|
46
|
+
if (!canHandleKeys(props)) return
|
|
48
47
|
const key = keyNotation(event)
|
|
49
48
|
if (!key || !passThroughKey(event, key, props.state.mode())) return
|
|
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,
|
|
@@ -62,8 +61,8 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
|
|
|
62
61
|
pending: props.state.pending(),
|
|
63
62
|
})
|
|
64
63
|
|
|
65
|
-
if (!
|
|
66
|
-
props.log("keyboard.skip", { enabled: props.enabled(), disabled: props.ctx.disabled, visible: props.ctx.visible })
|
|
64
|
+
if (!canHandleKeys(props)) {
|
|
65
|
+
props.log("keyboard.skip", { enabled: props.enabled(), disabled: props.ctx.disabled, visible: props.ctx.visible, dialogOpen: props.ctx.api.ui.dialog.open })
|
|
67
66
|
return
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -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
|
})
|
|
@@ -112,6 +112,10 @@ function VimKeyboard(props: { ctx: PromptContext; config: VimConfig; state: Retu
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function canHandleKeys(props: { ctx: PromptContext; enabled: Accessor<boolean> }) {
|
|
116
|
+
return props.enabled() && !props.ctx.disabled && props.ctx.visible !== false && !props.ctx.api.ui.dialog.open
|
|
117
|
+
}
|
|
118
|
+
|
|
115
119
|
function readablePending(sequence: string) {
|
|
116
120
|
if (!sequence) return undefined
|
|
117
121
|
return sequence.replaceAll("><", " ")
|
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"
|