opencode-vim 0.0.22 → 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 +25 -63
- package/assets/vim-in-motion-dialogs.gif +0 -0
- package/dist/tui.js +3326 -0
- package/docs/configuration.md +50 -400
- package/docs/keymap-actions.md +60 -188
- package/docs/vim-behavior.md +93 -0
- package/package.json +6 -6
- 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 -85
- package/src/modules/vim/config.ts +0 -125
- 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 -198
- 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 -1146
- 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/update.ts +0 -120
- package/tui.tsx +0 -275
- package/view.tsx +0 -29
package/src/modules/vim/vimee.ts
DELETED
|
@@ -1,1146 +0,0 @@
|
|
|
1
|
-
import type { KeyEvent } from "@opentui/core"
|
|
2
|
-
import { TextBuffer, createInitialContext, createKeybindMap, 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, derivePromptMap, hostOffset, hostPosition, type PromptMap } from "./map"
|
|
9
|
-
import type { createVimState } from "./state"
|
|
10
|
-
|
|
11
|
-
type VimState = ReturnType<typeof createVimState>
|
|
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
|
-
type HostRange = { start: number; end: number }
|
|
16
|
-
|
|
17
|
-
const YANK_FLASH_MS = 250
|
|
18
|
-
|
|
19
|
-
export type VimeeAdapter = ReturnType<typeof createVimeeAdapter>
|
|
20
|
-
|
|
21
|
-
export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimLog) {
|
|
22
|
-
const codec = createGraphemeCodec()
|
|
23
|
-
let buffer = new TextBuffer("")
|
|
24
|
-
let activeMap = createPromptMap("", undefined, codec)
|
|
25
|
-
const maps = new Map([[activeMap.vimText, activeMap]])
|
|
26
|
-
let vim = createInitialContext({ line: 0, col: 0 })
|
|
27
|
-
const keybinds = createKeybinds(config, log)
|
|
28
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
29
|
-
let yankTimer: ReturnType<typeof setTimeout> | undefined
|
|
30
|
-
let yankFlashActive = false
|
|
31
|
-
let pendingInsert = ""
|
|
32
|
-
let pendingTarget: { input: EditBufferLike; offset: number } | undefined
|
|
33
|
-
let pendingContext: PromptContext | undefined
|
|
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")
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
attach,
|
|
47
|
-
suspend,
|
|
48
|
-
isPending: () => vim.phase !== "idle" || vim.count > 0 || !!keybinds?.isPending(),
|
|
49
|
-
handle(event: KeyEvent, key: string, ctx: PromptContext) {
|
|
50
|
-
const ref = ctx.prompt()
|
|
51
|
-
if (!ref) return false
|
|
52
|
-
const input = focusedInput(ctx)
|
|
53
|
-
attach(ctx)
|
|
54
|
-
|
|
55
|
-
let vimeeKey = keyForVimee(event, key)
|
|
56
|
-
if (!vimeeKey) return false
|
|
57
|
-
vimeeKey = codec.encode(vimeeKey)
|
|
58
|
-
|
|
59
|
-
if (state.mode() === "insert") {
|
|
60
|
-
if (vimeeKey === "Escape") {
|
|
61
|
-
cancelPendingInsert(ctx)
|
|
62
|
-
enterNormal(ctx)
|
|
63
|
-
state.setPending("")
|
|
64
|
-
updateTimeout(ctx)
|
|
65
|
-
log("vimee.key", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
|
|
66
|
-
return true
|
|
67
|
-
}
|
|
68
|
-
return handleInsertKeybind(event, vimeeKey, ctx)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const text = input?.plainText ?? ref.current.input
|
|
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
|
|
87
|
-
|
|
88
|
-
const map = mapForHostText(text, input)
|
|
89
|
-
const cursor = hostPosition(map, displayOff)
|
|
90
|
-
|
|
91
|
-
const wasPending = keybinds?.isPending() ?? false
|
|
92
|
-
const pendingBefore = pendingInsert
|
|
93
|
-
sync(map, cursor)
|
|
94
|
-
|
|
95
|
-
if (vimeeKey === "A" && vim.mode === "normal" && vim.phase === "idle" && !wasPending && !appendMapped && appendVisualLine(input, map)) {
|
|
96
|
-
state.setPending("")
|
|
97
|
-
updateTimeout(ctx)
|
|
98
|
-
log("vimee.key", { key, vimeeKey, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
|
|
99
|
-
return true
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const hostEnd = vimeeKey === "e" && vim.phase === "idle" ? charToDisplay(map.hostText, endMotionOffset(map.hostText, charOff, vim.count || 1)) : undefined
|
|
103
|
-
const shouldFlashYank = shouldFlashYankFor(vimeeKey)
|
|
104
|
-
const visualYankRange = visualYankRangeFor(map)
|
|
105
|
-
vimeeKey = textObjectAlias(vimeeKey, vim) ?? vimeeKey
|
|
106
|
-
const textObjectHandled = handleTextObject(vimeeKey, ctx, map)
|
|
107
|
-
if (textObjectHandled !== undefined) return textObjectHandled
|
|
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
|
-
}
|
|
131
|
-
vim = result.newCtx
|
|
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) }
|
|
134
|
-
let clampFinalCursor = true
|
|
135
|
-
const content = actions.find((action) => action.type === "content-change")?.content
|
|
136
|
-
if (vimeeKey === "x" && state.mode() === "normal" && content !== undefined) {
|
|
137
|
-
const next = nextMap(map, content)
|
|
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) {
|
|
141
|
-
vim = { ...vim, cursor: hostPosition(next, target) }
|
|
142
|
-
clampFinalCursor = false
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
applyActions(actions, ctx, map, clampFinalCursor)
|
|
146
|
-
if (shouldFlashYank) flashYank(ctx, activeMap, yankAction(actions), visualYankRange)
|
|
147
|
-
syncMode(state, vim.mode)
|
|
148
|
-
|
|
149
|
-
const keybindPending = keybinds?.isPending() ?? false
|
|
150
|
-
if (wasPending && !keybindPending && pendingBefore && state.mode() === "insert") flushPendingInsert(ctx, pendingBefore, charOff)
|
|
151
|
-
pendingInsert = keybindPending && state.mode() === "insert" ? plainPending(vim.statusMessage) : ""
|
|
152
|
-
state.setPending(pendingDisplay(vim, keybindPending))
|
|
153
|
-
updateTimeout(ctx)
|
|
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)
|
|
156
|
-
},
|
|
157
|
-
cleanup() {
|
|
158
|
-
suspend()
|
|
159
|
-
maps.clear()
|
|
160
|
-
},
|
|
161
|
-
}
|
|
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
|
-
|
|
192
|
-
function sync(map: PromptMap, cursor: CursorPosition) {
|
|
193
|
-
if (buffer.getContent() !== map.vimText) buffer.replaceContent(map.vimText)
|
|
194
|
-
vim = { ...vim, cursor, mode: state.mode() }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function mapForHostText(text: string, input: ReturnType<typeof focusedInput>) {
|
|
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()
|
|
205
|
-
rememberMap(activeMap)
|
|
206
|
-
if (textChanged && state.mode() === "insert") {
|
|
207
|
-
if (!nativeInsertUndoSaved) {
|
|
208
|
-
buffer.saveUndoPoint(vim.cursor)
|
|
209
|
-
nativeInsertUndoSaved = true
|
|
210
|
-
}
|
|
211
|
-
buffer.replaceContent(activeMap.vimText)
|
|
212
|
-
} else if (textChanged) {
|
|
213
|
-
buffer = new TextBuffer(activeMap.vimText)
|
|
214
|
-
nativeInsertUndoSaved = false
|
|
215
|
-
} else {
|
|
216
|
-
buffer.replaceContent(activeMap.vimText)
|
|
217
|
-
}
|
|
218
|
-
return activeMap
|
|
219
|
-
}
|
|
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
|
-
|
|
237
|
-
function rememberMap(map: PromptMap) {
|
|
238
|
-
maps.set(map.vimText, map)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function nextMap(map: PromptMap, vimText: string) {
|
|
242
|
-
const known = maps.get(vimText)
|
|
243
|
-
if (known) return known
|
|
244
|
-
const next = derivePromptMap(map, vimText)
|
|
245
|
-
rememberMap(next)
|
|
246
|
-
return next
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function applyActions(actions: HostAction[], ctx: PromptContext, map: PromptMap, clampFinalCursor = true) {
|
|
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
|
-
rememberMap(currentMap)
|
|
259
|
-
currentMap = nextMap(currentMap, action.content)
|
|
260
|
-
activeMap = currentMap
|
|
261
|
-
setInput(ref, currentMap.hostText)
|
|
262
|
-
buffer.replaceContent(action.content)
|
|
263
|
-
break
|
|
264
|
-
case "cursor-move":
|
|
265
|
-
setCursor(input, currentMap, action.position)
|
|
266
|
-
break
|
|
267
|
-
case "mode-change":
|
|
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
|
-
}
|
|
273
|
-
syncMode(state, action.mode)
|
|
274
|
-
break
|
|
275
|
-
case "quit":
|
|
276
|
-
ref.blur()
|
|
277
|
-
break
|
|
278
|
-
case "submit":
|
|
279
|
-
ref.submit()
|
|
280
|
-
break
|
|
281
|
-
case "command":
|
|
282
|
-
dispatchCommand(action.command, ctx)
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
setCursor(input, currentMap, vim.cursor, clampFinalCursor)
|
|
288
|
-
syncVisualSelection(input, currentMap, ctx)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function handleInsertKeybind(event: KeyEvent, key: string, ctx: PromptContext): boolean {
|
|
292
|
-
if (!keybinds?.hasKeybinds("insert") && !keybinds?.isPending()) return false
|
|
293
|
-
|
|
294
|
-
const wasPending = keybinds.isPending()
|
|
295
|
-
const pendingBefore = pendingInsert
|
|
296
|
-
const resolved = keybinds.resolve(key, "insert", event.ctrl)
|
|
297
|
-
|
|
298
|
-
switch (resolved.status) {
|
|
299
|
-
case "pending":
|
|
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))
|
|
306
|
-
state.setPending(resolved.display)
|
|
307
|
-
updateTimeout(ctx)
|
|
308
|
-
return true
|
|
309
|
-
case "matched": {
|
|
310
|
-
if (applyInsertKeybind(resolved.definition, ctx)) {
|
|
311
|
-
pendingInsert = ""
|
|
312
|
-
state.setPending("")
|
|
313
|
-
updateTimeout(ctx)
|
|
314
|
-
log("vimee.keybind", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["mode-change"] })
|
|
315
|
-
return true
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore)
|
|
319
|
-
pendingInsert = ""
|
|
320
|
-
state.setPending("")
|
|
321
|
-
updateTimeout(ctx)
|
|
322
|
-
log("vimee.keybind.unsupported", { key })
|
|
323
|
-
return false
|
|
324
|
-
}
|
|
325
|
-
case "none":
|
|
326
|
-
if (wasPending && pendingBefore) flushPendingInsert(ctx, pendingBefore)
|
|
327
|
-
pendingInsert = ""
|
|
328
|
-
state.setPending("")
|
|
329
|
-
updateTimeout(ctx)
|
|
330
|
-
return wasPending ? handleInsertKeybind(event, key, ctx) : false
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function applyInsertKeybind(definition: KeybindDefinition, ctx: PromptContext) {
|
|
335
|
-
switch (insertHostAction(definition)) {
|
|
336
|
-
case "normal":
|
|
337
|
-
enterNormal(ctx)
|
|
338
|
-
return true
|
|
339
|
-
case "submit":
|
|
340
|
-
ctx.prompt()?.submit()
|
|
341
|
-
return true
|
|
342
|
-
case "command":
|
|
343
|
-
dispatchCommand((definition as HostKeybindDefinition).command!, ctx)
|
|
344
|
-
return true
|
|
345
|
-
default:
|
|
346
|
-
return false
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function enterNormal(ctx: PromptContext) {
|
|
351
|
-
const ref = ctx.prompt()
|
|
352
|
-
const input = focusedInput(ctx)
|
|
353
|
-
const text = input?.plainText ?? ref?.current.input ?? ""
|
|
354
|
-
const map = mapForHostText(text, input)
|
|
355
|
-
|
|
356
|
-
if (input && text.length > 0) {
|
|
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
|
-
}
|
|
363
|
-
clampNormalCursor(input)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const lastChange = nativeInsertUndoSaved ? [...vim.pendingChange, "Escape"] : vim.lastChange
|
|
367
|
-
vim = { ...resetContext(vim), cursor: hostPosition(map, input?.cursorOffset ?? 0), mode: "normal", statusMessage: "", lastChange, pendingChange: [] }
|
|
368
|
-
nativeInsertUndoSaved = false
|
|
369
|
-
syncMode(state, "normal")
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function insertHostAction(definition: KeybindDefinition): HostKeybindAction | undefined {
|
|
373
|
-
const action = (definition as HostKeybindDefinition).hostAction
|
|
374
|
-
if (action) return action
|
|
375
|
-
if ("execute" in definition) return undefined
|
|
376
|
-
if (definition.keys === "<Esc>" || definition.keys === "<C-[>" || definition.keys === "Escape") return "normal"
|
|
377
|
-
return undefined
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function applyKeybind(definition: KeybindDefinition, map: PromptMap) {
|
|
381
|
-
if ("execute" in definition) {
|
|
382
|
-
const actions = definition.execute(vim, buffer) as HostAction[]
|
|
383
|
-
vim = { ...vim, cursor: hostPosition(map, cursorOffset(map, vim.cursor)) }
|
|
384
|
-
return actions
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
let actions: HostAction[] = []
|
|
388
|
-
for (const token of parseKeySequence(definition.keys)) {
|
|
389
|
-
const result = processKey(codec.encode(keyToken(token)), tokenCtrl(token))
|
|
390
|
-
vim = result.newCtx
|
|
391
|
-
actions = [...actions, ...(result.actions as HostAction[])]
|
|
392
|
-
}
|
|
393
|
-
return actions
|
|
394
|
-
}
|
|
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
|
-
|
|
480
|
-
function handleTextObject(key: string, ctx: PromptContext, map: PromptMap) {
|
|
481
|
-
if (vim.phase !== "text-object-pending" || !vim.textObjectModifier) return undefined
|
|
482
|
-
const range = resolvePromptTextObject(vim.textObjectModifier, key, vim.cursor, buffer)
|
|
483
|
-
if (!range) return undefined
|
|
484
|
-
|
|
485
|
-
const flashRange = vim.operator === "y" ? motionHostRange(map, range) : undefined
|
|
486
|
-
|
|
487
|
-
if (!vim.operator) {
|
|
488
|
-
vim = { ...resetContext(vim), visualAnchor: range.start, cursor: range.end }
|
|
489
|
-
applyActions([{ type: "cursor-move", position: range.end }], ctx, map)
|
|
490
|
-
state.setPending("")
|
|
491
|
-
updateTimeout(ctx)
|
|
492
|
-
log("vimee.textobject", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: ["cursor-move"] })
|
|
493
|
-
return true
|
|
494
|
-
}
|
|
495
|
-
if (!textObjectOperator(vim.operator)) return undefined
|
|
496
|
-
|
|
497
|
-
const result = processKey(key)
|
|
498
|
-
vim = result.newCtx
|
|
499
|
-
applyActions(result.actions, ctx, map)
|
|
500
|
-
if (flashRange) flashYank(ctx, activeMap, result.actions.find((action): action is YankAction => action.type === "yank"), flashRange)
|
|
501
|
-
syncMode(state, vim.mode)
|
|
502
|
-
state.setPending("")
|
|
503
|
-
updateTimeout(ctx)
|
|
504
|
-
log("vimee.textobject", { key, mode: vim.mode, phase: vim.phase, cursor: vim.cursor, actions: result.actions.map((action) => action.type) })
|
|
505
|
-
return true
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function updateTimeout(ctx: PromptContext) {
|
|
509
|
-
if (timer) clearTimeout(timer)
|
|
510
|
-
timer = undefined
|
|
511
|
-
if (!keybinds?.isPending()) return
|
|
512
|
-
timer = setTimeout(() => {
|
|
513
|
-
keybinds.cancel()
|
|
514
|
-
flushPendingInsert(ctx, pendingInsert)
|
|
515
|
-
pendingInsert = ""
|
|
516
|
-
state.setPending("")
|
|
517
|
-
ctx.requestRender()
|
|
518
|
-
}, config.keymapTimeout)
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function cancelPendingInsert(ctx: PromptContext, offset?: number, flush = true) {
|
|
522
|
-
if (!keybinds?.isPending()) return
|
|
523
|
-
if (timer) clearTimeout(timer)
|
|
524
|
-
timer = undefined
|
|
525
|
-
keybinds.cancel()
|
|
526
|
-
if (flush) flushPendingInsert(ctx, pendingInsert, offset)
|
|
527
|
-
pendingInsert = ""
|
|
528
|
-
pendingTarget = undefined
|
|
529
|
-
pendingContext = undefined
|
|
530
|
-
state.setPending("")
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function flushPendingInsert(ctx: PromptContext, value: string, charOffset?: number) {
|
|
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
|
-
}
|
|
545
|
-
const ref = ctx.prompt()
|
|
546
|
-
if (!ref) return
|
|
547
|
-
const input = focusedInput(ctx)
|
|
548
|
-
const text = input?.plainText ?? ref.current.input
|
|
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)
|
|
553
|
-
setInput(ref, next)
|
|
554
|
-
const nextCharOff = currentCharOff >= insertAtChar ? currentCharOff + value.length : currentCharOff
|
|
555
|
-
if (input) input.cursorOffset = displayWidth(next.slice(0, nextCharOff))
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function cursorOffset(map: PromptMap, position: CursorPosition) {
|
|
559
|
-
return hostOffset(map, position, vim.mode === "insert" ? "next" : "previous")
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function setCursor(input: EditBufferLike | undefined, map: PromptMap, position: CursorPosition, clampCursor = true) {
|
|
563
|
-
if (!input) return
|
|
564
|
-
input.cursorOffset = cursorOffset(map, position)
|
|
565
|
-
if (clampCursor && vim.mode !== "insert") clampNormalCursor(input)
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function appendVisualLine(input: EditBufferLike | undefined, map: PromptMap) {
|
|
569
|
-
if (!input?.gotoVisualLineEnd) return false
|
|
570
|
-
clearVisualSelection(input)
|
|
571
|
-
input.gotoVisualLineEnd()
|
|
572
|
-
vim = { ...vim, cursor: hostPosition(map, input.cursorOffset ?? displayWidth(map.hostText)), mode: "insert", phase: "idle", count: 0, operator: null, pendingChange: ["A"], statusMessage: "-- INSERT --" }
|
|
573
|
-
nativeInsertUndoSaved = false
|
|
574
|
-
syncMode(state, "insert")
|
|
575
|
-
return true
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function syncVisualSelection(input: EditBufferLike | undefined, map: PromptMap, ctx: PromptContext) {
|
|
579
|
-
if (!input) return
|
|
580
|
-
if (!isVisualMode(vim.mode) || !vim.visualAnchor) {
|
|
581
|
-
if (!yankFlashActive) clearVisualSelection(input)
|
|
582
|
-
return
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
cancelYankFlash()
|
|
586
|
-
const range = vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor)
|
|
587
|
-
if (!range) {
|
|
588
|
-
clearVisualSelection(input)
|
|
589
|
-
return
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
setSelection(input, range.start, range.end, ctx)
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function visualYankRangeFor(map: PromptMap) {
|
|
596
|
-
if (!isVisualMode(vim.mode) || !vim.visualAnchor) return undefined
|
|
597
|
-
return vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor)
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function flashYank(ctx: PromptContext, map: PromptMap, action: YankAction | undefined, visualRange: HostRange | undefined) {
|
|
601
|
-
if (!action) return
|
|
602
|
-
const input = focusedInput(ctx)
|
|
603
|
-
if (!input) return
|
|
604
|
-
const range = visualRange ?? yankedTextRange(map, vim.cursor, action.text)
|
|
605
|
-
if (!range) return
|
|
606
|
-
|
|
607
|
-
cancelYankFlash()
|
|
608
|
-
yankFlashActive = true
|
|
609
|
-
setYankSelection(input, range.start, range.end, ctx)
|
|
610
|
-
ctx.requestRender()
|
|
611
|
-
yankTimer = setTimeout(() => {
|
|
612
|
-
yankTimer = undefined
|
|
613
|
-
if (!yankFlashActive) return
|
|
614
|
-
yankFlashActive = false
|
|
615
|
-
clearVisualSelection(input)
|
|
616
|
-
ctx.requestRender()
|
|
617
|
-
}, YANK_FLASH_MS)
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function cancelYankFlash() {
|
|
621
|
-
if (yankTimer) clearTimeout(yankTimer)
|
|
622
|
-
yankTimer = undefined
|
|
623
|
-
yankFlashActive = false
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function shouldFlashYankFor(key: string) {
|
|
627
|
-
if (isVisualMode(vim.mode)) return key === "y"
|
|
628
|
-
return vim.operator === "y"
|
|
629
|
-
}
|
|
630
|
-
}
|
|
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
|
-
|
|
656
|
-
function visualCharRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined {
|
|
657
|
-
const anchorOffset = hostOffset(map, anchor, "next")
|
|
658
|
-
const cursorOffset = hostOffset(map, cursor, "previous")
|
|
659
|
-
return hostRange(map, anchorOffset, cursorOffset)
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function visualLineRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined {
|
|
663
|
-
const startLine = Math.min(anchor.line, cursor.line)
|
|
664
|
-
const endLine = Math.max(anchor.line, cursor.line)
|
|
665
|
-
const start = hostOffset(map, { line: startLine, col: 0 }, "next")
|
|
666
|
-
const end = hostOffset(map, { line: endLine, col: vimLineLength(map.vimText, endLine) }, "previous")
|
|
667
|
-
return hostRange(map, start, end)
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function yankedTextRange(map: PromptMap, cursor: CursorPosition, text: string): HostRange | undefined {
|
|
671
|
-
if (!text) return undefined
|
|
672
|
-
if (text.endsWith("\n")) {
|
|
673
|
-
const lineCount = text.split("\n").length - 1
|
|
674
|
-
return visualLineRange(map, cursor, { line: cursor.line + Math.max(0, lineCount - 1), col: 0 })
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const start = vimOffsetFromPosition(map.vimText, cursor)
|
|
678
|
-
return vimOffsetRange(map, start, start + text.length - 1)
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function motionHostRange(map: PromptMap, range: MotionRange): HostRange | undefined {
|
|
682
|
-
if (range.linewise) return visualLineRange(map, range.start, range.end)
|
|
683
|
-
const start = vimOffsetFromPosition(map.vimText, range.start)
|
|
684
|
-
const end = vimOffsetFromPosition(map.vimText, range.end)
|
|
685
|
-
return vimOffsetRange(map, start, end)
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function vimOffsetRange(map: PromptMap, left: number, right: number): HostRange | undefined {
|
|
689
|
-
const start = hostFromVimOffset(map, Math.min(left, right), "next")
|
|
690
|
-
const end = hostFromVimOffset(map, Math.max(left, right), "previous")
|
|
691
|
-
return hostRange(map, start, end)
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function hostRange(map: PromptMap, left: number, right: number): HostRange | undefined {
|
|
695
|
-
if (!map.hostText) return undefined
|
|
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))
|
|
699
|
-
return { start, end }
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
type YankAction = Extract<VimeeAction, { type: "yank" }>
|
|
703
|
-
|
|
704
|
-
function yankAction(actions: HostAction[]): YankAction | undefined {
|
|
705
|
-
return actions.find((action): action is YankAction => action.type === "yank")
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function setSelection(input: EditBufferLike, start: number, end: number, ctx: PromptContext) {
|
|
709
|
-
setSelectionColors(input, start, end, ctx.api.theme.current.warning, ctx.api.theme.current.background)
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function setYankSelection(input: EditBufferLike, start: number, end: number, ctx: PromptContext) {
|
|
713
|
-
setSelectionColors(input, start, end, ctx.api.theme.current.info, ctx.api.theme.current.background)
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function setSelectionColors(input: EditBufferLike, start: number, end: number, background: PromptContext["api"]["theme"]["current"]["warning"], foreground: PromptContext["api"]["theme"]["current"]["background"]) {
|
|
717
|
-
input.selectionBg = background
|
|
718
|
-
input.selectionFg = foreground
|
|
719
|
-
|
|
720
|
-
if (input.setSelectionInclusive) {
|
|
721
|
-
input.setSelectionInclusive(start, end)
|
|
722
|
-
return
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const exclusiveEnd = end + 1
|
|
726
|
-
if (input.setSelection) {
|
|
727
|
-
input.setSelection(start, exclusiveEnd)
|
|
728
|
-
return
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
input.editorView?.setSelection?.(start, exclusiveEnd, background, foreground)
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function clearVisualSelection(input: EditBufferLike) {
|
|
735
|
-
if (input.clearSelection) {
|
|
736
|
-
input.clearSelection()
|
|
737
|
-
return
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
input.editorView?.resetSelection?.()
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function isVisualMode(mode: VimContext["mode"]): mode is "visual" | "visual-line" {
|
|
744
|
-
return mode === "visual" || mode === "visual-line"
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function vimLineLength(text: string, line: number) {
|
|
748
|
-
return text.split("\n")[line]?.length ?? 0
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function vimOffsetFromPosition(text: string, position: CursorPosition) {
|
|
752
|
-
const lines = text.split("\n")
|
|
753
|
-
const line = clamp(position.line, 0, Math.max(0, lines.length - 1))
|
|
754
|
-
let offset = 0
|
|
755
|
-
for (let index = 0; index < line; index++) offset += lines[index].length + 1
|
|
756
|
-
return offset + clamp(position.col, 0, lines[line]?.length ?? 0)
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function hostFromVimOffset(map: PromptMap, offset: number, bias: "previous" | "next") {
|
|
760
|
-
const current = clamp(offset, 0, map.vimText.length)
|
|
761
|
-
if (current === map.vimText.length) return displayWidth(map.hostText)
|
|
762
|
-
|
|
763
|
-
const host = map.vimToHost[current]
|
|
764
|
-
if (host !== undefined) return charToDisplay(map.hostText, host)
|
|
765
|
-
|
|
766
|
-
if (bias === "previous") {
|
|
767
|
-
for (let previous = current - 1; previous >= 0; previous--) {
|
|
768
|
-
const previousHost = map.vimToHost[previous]
|
|
769
|
-
if (previousHost !== undefined) return charToDisplay(map.hostText, previousHost)
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
for (let next = current + 1; next < map.vimToHost.length; next++) {
|
|
774
|
-
const nextHost = map.vimToHost[next]
|
|
775
|
-
if (nextHost !== undefined) return charToDisplay(map.hostText, nextHost)
|
|
776
|
-
}
|
|
777
|
-
return displayWidth(map.hostText)
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function clampNormalCursor(input: EditBufferLike) {
|
|
781
|
-
const cursor = input.visualCursor
|
|
782
|
-
const offset = input.cursorOffset
|
|
783
|
-
const text = input.plainText
|
|
784
|
-
if (!cursor || offset === undefined || text === undefined) return
|
|
785
|
-
if (cursor.visualCol === 0) return
|
|
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
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function endMotionOffset(text: string, offset: number, count: number) {
|
|
829
|
-
let index = offset
|
|
830
|
-
for (let step = 0; step < count; step++) {
|
|
831
|
-
index++
|
|
832
|
-
while (index < text.length && isWhitespace(text[index])) index++
|
|
833
|
-
const kind = wordKind(text[index])
|
|
834
|
-
if (!kind) return Math.max(0, text.length - 1)
|
|
835
|
-
while (wordKind(text[index + 1]) === kind) index++
|
|
836
|
-
}
|
|
837
|
-
return clamp(index, 0, Math.max(0, text.length - 1))
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function isWhitespace(value: string | undefined) {
|
|
841
|
-
return value === " " || value === "\t" || value === "\n"
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function wordKind(value: string | undefined) {
|
|
845
|
-
if (!value || isWhitespace(value)) return undefined
|
|
846
|
-
return /\w/.test(value) ? "word" : "punctuation"
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function createKeybinds(config: VimConfig, log: VimLog): KeybindMap | undefined {
|
|
850
|
-
const map = createKeybindMap()
|
|
851
|
-
let count = 0
|
|
852
|
-
|
|
853
|
-
for (const [mode, keymaps] of Object.entries(config.keymaps) as Array<[VimeeMode, Record<string, string> | undefined]>) {
|
|
854
|
-
if (!keymaps) continue
|
|
855
|
-
for (const [keys, action] of Object.entries(keymaps)) {
|
|
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")
|
|
859
|
-
map.addKeybind(mode, keys as ValidKeySequence<typeof keys>, keybindAction(action))
|
|
860
|
-
count++
|
|
861
|
-
} catch (error) {
|
|
862
|
-
log("vimee.keymap.invalid", { mode, keys, action, error: error instanceof Error ? error.message : String(error) })
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return count > 0 ? map : undefined
|
|
868
|
-
}
|
|
869
|
-
|
|
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
|
-
}
|
|
880
|
-
switch (action) {
|
|
881
|
-
case "normal":
|
|
882
|
-
return { keys: "<Esc>", hostAction: "normal" }
|
|
883
|
-
case "insert":
|
|
884
|
-
return { keys: "i" }
|
|
885
|
-
case "submit":
|
|
886
|
-
return { execute: () => [{ type: "submit" } as unknown as VimeeAction], hostAction: "submit" }
|
|
887
|
-
default:
|
|
888
|
-
return { keys: action }
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function keyForVimee(event: KeyEvent, key: string) {
|
|
893
|
-
if (key === "<C-[>") return "Escape"
|
|
894
|
-
if (event.ctrl) return event.name?.toLowerCase()
|
|
895
|
-
const token = keyToken(key)
|
|
896
|
-
return token.startsWith("<") ? undefined : token
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function keyToken(token: string) {
|
|
900
|
-
if (token === "<Esc>" || token === "<C-[>") return "Escape"
|
|
901
|
-
if (token === "<CR>") return "Enter"
|
|
902
|
-
if (token === "<Tab>") return "Tab"
|
|
903
|
-
if (token === "<BS>") return "Backspace"
|
|
904
|
-
if (token === "<Del>") return "Delete"
|
|
905
|
-
if (token === "<Space>") return " "
|
|
906
|
-
if (token === "<Up>") return "ArrowUp"
|
|
907
|
-
if (token === "<Down>") return "ArrowDown"
|
|
908
|
-
if (token === "<Left>") return "ArrowLeft"
|
|
909
|
-
if (token === "<Right>") return "ArrowRight"
|
|
910
|
-
if (token === "<Home>") return "Home"
|
|
911
|
-
if (token === "<End>") return "End"
|
|
912
|
-
if (token.startsWith("<C-") && token.endsWith(">")) return token.slice(3, -1).toLowerCase()
|
|
913
|
-
return token
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
function tokenCtrl(token: string) {
|
|
917
|
-
return token.startsWith("<C-") && token !== "<C-[>"
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
function syncMode(state: VimState, mode: VimContext["mode"]) {
|
|
921
|
-
state.setMode(mode === "insert" || mode === "visual" || mode === "visual-line" ? mode : "normal")
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
function pendingDisplay(ctx: VimContext, keybindPending: boolean) {
|
|
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 ?? "")
|
|
929
|
-
if (keybindPending) return ctx.statusMessage
|
|
930
|
-
return count
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function plainPending(value: string) {
|
|
934
|
-
return value.includes("<") || value.includes(">") ? "" : value
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function consumesKey(key: string, actions: HostAction[], ctx: VimContext, keybindPending: boolean) {
|
|
938
|
-
if (actions.length > 0) return true
|
|
939
|
-
if (keybindPending) return true
|
|
940
|
-
if (ctx.phase !== "idle") return true
|
|
941
|
-
return ctx.mode !== "insert" || key === "Escape"
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function textObjectAlias(key: string, ctx: VimContext) {
|
|
945
|
-
if (ctx.phase !== "text-object-pending") return undefined
|
|
946
|
-
if (key === "b") return "("
|
|
947
|
-
if (key === "B") return "{"
|
|
948
|
-
return undefined
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function textObjectOperator(operator: Operator): operator is "y" | "d" | "c" {
|
|
952
|
-
return operator === "y" || operator === "d" || operator === "c"
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
function resolvePromptTextObject(modifier: "i" | "a", key: string, cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
|
|
956
|
-
if (key === "q") return quoteRange(modifier, cursor, buffer)
|
|
957
|
-
if (key !== "p") return null
|
|
958
|
-
return paragraphRange(modifier, cursor, buffer)
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
function quoteRange(modifier: "i" | "a", cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
|
|
962
|
-
const pair = quoteObjectPair(cursor, buffer)
|
|
963
|
-
if (!pair) return null
|
|
964
|
-
const start = modifier === "i" ? pair.open + 1 : pair.open
|
|
965
|
-
const end = modifier === "i" ? pair.close - 1 : pair.close
|
|
966
|
-
return {
|
|
967
|
-
start: { line: cursor.line, col: start },
|
|
968
|
-
end: { line: cursor.line, col: Math.max(start, end) },
|
|
969
|
-
linewise: false,
|
|
970
|
-
inclusive: end >= start,
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function paragraphRange(modifier: "i" | "a", cursor: CursorPosition, buffer: TextBuffer): MotionRange | null {
|
|
975
|
-
const count = buffer.getLineCount()
|
|
976
|
-
if (count === 0) return null
|
|
977
|
-
|
|
978
|
-
let start = clamp(cursor.line, 0, count - 1)
|
|
979
|
-
while (start < count && blankLine(buffer.getLine(start))) start++
|
|
980
|
-
if (start >= count) {
|
|
981
|
-
start = clamp(cursor.line, 0, count - 1)
|
|
982
|
-
while (start >= 0 && blankLine(buffer.getLine(start))) start--
|
|
983
|
-
}
|
|
984
|
-
if (start < 0 || start >= count) return null
|
|
985
|
-
|
|
986
|
-
let end = start
|
|
987
|
-
while (start > 0 && !blankLine(buffer.getLine(start - 1))) start--
|
|
988
|
-
while (end < count - 1 && !blankLine(buffer.getLine(end + 1))) end++
|
|
989
|
-
|
|
990
|
-
if (modifier === "a") {
|
|
991
|
-
if (end < count - 1 && blankLine(buffer.getLine(end + 1))) {
|
|
992
|
-
end++
|
|
993
|
-
while (end < count - 1 && blankLine(buffer.getLine(end + 1))) end++
|
|
994
|
-
} else {
|
|
995
|
-
while (start > 0 && blankLine(buffer.getLine(start - 1))) start--
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
return {
|
|
1000
|
-
start: { line: start, col: 0 },
|
|
1001
|
-
end: { line: end, col: Math.max(0, buffer.getLineLength(end) - 1) },
|
|
1002
|
-
linewise: true,
|
|
1003
|
-
inclusive: true,
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
function quoteObjectPair(cursor: CursorPosition, buffer: TextBuffer) {
|
|
1008
|
-
let best: { open: number; close: number; distance: number } | undefined
|
|
1009
|
-
for (const quote of ['"', "'", "`"] as const) {
|
|
1010
|
-
const pair = quotePair(cursor, buffer, quote)
|
|
1011
|
-
if (!pair) continue
|
|
1012
|
-
if (!best || pair.distance < best.distance) best = pair
|
|
1013
|
-
}
|
|
1014
|
-
return best
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function quotePair(cursor: CursorPosition, buffer: TextBuffer, quote: string) {
|
|
1018
|
-
const line = buffer.getLine(cursor.line)
|
|
1019
|
-
let open = -1
|
|
1020
|
-
let close = -1
|
|
1021
|
-
let inQuote = false
|
|
1022
|
-
let quoteStart = -1
|
|
1023
|
-
|
|
1024
|
-
for (let index = 0; index < line.length; index++) {
|
|
1025
|
-
if (line[index] !== quote || escaped(line, index)) continue
|
|
1026
|
-
if (!inQuote) {
|
|
1027
|
-
quoteStart = index
|
|
1028
|
-
inQuote = true
|
|
1029
|
-
continue
|
|
1030
|
-
}
|
|
1031
|
-
if (cursor.col >= quoteStart && cursor.col <= index) {
|
|
1032
|
-
return { open: quoteStart, close: index, distance: 0 }
|
|
1033
|
-
}
|
|
1034
|
-
inQuote = false
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
for (let index = cursor.col; index < line.length; index++) {
|
|
1038
|
-
if (line[index] !== quote || escaped(line, index)) continue
|
|
1039
|
-
if (open === -1) open = index
|
|
1040
|
-
else {
|
|
1041
|
-
close = index
|
|
1042
|
-
break
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
if (open !== -1 && close !== -1) return { open, close, distance: open - cursor.col }
|
|
1046
|
-
|
|
1047
|
-
close = -1
|
|
1048
|
-
open = -1
|
|
1049
|
-
for (let index = cursor.col; index >= 0; index--) {
|
|
1050
|
-
if (line[index] !== quote || escaped(line, index)) continue
|
|
1051
|
-
if (close === -1) close = index
|
|
1052
|
-
else {
|
|
1053
|
-
open = index
|
|
1054
|
-
break
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
if (open !== -1 && close !== -1) return { open, close, distance: cursor.col - close }
|
|
1058
|
-
return undefined
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
function executeTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer, ctx: VimContext) {
|
|
1062
|
-
if (operator !== "y") buffer.saveUndoPoint(ctx.cursor)
|
|
1063
|
-
const result = range.linewise ? executeLinewiseTextObject(operator, range, buffer) : executeCharwiseTextObject(operator, range, buffer)
|
|
1064
|
-
const registers = ctx.selectedRegister ? { ...ctx.registers, [ctx.selectedRegister]: result.yankedText } : ctx.registers
|
|
1065
|
-
const context = {
|
|
1066
|
-
...resetContext(ctx),
|
|
1067
|
-
mode: result.mode,
|
|
1068
|
-
cursor: result.cursor,
|
|
1069
|
-
register: result.yankedText,
|
|
1070
|
-
registers,
|
|
1071
|
-
statusMessage: result.statusMessage,
|
|
1072
|
-
}
|
|
1073
|
-
const actions: VimeeAction[] = [{ type: "yank", text: result.yankedText }, ...result.actions, { type: "mode-change", mode: result.mode }, { type: "cursor-move", position: result.cursor }]
|
|
1074
|
-
return { actions, context, yankedText: result.yankedText }
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function executeLinewiseTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer) {
|
|
1078
|
-
const startLine = Math.min(range.start.line, range.end.line)
|
|
1079
|
-
const endLine = Math.max(range.start.line, range.end.line)
|
|
1080
|
-
const lineCount = endLine - startLine + 1
|
|
1081
|
-
const yankedText = buffer.getLines().slice(startLine, endLine + 1).join("\n") + "\n"
|
|
1082
|
-
if (operator === "y") {
|
|
1083
|
-
return { actions: [] as VimeeAction[], cursor: { line: startLine, col: 0 }, mode: "normal" as VimeeMode, yankedText, statusMessage: lineCount >= 2 ? `${lineCount} lines yanked` : "" }
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
buffer.deleteLines(startLine, lineCount)
|
|
1087
|
-
if (operator === "c") buffer.insertLine(startLine, "")
|
|
1088
|
-
else if (buffer.getLineCount() === 0) buffer.insertLine(0, "")
|
|
1089
|
-
const line = Math.min(startLine, buffer.getLineCount() - 1)
|
|
1090
|
-
return {
|
|
1091
|
-
actions: [{ type: "content-change", content: buffer.getContent() }] as VimeeAction[],
|
|
1092
|
-
cursor: { line, col: 0 },
|
|
1093
|
-
mode: (operator === "c" ? "insert" : "normal") as VimeeMode,
|
|
1094
|
-
yankedText,
|
|
1095
|
-
statusMessage: lineCount >= 2 ? `${lineCount} fewer lines` : "",
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function executeCharwiseTextObject(operator: Operator, range: MotionRange, buffer: TextBuffer) {
|
|
1100
|
-
const ordered = orderedRange(range)
|
|
1101
|
-
const endCol = range.inclusive ? ordered.end.col + 1 : ordered.end.col
|
|
1102
|
-
const yankedText = textInRange(buffer, ordered.start, { line: ordered.end.line, col: endCol })
|
|
1103
|
-
if (operator === "y") {
|
|
1104
|
-
return { actions: [] as VimeeAction[], cursor: ordered.start, mode: "normal" as VimeeMode, yankedText, statusMessage: yankedText.split("\n").length >= 2 ? `${yankedText.split("\n").length} lines yanked` : "" }
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
const linesBefore = buffer.getLineCount()
|
|
1108
|
-
buffer.deleteRange(ordered.start.line, ordered.start.col, ordered.end.line, endCol)
|
|
1109
|
-
const linesRemoved = linesBefore - buffer.getLineCount()
|
|
1110
|
-
return {
|
|
1111
|
-
actions: [{ type: "content-change", content: buffer.getContent() }] as VimeeAction[],
|
|
1112
|
-
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)) },
|
|
1113
|
-
mode: (operator === "c" ? "insert" : "normal") as VimeeMode,
|
|
1114
|
-
yankedText,
|
|
1115
|
-
statusMessage: linesRemoved >= 2 ? `${linesRemoved} fewer lines` : "",
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
function orderedRange(range: MotionRange) {
|
|
1120
|
-
if (range.start.line > range.end.line || (range.start.line === range.end.line && range.start.col > range.end.col)) {
|
|
1121
|
-
return { start: range.end, end: range.start }
|
|
1122
|
-
}
|
|
1123
|
-
return { start: range.start, end: range.end }
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
function textInRange(buffer: TextBuffer, start: CursorPosition, end: CursorPosition) {
|
|
1127
|
-
if (start.line === end.line) return buffer.getLine(start.line).slice(start.col, end.col)
|
|
1128
|
-
const lines = [buffer.getLine(start.line).slice(start.col)]
|
|
1129
|
-
for (let line = start.line + 1; line < end.line; line++) lines.push(buffer.getLine(line))
|
|
1130
|
-
lines.push(buffer.getLine(end.line).slice(0, end.col))
|
|
1131
|
-
return lines.join("\n")
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function blankLine(line: string) {
|
|
1135
|
-
return line.trim().length === 0
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
function escaped(line: string, index: number) {
|
|
1139
|
-
let slashCount = 0
|
|
1140
|
-
for (let cursor = index - 1; cursor >= 0 && line[cursor] === "\\"; cursor--) slashCount++
|
|
1141
|
-
return slashCount % 2 === 1
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
function clamp(value: number, min: number, max: number) {
|
|
1145
|
-
return Math.max(min, Math.min(max, value))
|
|
1146
|
-
}
|