opencode-vim 0.0.20 → 0.0.21
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 +66 -26
- package/package.json +18 -16
- package/src/modules/vim/actions.ts +32 -7
- package/src/modules/vim/edit.test.ts +51 -0
- package/src/modules/vim/edit.ts +33 -0
- package/src/modules/vim/graphemes.ts +38 -0
- package/src/modules/vim/keys.ts +5 -1
- package/src/modules/vim/map.ts +26 -26
- package/src/modules/vim/vimee.test.ts +1 -1
- package/src/modules/vim/vimee.ts +238 -42
- package/tui.tsx +281 -1
- package/view.tsx +46 -0
- package/tui.json.example +0 -37
package/src/modules/vim/vimee.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { KeyEvent } from "@opentui/core"
|
|
2
|
-
import { TextBuffer, createInitialContext, createKeybindMap, parseKeySequence, processKeystroke, resetContext } from "@vimee/core"
|
|
2
|
+
import { TextBuffer, createInitialContext, createKeybindMap, parseKeySequence, processKeystroke, resetContext, resolveMotion } from "@vimee/core"
|
|
3
3
|
import type { CursorPosition, KeybindDefinition, KeybindMap, MotionRange, Operator, ValidKeySequence, VimAction as VimeeAction, VimContext, VimMode as VimeeMode } from "@vimee/core"
|
|
4
|
-
import type
|
|
5
|
-
import { focusedInput, setInput, type EditBufferLike } from "./actions"
|
|
4
|
+
import { focusedInput, setInput, type EditBufferLike, type PromptContext } from "./actions"
|
|
6
5
|
import type { VimConfig } from "./config"
|
|
7
6
|
import type { VimLog } from "./log"
|
|
7
|
+
import { createGraphemeCodec } from "./graphemes"
|
|
8
8
|
import { charToDisplay, displayToChar, displayWidth, createPromptMap, derivePromptMap, hostOffset, hostPosition, type PromptMap } from "./map"
|
|
9
9
|
import type { createVimState } from "./state"
|
|
10
10
|
|
|
@@ -19,8 +19,9 @@ const YANK_FLASH_MS = 250
|
|
|
19
19
|
export type VimeeAdapter = ReturnType<typeof createVimeeAdapter>
|
|
20
20
|
|
|
21
21
|
export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimLog) {
|
|
22
|
+
const codec = createGraphemeCodec()
|
|
22
23
|
let buffer = new TextBuffer("")
|
|
23
|
-
let activeMap = createPromptMap("")
|
|
24
|
+
let activeMap = createPromptMap("", undefined, codec)
|
|
24
25
|
const maps = new Map([[activeMap.vimText, activeMap]])
|
|
25
26
|
let vim = createInitialContext({ line: 0, col: 0 })
|
|
26
27
|
const keybinds = createKeybinds(config, log)
|
|
@@ -28,24 +29,36 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
28
29
|
let yankTimer: ReturnType<typeof setTimeout> | undefined
|
|
29
30
|
let yankFlashActive = false
|
|
30
31
|
let pendingInsert = ""
|
|
32
|
+
let pendingTarget: { input: EditBufferLike; offset: number } | undefined
|
|
33
|
+
let pendingContext: PromptContext | undefined
|
|
31
34
|
let nativeInsertUndoSaved = false
|
|
32
35
|
let historyText: string | undefined
|
|
36
|
+
let activeInput: EditBufferLike | undefined
|
|
37
|
+
let mapWidth: number | undefined
|
|
38
|
+
let mapWrapMode: string | undefined
|
|
33
39
|
const defaultHistoryKeys = {
|
|
34
40
|
k: !hasNormalKeyPrefix(config, "k"),
|
|
35
41
|
j: !hasNormalKeyPrefix(config, "j"),
|
|
36
42
|
}
|
|
43
|
+
const appendMapped = hasNormalKeyPrefix(config, "A")
|
|
37
44
|
|
|
38
45
|
return {
|
|
46
|
+
attach,
|
|
47
|
+
suspend,
|
|
48
|
+
isPending: () => vim.phase !== "idle" || vim.count > 0 || !!keybinds?.isPending(),
|
|
39
49
|
handle(event: KeyEvent, key: string, ctx: PromptContext) {
|
|
40
50
|
const ref = ctx.prompt()
|
|
41
51
|
if (!ref) return false
|
|
52
|
+
const input = focusedInput(ctx)
|
|
53
|
+
attach(ctx)
|
|
42
54
|
|
|
43
55
|
let vimeeKey = keyForVimee(event, key)
|
|
44
56
|
if (!vimeeKey) return false
|
|
57
|
+
vimeeKey = codec.encode(vimeeKey)
|
|
45
58
|
|
|
46
59
|
if (state.mode() === "insert") {
|
|
47
60
|
if (vimeeKey === "Escape") {
|
|
48
|
-
cancelPendingInsert(ctx
|
|
61
|
+
cancelPendingInsert(ctx)
|
|
49
62
|
enterNormal(ctx)
|
|
50
63
|
state.setPending("")
|
|
51
64
|
updateTimeout(ctx)
|
|
@@ -55,7 +68,6 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
55
68
|
return handleInsertKeybind(event, vimeeKey, ctx)
|
|
56
69
|
}
|
|
57
70
|
|
|
58
|
-
const input = focusedInput(ctx)
|
|
59
71
|
const text = input?.plainText ?? ref.current.input
|
|
60
72
|
const dw = displayWidth(text)
|
|
61
73
|
const displayOff = clamp(input?.cursorOffset ?? dw, 0, dw)
|
|
@@ -80,14 +92,14 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
80
92
|
const pendingBefore = pendingInsert
|
|
81
93
|
sync(map, cursor)
|
|
82
94
|
|
|
83
|
-
if (vimeeKey === "A" && appendVisualLine(input, map)) {
|
|
95
|
+
if (vimeeKey === "A" && vim.mode === "normal" && vim.phase === "idle" && !wasPending && !appendMapped && appendVisualLine(input, map)) {
|
|
84
96
|
state.setPending("")
|
|
85
97
|
updateTimeout(ctx)
|
|
86
98
|
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
|
|
87
99
|
return true
|
|
88
100
|
}
|
|
89
101
|
|
|
90
|
-
const hostEnd = vimeeKey === "e" ? endMotionOffset(map.hostText, charOff, vim.count || 1) : undefined
|
|
102
|
+
const hostEnd = vimeeKey === "e" && vim.phase === "idle" ? charToDisplay(map.hostText, endMotionOffset(map.hostText, charOff, vim.count || 1)) : undefined
|
|
91
103
|
const shouldFlashYank = shouldFlashYankFor(vimeeKey)
|
|
92
104
|
const visualYankRange = visualYankRangeFor(map)
|
|
93
105
|
vimeeKey = textObjectAlias(vimeeKey, vim) ?? vimeeKey
|
|
@@ -114,14 +126,14 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
114
126
|
const actions = applyKeybind(resolved.definition, map)
|
|
115
127
|
result = { newCtx: vim, actions }
|
|
116
128
|
} else {
|
|
117
|
-
result =
|
|
129
|
+
result = processKey(vimeeKey, event.ctrl)
|
|
118
130
|
}
|
|
119
131
|
vim = result.newCtx
|
|
120
132
|
const actions = result.actions as HostAction[]
|
|
121
133
|
if (hostEnd !== undefined && actions.every((action) => action.type === "cursor-move") && hostEnd > hostOffset(map, vim.cursor, "previous")) vim = { ...vim, cursor: hostPosition(map, hostEnd) }
|
|
122
134
|
let clampFinalCursor = true
|
|
123
135
|
const content = actions.find((action) => action.type === "content-change")?.content
|
|
124
|
-
if (vimeeKey === "x" && content !== undefined) {
|
|
136
|
+
if (vimeeKey === "x" && state.mode() === "normal" && content !== undefined) {
|
|
125
137
|
const next = nextMap(map, content)
|
|
126
138
|
const nextDW = displayWidth(next.hostText)
|
|
127
139
|
const target = clamp(displayOff, 0, Math.max(0, nextDW - 1))
|
|
@@ -143,33 +155,85 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
143
155
|
return consumesKey(vimeeKey, actions, vim, keybindPending)
|
|
144
156
|
},
|
|
145
157
|
cleanup() {
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
suspend()
|
|
159
|
+
maps.clear()
|
|
148
160
|
},
|
|
149
161
|
}
|
|
150
162
|
|
|
163
|
+
function attach(ctx: PromptContext) {
|
|
164
|
+
const input = focusedInput(ctx)
|
|
165
|
+
if (activeInput === input) return
|
|
166
|
+
suspend()
|
|
167
|
+
activeInput = input
|
|
168
|
+
activeMap = createPromptMap(input?.plainText ?? ctx.prompt()?.current.input ?? "", input, codec)
|
|
169
|
+
maps.clear()
|
|
170
|
+
rememberMap(activeMap)
|
|
171
|
+
buffer = new TextBuffer(activeMap.vimText)
|
|
172
|
+
nativeInsertUndoSaved = false
|
|
173
|
+
vim = { ...resetContext(vim), cursor: hostPosition(activeMap, input?.cursorOffset ?? 0), mode: state.mode() }
|
|
174
|
+
mapWidth = input?.width
|
|
175
|
+
mapWrapMode = input?.wrapMode
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function suspend() {
|
|
179
|
+
if (pendingContext) cancelPendingInsert(pendingContext)
|
|
180
|
+
if (timer) clearTimeout(timer)
|
|
181
|
+
timer = undefined
|
|
182
|
+
keybinds?.cancel()
|
|
183
|
+
cancelYankFlash()
|
|
184
|
+
if (activeInput && !activeInput.isDestroyed) clearVisualSelection(activeInput)
|
|
185
|
+
if (isVisualMode(state.mode())) state.setMode("normal")
|
|
186
|
+
vim = resetContext(vim)
|
|
187
|
+
state.setPending("")
|
|
188
|
+
pendingTarget = undefined
|
|
189
|
+
pendingContext = undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
151
192
|
function sync(map: PromptMap, cursor: CursorPosition) {
|
|
152
193
|
if (buffer.getContent() !== map.vimText) buffer.replaceContent(map.vimText)
|
|
153
194
|
vim = { ...vim, cursor, mode: state.mode() }
|
|
154
195
|
}
|
|
155
196
|
|
|
156
197
|
function mapForHostText(text: string, input: ReturnType<typeof focusedInput>) {
|
|
157
|
-
|
|
158
|
-
|
|
198
|
+
const textChanged = activeMap.hostText !== text
|
|
199
|
+
if (!textChanged && mapWidth === input?.width && mapWrapMode === input?.wrapMode) return activeMap
|
|
200
|
+
if (textChanged && state.mode() === "insert") recordNativeChange(text)
|
|
201
|
+
activeMap = createPromptMap(text, input, codec)
|
|
202
|
+
mapWidth = input?.width
|
|
203
|
+
mapWrapMode = input?.wrapMode
|
|
204
|
+
if (textChanged && state.mode() !== "insert") maps.clear()
|
|
159
205
|
rememberMap(activeMap)
|
|
160
|
-
if (state.mode() === "insert") {
|
|
206
|
+
if (textChanged && state.mode() === "insert") {
|
|
161
207
|
if (!nativeInsertUndoSaved) {
|
|
162
208
|
buffer.saveUndoPoint(vim.cursor)
|
|
163
209
|
nativeInsertUndoSaved = true
|
|
164
210
|
}
|
|
165
211
|
buffer.replaceContent(activeMap.vimText)
|
|
166
|
-
} else {
|
|
212
|
+
} else if (textChanged) {
|
|
167
213
|
buffer = new TextBuffer(activeMap.vimText)
|
|
168
214
|
nativeInsertUndoSaved = false
|
|
215
|
+
} else {
|
|
216
|
+
buffer.replaceContent(activeMap.vimText)
|
|
169
217
|
}
|
|
170
218
|
return activeMap
|
|
171
219
|
}
|
|
172
220
|
|
|
221
|
+
function recordNativeChange(text: string) {
|
|
222
|
+
const before = codec.encode(activeMap.hostText)
|
|
223
|
+
const after = codec.encode(text)
|
|
224
|
+
const start = codec.encode(activeMap.hostText.slice(0, displayToChar(activeMap.hostText, hostOffset(activeMap, vim.cursor)))).length
|
|
225
|
+
let prefix = 0
|
|
226
|
+
while (prefix < start && prefix < after.length && before[prefix] === after[prefix]) prefix++
|
|
227
|
+
let suffix = 0
|
|
228
|
+
while (suffix < before.length - start && suffix < after.length - prefix && before[before.length - suffix - 1] === after[after.length - suffix - 1]) suffix++
|
|
229
|
+
const keys = [...vim.pendingChange]
|
|
230
|
+
if (!keys.length) keys.push("i")
|
|
231
|
+
for (let index = prefix; index < start; index++) keys.push("Backspace")
|
|
232
|
+
for (let index = start; index < before.length - suffix; index++) keys.push("Delete")
|
|
233
|
+
for (const key of after.slice(prefix, after.length - suffix)) keys.push(key === "\n" ? "Enter" : key === "\t" ? "Tab" : key)
|
|
234
|
+
vim = { ...vim, pendingChange: keys }
|
|
235
|
+
}
|
|
236
|
+
|
|
173
237
|
function rememberMap(map: PromptMap) {
|
|
174
238
|
maps.set(map.vimText, map)
|
|
175
239
|
}
|
|
@@ -201,7 +265,11 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
201
265
|
setCursor(input, currentMap, action.position)
|
|
202
266
|
break
|
|
203
267
|
case "mode-change":
|
|
204
|
-
nativeInsertUndoSaved =
|
|
268
|
+
nativeInsertUndoSaved = action.mode === "insert" && actions.some((item) => item.type === "content-change")
|
|
269
|
+
if (action.mode === "insert") {
|
|
270
|
+
cancelYankFlash()
|
|
271
|
+
if (input) clearVisualSelection(input)
|
|
272
|
+
}
|
|
205
273
|
syncMode(state, action.mode)
|
|
206
274
|
break
|
|
207
275
|
case "quit":
|
|
@@ -220,7 +288,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
220
288
|
syncVisualSelection(input, currentMap, ctx)
|
|
221
289
|
}
|
|
222
290
|
|
|
223
|
-
function handleInsertKeybind(event: KeyEvent, key: string, ctx: PromptContext) {
|
|
291
|
+
function handleInsertKeybind(event: KeyEvent, key: string, ctx: PromptContext): boolean {
|
|
224
292
|
if (!keybinds?.hasKeybinds("insert") && !keybinds?.isPending()) return false
|
|
225
293
|
|
|
226
294
|
const wasPending = keybinds.isPending()
|
|
@@ -229,7 +297,12 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
229
297
|
|
|
230
298
|
switch (resolved.status) {
|
|
231
299
|
case "pending":
|
|
232
|
-
|
|
300
|
+
if (!wasPending) {
|
|
301
|
+
const input = focusedInput(ctx)
|
|
302
|
+
pendingTarget = input ? { input, offset: input.cursorOffset ?? 0 } : undefined
|
|
303
|
+
pendingContext = ctx
|
|
304
|
+
}
|
|
305
|
+
pendingInsert = codec.decode(plainPending(resolved.display))
|
|
233
306
|
state.setPending(resolved.display)
|
|
234
307
|
updateTimeout(ctx)
|
|
235
308
|
return true
|
|
@@ -254,7 +327,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
254
327
|
pendingInsert = ""
|
|
255
328
|
state.setPending("")
|
|
256
329
|
updateTimeout(ctx)
|
|
257
|
-
return false
|
|
330
|
+
return wasPending ? handleInsertKeybind(event, key, ctx) : false
|
|
258
331
|
}
|
|
259
332
|
}
|
|
260
333
|
|
|
@@ -278,15 +351,20 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
278
351
|
const ref = ctx.prompt()
|
|
279
352
|
const input = focusedInput(ctx)
|
|
280
353
|
const text = input?.plainText ?? ref?.current.input ?? ""
|
|
354
|
+
const map = mapForHostText(text, input)
|
|
281
355
|
|
|
282
356
|
if (input && text.length > 0) {
|
|
283
357
|
const dw = displayWidth(text)
|
|
284
358
|
const offset = clamp(input?.cursorOffset ?? dw, 0, dw)
|
|
285
|
-
|
|
359
|
+
const charOffset = displayToChar(text, offset)
|
|
360
|
+
if (charOffset > 0 && text[charOffset - 1] !== "\n") {
|
|
361
|
+
input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, offset - 1)))
|
|
362
|
+
}
|
|
286
363
|
clampNormalCursor(input)
|
|
287
364
|
}
|
|
288
365
|
|
|
289
|
-
|
|
366
|
+
const lastChange = nativeInsertUndoSaved ? [...vim.pendingChange, "Escape"] : vim.lastChange
|
|
367
|
+
vim = { ...resetContext(vim), cursor: hostPosition(map, input?.cursorOffset ?? 0), mode: "normal", statusMessage: "", lastChange, pendingChange: [] }
|
|
290
368
|
nativeInsertUndoSaved = false
|
|
291
369
|
syncMode(state, "normal")
|
|
292
370
|
}
|
|
@@ -308,13 +386,97 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
308
386
|
|
|
309
387
|
let actions: HostAction[] = []
|
|
310
388
|
for (const token of parseKeySequence(definition.keys)) {
|
|
311
|
-
const result =
|
|
389
|
+
const result = processKey(codec.encode(keyToken(token)), tokenCtrl(token))
|
|
312
390
|
vim = result.newCtx
|
|
313
391
|
actions = [...actions, ...(result.actions as HostAction[])]
|
|
314
392
|
}
|
|
315
393
|
return actions
|
|
316
394
|
}
|
|
317
395
|
|
|
396
|
+
function processKey(key: string, ctrl = false): { newCtx: VimContext; actions: HostAction[] } {
|
|
397
|
+
let wordRange: MotionRange | undefined
|
|
398
|
+
if (!ctrl && vim.mode !== "insert" && (key === "w" || key === "W") && (vim.phase === "idle" || vim.phase === "operator-pending")) {
|
|
399
|
+
wordRange = forwardWordRange(key, vim.cursor, buffer, vim.count || 1)
|
|
400
|
+
if (vim.phase === "idle") {
|
|
401
|
+
const cursor = { ...wordRange.end, col: Math.min(wordRange.end.col, Math.max(0, buffer.getLineLength(wordRange.end.line) - 1)) }
|
|
402
|
+
return { newCtx: { ...resetContext(vim), cursor }, actions: [{ type: "cursor-move", position: cursor }] }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (key === "." && !ctrl && vim.mode === "normal" && vim.phase === "idle" && vim.lastChange.length) {
|
|
406
|
+
// A repeated insert/change is one undo step, including native typing.
|
|
407
|
+
const original = buffer
|
|
408
|
+
const before = buffer.getContent()
|
|
409
|
+
const cursor = vim.cursor
|
|
410
|
+
const keys = [...vim.lastChange]
|
|
411
|
+
if (vim.count) {
|
|
412
|
+
while (/^[0-9]$/.test(keys[0] ?? "")) keys.shift()
|
|
413
|
+
keys.unshift(...String(vim.count))
|
|
414
|
+
}
|
|
415
|
+
vim = { ...vim, count: 0 }
|
|
416
|
+
const actions: HostAction[] = []
|
|
417
|
+
buffer = new TextBuffer(before)
|
|
418
|
+
try {
|
|
419
|
+
for (const token of keys) {
|
|
420
|
+
const result = processKey(token)
|
|
421
|
+
vim = result.newCtx
|
|
422
|
+
actions.push(...result.actions)
|
|
423
|
+
}
|
|
424
|
+
if (buffer.getContent() !== before) {
|
|
425
|
+
original.saveUndoPoint(cursor)
|
|
426
|
+
original.replaceContent(buffer.getContent())
|
|
427
|
+
}
|
|
428
|
+
} finally {
|
|
429
|
+
buffer = original
|
|
430
|
+
}
|
|
431
|
+
return { newCtx: { ...vim, lastChange: keys, pendingChange: [] }, actions }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let range: MotionRange | undefined
|
|
435
|
+
const operator = vim.operator
|
|
436
|
+
if (!ctrl && operator && textObjectOperator(operator)) {
|
|
437
|
+
if (vim.phase === "operator-pending") {
|
|
438
|
+
const count = vim.count || 1
|
|
439
|
+
if (key === "l" || key === "ArrowRight") {
|
|
440
|
+
// Vimee 0.3 treats l as inclusive; Vim's operator motion is exclusive.
|
|
441
|
+
range = { start: vim.cursor, end: { line: vim.cursor.line, col: Math.min(buffer.getLineLength(vim.cursor.line), vim.cursor.col + count) }, inclusive: false, linewise: false }
|
|
442
|
+
} else if (key === operator) {
|
|
443
|
+
range = { start: { line: vim.cursor.line, col: 0 }, end: { line: Math.min(buffer.getLineCount() - 1, vim.cursor.line + count - 1), col: 0 }, inclusive: true, linewise: true }
|
|
444
|
+
} else if (operator === "c" && key === "w" && /\S/.test(buffer.getLine(vim.cursor.line)[vim.cursor.col] ?? "")) {
|
|
445
|
+
range = resolveMotion("e", { ...vim.cursor, col: vim.cursor.col - 1 }, buffer, count, vim.count > 0, vim)?.range
|
|
446
|
+
if (range) range.start = vim.cursor
|
|
447
|
+
} else if (wordRange) {
|
|
448
|
+
range = wordRange
|
|
449
|
+
if (operator === "d" && range.end.line > range.start.line && /\S/.test(buffer.getLine(range.start.line)[range.start.col] ?? "") && range.end.col === Math.max(0, buffer.getLine(range.end.line).search(/\S/))) {
|
|
450
|
+
range.end = { line: range.end.line - 1, col: buffer.getLineLength(range.end.line - 1) }
|
|
451
|
+
}
|
|
452
|
+
} else if (operator === "c") {
|
|
453
|
+
const motion = resolveMotion(key, vim.cursor, buffer, count, vim.count > 0, vim)
|
|
454
|
+
if (motion?.range.linewise) range = motion.range
|
|
455
|
+
}
|
|
456
|
+
} else if (vim.phase === "text-object-pending" && vim.textObjectModifier) {
|
|
457
|
+
range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer) ?? undefined
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (range && operator) {
|
|
461
|
+
const keys: string[] = []
|
|
462
|
+
if (vim.count) keys.push(...String(vim.count))
|
|
463
|
+
for (const token of vim.pendingChange) {
|
|
464
|
+
if (!/^[0-9]$/.test(token)) keys.push(token)
|
|
465
|
+
}
|
|
466
|
+
keys.push(key)
|
|
467
|
+
const result = executeTextObject(operator, range, buffer, vim)
|
|
468
|
+
if (operator === "y") result.context.pendingChange = []
|
|
469
|
+
else if (result.context.mode === "insert") result.context.pendingChange = keys
|
|
470
|
+
else { result.context.lastChange = keys; result.context.pendingChange = [] }
|
|
471
|
+
return { newCtx: result.context, actions: result.actions }
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// The engine saves undo points for yanks too. Run read-only operations
|
|
475
|
+
// against a disposable buffer so they cannot alter the editing history.
|
|
476
|
+
const yanking = operator === "y" || (isVisualMode(vim.mode) && key === "y") || (vim.mode === "normal" && key === "Y")
|
|
477
|
+
return processKeystroke(key, vim, yanking ? new TextBuffer(buffer.getContent()) : buffer, ctrl, false)
|
|
478
|
+
}
|
|
479
|
+
|
|
318
480
|
function handleTextObject(key: string, ctx: PromptContext, map: PromptMap) {
|
|
319
481
|
if (vim.phase !== "text-object-pending" || !vim.textObjectModifier) return undefined
|
|
320
482
|
const range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer)
|
|
@@ -332,15 +494,10 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
332
494
|
}
|
|
333
495
|
if (!textObjectOperator(vim.operator)) return undefined
|
|
334
496
|
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
if (vim.operator === "y") {
|
|
338
|
-
result.context = { ...result.context, cursor: originalCursor }
|
|
339
|
-
result.actions = result.actions.map((action) => (action.type === "cursor-move" ? { ...action, position: originalCursor } : action))
|
|
340
|
-
}
|
|
341
|
-
vim = result.context
|
|
497
|
+
const result = processKey(key)
|
|
498
|
+
vim = result.newCtx
|
|
342
499
|
applyActions(result.actions, ctx, map)
|
|
343
|
-
if (flashRange) flashYank(ctx, activeMap,
|
|
500
|
+
if (flashRange) flashYank(ctx, activeMap, result.actions.find((action): action is YankAction => action.type === "yank"), flashRange)
|
|
344
501
|
syncMode(state, vim.mode)
|
|
345
502
|
state.setPending("")
|
|
346
503
|
updateTimeout(ctx)
|
|
@@ -368,11 +525,23 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
368
525
|
keybinds.cancel()
|
|
369
526
|
if (flush) flushPendingInsert(ctx, pendingInsert, offset)
|
|
370
527
|
pendingInsert = ""
|
|
528
|
+
pendingTarget = undefined
|
|
529
|
+
pendingContext = undefined
|
|
371
530
|
state.setPending("")
|
|
372
531
|
}
|
|
373
532
|
|
|
374
533
|
function flushPendingInsert(ctx: PromptContext, value: string, charOffset?: number) {
|
|
375
534
|
if (!value || state.mode() !== "insert") return
|
|
535
|
+
if (pendingTarget?.input.isDestroyed) return
|
|
536
|
+
if (pendingTarget?.input.insertText && !pendingTarget.input.isDestroyed) {
|
|
537
|
+
const { input, offset } = pendingTarget
|
|
538
|
+
const cursor = input.cursorOffset ?? 0
|
|
539
|
+
input.clearSelection?.()
|
|
540
|
+
input.cursorOffset = offset
|
|
541
|
+
input.insertText!(value)
|
|
542
|
+
input.cursorOffset = cursor >= offset ? cursor + displayWidth(value) : cursor
|
|
543
|
+
return
|
|
544
|
+
}
|
|
376
545
|
const ref = ctx.prompt()
|
|
377
546
|
if (!ref) return
|
|
378
547
|
const input = focusedInput(ctx)
|
|
@@ -400,7 +569,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
400
569
|
if (!input?.gotoVisualLineEnd) return false
|
|
401
570
|
clearVisualSelection(input)
|
|
402
571
|
input.gotoVisualLineEnd()
|
|
403
|
-
vim = { ...vim, cursor: hostPosition(map, input.cursorOffset ?? displayWidth(map.hostText)), mode: "insert", phase: "idle", count: 0, operator: null, statusMessage: "-- INSERT --" }
|
|
572
|
+
vim = { ...vim, cursor: hostPosition(map, input.cursorOffset ?? displayWidth(map.hostText)), mode: "insert", phase: "idle", count: 0, operator: null, pendingChange: ["A"], statusMessage: "-- INSERT --" }
|
|
404
573
|
nativeInsertUndoSaved = false
|
|
405
574
|
syncMode(state, "insert")
|
|
406
575
|
return true
|
|
@@ -460,6 +629,30 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
460
629
|
}
|
|
461
630
|
}
|
|
462
631
|
|
|
632
|
+
function forwardWordRange(key: string, start: CursorPosition, buffer: TextBuffer, count: number): MotionRange {
|
|
633
|
+
let { line, col } = start
|
|
634
|
+
for (let index = 0; index < count; index++) {
|
|
635
|
+
const text = buffer.getLine(line)
|
|
636
|
+
const kind = wordClass(text[col], key === "W")
|
|
637
|
+
while (col < text.length && wordClass(text[col], key === "W") === kind) col++
|
|
638
|
+
while (col < text.length && /\s/.test(text[col])) col++
|
|
639
|
+
while (col >= buffer.getLineLength(line) && line < buffer.getLineCount() - 1) {
|
|
640
|
+
line++
|
|
641
|
+
col = 0
|
|
642
|
+
const next = buffer.getLine(line)
|
|
643
|
+
if (!next.length) break
|
|
644
|
+
while (col < next.length && /\s/.test(next[col])) col++
|
|
645
|
+
if (col < next.length) break
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return { start, end: { line, col }, linewise: false, inclusive: false }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function wordClass(char: string | undefined, big: boolean) {
|
|
652
|
+
if (!char || /\s/.test(char)) return "space"
|
|
653
|
+
return big || /\w/.test(char) ? "word" : "punctuation"
|
|
654
|
+
}
|
|
655
|
+
|
|
463
656
|
function visualCharRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined {
|
|
464
657
|
const anchorOffset = hostOffset(map, anchor, "next")
|
|
465
658
|
const cursorOffset = hostOffset(map, cursor, "previous")
|
|
@@ -592,12 +785,12 @@ function clampNormalCursor(input: EditBufferLike) {
|
|
|
592
785
|
if (cursor.visualCol === 0) return
|
|
593
786
|
const dw = displayWidth(text)
|
|
594
787
|
if (offset >= dw) {
|
|
595
|
-
input.cursorOffset = Math.max(0, dw - 1)
|
|
788
|
+
input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, dw - 1)))
|
|
596
789
|
return
|
|
597
790
|
}
|
|
598
791
|
const charIdx = displayToChar(text, offset)
|
|
599
792
|
if (charIdx < text.length && text[charIdx] === '\n') {
|
|
600
|
-
input.cursorOffset = Math.max(0, offset - 1)
|
|
793
|
+
input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, offset - 1)))
|
|
601
794
|
}
|
|
602
795
|
}
|
|
603
796
|
|
|
@@ -623,7 +816,7 @@ function defaultHistoryCommand(key: string, text: string, historyText: string |
|
|
|
623
816
|
return key === "k" ? "prompt.history.previous" : "prompt.history.next"
|
|
624
817
|
}
|
|
625
818
|
|
|
626
|
-
function hasNormalKeyPrefix(config: VimConfig, key:
|
|
819
|
+
function hasNormalKeyPrefix(config: VimConfig, key: string) {
|
|
627
820
|
for (const sequence of Object.keys(config.keymaps.normal ?? {})) {
|
|
628
821
|
try {
|
|
629
822
|
if (keyToken(parseKeySequence(sequence)[0] ?? "") === key) return true
|
|
@@ -697,6 +890,7 @@ function keybindAction(action: string): HostKeybindDefinition {
|
|
|
697
890
|
}
|
|
698
891
|
|
|
699
892
|
function keyForVimee(event: KeyEvent, key: string) {
|
|
893
|
+
if (key === "<C-[>") return "Escape"
|
|
700
894
|
if (event.ctrl) return event.name?.toLowerCase()
|
|
701
895
|
const token = keyToken(key)
|
|
702
896
|
return token.startsWith("<") ? undefined : token
|
|
@@ -728,10 +922,12 @@ function syncMode(state: VimState, mode: VimContext["mode"]) {
|
|
|
728
922
|
}
|
|
729
923
|
|
|
730
924
|
function pendingDisplay(ctx: VimContext, keybindPending: boolean) {
|
|
731
|
-
|
|
732
|
-
if (ctx.phase === "
|
|
925
|
+
const count = ctx.count > 0 ? String(ctx.count) : ""
|
|
926
|
+
if (ctx.phase === "operator-pending") return count + (ctx.operator ?? "")
|
|
927
|
+
if (ctx.phase === "text-object-pending") return count + (ctx.operator ?? "") + (ctx.textObjectModifier ?? "")
|
|
928
|
+
if (ctx.phase === "char-pending") return count + (ctx.operator ?? "") + (ctx.charCommand ?? "")
|
|
733
929
|
if (keybindPending) return ctx.statusMessage
|
|
734
|
-
return
|
|
930
|
+
return count
|
|
735
931
|
}
|
|
736
932
|
|
|
737
933
|
function plainPending(value: string) {
|
|
@@ -771,7 +967,7 @@ function quoteRange(modifier: "i" | "a", cursor: CursorPosition, buffer: TextBuf
|
|
|
771
967
|
start: { line: cursor.line, col: start },
|
|
772
968
|
end: { line: cursor.line, col: Math.max(start, end) },
|
|
773
969
|
linewise: false,
|
|
774
|
-
inclusive:
|
|
970
|
+
inclusive: end >= start,
|
|
775
971
|
}
|
|
776
972
|
}
|
|
777
973
|
|
|
@@ -863,7 +1059,7 @@ function quotePair(cursor: CursorPosition, buffer: TextBuffer, quote: string) {
|
|
|
863
1059
|
}
|
|
864
1060
|
|
|
865
1061
|
function executeTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer, ctx: VimContext) {
|
|
866
|
-
buffer.saveUndoPoint(ctx.cursor)
|
|
1062
|
+
if (operator !== "y") buffer.saveUndoPoint(ctx.cursor)
|
|
867
1063
|
const result = range.linewise ? executeLinewiseTextObject(operator, range, buffer) : executeCharwiseTextObject(operator, range, buffer)
|
|
868
1064
|
const registers = ctx.selectedRegister ? { ...ctx.registers, [ctx.selectedRegister]: result.yankedText } : ctx.registers
|
|
869
1065
|
const context = {
|
|
@@ -888,9 +1084,9 @@ function executeLinewiseTextObject(operator: Operator, range: MotionRange, buffe
|
|
|
888
1084
|
}
|
|
889
1085
|
|
|
890
1086
|
buffer.deleteLines(startLine, lineCount)
|
|
891
|
-
if (
|
|
1087
|
+
if (operator === "c") buffer.insertLine(startLine, "")
|
|
1088
|
+
else if (buffer.getLineCount() === 0) buffer.insertLine(0, "")
|
|
892
1089
|
const line = Math.min(startLine, buffer.getLineCount() - 1)
|
|
893
|
-
if (operator === "c") buffer.insertLine(line, "")
|
|
894
1090
|
return {
|
|
895
1091
|
actions: [{ type: "content-change", content: buffer.getContent() }] as VimeeAction[],
|
|
896
1092
|
cursor: { line, col: 0 },
|