opencode-vim 0.0.19 → 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 +70 -18
- package/docs/configuration.md +13 -3
- package/docs/keymap-actions.md +213 -0
- 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/index.tsx +15 -27
- package/src/modules/vim/keys.ts +5 -1
- package/src/modules/vim/map.test.ts +45 -0
- package/src/modules/vim/map.ts +62 -29
- package/src/modules/vim/state.test.ts +18 -0
- package/src/modules/vim/state.ts +7 -1
- package/src/modules/vim/view.tsx +2 -0
- package/src/modules/vim/vimee.test.ts +211 -0
- package/src/modules/vim/vimee.ts +372 -82
- package/tui.tsx +281 -1
- package/view.tsx +46 -0
- package/tui.json.example +0 -37
package/src/modules/vim/vimee.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
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"
|
|
8
|
-
import {
|
|
7
|
+
import { createGraphemeCodec } from "./graphemes"
|
|
8
|
+
import { charToDisplay, displayToChar, displayWidth, createPromptMap, derivePromptMap, hostOffset, hostPosition, type PromptMap } from "./map"
|
|
9
9
|
import type { createVimState } from "./state"
|
|
10
10
|
|
|
11
11
|
type VimState = ReturnType<typeof createVimState>
|
|
12
|
-
type HostAction = VimeeAction | { type: "submit" }
|
|
13
|
-
type HostKeybindAction = "normal" | "submit"
|
|
14
|
-
type HostKeybindDefinition = KeybindDefinition & { hostAction?: HostKeybindAction }
|
|
12
|
+
type HostAction = VimeeAction | { type: "submit" } | { type: "command"; command: string }
|
|
13
|
+
type HostKeybindAction = "normal" | "submit" | "command"
|
|
14
|
+
type HostKeybindDefinition = KeybindDefinition & { hostAction?: HostKeybindAction; command?: string }
|
|
15
15
|
type HostRange = { start: number; end: number }
|
|
16
16
|
|
|
17
17
|
const YANK_FLASH_MS = 250
|
|
@@ -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,28 +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
|
|
35
|
+
let historyText: string | undefined
|
|
36
|
+
let activeInput: EditBufferLike | undefined
|
|
37
|
+
let mapWidth: number | undefined
|
|
38
|
+
let mapWrapMode: string | undefined
|
|
39
|
+
const defaultHistoryKeys = {
|
|
40
|
+
k: !hasNormalKeyPrefix(config, "k"),
|
|
41
|
+
j: !hasNormalKeyPrefix(config, "j"),
|
|
42
|
+
}
|
|
43
|
+
const appendMapped = hasNormalKeyPrefix(config, "A")
|
|
32
44
|
|
|
33
45
|
return {
|
|
46
|
+
attach,
|
|
47
|
+
suspend,
|
|
48
|
+
isPending: () => vim.phase !== "idle" || vim.count > 0 || !!keybinds?.isPending(),
|
|
34
49
|
handle(event: KeyEvent, key: string, ctx: PromptContext) {
|
|
35
50
|
const ref = ctx.prompt()
|
|
36
51
|
if (!ref) return false
|
|
52
|
+
const input = focusedInput(ctx)
|
|
53
|
+
attach(ctx)
|
|
37
54
|
|
|
38
55
|
let vimeeKey = keyForVimee(event, key)
|
|
39
56
|
if (!vimeeKey) return false
|
|
40
|
-
|
|
41
|
-
if (state.mode() === "normal" && key === "<CR>") {
|
|
42
|
-
ref.submit()
|
|
43
|
-
return true
|
|
44
|
-
}
|
|
57
|
+
vimeeKey = codec.encode(vimeeKey)
|
|
45
58
|
|
|
46
59
|
if (state.mode() === "insert") {
|
|
47
|
-
if (key === "<CR>") {
|
|
48
|
-
cancelPendingInsert(ctx)
|
|
49
|
-
return false
|
|
50
|
-
}
|
|
51
60
|
if (vimeeKey === "Escape") {
|
|
52
|
-
cancelPendingInsert(ctx
|
|
61
|
+
cancelPendingInsert(ctx)
|
|
53
62
|
enterNormal(ctx)
|
|
54
63
|
state.setPending("")
|
|
55
64
|
updateTimeout(ctx)
|
|
@@ -59,82 +68,172 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
59
68
|
return handleInsertKeybind(event, vimeeKey, ctx)
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
const input = focusedInput(ctx)
|
|
63
71
|
const text = input?.plainText ?? ref.current.input
|
|
64
|
-
const
|
|
72
|
+
const dw = displayWidth(text)
|
|
73
|
+
const displayOff = clamp(input?.cursorOffset ?? dw, 0, dw)
|
|
74
|
+
const charOff = displayToChar(text, displayOff)
|
|
75
|
+
|
|
76
|
+
const canBrowseHistory = vim.phase === "idle" && vim.count === 0 && !keybinds?.isPending()
|
|
77
|
+
const historyCommand = canBrowseHistory ? defaultHistoryCommand(vimeeKey, text, historyText, defaultHistoryKeys) : undefined
|
|
78
|
+
if (historyCommand) {
|
|
79
|
+
const result = dispatchCommand(historyCommand, ctx)
|
|
80
|
+
historyText = result.ok ? (focusedInput(ctx)?.plainText ?? ref.current.input) : undefined
|
|
81
|
+
state.setPending("")
|
|
82
|
+
updateTimeout(ctx)
|
|
83
|
+
log("vimee.history", { key, command: historyCommand, handled: result.ok })
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
historyText = undefined
|
|
65
87
|
|
|
66
88
|
const map = mapForHostText(text, input)
|
|
67
|
-
const cursor = hostPosition(map,
|
|
89
|
+
const cursor = hostPosition(map, displayOff)
|
|
68
90
|
|
|
69
91
|
const wasPending = keybinds?.isPending() ?? false
|
|
70
92
|
const pendingBefore = pendingInsert
|
|
71
93
|
sync(map, cursor)
|
|
72
94
|
|
|
73
|
-
if (vimeeKey === "A" && appendVisualLine(input, map)) {
|
|
95
|
+
if (vimeeKey === "A" && vim.mode === "normal" && vim.phase === "idle" && !wasPending && !appendMapped && appendVisualLine(input, map)) {
|
|
74
96
|
state.setPending("")
|
|
75
97
|
updateTimeout(ctx)
|
|
76
98
|
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
|
|
77
99
|
return true
|
|
78
100
|
}
|
|
79
101
|
|
|
80
|
-
const hostEnd = vimeeKey === "e" ? endMotionOffset(map.hostText,
|
|
102
|
+
const hostEnd = vimeeKey === "e" && vim.phase === "idle" ? charToDisplay(map.hostText, endMotionOffset(map.hostText, charOff, vim.count || 1)) : undefined
|
|
81
103
|
const shouldFlashYank = shouldFlashYankFor(vimeeKey)
|
|
82
104
|
const visualYankRange = visualYankRangeFor(map)
|
|
83
105
|
vimeeKey = textObjectAlias(vimeeKey, vim) ?? vimeeKey
|
|
84
106
|
const textObjectHandled = handleTextObject(vimeeKey, ctx, map)
|
|
85
107
|
if (textObjectHandled !== undefined) return textObjectHandled
|
|
86
|
-
const
|
|
108
|
+
const resolved = keybinds?.resolve(vimeeKey, vim.mode, event.ctrl)
|
|
109
|
+
if (resolved?.status === "pending") {
|
|
110
|
+
vim = { ...vim, statusMessage: resolved.display }
|
|
111
|
+
state.setPending(resolved.display)
|
|
112
|
+
updateTimeout(ctx)
|
|
113
|
+
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: [] })
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
if (vimeeKey === "Enter" && state.mode() === "normal" && vim.phase === "idle" && !wasPending && resolved?.status !== "matched") {
|
|
117
|
+
ref.submit()
|
|
118
|
+
state.setPending("")
|
|
119
|
+
updateTimeout(ctx)
|
|
120
|
+
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["submit"] })
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let result
|
|
125
|
+
if (resolved?.status === "matched") {
|
|
126
|
+
const actions = applyKeybind(resolved.definition, map)
|
|
127
|
+
result = { newCtx: vim, actions }
|
|
128
|
+
} else {
|
|
129
|
+
result = processKey(vimeeKey, event.ctrl)
|
|
130
|
+
}
|
|
87
131
|
vim = result.newCtx
|
|
88
|
-
|
|
132
|
+
const actions = result.actions as HostAction[]
|
|
133
|
+
if (hostEnd !== undefined && actions.every((action) => action.type === "cursor-move") && hostEnd > hostOffset(map, vim.cursor, "previous")) vim = { ...vim, cursor: hostPosition(map, hostEnd) }
|
|
89
134
|
let clampFinalCursor = true
|
|
90
|
-
const content =
|
|
91
|
-
if (vimeeKey === "x" && content !== undefined) {
|
|
135
|
+
const content = actions.find((action) => action.type === "content-change")?.content
|
|
136
|
+
if (vimeeKey === "x" && state.mode() === "normal" && content !== undefined) {
|
|
92
137
|
const next = nextMap(map, content)
|
|
93
|
-
const
|
|
94
|
-
|
|
138
|
+
const nextDW = displayWidth(next.hostText)
|
|
139
|
+
const target = clamp(displayOff, 0, Math.max(0, nextDW - 1))
|
|
140
|
+
if (charOff < map.hostText.length - 1 && map.hostText[charOff + 1] !== "\n" && hostOffset(next, vim.cursor, "previous") < target) {
|
|
95
141
|
vim = { ...vim, cursor: hostPosition(next, target) }
|
|
96
142
|
clampFinalCursor = false
|
|
97
143
|
}
|
|
98
144
|
}
|
|
99
|
-
applyActions(
|
|
100
|
-
if (shouldFlashYank) flashYank(ctx, activeMap, yankAction(
|
|
145
|
+
applyActions(actions, ctx, map, clampFinalCursor)
|
|
146
|
+
if (shouldFlashYank) flashYank(ctx, activeMap, yankAction(actions), visualYankRange)
|
|
101
147
|
syncMode(state, vim.mode)
|
|
148
|
+
|
|
102
149
|
const keybindPending = keybinds?.isPending() ?? false
|
|
103
|
-
if (wasPending && !keybindPending && pendingBefore && state.mode() === "insert") flushPendingInsert(ctx, pendingBefore,
|
|
150
|
+
if (wasPending && !keybindPending && pendingBefore && state.mode() === "insert") flushPendingInsert(ctx, pendingBefore, charOff)
|
|
104
151
|
pendingInsert = keybindPending && state.mode() === "insert" ? plainPending(vim.statusMessage) : ""
|
|
105
152
|
state.setPending(pendingDisplay(vim, keybindPending))
|
|
106
153
|
updateTimeout(ctx)
|
|
107
|
-
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions:
|
|
108
|
-
return consumesKey(vimeeKey,
|
|
154
|
+
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: actions.map((action) => action.type) })
|
|
155
|
+
return consumesKey(vimeeKey, actions, vim, keybindPending)
|
|
109
156
|
},
|
|
110
157
|
cleanup() {
|
|
111
|
-
|
|
112
|
-
|
|
158
|
+
suspend()
|
|
159
|
+
maps.clear()
|
|
113
160
|
},
|
|
114
161
|
}
|
|
115
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
|
+
|
|
116
192
|
function sync(map: PromptMap, cursor: CursorPosition) {
|
|
117
193
|
if (buffer.getContent() !== map.vimText) buffer.replaceContent(map.vimText)
|
|
118
194
|
vim = { ...vim, cursor, mode: state.mode() }
|
|
119
195
|
}
|
|
120
196
|
|
|
121
197
|
function mapForHostText(text: string, input: ReturnType<typeof focusedInput>) {
|
|
122
|
-
|
|
123
|
-
|
|
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()
|
|
124
205
|
rememberMap(activeMap)
|
|
125
|
-
if (state.mode() === "insert") {
|
|
206
|
+
if (textChanged && state.mode() === "insert") {
|
|
126
207
|
if (!nativeInsertUndoSaved) {
|
|
127
208
|
buffer.saveUndoPoint(vim.cursor)
|
|
128
209
|
nativeInsertUndoSaved = true
|
|
129
210
|
}
|
|
130
211
|
buffer.replaceContent(activeMap.vimText)
|
|
131
|
-
} else {
|
|
212
|
+
} else if (textChanged) {
|
|
132
213
|
buffer = new TextBuffer(activeMap.vimText)
|
|
133
214
|
nativeInsertUndoSaved = false
|
|
215
|
+
} else {
|
|
216
|
+
buffer.replaceContent(activeMap.vimText)
|
|
134
217
|
}
|
|
135
218
|
return activeMap
|
|
136
219
|
}
|
|
137
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
|
+
|
|
138
237
|
function rememberMap(map: PromptMap) {
|
|
139
238
|
maps.set(map.vimText, map)
|
|
140
239
|
}
|
|
@@ -166,7 +265,11 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
166
265
|
setCursor(input, currentMap, action.position)
|
|
167
266
|
break
|
|
168
267
|
case "mode-change":
|
|
169
|
-
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
|
+
}
|
|
170
273
|
syncMode(state, action.mode)
|
|
171
274
|
break
|
|
172
275
|
case "quit":
|
|
@@ -175,6 +278,9 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
175
278
|
case "submit":
|
|
176
279
|
ref.submit()
|
|
177
280
|
break
|
|
281
|
+
case "command":
|
|
282
|
+
dispatchCommand(action.command, ctx)
|
|
283
|
+
return
|
|
178
284
|
}
|
|
179
285
|
}
|
|
180
286
|
|
|
@@ -182,7 +288,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
182
288
|
syncVisualSelection(input, currentMap, ctx)
|
|
183
289
|
}
|
|
184
290
|
|
|
185
|
-
function handleInsertKeybind(event: KeyEvent, key: string, ctx: PromptContext) {
|
|
291
|
+
function handleInsertKeybind(event: KeyEvent, key: string, ctx: PromptContext): boolean {
|
|
186
292
|
if (!keybinds?.hasKeybinds("insert") && !keybinds?.isPending()) return false
|
|
187
293
|
|
|
188
294
|
const wasPending = keybinds.isPending()
|
|
@@ -191,7 +297,12 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
191
297
|
|
|
192
298
|
switch (resolved.status) {
|
|
193
299
|
case "pending":
|
|
194
|
-
|
|
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))
|
|
195
306
|
state.setPending(resolved.display)
|
|
196
307
|
updateTimeout(ctx)
|
|
197
308
|
return true
|
|
@@ -216,7 +327,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
216
327
|
pendingInsert = ""
|
|
217
328
|
state.setPending("")
|
|
218
329
|
updateTimeout(ctx)
|
|
219
|
-
return false
|
|
330
|
+
return wasPending ? handleInsertKeybind(event, key, ctx) : false
|
|
220
331
|
}
|
|
221
332
|
}
|
|
222
333
|
|
|
@@ -228,6 +339,9 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
228
339
|
case "submit":
|
|
229
340
|
ctx.prompt()?.submit()
|
|
230
341
|
return true
|
|
342
|
+
case "command":
|
|
343
|
+
dispatchCommand((definition as HostKeybindDefinition).command!, ctx)
|
|
344
|
+
return true
|
|
231
345
|
default:
|
|
232
346
|
return false
|
|
233
347
|
}
|
|
@@ -237,14 +351,20 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
237
351
|
const ref = ctx.prompt()
|
|
238
352
|
const input = focusedInput(ctx)
|
|
239
353
|
const text = input?.plainText ?? ref?.current.input ?? ""
|
|
240
|
-
const
|
|
354
|
+
const map = mapForHostText(text, input)
|
|
241
355
|
|
|
242
356
|
if (input && text.length > 0) {
|
|
243
|
-
|
|
357
|
+
const dw = displayWidth(text)
|
|
358
|
+
const offset = clamp(input?.cursorOffset ?? dw, 0, dw)
|
|
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
|
+
}
|
|
244
363
|
clampNormalCursor(input)
|
|
245
364
|
}
|
|
246
365
|
|
|
247
|
-
|
|
366
|
+
const lastChange = nativeInsertUndoSaved ? [...vim.pendingChange, "Escape"] : vim.lastChange
|
|
367
|
+
vim = { ...resetContext(vim), cursor: hostPosition(map, input?.cursorOffset ?? 0), mode: "normal", statusMessage: "", lastChange, pendingChange: [] }
|
|
248
368
|
nativeInsertUndoSaved = false
|
|
249
369
|
syncMode(state, "normal")
|
|
250
370
|
}
|
|
@@ -266,13 +386,97 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
266
386
|
|
|
267
387
|
let actions: HostAction[] = []
|
|
268
388
|
for (const token of parseKeySequence(definition.keys)) {
|
|
269
|
-
const result =
|
|
389
|
+
const result = processKey(codec.encode(keyToken(token)), tokenCtrl(token))
|
|
270
390
|
vim = result.newCtx
|
|
271
391
|
actions = [...actions, ...(result.actions as HostAction[])]
|
|
272
392
|
}
|
|
273
393
|
return actions
|
|
274
394
|
}
|
|
275
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
|
+
|
|
276
480
|
function handleTextObject(key: string, ctx: PromptContext, map: PromptMap) {
|
|
277
481
|
if (vim.phase !== "text-object-pending" || !vim.textObjectModifier) return undefined
|
|
278
482
|
const range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer)
|
|
@@ -290,15 +494,10 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
290
494
|
}
|
|
291
495
|
if (!textObjectOperator(vim.operator)) return undefined
|
|
292
496
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
if (vim.operator === "y") {
|
|
296
|
-
result.context = { ...result.context, cursor: originalCursor }
|
|
297
|
-
result.actions = result.actions.map((action) => (action.type === "cursor-move" ? { ...action, position: originalCursor } : action))
|
|
298
|
-
}
|
|
299
|
-
vim = result.context
|
|
497
|
+
const result = processKey(key)
|
|
498
|
+
vim = result.newCtx
|
|
300
499
|
applyActions(result.actions, ctx, map)
|
|
301
|
-
if (flashRange) flashYank(ctx, activeMap,
|
|
500
|
+
if (flashRange) flashYank(ctx, activeMap, result.actions.find((action): action is YankAction => action.type === "yank"), flashRange)
|
|
302
501
|
syncMode(state, vim.mode)
|
|
303
502
|
state.setPending("")
|
|
304
503
|
updateTimeout(ctx)
|
|
@@ -326,21 +525,34 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
326
525
|
keybinds.cancel()
|
|
327
526
|
if (flush) flushPendingInsert(ctx, pendingInsert, offset)
|
|
328
527
|
pendingInsert = ""
|
|
528
|
+
pendingTarget = undefined
|
|
529
|
+
pendingContext = undefined
|
|
329
530
|
state.setPending("")
|
|
330
531
|
}
|
|
331
532
|
|
|
332
|
-
function flushPendingInsert(ctx: PromptContext, value: string,
|
|
533
|
+
function flushPendingInsert(ctx: PromptContext, value: string, charOffset?: number) {
|
|
333
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
|
+
}
|
|
334
545
|
const ref = ctx.prompt()
|
|
335
546
|
if (!ref) return
|
|
336
547
|
const input = focusedInput(ctx)
|
|
337
548
|
const text = input?.plainText ?? ref.current.input
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
const
|
|
549
|
+
const currentDisplayOff = input?.cursorOffset ?? 0
|
|
550
|
+
const currentCharOff = displayToChar(text, currentDisplayOff)
|
|
551
|
+
const insertAtChar = charOffset ?? currentCharOff
|
|
552
|
+
const next = text.slice(0, insertAtChar) + value + text.slice(insertAtChar)
|
|
341
553
|
setInput(ref, next)
|
|
342
|
-
const
|
|
343
|
-
if (input) input.cursorOffset =
|
|
554
|
+
const nextCharOff = currentCharOff >= insertAtChar ? currentCharOff + value.length : currentCharOff
|
|
555
|
+
if (input) input.cursorOffset = displayWidth(next.slice(0, nextCharOff))
|
|
344
556
|
}
|
|
345
557
|
|
|
346
558
|
function cursorOffset(map: PromptMap, position: CursorPosition) {
|
|
@@ -357,7 +569,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
357
569
|
if (!input?.gotoVisualLineEnd) return false
|
|
358
570
|
clearVisualSelection(input)
|
|
359
571
|
input.gotoVisualLineEnd()
|
|
360
|
-
vim = { ...vim, cursor: hostPosition(map, input.cursorOffset ?? map.hostText
|
|
572
|
+
vim = { ...vim, cursor: hostPosition(map, input.cursorOffset ?? displayWidth(map.hostText)), mode: "insert", phase: "idle", count: 0, operator: null, pendingChange: ["A"], statusMessage: "-- INSERT --" }
|
|
361
573
|
nativeInsertUndoSaved = false
|
|
362
574
|
syncMode(state, "insert")
|
|
363
575
|
return true
|
|
@@ -417,6 +629,30 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
417
629
|
}
|
|
418
630
|
}
|
|
419
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
|
+
|
|
420
656
|
function visualCharRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined {
|
|
421
657
|
const anchorOffset = hostOffset(map, anchor, "next")
|
|
422
658
|
const cursorOffset = hostOffset(map, cursor, "previous")
|
|
@@ -457,14 +693,15 @@ function vimOffsetRange(map: PromptMap, left: number, right: number): HostRange
|
|
|
457
693
|
|
|
458
694
|
function hostRange(map: PromptMap, left: number, right: number): HostRange | undefined {
|
|
459
695
|
if (!map.hostText) return undefined
|
|
460
|
-
const
|
|
461
|
-
const
|
|
696
|
+
const dw = displayWidth(map.hostText)
|
|
697
|
+
const start = clamp(Math.min(left, right), 0, Math.max(0, dw - 1))
|
|
698
|
+
const end = clamp(Math.max(left, right), 0, Math.max(0, dw - 1))
|
|
462
699
|
return { start, end }
|
|
463
700
|
}
|
|
464
701
|
|
|
465
702
|
type YankAction = Extract<VimeeAction, { type: "yank" }>
|
|
466
703
|
|
|
467
|
-
function yankAction(actions:
|
|
704
|
+
function yankAction(actions: HostAction[]): YankAction | undefined {
|
|
468
705
|
return actions.find((action): action is YankAction => action.type === "yank")
|
|
469
706
|
}
|
|
470
707
|
|
|
@@ -521,32 +758,71 @@ function vimOffsetFromPosition(text: string, position: CursorPosition) {
|
|
|
521
758
|
|
|
522
759
|
function hostFromVimOffset(map: PromptMap, offset: number, bias: "previous" | "next") {
|
|
523
760
|
const current = clamp(offset, 0, map.vimText.length)
|
|
524
|
-
if (current === map.vimText.length) return map.hostText
|
|
761
|
+
if (current === map.vimText.length) return displayWidth(map.hostText)
|
|
525
762
|
|
|
526
763
|
const host = map.vimToHost[current]
|
|
527
|
-
if (host !== undefined) return host
|
|
764
|
+
if (host !== undefined) return charToDisplay(map.hostText, host)
|
|
528
765
|
|
|
529
766
|
if (bias === "previous") {
|
|
530
767
|
for (let previous = current - 1; previous >= 0; previous--) {
|
|
531
768
|
const previousHost = map.vimToHost[previous]
|
|
532
|
-
if (previousHost !== undefined) return previousHost
|
|
769
|
+
if (previousHost !== undefined) return charToDisplay(map.hostText, previousHost)
|
|
533
770
|
}
|
|
534
771
|
}
|
|
535
772
|
|
|
536
773
|
for (let next = current + 1; next < map.vimToHost.length; next++) {
|
|
537
774
|
const nextHost = map.vimToHost[next]
|
|
538
|
-
if (nextHost !== undefined) return nextHost
|
|
775
|
+
if (nextHost !== undefined) return charToDisplay(map.hostText, nextHost)
|
|
539
776
|
}
|
|
540
|
-
return map.hostText
|
|
777
|
+
return displayWidth(map.hostText)
|
|
541
778
|
}
|
|
542
779
|
|
|
543
780
|
function clampNormalCursor(input: EditBufferLike) {
|
|
544
781
|
const cursor = input.visualCursor
|
|
545
|
-
const eol = input.editorView?.getVisualEOL?.()
|
|
546
782
|
const offset = input.cursorOffset
|
|
547
|
-
|
|
783
|
+
const text = input.plainText
|
|
784
|
+
if (!cursor || offset === undefined || text === undefined) return
|
|
548
785
|
if (cursor.visualCol === 0) return
|
|
549
|
-
|
|
786
|
+
const dw = displayWidth(text)
|
|
787
|
+
if (offset >= dw) {
|
|
788
|
+
input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, dw - 1)))
|
|
789
|
+
return
|
|
790
|
+
}
|
|
791
|
+
const charIdx = displayToChar(text, offset)
|
|
792
|
+
if (charIdx < text.length && text[charIdx] === '\n') {
|
|
793
|
+
input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, offset - 1)))
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function dispatchCommand(command: string, ctx: PromptContext) {
|
|
798
|
+
const input = focusedInput(ctx)
|
|
799
|
+
if (command === "prompt.history.previous" && input) input.cursorOffset = 0
|
|
800
|
+
if (command === "prompt.history.next" && input) input.cursorOffset = input.plainText?.length ?? 0
|
|
801
|
+
|
|
802
|
+
const result = ctx.api.keymap.dispatchCommand(command)
|
|
803
|
+
|
|
804
|
+
// The host currently checks history-next using character length, then leaves
|
|
805
|
+
// the cursor in that unit. Restore the display-width offset after dispatch.
|
|
806
|
+
if (command !== "prompt.history.next" || !result.ok) return result
|
|
807
|
+
const nextInput = focusedInput(ctx)
|
|
808
|
+
if (nextInput?.plainText !== undefined) nextInput.cursorOffset = displayWidth(nextInput.plainText)
|
|
809
|
+
return result
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function defaultHistoryCommand(key: string, text: string, historyText: string | undefined, enabled: Record<"j" | "k", boolean>) {
|
|
813
|
+
if (key !== "j" && key !== "k") return
|
|
814
|
+
if (!enabled[key]) return
|
|
815
|
+
if (text.length > 0 && text !== historyText) return
|
|
816
|
+
return key === "k" ? "prompt.history.previous" : "prompt.history.next"
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function hasNormalKeyPrefix(config: VimConfig, key: string) {
|
|
820
|
+
for (const sequence of Object.keys(config.keymaps.normal ?? {})) {
|
|
821
|
+
try {
|
|
822
|
+
if (keyToken(parseKeySequence(sequence)[0] ?? "") === key) return true
|
|
823
|
+
} catch {}
|
|
824
|
+
}
|
|
825
|
+
return false
|
|
550
826
|
}
|
|
551
827
|
|
|
552
828
|
function endMotionOffset(text: string, offset: number, count: number) {
|
|
@@ -578,6 +854,8 @@ function createKeybinds(config: VimConfig, log: VimLog): KeybindMap | undefined
|
|
|
578
854
|
if (!keymaps) continue
|
|
579
855
|
for (const [keys, action] of Object.entries(keymaps)) {
|
|
580
856
|
try {
|
|
857
|
+
const tokens = parseKeySequence(keys)
|
|
858
|
+
if (tokens.length > 1 && tokens[0] === "<CR>") throw new Error("<CR> cannot start a multi-key mapping")
|
|
581
859
|
map.addKeybind(mode, keys as ValidKeySequence<typeof keys>, keybindAction(action))
|
|
582
860
|
count++
|
|
583
861
|
} catch (error) {
|
|
@@ -590,6 +868,15 @@ function createKeybinds(config: VimConfig, log: VimLog): KeybindMap | undefined
|
|
|
590
868
|
}
|
|
591
869
|
|
|
592
870
|
function keybindAction(action: string): HostKeybindDefinition {
|
|
871
|
+
if (action.startsWith("command:")) {
|
|
872
|
+
const command = action.slice(8).trim()
|
|
873
|
+
if (!command) throw new Error("Command name is required")
|
|
874
|
+
return {
|
|
875
|
+
execute: () => [{ type: "command", command } as unknown as VimeeAction],
|
|
876
|
+
hostAction: "command",
|
|
877
|
+
command,
|
|
878
|
+
}
|
|
879
|
+
}
|
|
593
880
|
switch (action) {
|
|
594
881
|
case "normal":
|
|
595
882
|
return { keys: "<Esc>", hostAction: "normal" }
|
|
@@ -603,6 +890,7 @@ function keybindAction(action: string): HostKeybindDefinition {
|
|
|
603
890
|
}
|
|
604
891
|
|
|
605
892
|
function keyForVimee(event: KeyEvent, key: string) {
|
|
893
|
+
if (key === "<C-[>") return "Escape"
|
|
606
894
|
if (event.ctrl) return event.name?.toLowerCase()
|
|
607
895
|
const token = keyToken(key)
|
|
608
896
|
return token.startsWith("<") ? undefined : token
|
|
@@ -634,17 +922,19 @@ function syncMode(state: VimState, mode: VimContext["mode"]) {
|
|
|
634
922
|
}
|
|
635
923
|
|
|
636
924
|
function pendingDisplay(ctx: VimContext, keybindPending: boolean) {
|
|
637
|
-
|
|
638
|
-
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 ?? "")
|
|
639
929
|
if (keybindPending) return ctx.statusMessage
|
|
640
|
-
return
|
|
930
|
+
return count
|
|
641
931
|
}
|
|
642
932
|
|
|
643
933
|
function plainPending(value: string) {
|
|
644
934
|
return value.includes("<") || value.includes(">") ? "" : value
|
|
645
935
|
}
|
|
646
936
|
|
|
647
|
-
function consumesKey(key: string, actions:
|
|
937
|
+
function consumesKey(key: string, actions: HostAction[], ctx: VimContext, keybindPending: boolean) {
|
|
648
938
|
if (actions.length > 0) return true
|
|
649
939
|
if (keybindPending) return true
|
|
650
940
|
if (ctx.phase !== "idle") return true
|
|
@@ -677,7 +967,7 @@ function quoteRange(modifier: "i" | "a", cursor: CursorPosition, buffer: TextBuf
|
|
|
677
967
|
start: { line: cursor.line, col: start },
|
|
678
968
|
end: { line: cursor.line, col: Math.max(start, end) },
|
|
679
969
|
linewise: false,
|
|
680
|
-
inclusive:
|
|
970
|
+
inclusive: end >= start,
|
|
681
971
|
}
|
|
682
972
|
}
|
|
683
973
|
|
|
@@ -769,7 +1059,7 @@ function quotePair(cursor: CursorPosition, buffer: TextBuffer, quote: string) {
|
|
|
769
1059
|
}
|
|
770
1060
|
|
|
771
1061
|
function executeTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer, ctx: VimContext) {
|
|
772
|
-
buffer.saveUndoPoint(ctx.cursor)
|
|
1062
|
+
if (operator !== "y") buffer.saveUndoPoint(ctx.cursor)
|
|
773
1063
|
const result = range.linewise ? executeLinewiseTextObject(operator, range, buffer) : executeCharwiseTextObject(operator, range, buffer)
|
|
774
1064
|
const registers = ctx.selectedRegister ? { ...ctx.registers, [ctx.selectedRegister]: result.yankedText } : ctx.registers
|
|
775
1065
|
const context = {
|
|
@@ -794,9 +1084,9 @@ function executeLinewiseTextObject(operator: Operator, range: MotionRange, buffe
|
|
|
794
1084
|
}
|
|
795
1085
|
|
|
796
1086
|
buffer.deleteLines(startLine, lineCount)
|
|
797
|
-
if (
|
|
1087
|
+
if (operator === "c") buffer.insertLine(startLine, "")
|
|
1088
|
+
else if (buffer.getLineCount() === 0) buffer.insertLine(0, "")
|
|
798
1089
|
const line = Math.min(startLine, buffer.getLineCount() - 1)
|
|
799
|
-
if (operator === "c") buffer.insertLine(line, "")
|
|
800
1090
|
return {
|
|
801
1091
|
actions: [{ type: "content-change", content: buffer.getContent() }] as VimeeAction[],
|
|
802
1092
|
cursor: { line, col: 0 },
|