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/tui.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import { Plugin } from "@opencode/plugin/tui"
3
- import { InputRenderable, type CursorStyleOptions, type KeyEvent } from "@opentui/core"
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
- consumed = vimee.handle(event as never, key, ctx as never)
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(config.defaultMode)
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>