opencode-vim 0.0.23 → 0.0.24
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 +11 -0
- package/dist/tui.js +3326 -0
- package/package.json +6 -6
- package/src/clipboard.tsx +0 -50
- package/src/modules/snippets/index.tsx +0 -17
- package/src/modules/snippets/loader.ts +0 -147
- package/src/modules/snippets/search.ts +0 -118
- package/src/modules/snippets/skill-loader.ts +0 -81
- package/src/modules/snippets/state.ts +0 -40
- package/src/modules/snippets/trigger.ts +0 -68
- package/src/modules/snippets/types.ts +0 -50
- package/src/modules/snippets/view.tsx +0 -674
- package/src/modules/vim/actions.ts +0 -82
- package/src/modules/vim/config.ts +0 -137
- package/src/modules/vim/edit.test.ts +0 -51
- package/src/modules/vim/edit.ts +0 -33
- package/src/modules/vim/graphemes.ts +0 -38
- package/src/modules/vim/index.tsx +0 -200
- package/src/modules/vim/keys.ts +0 -38
- package/src/modules/vim/log.ts +0 -37
- package/src/modules/vim/map.test.ts +0 -45
- package/src/modules/vim/map.ts +0 -90
- package/src/modules/vim/state.test.ts +0 -18
- package/src/modules/vim/state.tsx +0 -46
- package/src/modules/vim/view.tsx +0 -130
- package/src/modules/vim/vimee.test.ts +0 -211
- package/src/modules/vim/vimee.ts +0 -1211
- package/src/plugin.tsx +0 -116
- package/src/prompt/host.tsx +0 -38
- package/src/prompt/modules.ts +0 -16
- package/src/prompt/root.tsx +0 -69
- package/src/prompt/types.ts +0 -42
- package/src/session.tsx +0 -291
- package/src/transcript-items.tsx +0 -168
- package/src/transcript.tsx +0 -179
- package/src/update.ts +0 -120
- package/tui.tsx +0 -340
- package/view.tsx +0 -29
package/src/modules/vim/vimee.ts
DELETED
|
@@ -1,1211 +0,0 @@
|
|
|
1
|
-
import type { KeyEvent } from "@opentui/core"
|
|
2
|
-
import { TextBuffer, createInitialContext, createKeybindMap, executeOperatorOnRange, parseKeySequence, processKeystroke, resetContext, resolveMotion } from "@vimee/core"
|
|
3
|
-
import type { CursorPosition, KeybindDefinition, KeybindMap, MotionRange, Operator, ValidKeySequence, VimAction as VimeeAction, VimContext, VimMode as VimeeMode } from "@vimee/core"
|
|
4
|
-
import { focusedInput, setInput, type EditBufferLike, type PromptContext } from "./actions"
|
|
5
|
-
import type { VimConfig } from "./config"
|
|
6
|
-
import type { VimLog } from "./log"
|
|
7
|
-
import { createGraphemeCodec } from "./graphemes"
|
|
8
|
-
import { charToDisplay, displayToChar, displayWidth, createPromptMap, hostFromVimOffset, hostOffset, hostPosition, type PromptMap } from "./map"
|
|
9
|
-
import type { createVimState } from "./state"
|
|
10
|
-
|
|
11
|
-
type VimState = ReturnType<typeof createVimState>
|
|
12
|
-
type HostAction = VimeeAction & { register?: string } | { type: "submit" } | { type: "command"; command: string }
|
|
13
|
-
type HostKeybindAction = "normal" | "submit" | "command"
|
|
14
|
-
type HostKeybindDefinition = KeybindDefinition & { hostAction?: HostKeybindAction; command?: string }
|
|
15
|
-
type HostRange = { start: number; end: number }
|
|
16
|
-
|
|
17
|
-
export const YANK_FLASH_MS = 250
|
|
18
|
-
|
|
19
|
-
export type VimeeAdapter = ReturnType<typeof createVimeeAdapter>
|
|
20
|
-
|
|
21
|
-
type AdapterOptions = {
|
|
22
|
-
readOnly?: boolean
|
|
23
|
-
onYank?: (text: string) => void
|
|
24
|
-
readClipboard?: () => Promise<string>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimLog, options: AdapterOptions = {}) {
|
|
28
|
-
const codec = createGraphemeCodec()
|
|
29
|
-
let buffer = new TextBuffer("")
|
|
30
|
-
let activeMap = createPromptMap("", codec)
|
|
31
|
-
let vim = createInitialContext({ line: 0, col: 0 })
|
|
32
|
-
const keybinds = options.readOnly ? undefined : createKeybinds(config, log)
|
|
33
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
34
|
-
let yankTimer: ReturnType<typeof setTimeout> | undefined
|
|
35
|
-
let yankFlashActive = false
|
|
36
|
-
let pendingInsert = ""
|
|
37
|
-
let pendingTarget: { input: EditBufferLike; offset: number } | undefined
|
|
38
|
-
let pendingContext: PromptContext | undefined
|
|
39
|
-
let nativeInsertUndoSaved = false
|
|
40
|
-
let historyText: string | undefined
|
|
41
|
-
let activeInput: EditBufferLike | undefined
|
|
42
|
-
let preferredColumn: number | undefined
|
|
43
|
-
let preferredScreen = false
|
|
44
|
-
let generation = 0
|
|
45
|
-
const defaultHistoryKeys = {
|
|
46
|
-
k: !hasNormalKeyPrefix(config, "k"),
|
|
47
|
-
j: !hasNormalKeyPrefix(config, "j"),
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
attach,
|
|
52
|
-
suspend,
|
|
53
|
-
setRegister(text: string) { vim = { ...vim, register: codec.encode(text) } },
|
|
54
|
-
isPending: () => vim.phase !== "idle" || vim.count > 0 || !!keybinds?.isPending(),
|
|
55
|
-
handle(event: KeyEvent, key: string, ctx: PromptContext): boolean | Promise<boolean> {
|
|
56
|
-
const ref = ctx.prompt()
|
|
57
|
-
if (!ref) return false
|
|
58
|
-
const input = focusedInput(ctx)
|
|
59
|
-
attach(ctx)
|
|
60
|
-
|
|
61
|
-
let vimeeKey = keyForVimee(event, key)
|
|
62
|
-
if (!vimeeKey) return false
|
|
63
|
-
if (options.readOnly && !readOnlyKey(vimeeKey, event.ctrl, vim)) {
|
|
64
|
-
vim = resetContext(vim)
|
|
65
|
-
state.setPending("")
|
|
66
|
-
return true
|
|
67
|
-
}
|
|
68
|
-
vimeeKey = codec.encode(vimeeKey)
|
|
69
|
-
|
|
70
|
-
if (state.mode() === "insert") {
|
|
71
|
-
if (vimeeKey === "Escape") {
|
|
72
|
-
cancelPendingInsert(ctx)
|
|
73
|
-
enterNormal(ctx)
|
|
74
|
-
state.setPending("")
|
|
75
|
-
updateTimeout(ctx)
|
|
76
|
-
log("vimee.key", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
|
|
77
|
-
return true
|
|
78
|
-
}
|
|
79
|
-
return handleInsertKeybind(event, vimeeKey, ctx)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const text = input?.plainText ?? ref.current.input
|
|
83
|
-
const dw = displayWidth(text)
|
|
84
|
-
const displayOff = clamp(input?.cursorOffset ?? dw, 0, dw)
|
|
85
|
-
const charOff = displayToChar(text, displayOff)
|
|
86
|
-
|
|
87
|
-
const canBrowseHistory = !options.readOnly && vim.phase === "idle" && vim.count === 0 && !keybinds?.isPending()
|
|
88
|
-
const historyCommand = canBrowseHistory ? defaultHistoryCommand(vimeeKey, text, historyText, defaultHistoryKeys) : undefined
|
|
89
|
-
if (historyCommand) {
|
|
90
|
-
const result = dispatchCommand(historyCommand, ctx)
|
|
91
|
-
historyText = result.ok ? (focusedInput(ctx)?.plainText ?? ref.current.input) : undefined
|
|
92
|
-
state.setPending("")
|
|
93
|
-
updateTimeout(ctx)
|
|
94
|
-
log("vimee.history", { key, command: historyCommand, handled: result.ok })
|
|
95
|
-
return true
|
|
96
|
-
}
|
|
97
|
-
historyText = undefined
|
|
98
|
-
|
|
99
|
-
const map = mapForHostText(text)
|
|
100
|
-
const cursor = hostPosition(map, displayOff)
|
|
101
|
-
|
|
102
|
-
const wasPending = keybinds?.isPending() ?? false
|
|
103
|
-
const pendingBefore = pendingInsert
|
|
104
|
-
sync(map, cursor)
|
|
105
|
-
|
|
106
|
-
const shouldFlashYank = shouldFlashYankFor(vimeeKey)
|
|
107
|
-
const visualYankRange = visualYankRangeFor(map)
|
|
108
|
-
vimeeKey = textObjectAlias(vimeeKey, vim) ?? vimeeKey
|
|
109
|
-
const textObjectHandled = handleTextObject(vimeeKey, ctx, map)
|
|
110
|
-
if (textObjectHandled !== undefined) return textObjectHandled
|
|
111
|
-
const resolved = keybinds?.resolve(vimeeKey, vim.mode, event.ctrl)
|
|
112
|
-
if (resolved?.status === "pending") {
|
|
113
|
-
vim = { ...vim, statusMessage: resolved.display }
|
|
114
|
-
state.setPending(resolved.display)
|
|
115
|
-
updateTimeout(ctx)
|
|
116
|
-
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: [] })
|
|
117
|
-
return true
|
|
118
|
-
}
|
|
119
|
-
if (vimeeKey === "Enter" && state.mode() === "normal" && vim.phase === "idle" && !wasPending && resolved?.status !== "matched") {
|
|
120
|
-
ref.submit()
|
|
121
|
-
state.setPending("")
|
|
122
|
-
updateTimeout(ctx)
|
|
123
|
-
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["submit"] })
|
|
124
|
-
return true
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const commandKey = vimeeKey
|
|
128
|
-
const definition = resolved?.status === "matched" ? resolved.definition : undefined
|
|
129
|
-
if (options.readClipboard && needsClipboard(commandKey, event.ctrl, definition)) {
|
|
130
|
-
const before = generation
|
|
131
|
-
return options.readClipboard().then((text) => {
|
|
132
|
-
// A focus change, mouse move or external edit cancels a pending put.
|
|
133
|
-
if (before !== generation || input?.isDestroyed || input?.plainText !== map.hostText || input?.cursorOffset !== displayOff) return false
|
|
134
|
-
vim = { ...vim, register: codec.encode(text) }
|
|
135
|
-
return finish()
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
return finish()
|
|
139
|
-
|
|
140
|
-
function finish() {
|
|
141
|
-
let result
|
|
142
|
-
if (resolved?.status === "matched") {
|
|
143
|
-
const actions = applyKeybind(resolved.definition, ctx, map)
|
|
144
|
-
result = { newCtx: vim, actions }
|
|
145
|
-
} else {
|
|
146
|
-
// Match LazyVim's uncounted j/k and arrow mappings in normal/visual mode.
|
|
147
|
-
// Apply this at the input boundary so custom mapping sequences stay literal.
|
|
148
|
-
if (!event.ctrl && vim.phase === "idle" && vim.count === 0 && (commandKey === "j" || commandKey === "k" || commandKey === "ArrowDown" || commandKey === "ArrowUp")) {
|
|
149
|
-
vim = processKey("g").newCtx
|
|
150
|
-
}
|
|
151
|
-
result = processKey(commandKey, event.ctrl)
|
|
152
|
-
}
|
|
153
|
-
vim = result.newCtx
|
|
154
|
-
const actions = result.actions as HostAction[]
|
|
155
|
-
if (resolved?.status !== "matched") applyActions(actions, ctx, map)
|
|
156
|
-
if (shouldFlashYank) flashYank(ctx, activeMap, yankAction(actions), visualYankRange)
|
|
157
|
-
syncMode(state, vim.mode)
|
|
158
|
-
|
|
159
|
-
const keybindPending = keybinds?.isPending() ?? false
|
|
160
|
-
if (wasPending && !keybindPending && pendingBefore && state.mode() === "insert") flushPendingInsert(ctx, pendingBefore, charOff)
|
|
161
|
-
pendingInsert = keybindPending && state.mode() === "insert" ? plainPending(vim.statusMessage) : ""
|
|
162
|
-
state.setPending(pendingDisplay(vim, keybindPending))
|
|
163
|
-
updateTimeout(ctx)
|
|
164
|
-
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: actions.map((action) => action.type) })
|
|
165
|
-
return consumesKey(commandKey, actions, vim, keybindPending)
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
cleanup: suspend,
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function needsClipboard(key: string, ctrl: boolean, definition?: KeybindDefinition) {
|
|
172
|
-
if (definition) {
|
|
173
|
-
if ("execute" in definition) return false
|
|
174
|
-
return parseKeySequence(definition.keys).some((token) => ["p", "P", ".", "@"].includes(token))
|
|
175
|
-
}
|
|
176
|
-
if (ctrl) return false
|
|
177
|
-
if (vim.phase === "macro-execute-pending") return true
|
|
178
|
-
if (vim.phase !== "idle" || vim.selectedRegister) return false
|
|
179
|
-
if (key === "p" || key === "P") return true
|
|
180
|
-
return key === "." && vim.lastChange.some((token) => ["p", "P", "@"].includes(token))
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function attach(ctx: PromptContext) {
|
|
184
|
-
const input = focusedInput(ctx)
|
|
185
|
-
if (activeInput === input) return
|
|
186
|
-
suspend()
|
|
187
|
-
activeInput = input
|
|
188
|
-
activeMap = createPromptMap(input?.plainText ?? ctx.prompt()?.current.input ?? "", codec)
|
|
189
|
-
buffer = new TextBuffer(activeMap.vimText)
|
|
190
|
-
nativeInsertUndoSaved = false
|
|
191
|
-
vim = { ...resetContext(vim), cursor: hostPosition(activeMap, input?.cursorOffset ?? 0), mode: state.mode() }
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function suspend() {
|
|
195
|
-
generation++
|
|
196
|
-
preferredColumn = undefined
|
|
197
|
-
if (pendingContext) cancelPendingInsert(pendingContext)
|
|
198
|
-
if (timer) clearTimeout(timer)
|
|
199
|
-
timer = undefined
|
|
200
|
-
keybinds?.cancel()
|
|
201
|
-
cancelYankFlash()
|
|
202
|
-
if (activeInput && !activeInput.isDestroyed) clearVisualSelection(activeInput)
|
|
203
|
-
if (isVisualMode(state.mode())) state.setMode("normal")
|
|
204
|
-
vim = resetContext(vim)
|
|
205
|
-
state.setPending("")
|
|
206
|
-
pendingTarget = undefined
|
|
207
|
-
pendingContext = undefined
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function sync(map: PromptMap, cursor: CursorPosition) {
|
|
211
|
-
if (cursor.line !== vim.cursor.line || cursor.col !== vim.cursor.col) preferredColumn = undefined
|
|
212
|
-
if (buffer.getContent() !== map.vimText) buffer.replaceContent(map.vimText)
|
|
213
|
-
vim = { ...vim, cursor, mode: state.mode() }
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function mapForHostText(text: string) {
|
|
217
|
-
if (activeMap.hostText === text) return activeMap
|
|
218
|
-
if (state.mode() === "insert") recordNativeChange(text)
|
|
219
|
-
activeMap = createPromptMap(text, codec)
|
|
220
|
-
if (state.mode() === "insert") {
|
|
221
|
-
if (!nativeInsertUndoSaved) {
|
|
222
|
-
buffer.saveUndoPoint(vim.cursor)
|
|
223
|
-
nativeInsertUndoSaved = true
|
|
224
|
-
}
|
|
225
|
-
buffer.replaceContent(activeMap.vimText)
|
|
226
|
-
} else {
|
|
227
|
-
buffer = new TextBuffer(activeMap.vimText)
|
|
228
|
-
nativeInsertUndoSaved = false
|
|
229
|
-
}
|
|
230
|
-
return activeMap
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function recordNativeChange(text: string) {
|
|
234
|
-
const before = codec.encode(activeMap.hostText)
|
|
235
|
-
const after = codec.encode(text)
|
|
236
|
-
const start = codec.encode(activeMap.hostText.slice(0, displayToChar(activeMap.hostText, hostOffset(activeMap, vim.cursor)))).length
|
|
237
|
-
let prefix = 0
|
|
238
|
-
while (prefix < start && prefix < after.length && before[prefix] === after[prefix]) prefix++
|
|
239
|
-
let suffix = 0
|
|
240
|
-
while (suffix < before.length - start && suffix < after.length - prefix && before[before.length - suffix - 1] === after[after.length - suffix - 1]) suffix++
|
|
241
|
-
const keys = [...vim.pendingChange]
|
|
242
|
-
if (!keys.length) keys.push("i")
|
|
243
|
-
for (let index = prefix; index < start; index++) keys.push("Backspace")
|
|
244
|
-
for (let index = start; index < before.length - suffix; index++) keys.push("Delete")
|
|
245
|
-
for (const key of after.slice(prefix, after.length - suffix)) keys.push(key === "\n" ? "Enter" : key === "\t" ? "Tab" : key)
|
|
246
|
-
vim = { ...vim, pendingChange: keys }
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function applyActions(actions: HostAction[], ctx: PromptContext, map: PromptMap) {
|
|
250
|
-
const ref = ctx.prompt()
|
|
251
|
-
const input = focusedInput(ctx)
|
|
252
|
-
let currentMap = map
|
|
253
|
-
if (!ref) return
|
|
254
|
-
|
|
255
|
-
for (const action of actions) {
|
|
256
|
-
switch (action.type) {
|
|
257
|
-
case "content-change":
|
|
258
|
-
currentMap = createPromptMap(codec.decode(action.content), codec)
|
|
259
|
-
activeMap = currentMap
|
|
260
|
-
setInput(ref, currentMap.hostText)
|
|
261
|
-
buffer.replaceContent(action.content)
|
|
262
|
-
break
|
|
263
|
-
case "cursor-move":
|
|
264
|
-
setCursor(input, currentMap, action.position)
|
|
265
|
-
break
|
|
266
|
-
case "yank":
|
|
267
|
-
if (!action.register) options.onYank?.(codec.decode(action.text))
|
|
268
|
-
break
|
|
269
|
-
case "mode-change":
|
|
270
|
-
nativeInsertUndoSaved = action.mode === "insert" && actions.some((item) => item.type === "content-change")
|
|
271
|
-
if (action.mode === "insert") {
|
|
272
|
-
cancelYankFlash()
|
|
273
|
-
if (input) clearVisualSelection(input)
|
|
274
|
-
}
|
|
275
|
-
syncMode(state, action.mode)
|
|
276
|
-
break
|
|
277
|
-
case "quit":
|
|
278
|
-
ref.blur()
|
|
279
|
-
break
|
|
280
|
-
case "submit":
|
|
281
|
-
ref.submit()
|
|
282
|
-
break
|
|
283
|
-
case "command":
|
|
284
|
-
dispatchCommand(action.command, ctx)
|
|
285
|
-
return
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
setCursor(input, currentMap, vim.cursor)
|
|
290
|
-
syncVisualSelection(input, currentMap, ctx)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function handleInsertKeybind(event: KeyEvent, key: string, ctx: PromptContext): boolean {
|
|
294
|
-
if (!keybinds?.hasKeybinds("insert") && !keybinds?.isPending()) return false
|
|
295
|
-
|
|
296
|
-
const wasPending = keybinds.isPending()
|
|
297
|
-
const pendingBefore = pendingInsert
|
|
298
|
-
const resolved = keybinds.resolve(key, "insert", event.ctrl)
|
|
299
|
-
|
|
300
|
-
switch (resolved.status) {
|
|
301
|
-
case "pending":
|
|
302
|
-
if (!wasPending) {
|
|
303
|
-
const input = focusedInput(ctx)
|
|
304
|
-
pendingTarget = input ? { input, offset: input.cursorOffset ?? 0 } : undefined
|
|
305
|
-
pendingContext = ctx
|
|
306
|
-
}
|
|
307
|
-
pendingInsert = codec.decode(plainPending(resolved.display))
|
|
308
|
-
state.setPending(resolved.display)
|
|
309
|
-
updateTimeout(ctx)
|
|
310
|
-
return true
|
|
311
|
-
case "matched": {
|
|
312
|
-
if (applyInsertKeybind(resolved.definition, ctx)) {
|
|
313
|
-
pendingInsert = ""
|
|
314
|
-
state.setPending("")
|
|
315
|
-
updateTimeout(ctx)
|
|
316
|
-
log("vimee.keybind", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
|
|
317
|
-
return true
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore)
|
|
321
|
-
pendingInsert = ""
|
|
322
|
-
state.setPending("")
|
|
323
|
-
updateTimeout(ctx)
|
|
324
|
-
log("vimee.keybind.unsupported", { key })
|
|
325
|
-
return false
|
|
326
|
-
}
|
|
327
|
-
case "none":
|
|
328
|
-
if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore)
|
|
329
|
-
pendingInsert = ""
|
|
330
|
-
state.setPending("")
|
|
331
|
-
updateTimeout(ctx)
|
|
332
|
-
return wasPending ? handleInsertKeybind(event, key, ctx) : false
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function applyInsertKeybind(definition: KeybindDefinition, ctx: PromptContext) {
|
|
337
|
-
switch (insertHostAction(definition)) {
|
|
338
|
-
case "normal":
|
|
339
|
-
enterNormal(ctx)
|
|
340
|
-
return true
|
|
341
|
-
case "submit":
|
|
342
|
-
ctx.prompt()?.submit()
|
|
343
|
-
return true
|
|
344
|
-
case "command":
|
|
345
|
-
dispatchCommand((definition as HostKeybindDefinition).command!, ctx)
|
|
346
|
-
return true
|
|
347
|
-
default:
|
|
348
|
-
return false
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function enterNormal(ctx: PromptContext) {
|
|
353
|
-
const ref = ctx.prompt()
|
|
354
|
-
const input = focusedInput(ctx)
|
|
355
|
-
const text = input?.plainText ?? ref?.current.input ?? ""
|
|
356
|
-
const map = mapForHostText(text)
|
|
357
|
-
|
|
358
|
-
if (input && text.length > 0) {
|
|
359
|
-
const dw = displayWidth(text)
|
|
360
|
-
const offset = clamp(input?.cursorOffset ?? dw, 0, dw)
|
|
361
|
-
const charOffset = displayToChar(text, offset)
|
|
362
|
-
if (charOffset > 0 && text[charOffset - 1] !== "\n") {
|
|
363
|
-
input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, offset - 1)))
|
|
364
|
-
}
|
|
365
|
-
clampNormalCursor(input)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const lastChange = nativeInsertUndoSaved ? [...vim.pendingChange, "Escape"] : vim.lastChange
|
|
369
|
-
vim = { ...resetContext(vim), cursor: hostPosition(map, input?.cursorOffset ?? 0), mode: "normal", statusMessage: "", lastChange, pendingChange: [] }
|
|
370
|
-
nativeInsertUndoSaved = false
|
|
371
|
-
syncMode(state, "normal")
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function insertHostAction(definition: KeybindDefinition): HostKeybindAction | undefined {
|
|
375
|
-
const action = (definition as HostKeybindDefinition).hostAction
|
|
376
|
-
if (action) return action
|
|
377
|
-
if ("execute" in definition) return undefined
|
|
378
|
-
if (definition.keys === "<Esc>" || definition.keys === "<C-[>" || definition.keys === "Escape") return "normal"
|
|
379
|
-
return undefined
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function applyKeybind(definition: KeybindDefinition, ctx: PromptContext, map: PromptMap) {
|
|
383
|
-
if ("execute" in definition) {
|
|
384
|
-
const actions = definition.execute(vim, buffer) as HostAction[]
|
|
385
|
-
vim = { ...vim, cursor: hostPosition(map, hostOffset(map, vim.cursor)) }
|
|
386
|
-
applyActions(actions, ctx, map)
|
|
387
|
-
return actions
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
let actions: HostAction[] = []
|
|
391
|
-
for (const token of parseKeySequence(definition.keys)) {
|
|
392
|
-
const result = processKey(codec.encode(keyToken(token)), tokenCtrl(token))
|
|
393
|
-
vim = result.newCtx
|
|
394
|
-
// Keep the host layout current for screen motions later in the mapping.
|
|
395
|
-
applyActions(result.actions, ctx, activeMap)
|
|
396
|
-
actions = [...actions, ...(result.actions as HostAction[])]
|
|
397
|
-
}
|
|
398
|
-
return actions
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function processKey(key: string, ctrl = false): { newCtx: VimContext; actions: HostAction[] } {
|
|
402
|
-
const register = vim.selectedRegister
|
|
403
|
-
const moving = !ctrl && vim.mode !== "insert" && (vim.phase === "idle" || vim.phase === "operator-pending" || vim.phase === "g-pending")
|
|
404
|
-
const vertical = moving && (key === "j" || key === "k" || key === "ArrowDown" || key === "ArrowUp")
|
|
405
|
-
const lineEnd = moving && key === "$" && vim.phase === "idle"
|
|
406
|
-
const result = processKeyInner(key, ctrl)
|
|
407
|
-
if (register) {
|
|
408
|
-
for (const action of result.actions) {
|
|
409
|
-
if (action.type === "yank") action.register = register
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
if (result.actions.some((action) => action.type === "content-change" || action.type === "mode-change")) preferredColumn = undefined
|
|
413
|
-
else if (!vertical && result.actions.some((action) => action.type === "cursor-move")) preferredColumn = lineEnd ? Infinity : undefined
|
|
414
|
-
return result
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function processKeyInner(key: string, ctrl: boolean): { newCtx: VimContext; actions: HostAction[] } {
|
|
418
|
-
let range = ctrl ? undefined : lineMotion(key)
|
|
419
|
-
if (range && vim.operator && (key === "j" || key === "k" || key === "ArrowDown" || key === "ArrowUp") && range.start.line === range.end.line && range.start.col === range.end.col) {
|
|
420
|
-
return { newCtx: { ...resetContext(vim), pendingChange: [] }, actions: [] }
|
|
421
|
-
}
|
|
422
|
-
if (range && !vim.operator) {
|
|
423
|
-
return { newCtx: { ...resetContext(vim), cursor: range.end }, actions: [{ type: "cursor-move", position: range.end }] }
|
|
424
|
-
}
|
|
425
|
-
let wordRange: MotionRange | undefined
|
|
426
|
-
if (!ctrl && vim.mode !== "insert" && (key === "w" || key === "W") && (vim.phase === "idle" || vim.phase === "operator-pending")) {
|
|
427
|
-
wordRange = forwardWordRange(key, vim.cursor, buffer, vim.count || 1)
|
|
428
|
-
if (vim.phase === "idle") {
|
|
429
|
-
const cursor = { ...wordRange.end, col: Math.min(wordRange.end.col, Math.max(0, buffer.getLineLength(wordRange.end.line) - 1)) }
|
|
430
|
-
return { newCtx: { ...resetContext(vim), cursor }, actions: [{ type: "cursor-move", position: cursor }] }
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (key === "." && !ctrl && vim.mode === "normal" && vim.phase === "idle" && vim.lastChange.length) {
|
|
434
|
-
// A repeated insert/change is one undo step, including native typing.
|
|
435
|
-
const original = buffer
|
|
436
|
-
const before = buffer.getContent()
|
|
437
|
-
const cursor = vim.cursor
|
|
438
|
-
const keys = [...vim.lastChange]
|
|
439
|
-
if (vim.count) {
|
|
440
|
-
while (/^[0-9]$/.test(keys[0] ?? "")) keys.shift()
|
|
441
|
-
keys.unshift(...String(vim.count))
|
|
442
|
-
}
|
|
443
|
-
vim = { ...vim, count: 0 }
|
|
444
|
-
const actions: HostAction[] = []
|
|
445
|
-
buffer = new TextBuffer(before)
|
|
446
|
-
try {
|
|
447
|
-
for (const token of keys) {
|
|
448
|
-
const result = processKey(token)
|
|
449
|
-
vim = result.newCtx
|
|
450
|
-
actions.push(...result.actions)
|
|
451
|
-
}
|
|
452
|
-
if (buffer.getContent() !== before) {
|
|
453
|
-
original.saveUndoPoint(cursor)
|
|
454
|
-
original.replaceContent(buffer.getContent())
|
|
455
|
-
}
|
|
456
|
-
} finally {
|
|
457
|
-
buffer = original
|
|
458
|
-
}
|
|
459
|
-
return { newCtx: { ...vim, lastChange: keys, pendingChange: [] }, actions }
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const operator = vim.operator
|
|
463
|
-
if (!range && !ctrl && operator && textObjectOperator(operator)) {
|
|
464
|
-
if (vim.phase === "operator-pending") {
|
|
465
|
-
const count = vim.count || 1
|
|
466
|
-
if (key === "l" || key === "ArrowRight") {
|
|
467
|
-
// Vimee 0.3 treats l as inclusive; Vim's operator motion is exclusive.
|
|
468
|
-
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 }
|
|
469
|
-
} else if (key === operator) {
|
|
470
|
-
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 }
|
|
471
|
-
} else if (operator === "c" && key === "w" && /\S/.test(buffer.getLine(vim.cursor.line)[vim.cursor.col] ?? "")) {
|
|
472
|
-
range = resolveMotion("e", { ...vim.cursor, col: vim.cursor.col - 1 }, buffer, count, vim.count > 0, vim)?.range
|
|
473
|
-
if (range) range.start = vim.cursor
|
|
474
|
-
} else if (wordRange) {
|
|
475
|
-
range = wordRange
|
|
476
|
-
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/))) {
|
|
477
|
-
range.end = { line: range.end.line - 1, col: buffer.getLineLength(range.end.line - 1) }
|
|
478
|
-
}
|
|
479
|
-
} else if (operator === "c") {
|
|
480
|
-
const motion = resolveMotion(key, vim.cursor, buffer, count, vim.count > 0, vim)
|
|
481
|
-
if (motion?.range.linewise) range = motion.range
|
|
482
|
-
}
|
|
483
|
-
} else if (vim.phase === "text-object-pending" && vim.textObjectModifier) {
|
|
484
|
-
range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer) ?? undefined
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
if (range && operator) {
|
|
488
|
-
// Vim's exclusive motions ending at column zero stop before the newline.
|
|
489
|
-
// Starting in the first nonblank column makes that range linewise instead.
|
|
490
|
-
if (vim.phase === "g-pending" && !range.inclusive) {
|
|
491
|
-
const ordered = orderedRange(range)
|
|
492
|
-
if (ordered.end.line > ordered.start.line && ordered.end.col === 0) {
|
|
493
|
-
const linewise = ordered.start.col <= Math.max(0, buffer.getLine(ordered.start.line).search(/\S/))
|
|
494
|
-
range = { start: ordered.start, end: { line: ordered.end.line - 1, col: buffer.getLineLength(ordered.end.line - 1) }, linewise, inclusive: false }
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
const keys: string[] = []
|
|
498
|
-
if (vim.count) keys.push(...String(vim.count))
|
|
499
|
-
for (const token of vim.pendingChange) {
|
|
500
|
-
if (!/^[0-9]$/.test(token)) keys.push(token)
|
|
501
|
-
}
|
|
502
|
-
keys.push(key)
|
|
503
|
-
const result = executeTextObject(operator, range, buffer, vim)
|
|
504
|
-
if (operator === "y") result.context.pendingChange = []
|
|
505
|
-
else if (result.context.mode === "insert") result.context.pendingChange = keys
|
|
506
|
-
else { result.context.lastChange = keys; result.context.pendingChange = [] }
|
|
507
|
-
return { newCtx: result.context, actions: result.actions }
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// The engine saves undo points for yanks too. Run read-only operations
|
|
511
|
-
// against a disposable buffer so they cannot alter the editing history.
|
|
512
|
-
const yanking = operator === "y" || (isVisualMode(vim.mode) && key === "y") || (vim.mode === "normal" && key === "Y")
|
|
513
|
-
const register = vim.selectedRegister ? vim.registers[vim.selectedRegister] ?? "" : vim.register
|
|
514
|
-
const emptyLinePaste = !ctrl && vim.mode === "normal" && vim.phase === "idle" && (key === "p" || key === "P")
|
|
515
|
-
&& buffer.getContent() === "" && register.endsWith("\n")
|
|
516
|
-
const result = processKeystroke(key, vim, yanking ? new TextBuffer(buffer.getContent()) : buffer, ctrl, false)
|
|
517
|
-
if (emptyLinePaste) {
|
|
518
|
-
// Linewise puts should fill an empty prompt, not keep its placeholder line.
|
|
519
|
-
// Remove just that line, keeping the engine's undo point and repeat keys.
|
|
520
|
-
buffer.deleteLines(key === "p" ? 0 : buffer.getLineCount() - 1, 1)
|
|
521
|
-
result.newCtx.cursor = { line: 0, col: 0 }
|
|
522
|
-
for (const action of result.actions) {
|
|
523
|
-
if (action.type === "content-change") action.content = buffer.getContent()
|
|
524
|
-
if (action.type === "cursor-move") action.position = result.newCtx.cursor
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
return result
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function lineMotion(key: string): MotionRange | undefined {
|
|
531
|
-
if (vim.mode !== "normal" && !isVisualMode(vim.mode)) return
|
|
532
|
-
const screen = vim.phase === "g-pending"
|
|
533
|
-
if (!screen && vim.phase !== "idle" && vim.phase !== "operator-pending") return
|
|
534
|
-
const down = key === "j" || key === "ArrowDown"
|
|
535
|
-
const up = key === "k" || key === "ArrowUp"
|
|
536
|
-
if (!down && !up && !(screen && (key === "0" || key === "^" || key === "$"))) return
|
|
537
|
-
|
|
538
|
-
const map = buffer.getContent() === activeMap.vimText ? activeMap : createPromptMap(codec.decode(buffer.getContent()), codec)
|
|
539
|
-
const offset = hostOffset(map, vim.cursor)
|
|
540
|
-
const count = vim.count || 1
|
|
541
|
-
let start: number
|
|
542
|
-
let end: number
|
|
543
|
-
let column: number
|
|
544
|
-
if (screen) {
|
|
545
|
-
const lines = activeInput?.editorView?.getLogicalLineInfo?.()
|
|
546
|
-
if (!lines?.lineStartCols.length) return
|
|
547
|
-
let row = 0
|
|
548
|
-
while (row + 1 < lines.lineStartCols.length && lines.lineStartCols[row + 1] <= offset) row++
|
|
549
|
-
column = offset - lines.lineStartCols[row]
|
|
550
|
-
const delta = down ? count : up ? -count : key === "$" ? count - 1 : 0
|
|
551
|
-
const target = clamp(row + delta, 0, lines.lineStartCols.length - 1)
|
|
552
|
-
start = lines.lineStartCols[target]
|
|
553
|
-
end = start + Math.max(0, lines.lineWidthCols[target] - 1)
|
|
554
|
-
if ((down || up) && target === row) return { start: vim.cursor, end: vim.cursor, linewise: false, inclusive: false }
|
|
555
|
-
} else {
|
|
556
|
-
const lineStart = hostOffset(map, { line: vim.cursor.line, col: 0 })
|
|
557
|
-
column = offset - lineStart
|
|
558
|
-
const line = clamp(vim.cursor.line + (down ? count : -count), 0, buffer.getLineCount() - 1)
|
|
559
|
-
start = hostOffset(map, { line, col: 0 })
|
|
560
|
-
end = Math.max(start, hostOffset(map, { line, col: buffer.getLineLength(line) }) - 1)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
let target: number
|
|
564
|
-
if (down || up) {
|
|
565
|
-
if (preferredColumn === undefined || (preferredScreen !== screen && preferredColumn !== Infinity)) preferredColumn = column
|
|
566
|
-
preferredScreen = screen
|
|
567
|
-
target = Math.min(start + preferredColumn, end)
|
|
568
|
-
} else if (key === "$") {
|
|
569
|
-
target = end
|
|
570
|
-
} else {
|
|
571
|
-
target = start
|
|
572
|
-
if (key === "^") {
|
|
573
|
-
const from = displayToChar(map.hostText, start)
|
|
574
|
-
const to = displayToChar(map.hostText, end + 1)
|
|
575
|
-
const first = map.hostText.slice(from, to).search(/\S/)
|
|
576
|
-
if (first >= 0) target = charToDisplay(map.hostText, from + first)
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
return { start: vim.cursor, end: hostPosition(map, target), linewise: !screen, inclusive: !screen || key === "$" }
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function handleTextObject(key: string, ctx: PromptContext, map: PromptMap) {
|
|
583
|
-
if (vim.phase !== "text-object-pending" || !vim.textObjectModifier) return undefined
|
|
584
|
-
const range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer)
|
|
585
|
-
if (!range) return undefined
|
|
586
|
-
|
|
587
|
-
const flashRange = vim.operator === "y" ? motionHostRange(map, range) : undefined
|
|
588
|
-
|
|
589
|
-
if (!vim.operator) {
|
|
590
|
-
vim = { ...resetContext(vim), visualAnchor: range.start, cursor: range.end }
|
|
591
|
-
applyActions([{ type: "cursor-move", position: range.end }], ctx, map)
|
|
592
|
-
state.setPending("")
|
|
593
|
-
updateTimeout(ctx)
|
|
594
|
-
log("vimee.textobject", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["cursor-move"] })
|
|
595
|
-
return true
|
|
596
|
-
}
|
|
597
|
-
if (!textObjectOperator(vim.operator)) return undefined
|
|
598
|
-
|
|
599
|
-
const result = processKey(key)
|
|
600
|
-
vim = result.newCtx
|
|
601
|
-
applyActions(result.actions, ctx, map)
|
|
602
|
-
if (flashRange) flashYank(ctx, activeMap, result.actions.find((action): action is YankAction => action.type === "yank"), flashRange)
|
|
603
|
-
syncMode(state, vim.mode)
|
|
604
|
-
state.setPending("")
|
|
605
|
-
updateTimeout(ctx)
|
|
606
|
-
log("vimee.textobject", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: result.actions.map((action) => action.type) })
|
|
607
|
-
return true
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function updateTimeout(ctx: PromptContext) {
|
|
611
|
-
if (timer) clearTimeout(timer)
|
|
612
|
-
timer = undefined
|
|
613
|
-
if (!keybinds?.isPending()) return
|
|
614
|
-
timer = setTimeout(() => {
|
|
615
|
-
keybinds.cancel()
|
|
616
|
-
flushPendingInsert(ctx, pendingInsert)
|
|
617
|
-
pendingInsert = ""
|
|
618
|
-
state.setPending("")
|
|
619
|
-
ctx.requestRender()
|
|
620
|
-
}, config.keymapTimeout)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function cancelPendingInsert(ctx: PromptContext, offset?: number, flush = true) {
|
|
624
|
-
if (!keybinds?.isPending()) return
|
|
625
|
-
if (timer) clearTimeout(timer)
|
|
626
|
-
timer = undefined
|
|
627
|
-
keybinds.cancel()
|
|
628
|
-
if (flush) flushPendingInsert(ctx, pendingInsert, offset)
|
|
629
|
-
pendingInsert = ""
|
|
630
|
-
pendingTarget = undefined
|
|
631
|
-
pendingContext = undefined
|
|
632
|
-
state.setPending("")
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function flushPendingInsert(ctx: PromptContext, value: string, charOffset?: number) {
|
|
636
|
-
if (!value || state.mode() !== "insert") return
|
|
637
|
-
if (pendingTarget?.input.isDestroyed) return
|
|
638
|
-
if (pendingTarget?.input.insertText && !pendingTarget.input.isDestroyed) {
|
|
639
|
-
const { input, offset } = pendingTarget
|
|
640
|
-
const cursor = input.cursorOffset ?? 0
|
|
641
|
-
input.clearSelection?.()
|
|
642
|
-
input.cursorOffset = offset
|
|
643
|
-
input.insertText!(value)
|
|
644
|
-
input.cursorOffset = cursor >= offset ? cursor + displayWidth(value) : cursor
|
|
645
|
-
return
|
|
646
|
-
}
|
|
647
|
-
const ref = ctx.prompt()
|
|
648
|
-
if (!ref) return
|
|
649
|
-
const input = focusedInput(ctx)
|
|
650
|
-
const text = input?.plainText ?? ref.current.input
|
|
651
|
-
const currentDisplayOff = input?.cursorOffset ?? 0
|
|
652
|
-
const currentCharOff = displayToChar(text, currentDisplayOff)
|
|
653
|
-
const insertAtChar = charOffset ?? currentCharOff
|
|
654
|
-
const next = text.slice(0, insertAtChar) + value + text.slice(insertAtChar)
|
|
655
|
-
setInput(ref, next)
|
|
656
|
-
const nextCharOff = currentCharOff >= insertAtChar ? currentCharOff + value.length : currentCharOff
|
|
657
|
-
if (input) input.cursorOffset = displayWidth(next.slice(0, nextCharOff))
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function setCursor(input: EditBufferLike | undefined, map: PromptMap, position: CursorPosition) {
|
|
661
|
-
if (!input) return
|
|
662
|
-
input.cursorOffset = hostOffset(map, position)
|
|
663
|
-
if (vim.mode !== "insert") clampNormalCursor(input)
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function syncVisualSelection(input: EditBufferLike | undefined, map: PromptMap, ctx: PromptContext) {
|
|
667
|
-
if (!input) return
|
|
668
|
-
if (!isVisualMode(vim.mode) || !vim.visualAnchor) {
|
|
669
|
-
if (!yankFlashActive) clearVisualSelection(input)
|
|
670
|
-
return
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
cancelYankFlash()
|
|
674
|
-
const range = vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor)
|
|
675
|
-
if (!range) {
|
|
676
|
-
clearVisualSelection(input)
|
|
677
|
-
return
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
setSelection(input, range.start, range.end, ctx)
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function visualYankRangeFor(map: PromptMap) {
|
|
684
|
-
if (!isVisualMode(vim.mode) || !vim.visualAnchor) return undefined
|
|
685
|
-
return vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor)
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function flashYank(ctx: PromptContext, map: PromptMap, action: YankAction | undefined, visualRange: HostRange | undefined) {
|
|
689
|
-
if (!action) return
|
|
690
|
-
const input = focusedInput(ctx)
|
|
691
|
-
if (!input) return
|
|
692
|
-
const range = visualRange ?? yankedTextRange(map, vim.cursor, action.text)
|
|
693
|
-
if (!range) return
|
|
694
|
-
|
|
695
|
-
cancelYankFlash()
|
|
696
|
-
yankFlashActive = true
|
|
697
|
-
setYankSelection(input, range.start, range.end, ctx)
|
|
698
|
-
ctx.requestRender()
|
|
699
|
-
yankTimer = setTimeout(() => {
|
|
700
|
-
yankTimer = undefined
|
|
701
|
-
if (!yankFlashActive) return
|
|
702
|
-
yankFlashActive = false
|
|
703
|
-
clearVisualSelection(input)
|
|
704
|
-
ctx.requestRender()
|
|
705
|
-
}, YANK_FLASH_MS)
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function cancelYankFlash() {
|
|
709
|
-
if (yankTimer) clearTimeout(yankTimer)
|
|
710
|
-
yankTimer = undefined
|
|
711
|
-
yankFlashActive = false
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function shouldFlashYankFor(key: string) {
|
|
715
|
-
if (isVisualMode(vim.mode)) return key === "y"
|
|
716
|
-
return vim.operator === "y"
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
function readOnlyKey(key: string, ctrl: boolean, vim: VimContext) {
|
|
721
|
-
if (key === "Escape") return true
|
|
722
|
-
if (ctrl) return false
|
|
723
|
-
if (vim.phase === "char-pending" || vim.phase === "text-object-pending" || vim.phase === "register-pending") return true
|
|
724
|
-
if (vim.phase === "g-pending") return ["g", "j", "k", "0", "^", "$", "ArrowDown", "ArrowUp"].includes(key)
|
|
725
|
-
if ((vim.phase === "operator-pending" || isVisualMode(vim.mode)) && (key === "i" || key === "a")) return true
|
|
726
|
-
return /^[0-9hjklwWbBeE$^gGfFtT;,vVy%{}()"]$/.test(key) || ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(key)
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function forwardWordRange(key: string, start: CursorPosition, buffer: TextBuffer, count: number): MotionRange {
|
|
730
|
-
let { line, col } = start
|
|
731
|
-
for (let index = 0; index < count; index++) {
|
|
732
|
-
const text = buffer.getLine(line)
|
|
733
|
-
const kind = wordClass(text[col], key === "W")
|
|
734
|
-
while (col < text.length && wordClass(text[col], key === "W") === kind) col++
|
|
735
|
-
while (col < text.length && /\s/.test(text[col])) col++
|
|
736
|
-
while (col >= buffer.getLineLength(line) && line < buffer.getLineCount() - 1) {
|
|
737
|
-
line++
|
|
738
|
-
col = 0
|
|
739
|
-
const next = buffer.getLine(line)
|
|
740
|
-
if (!next.length) break
|
|
741
|
-
while (col < next.length && /\s/.test(next[col])) col++
|
|
742
|
-
if (col < next.length) break
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return { start, end: { line, col }, linewise: false, inclusive: false }
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function wordClass(char: string | undefined, big: boolean) {
|
|
749
|
-
if (!char || /\s/.test(char)) return "space"
|
|
750
|
-
return big || /\w/.test(char) ? "word" : "punctuation"
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function visualCharRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined {
|
|
754
|
-
const anchorOffset = hostOffset(map, anchor)
|
|
755
|
-
const cursorOffset = hostOffset(map, cursor)
|
|
756
|
-
return hostRange(map, anchorOffset, cursorOffset)
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function visualLineRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined {
|
|
760
|
-
const startLine = Math.min(anchor.line, cursor.line)
|
|
761
|
-
const endLine = Math.max(anchor.line, cursor.line)
|
|
762
|
-
const start = hostOffset(map, { line: startLine, col: 0 })
|
|
763
|
-
const end = hostOffset(map, { line: endLine, col: vimLineLength(map.vimText, endLine) })
|
|
764
|
-
return hostRange(map, start, end)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function yankedTextRange(map: PromptMap, cursor: CursorPosition, text: string): HostRange | undefined {
|
|
768
|
-
if (!text) return undefined
|
|
769
|
-
if (text.endsWith("\n")) {
|
|
770
|
-
const lineCount = text.split("\n").length - 1
|
|
771
|
-
return visualLineRange(map, cursor, { line: cursor.line + Math.max(0, lineCount - 1), col: 0 })
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const start = vimOffsetFromPosition(map.vimText, cursor)
|
|
775
|
-
return vimOffsetRange(map, start, start + text.length - 1)
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function motionHostRange(map: PromptMap, range: MotionRange): HostRange | undefined {
|
|
779
|
-
if (range.linewise) return visualLineRange(map, range.start, range.end)
|
|
780
|
-
const start = vimOffsetFromPosition(map.vimText, range.start)
|
|
781
|
-
const end = vimOffsetFromPosition(map.vimText, range.end)
|
|
782
|
-
return vimOffsetRange(map, start, end)
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function vimOffsetRange(map: PromptMap, left: number, right: number): HostRange | undefined {
|
|
786
|
-
const start = hostFromVimOffset(map, Math.min(left, right))
|
|
787
|
-
const end = hostFromVimOffset(map, Math.max(left, right))
|
|
788
|
-
return hostRange(map, start, end)
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function hostRange(map: PromptMap, left: number, right: number): HostRange | undefined {
|
|
792
|
-
if (!map.hostText) return undefined
|
|
793
|
-
const dw = displayWidth(map.hostText)
|
|
794
|
-
const start = clamp(Math.min(left, right), 0, Math.max(0, dw - 1))
|
|
795
|
-
const end = clamp(Math.max(left, right), 0, Math.max(0, dw - 1))
|
|
796
|
-
return { start, end }
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
type YankAction = Extract<VimeeAction, { type: "yank" }>
|
|
800
|
-
|
|
801
|
-
function yankAction(actions: HostAction[]): YankAction | undefined {
|
|
802
|
-
return actions.find((action): action is YankAction => action.type === "yank")
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function setSelection(input: EditBufferLike, start: number, end: number, ctx: PromptContext) {
|
|
806
|
-
setSelectionColors(input, start, end, ctx.api.theme.current.warning, ctx.api.theme.current.background)
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function setYankSelection(input: EditBufferLike, start: number, end: number, ctx: PromptContext) {
|
|
810
|
-
setSelectionColors(input, start, end, ctx.api.theme.current.info, ctx.api.theme.current.background)
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function setSelectionColors(input: EditBufferLike, start: number, end: number, background: PromptContext["api"]["theme"]["current"]["warning"], foreground: PromptContext["api"]["theme"]["current"]["background"]) {
|
|
814
|
-
input.selectionBg = background
|
|
815
|
-
input.selectionFg = foreground
|
|
816
|
-
|
|
817
|
-
if (input.setSelectionInclusive) {
|
|
818
|
-
input.setSelectionInclusive(start, end)
|
|
819
|
-
return
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const exclusiveEnd = end + 1
|
|
823
|
-
if (input.setSelection) {
|
|
824
|
-
input.setSelection(start, exclusiveEnd)
|
|
825
|
-
return
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
input.editorView?.setSelection?.(start, exclusiveEnd, background, foreground)
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function clearVisualSelection(input: EditBufferLike) {
|
|
832
|
-
if (input.clearSelection) {
|
|
833
|
-
input.clearSelection()
|
|
834
|
-
return
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
input.editorView?.resetSelection?.()
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function isVisualMode(mode: VimContext["mode"]): mode is "visual" | "visual-line" {
|
|
841
|
-
return mode === "visual" || mode === "visual-line"
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function vimLineLength(text: string, line: number) {
|
|
845
|
-
return text.split("\n")[line]?.length ?? 0
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function vimOffsetFromPosition(text: string, position: CursorPosition) {
|
|
849
|
-
const lines = text.split("\n")
|
|
850
|
-
const line = clamp(position.line, 0, Math.max(0, lines.length - 1))
|
|
851
|
-
let offset = 0
|
|
852
|
-
for (let index = 0; index < line; index++) offset += lines[index].length + 1
|
|
853
|
-
return offset + clamp(position.col, 0, lines[line]?.length ?? 0)
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function clampNormalCursor(input: EditBufferLike) {
|
|
857
|
-
const cursor = input.visualCursor
|
|
858
|
-
const offset = input.cursorOffset
|
|
859
|
-
const text = input.plainText
|
|
860
|
-
if (!cursor || offset === undefined || text === undefined) return
|
|
861
|
-
if (cursor.visualCol === 0) return
|
|
862
|
-
const dw = displayWidth(text)
|
|
863
|
-
if (offset >= dw) {
|
|
864
|
-
input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, dw - 1)))
|
|
865
|
-
return
|
|
866
|
-
}
|
|
867
|
-
const charIdx = displayToChar(text, offset)
|
|
868
|
-
if (charIdx < text.length && text[charIdx] === '\n') {
|
|
869
|
-
input.cursorOffset = charToDisplay(text, displayToChar(text, Math.max(0, offset - 1)))
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function dispatchCommand(command: string, ctx: PromptContext) {
|
|
874
|
-
const input = focusedInput(ctx)
|
|
875
|
-
if (command === "prompt.history.previous" && input) input.cursorOffset = 0
|
|
876
|
-
if (command === "prompt.history.next" && input) input.cursorOffset = input.plainText?.length ?? 0
|
|
877
|
-
|
|
878
|
-
const result = ctx.api.keymap.dispatchCommand(command)
|
|
879
|
-
|
|
880
|
-
// The host currently checks history-next using character length, then leaves
|
|
881
|
-
// the cursor in that unit. Restore the display-width offset after dispatch.
|
|
882
|
-
if (command !== "prompt.history.next" || !result.ok) return result
|
|
883
|
-
const nextInput = focusedInput(ctx)
|
|
884
|
-
if (nextInput?.plainText !== undefined) nextInput.cursorOffset = displayWidth(nextInput.plainText)
|
|
885
|
-
return result
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function defaultHistoryCommand(key: string, text: string, historyText: string | undefined, enabled: Record<"j" | "k", boolean>) {
|
|
889
|
-
if (key !== "j" && key !== "k") return
|
|
890
|
-
if (!enabled[key]) return
|
|
891
|
-
if (text.length > 0 && text !== historyText) return
|
|
892
|
-
return key === "k" ? "prompt.history.previous" : "prompt.history.next"
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function hasNormalKeyPrefix(config: VimConfig, key: string) {
|
|
896
|
-
for (const sequence of Object.keys(config.keymaps.normal ?? {})) {
|
|
897
|
-
try {
|
|
898
|
-
if (keyToken(parseKeySequence(sequence)[0] ?? "") === key) return true
|
|
899
|
-
} catch {}
|
|
900
|
-
}
|
|
901
|
-
return false
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
function createKeybinds(config: VimConfig, log: VimLog): KeybindMap | undefined {
|
|
905
|
-
const map = createKeybindMap()
|
|
906
|
-
let count = 0
|
|
907
|
-
|
|
908
|
-
for (const [mode, keymaps] of Object.entries(config.keymaps) as Array<[VimeeMode, Record<string, string> | undefined]>) {
|
|
909
|
-
if (!keymaps) continue
|
|
910
|
-
for (const [keys, action] of Object.entries(keymaps)) {
|
|
911
|
-
try {
|
|
912
|
-
const tokens = parseKeySequence(keys)
|
|
913
|
-
if (tokens.length > 1 && tokens[0] === "<CR>") throw new Error("<CR> cannot start a multi-key mapping")
|
|
914
|
-
map.addKeybind(mode, keys as ValidKeySequence<typeof keys>, keybindAction(action))
|
|
915
|
-
count++
|
|
916
|
-
} catch (error) {
|
|
917
|
-
log("vimee.keymap.invalid", { mode, keys, action, error: error instanceof Error ? error.message : String(error) })
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
return count > 0 ? map : undefined
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
function keybindAction(action: string): HostKeybindDefinition {
|
|
926
|
-
if (action.startsWith("command:")) {
|
|
927
|
-
const command = action.slice(8).trim()
|
|
928
|
-
if (!command) throw new Error("Command name is required")
|
|
929
|
-
return {
|
|
930
|
-
execute: () => [{ type: "command", command } as unknown as VimeeAction],
|
|
931
|
-
hostAction: "command",
|
|
932
|
-
command,
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
switch (action) {
|
|
936
|
-
case "normal":
|
|
937
|
-
return { keys: "<Esc>", hostAction: "normal" }
|
|
938
|
-
case "insert":
|
|
939
|
-
return { keys: "i" }
|
|
940
|
-
case "submit":
|
|
941
|
-
return { execute: () => [{ type: "submit" } as unknown as VimeeAction], hostAction: "submit" }
|
|
942
|
-
default:
|
|
943
|
-
return { keys: action }
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function keyForVimee(event: KeyEvent, key: string) {
|
|
948
|
-
if (key === "<C-[>") return "Escape"
|
|
949
|
-
if (event.ctrl) return event.name?.toLowerCase()
|
|
950
|
-
const token = keyToken(key)
|
|
951
|
-
return token.startsWith("<") ? undefined : token
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function keyToken(token: string) {
|
|
955
|
-
if (token === "<Esc>" || token === "<C-[>") return "Escape"
|
|
956
|
-
if (token === "<CR>") return "Enter"
|
|
957
|
-
if (token === "<Tab>") return "Tab"
|
|
958
|
-
if (token === "<BS>") return "Backspace"
|
|
959
|
-
if (token === "<Del>") return "Delete"
|
|
960
|
-
if (token === "<Space>") return " "
|
|
961
|
-
if (token === "<Up>") return "ArrowUp"
|
|
962
|
-
if (token === "<Down>") return "ArrowDown"
|
|
963
|
-
if (token === "<Left>") return "ArrowLeft"
|
|
964
|
-
if (token === "<Right>") return "ArrowRight"
|
|
965
|
-
if (token === "<Home>") return "Home"
|
|
966
|
-
if (token === "<End>") return "End"
|
|
967
|
-
if (token.startsWith("<C-") && token.endsWith(">")) return token.slice(3, -1).toLowerCase()
|
|
968
|
-
return token
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function tokenCtrl(token: string) {
|
|
972
|
-
return token.startsWith("<C-") && token !== "<C-[>"
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
function syncMode(state: VimState, mode: VimContext["mode"]) {
|
|
976
|
-
state.setMode(mode === "insert" || mode === "visual" || mode === "visual-line" ? mode : "normal")
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function pendingDisplay(ctx: VimContext, keybindPending: boolean) {
|
|
980
|
-
const count = ctx.count > 0 ? String(ctx.count) : ""
|
|
981
|
-
if (ctx.phase === "operator-pending") return count + (ctx.operator ?? "")
|
|
982
|
-
if (ctx.phase === "text-object-pending") return count + (ctx.operator ?? "") + (ctx.textObjectModifier ?? "")
|
|
983
|
-
if (ctx.phase === "char-pending") return count + (ctx.operator ?? "") + (ctx.charCommand ?? "")
|
|
984
|
-
if (keybindPending) return ctx.statusMessage
|
|
985
|
-
return count
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function plainPending(value: string) {
|
|
989
|
-
return value.includes("<") || value.includes(">") ? "" : value
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function consumesKey(key: string, actions: HostAction[], ctx: VimContext, keybindPending: boolean) {
|
|
993
|
-
if (actions.length > 0) return true
|
|
994
|
-
if (keybindPending) return true
|
|
995
|
-
if (ctx.phase !== "idle") return true
|
|
996
|
-
return ctx.mode !== "insert" || key === "Escape"
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
function textObjectAlias(key: string, ctx: VimContext) {
|
|
1000
|
-
if (ctx.phase !== "text-object-pending") return undefined
|
|
1001
|
-
if (key === "b") return "("
|
|
1002
|
-
if (key === "B") return "{"
|
|
1003
|
-
return undefined
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
function textObjectOperator(operator: Operator): operator is "y" | "d" | "c" {
|
|
1007
|
-
return operator === "y" || operator === "d" || operator === "c"
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function resolvePromptTextObject(modifier: "i" | "a", key: string, cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
|
|
1011
|
-
if (key === "q") return quoteRange(modifier, cursor, buffer)
|
|
1012
|
-
if (key !== "p") return null
|
|
1013
|
-
return paragraphRange(modifier, cursor, buffer)
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function quoteRange(modifier: "i" | "a", cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
|
|
1017
|
-
const pair = quoteObjectPair(cursor, buffer)
|
|
1018
|
-
if (!pair) return null
|
|
1019
|
-
const start = modifier === "i" ? pair.open + 1 : pair.open
|
|
1020
|
-
const end = modifier === "i" ? pair.close - 1 : pair.close
|
|
1021
|
-
return {
|
|
1022
|
-
start: { line: cursor.line, col: start },
|
|
1023
|
-
end: { line: cursor.line, col: Math.max(start, end) },
|
|
1024
|
-
linewise: false,
|
|
1025
|
-
inclusive: end >= start,
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function paragraphRange(modifier: "i" | "a", cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
|
|
1030
|
-
const count = buffer.getLineCount()
|
|
1031
|
-
if (count === 0) return null
|
|
1032
|
-
|
|
1033
|
-
let start = clamp(cursor.line, 0, count - 1)
|
|
1034
|
-
while (start < count && blankLine(buffer.getLine(start))) start++
|
|
1035
|
-
if (start >= count) {
|
|
1036
|
-
start = clamp(cursor.line, 0, count - 1)
|
|
1037
|
-
while (start >= 0 && blankLine(buffer.getLine(start))) start--
|
|
1038
|
-
}
|
|
1039
|
-
if (start < 0 || start >= count) return null
|
|
1040
|
-
|
|
1041
|
-
let end = start
|
|
1042
|
-
while (start > 0 && !blankLine(buffer.getLine(start - 1))) start--
|
|
1043
|
-
while (end < count - 1 && !blankLine(buffer.getLine(end + 1))) end++
|
|
1044
|
-
|
|
1045
|
-
if (modifier === "a") {
|
|
1046
|
-
if (end < count - 1 && blankLine(buffer.getLine(end + 1))) {
|
|
1047
|
-
end++
|
|
1048
|
-
while (end < count - 1 && blankLine(buffer.getLine(end + 1))) end++
|
|
1049
|
-
} else {
|
|
1050
|
-
while (start > 0 && blankLine(buffer.getLine(start - 1))) start--
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
return {
|
|
1055
|
-
start: { line: start, col: 0 },
|
|
1056
|
-
end: { line: end, col: Math.max(0, buffer.getLineLength(end) - 1) },
|
|
1057
|
-
linewise: true,
|
|
1058
|
-
inclusive: true,
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function quoteObjectPair(cursor: CursorPosition, buffer: TextBuffer) {
|
|
1063
|
-
let best: { open: number; close: number; distance: number } | undefined
|
|
1064
|
-
for (const quote of ['"', "'", "`"] as const) {
|
|
1065
|
-
const pair = quotePair(cursor, buffer, quote)
|
|
1066
|
-
if (!pair) continue
|
|
1067
|
-
if (!best || pair.distance < best.distance) best = pair
|
|
1068
|
-
}
|
|
1069
|
-
return best
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
function quotePair(cursor: CursorPosition, buffer: TextBuffer, quote: string) {
|
|
1073
|
-
const line = buffer.getLine(cursor.line)
|
|
1074
|
-
let open = -1
|
|
1075
|
-
let close = -1
|
|
1076
|
-
let inQuote = false
|
|
1077
|
-
let quoteStart = -1
|
|
1078
|
-
|
|
1079
|
-
for (let index = 0; index < line.length; index++) {
|
|
1080
|
-
if (line[index] !== quote || escaped(line, index)) continue
|
|
1081
|
-
if (!inQuote) {
|
|
1082
|
-
quoteStart = index
|
|
1083
|
-
inQuote = true
|
|
1084
|
-
continue
|
|
1085
|
-
}
|
|
1086
|
-
if (cursor.col >= quoteStart && cursor.col <= index) {
|
|
1087
|
-
return { open: quoteStart, close: index, distance: 0 }
|
|
1088
|
-
}
|
|
1089
|
-
inQuote = false
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
for (let index = cursor.col; index < line.length; index++) {
|
|
1093
|
-
if (line[index] !== quote || escaped(line, index)) continue
|
|
1094
|
-
if (open === -1) open = index
|
|
1095
|
-
else {
|
|
1096
|
-
close = index
|
|
1097
|
-
break
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
if (open !== -1 && close !== -1) return { open, close, distance: open - cursor.col }
|
|
1101
|
-
|
|
1102
|
-
close = -1
|
|
1103
|
-
open = -1
|
|
1104
|
-
for (let index = cursor.col; index >= 0; index--) {
|
|
1105
|
-
if (line[index] !== quote || escaped(line, index)) continue
|
|
1106
|
-
if (close === -1) close = index
|
|
1107
|
-
else {
|
|
1108
|
-
open = index
|
|
1109
|
-
break
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
if (open !== -1 && close !== -1) return { open, close, distance: cursor.col - close }
|
|
1113
|
-
return undefined
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
function executeTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer, ctx: VimContext) {
|
|
1117
|
-
if (operator !== "y") buffer.saveUndoPoint(ctx.cursor)
|
|
1118
|
-
if (operator === ">" || operator === "<") {
|
|
1119
|
-
const result = executeOperatorOnRange(operator, range, buffer, ctx.cursor, { style: ctx.indentStyle, width: ctx.indentWidth })
|
|
1120
|
-
const cursor = { line: result.newCursor.line, col: Math.max(0, buffer.getLine(result.newCursor.line).search(/\S/)) }
|
|
1121
|
-
return {
|
|
1122
|
-
actions: [...result.actions, { type: "cursor-move", position: cursor }] as VimeeAction[],
|
|
1123
|
-
context: { ...resetContext(ctx), cursor },
|
|
1124
|
-
yankedText: "",
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
const result = range.linewise ? executeLinewiseTextObject(operator, range, buffer, ctx.cursor) : executeCharwiseTextObject(operator, range, buffer)
|
|
1128
|
-
const registers = ctx.selectedRegister ? { ...ctx.registers, [ctx.selectedRegister]: result.yankedText } : ctx.registers
|
|
1129
|
-
const context = {
|
|
1130
|
-
...resetContext(ctx),
|
|
1131
|
-
mode: result.mode,
|
|
1132
|
-
cursor: result.cursor,
|
|
1133
|
-
register: result.yankedText,
|
|
1134
|
-
registers,
|
|
1135
|
-
statusMessage: result.statusMessage,
|
|
1136
|
-
}
|
|
1137
|
-
const actions: VimeeAction[] = [{ type: "yank", text: result.yankedText }, ...result.actions, { type: "mode-change", mode: result.mode }, { type: "cursor-move", position: result.cursor }]
|
|
1138
|
-
return { actions, context, yankedText: result.yankedText }
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
function executeLinewiseTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer, cursor: CursorPosition) {
|
|
1142
|
-
const startLine = Math.min(range.start.line, range.end.line)
|
|
1143
|
-
const endLine = Math.max(range.start.line, range.end.line)
|
|
1144
|
-
const lineCount = endLine - startLine + 1
|
|
1145
|
-
const yankedText = buffer.getLines().slice(startLine, endLine + 1).join("\n") + "\n"
|
|
1146
|
-
if (operator === "y") {
|
|
1147
|
-
const col = Math.min(cursor.col, Math.max(0, buffer.getLineLength(startLine) - 1))
|
|
1148
|
-
return { actions: [] as VimeeAction[], cursor: { line: startLine, col }, mode: "normal" as VimeeMode, yankedText, statusMessage: lineCount >= 2 ? `${lineCount} lines yanked` : "" }
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
buffer.deleteLines(startLine, lineCount)
|
|
1152
|
-
if (operator === "c") buffer.insertLine(startLine, "")
|
|
1153
|
-
else if (buffer.getLineCount() === 0) buffer.insertLine(0, "")
|
|
1154
|
-
const line = Math.min(startLine, buffer.getLineCount() - 1)
|
|
1155
|
-
return {
|
|
1156
|
-
actions: [{ type: "content-change", content: buffer.getContent() }] as VimeeAction[],
|
|
1157
|
-
cursor: { line, col: 0 },
|
|
1158
|
-
mode: (operator === "c" ? "insert" : "normal") as VimeeMode,
|
|
1159
|
-
yankedText,
|
|
1160
|
-
statusMessage: lineCount >= 2 ? `${lineCount} fewer lines` : "",
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
function executeCharwiseTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer) {
|
|
1165
|
-
const ordered = orderedRange(range)
|
|
1166
|
-
const endCol = range.inclusive ? ordered.end.col + 1 : ordered.end.col
|
|
1167
|
-
const yankedText = textInRange(buffer, ordered.start, { line: ordered.end.line, col: endCol })
|
|
1168
|
-
if (operator === "y") {
|
|
1169
|
-
return { actions: [] as VimeeAction[], cursor: ordered.start, mode: "normal" as VimeeMode, yankedText, statusMessage: yankedText.split("\n").length >= 2 ? `${yankedText.split("\n").length} lines yanked` : "" }
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
const linesBefore = buffer.getLineCount()
|
|
1173
|
-
buffer.deleteRange(ordered.start.line, ordered.start.col, ordered.end.line, endCol)
|
|
1174
|
-
const linesRemoved = linesBefore - buffer.getLineCount()
|
|
1175
|
-
return {
|
|
1176
|
-
actions: [{ type: "content-change", content: buffer.getContent() }] as VimeeAction[],
|
|
1177
|
-
cursor: { line: ordered.start.line, col: operator === "c" ? ordered.start.col : Math.min(ordered.start.col, Math.max(0, buffer.getLineLength(ordered.start.line) - 1)) },
|
|
1178
|
-
mode: (operator === "c" ? "insert" : "normal") as VimeeMode,
|
|
1179
|
-
yankedText,
|
|
1180
|
-
statusMessage: linesRemoved >= 2 ? `${linesRemoved} fewer lines` : "",
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function orderedRange(range: MotionRange) {
|
|
1185
|
-
if (range.start.line > range.end.line || (range.start.line === range.end.line && range.start.col > range.end.col)) {
|
|
1186
|
-
return { start: range.end, end: range.start }
|
|
1187
|
-
}
|
|
1188
|
-
return { start: range.start, end: range.end }
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function textInRange(buffer: TextBuffer, start: CursorPosition, end: CursorPosition) {
|
|
1192
|
-
if (start.line === end.line) return buffer.getLine(start.line).slice(start.col, end.col)
|
|
1193
|
-
const lines = [buffer.getLine(start.line).slice(start.col)]
|
|
1194
|
-
for (let line = start.line + 1; line < end.line; line++) lines.push(buffer.getLine(line))
|
|
1195
|
-
lines.push(buffer.getLine(end.line).slice(0, end.col))
|
|
1196
|
-
return lines.join("\n")
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
function blankLine(line: string) {
|
|
1200
|
-
return line.trim().length === 0
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
function escaped(line: string, index: number) {
|
|
1204
|
-
let slashCount = 0
|
|
1205
|
-
for (let cursor = index - 1; cursor >= 0 && line[cursor] === "\\"; cursor--) slashCount++
|
|
1206
|
-
return slashCount % 2 === 1
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
function clamp(value: number, min: number, max: number) {
|
|
1210
|
-
return Math.max(min, Math.min(max, value))
|
|
1211
|
-
}
|