opencode-vim 0.0.7 → 0.0.9
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/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RGBA } from "@opentui/core"
|
|
1
2
|
import type { TuiPromptInfo, TuiPromptRef } from "@opencode-ai/plugin/tui"
|
|
2
3
|
import type { PromptContext } from "../../prompt/types"
|
|
3
4
|
import type { VimCursorStyle } from "./config"
|
|
@@ -6,12 +7,17 @@ export type EditBufferLike = {
|
|
|
6
7
|
cursorOffset?: number
|
|
7
8
|
plainText?: string
|
|
8
9
|
visualCursor?: VisualCursorLike
|
|
9
|
-
editorView?: { getVisualEOL?: () => VisualCursorLike | undefined }
|
|
10
|
+
editorView?: { getVisualEOL?: () => VisualCursorLike | undefined; setSelection?: (start: number, end: number, bgColor?: RGBA, fgColor?: RGBA) => void; resetSelection?: () => void }
|
|
10
11
|
cursorStyle?: VimCursorStyle
|
|
12
|
+
selectionBg?: RGBA
|
|
13
|
+
selectionFg?: RGBA
|
|
11
14
|
moveCursorLeft?: () => boolean
|
|
12
15
|
moveCursorRight?: () => boolean
|
|
13
16
|
moveCursorUp?: () => boolean
|
|
14
17
|
moveCursorDown?: () => boolean
|
|
18
|
+
setSelection?: (start: number, end: number) => void
|
|
19
|
+
setSelectionInclusive?: (start: number, end: number) => void
|
|
20
|
+
clearSelection?: () => void
|
|
15
21
|
gotoVisualLineEnd?: () => boolean
|
|
16
22
|
gotoLineEnd?: () => void
|
|
17
23
|
}
|
|
@@ -31,6 +31,8 @@ export type VimMappedAction = string
|
|
|
31
31
|
const DEFAULT_CURSOR_STYLES: Record<VimMode, VimCursorStyle> = {
|
|
32
32
|
insert: { style: "line", blinking: true },
|
|
33
33
|
normal: { style: "block", blinking: true },
|
|
34
|
+
visual: { style: "block", blinking: true },
|
|
35
|
+
"visual-line": { style: "block", blinking: true },
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function createVimConfig(options: unknown): VimConfig {
|
|
@@ -42,6 +44,8 @@ export function createVimConfig(options: unknown): VimConfig {
|
|
|
42
44
|
cursorStyles: {
|
|
43
45
|
insert: { ...DEFAULT_CURSOR_STYLES.insert, ...input.cursorStyles?.insert },
|
|
44
46
|
normal: { ...DEFAULT_CURSOR_STYLES.normal, ...input.cursorStyles?.normal },
|
|
47
|
+
visual: { ...DEFAULT_CURSOR_STYLES.visual, ...input.cursorStyles?.visual },
|
|
48
|
+
"visual-line": { ...DEFAULT_CURSOR_STYLES["visual-line"], ...input.cursorStyles?.["visual-line"] },
|
|
45
49
|
},
|
|
46
50
|
debug: input.debug ?? process.env.VIM_PROMPT_DEBUG === "1",
|
|
47
51
|
debugPath: input.debugPath,
|
|
@@ -71,7 +75,7 @@ function readKeymaps(input: unknown): VimKeymaps | undefined {
|
|
|
71
75
|
const source = input as Record<string, unknown>
|
|
72
76
|
const keymaps: VimKeymaps = {}
|
|
73
77
|
|
|
74
|
-
for (const mode of ["insert", "normal"] as const) {
|
|
78
|
+
for (const mode of ["insert", "normal", "visual", "visual-line"] as const) {
|
|
75
79
|
const raw = source[mode]
|
|
76
80
|
if (!raw || typeof raw !== "object") continue
|
|
77
81
|
keymaps[mode] = {}
|
|
@@ -89,6 +93,8 @@ function readCursorStyles(input: unknown): VimOptions["cursorStyles"] {
|
|
|
89
93
|
return {
|
|
90
94
|
insert: readCursorStyle(source.insert),
|
|
91
95
|
normal: readCursorStyle(source.normal),
|
|
96
|
+
visual: readCursorStyle(source.visual),
|
|
97
|
+
"visual-line": readCursorStyle(source["visual-line"]),
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -107,7 +113,7 @@ function isCursorStyle(value: unknown): value is VimCursorStyle["style"] {
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
function isMode(value: unknown): value is VimMode {
|
|
110
|
-
return value === "insert" || value === "normal"
|
|
116
|
+
return value === "insert" || value === "normal" || value === "visual" || value === "visual-line"
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
function isMappedAction(value: unknown): value is VimMappedAction {
|
package/src/modules/vim/map.ts
CHANGED
|
@@ -15,12 +15,13 @@ export function createPromptMap(hostText: string, input?: EditBufferLike): Promp
|
|
|
15
15
|
export function derivePromptMap(map: PromptMap, vimText: string): PromptMap {
|
|
16
16
|
const prefix = commonPrefix(map.vimText, vimText)
|
|
17
17
|
const suffix = commonSuffix(map.vimText, vimText, prefix)
|
|
18
|
+
const inserted = vimText.slice(prefix, vimText.length - suffix)
|
|
18
19
|
const synthetic = new Set<number>()
|
|
19
20
|
let hostText = ""
|
|
20
21
|
|
|
21
22
|
for (let vimOffset = 0; vimOffset < vimText.length; vimOffset++) {
|
|
22
23
|
const oldOffset = previousOffset(map, vimOffset, vimText.length, prefix, suffix)
|
|
23
|
-
if (oldOffset !== undefined && map.vimToHost[oldOffset] === undefined) {
|
|
24
|
+
if (oldOffset !== undefined && map.vimToHost[oldOffset] === undefined && preserveSynthetic(vimOffset, prefix, suffix, vimText.length, inserted)) {
|
|
24
25
|
synthetic.add(vimOffset)
|
|
25
26
|
continue
|
|
26
27
|
}
|
|
@@ -31,6 +32,11 @@ export function derivePromptMap(map: PromptMap, vimText: string): PromptMap {
|
|
|
31
32
|
return buildPromptMapFromSynthetic(hostText, vimText, synthetic)
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function preserveSynthetic(vimOffset: number, prefix: number, suffix: number, vimLength: number, inserted: string) {
|
|
36
|
+
if (!inserted.includes("\n")) return true
|
|
37
|
+
return vimOffset !== prefix - 1 && vimOffset !== vimLength - suffix
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
export function hostPosition(map: PromptMap, hostOffset: number): CursorPosition {
|
|
35
41
|
return positionFromOffset(map.vimText, map.hostToVim[clamp(hostOffset, 0, map.hostText.length)] ?? 0)
|
|
36
42
|
}
|
package/src/modules/vim/state.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createSignal } from "solid-js"
|
|
2
2
|
import type { VimLog } from "./log"
|
|
3
3
|
|
|
4
|
-
export type VimMode = "normal" | "insert"
|
|
4
|
+
export type VimMode = "normal" | "insert" | "visual" | "visual-line"
|
|
5
5
|
|
|
6
6
|
export function createVimState(defaultMode: VimMode, log: VimLog = () => {}) {
|
|
7
7
|
const [mode, setMode] = createSignal<VimMode>(defaultMode)
|
package/src/modules/vim/view.tsx
CHANGED
|
@@ -45,7 +45,7 @@ export function VimStatus(props: VimStatusProps) {
|
|
|
45
45
|
return (
|
|
46
46
|
<box paddingLeft={1} paddingRight={1} flexDirection="row">
|
|
47
47
|
{pending() ? <text fg={props.theme.info}>{pending()} </text> : undefined}
|
|
48
|
-
<text fg={props.disabled ? props.theme.textMuted : mode() === "
|
|
48
|
+
<text fg={props.disabled ? props.theme.textMuted : mode() === "insert" ? props.theme.success : props.theme.warning}>{modeLabel(mode())}</text>
|
|
49
49
|
</box>
|
|
50
50
|
)
|
|
51
51
|
|
|
@@ -84,3 +84,9 @@ export function VimStatus(props: VimStatusProps) {
|
|
|
84
84
|
props.requestRender?.()
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
function modeLabel(mode: VimMode) {
|
|
89
|
+
if (mode === "visual") return "VISUAL"
|
|
90
|
+
if (mode === "visual-line") return "VISUAL LINE"
|
|
91
|
+
return mode === "normal" ? "NORMAL" : "INSERT"
|
|
92
|
+
}
|
package/src/modules/vim/vimee.ts
CHANGED
|
@@ -10,6 +10,9 @@ import type { createVimState } from "./state"
|
|
|
10
10
|
|
|
11
11
|
type VimState = ReturnType<typeof createVimState>
|
|
12
12
|
type HostAction = VimeeAction | { type: "submit" }
|
|
13
|
+
type HostRange = { start: number; end: number }
|
|
14
|
+
|
|
15
|
+
const YANK_FLASH_MS = 250
|
|
13
16
|
|
|
14
17
|
export type VimeeAdapter = ReturnType<typeof createVimeeAdapter>
|
|
15
18
|
|
|
@@ -20,6 +23,8 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
20
23
|
let vim = createInitialContext({ line: 0, col: 0 })
|
|
21
24
|
const keybinds = createKeybinds(config, log)
|
|
22
25
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
26
|
+
let yankTimer: ReturnType<typeof setTimeout> | undefined
|
|
27
|
+
let yankFlashActive = false
|
|
23
28
|
let pendingInsert = ""
|
|
24
29
|
let nativeInsertUndoSaved = false
|
|
25
30
|
|
|
@@ -62,9 +67,24 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
62
67
|
return true
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
const hostEnd = vimeeKey === "e" ? endMotionOffset(map.hostText, offset, vim.count || 1) : undefined
|
|
71
|
+
const shouldFlashYank = shouldFlashYankFor(vimeeKey)
|
|
72
|
+
const visualYankRange = visualYankRangeFor(map)
|
|
65
73
|
const result = processKeystroke(vimeeKey, vim, buffer, event.ctrl, false, keybinds)
|
|
66
74
|
vim = result.newCtx
|
|
67
|
-
|
|
75
|
+
if (hostEnd !== undefined && result.actions.every((action) => action.type === "cursor-move") && hostEnd > hostOffset(map, vim.cursor, "previous")) vim = { ...vim, cursor: hostPosition(map, hostEnd) }
|
|
76
|
+
let clampFinalCursor = true
|
|
77
|
+
const content = result.actions.find((action) => action.type === "content-change")?.content
|
|
78
|
+
if (vimeeKey === "x" && content !== undefined) {
|
|
79
|
+
const next = nextMap(map, content)
|
|
80
|
+
const target = clamp(offset, 0, Math.max(0, next.hostText.length - 1))
|
|
81
|
+
if (offset < map.hostText.length - 1 && map.hostText[offset + 1] !== "\n" && hostOffset(next, vim.cursor, "previous") < target) {
|
|
82
|
+
vim = { ...vim, cursor: hostPosition(next, target) }
|
|
83
|
+
clampFinalCursor = false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
applyActions(result.actions as HostAction[], ctx, map, clampFinalCursor)
|
|
87
|
+
if (shouldFlashYank) flashYank(ctx, activeMap, yankAction(result.actions), visualYankRange)
|
|
68
88
|
syncMode(state, vim.mode)
|
|
69
89
|
const keybindPending = keybinds?.isPending() ?? false
|
|
70
90
|
if (wasPending && !keybindPending && pendingBefore && state.mode() === "insert") flushPendingInsert(ctx, pendingBefore, offset)
|
|
@@ -76,6 +96,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
76
96
|
},
|
|
77
97
|
cleanup() {
|
|
78
98
|
if (timer) clearTimeout(timer)
|
|
99
|
+
if (yankTimer) clearTimeout(yankTimer)
|
|
79
100
|
},
|
|
80
101
|
}
|
|
81
102
|
|
|
@@ -113,7 +134,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
113
134
|
return next
|
|
114
135
|
}
|
|
115
136
|
|
|
116
|
-
function applyActions(actions: HostAction[], ctx: PromptContext, map: PromptMap) {
|
|
137
|
+
function applyActions(actions: HostAction[], ctx: PromptContext, map: PromptMap, clampFinalCursor = true) {
|
|
117
138
|
const ref = ctx.prompt()
|
|
118
139
|
const input = focusedInput(ctx)
|
|
119
140
|
let currentMap = map
|
|
@@ -144,7 +165,8 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
144
165
|
}
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
setCursor(input, currentMap, vim.cursor)
|
|
168
|
+
setCursor(input, currentMap, vim.cursor, clampFinalCursor)
|
|
169
|
+
syncVisualSelection(input, currentMap, ctx)
|
|
148
170
|
}
|
|
149
171
|
|
|
150
172
|
function handleInsertMode(event: KeyEvent, key: string, ctx: PromptContext, map: PromptMap, cursor: CursorPosition, offset: number) {
|
|
@@ -253,20 +275,190 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
253
275
|
return hostOffset(map, position, vim.mode === "insert" ? "next" : "previous")
|
|
254
276
|
}
|
|
255
277
|
|
|
256
|
-
function setCursor(input: EditBufferLike | undefined, map: PromptMap, position: CursorPosition) {
|
|
278
|
+
function setCursor(input: EditBufferLike | undefined, map: PromptMap, position: CursorPosition, clampCursor = true) {
|
|
257
279
|
if (!input) return
|
|
258
280
|
input.cursorOffset = cursorOffset(map, position)
|
|
259
|
-
if (vim.mode !== "insert") clampNormalCursor(input)
|
|
281
|
+
if (clampCursor && vim.mode !== "insert") clampNormalCursor(input)
|
|
260
282
|
}
|
|
261
283
|
|
|
262
284
|
function appendVisualLine(input: EditBufferLike | undefined, map: PromptMap) {
|
|
263
285
|
if (!input?.gotoVisualLineEnd) return false
|
|
286
|
+
clearVisualSelection(input)
|
|
264
287
|
input.gotoVisualLineEnd()
|
|
265
288
|
vim = { ...vim, cursor: hostPosition(map, input.cursorOffset ?? map.hostText.length), mode: "insert", phase: "idle", count: 0, operator: null, statusMessage: "-- INSERT --" }
|
|
266
289
|
nativeInsertUndoSaved = false
|
|
267
290
|
syncMode(state, "insert")
|
|
268
291
|
return true
|
|
269
292
|
}
|
|
293
|
+
|
|
294
|
+
function syncVisualSelection(input: EditBufferLike | undefined, map: PromptMap, ctx: PromptContext) {
|
|
295
|
+
if (!input) return
|
|
296
|
+
if (!isVisualMode(vim.mode) || !vim.visualAnchor) {
|
|
297
|
+
if (!yankFlashActive) clearVisualSelection(input)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
cancelYankFlash()
|
|
302
|
+
const range = vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor)
|
|
303
|
+
if (!range) {
|
|
304
|
+
clearVisualSelection(input)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
setSelection(input, range.start, range.end, ctx)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function visualYankRangeFor(map: PromptMap) {
|
|
312
|
+
if (!isVisualMode(vim.mode) || !vim.visualAnchor) return undefined
|
|
313
|
+
return vim.mode === "visual-line" ? visualLineRange(map, vim.visualAnchor, vim.cursor) : visualCharRange(map, vim.visualAnchor, vim.cursor)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function flashYank(ctx: PromptContext, map: PromptMap, action: YankAction | undefined, visualRange: HostRange | undefined) {
|
|
317
|
+
if (!action) return
|
|
318
|
+
const input = focusedInput(ctx)
|
|
319
|
+
if (!input) return
|
|
320
|
+
const range = visualRange ?? yankedTextRange(map, vim.cursor, action.text)
|
|
321
|
+
if (!range) return
|
|
322
|
+
|
|
323
|
+
cancelYankFlash()
|
|
324
|
+
yankFlashActive = true
|
|
325
|
+
setYankSelection(input, range.start, range.end, ctx)
|
|
326
|
+
ctx.requestRender()
|
|
327
|
+
yankTimer = setTimeout(() => {
|
|
328
|
+
yankTimer = undefined
|
|
329
|
+
if (!yankFlashActive) return
|
|
330
|
+
yankFlashActive = false
|
|
331
|
+
clearVisualSelection(input)
|
|
332
|
+
ctx.requestRender()
|
|
333
|
+
}, YANK_FLASH_MS)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cancelYankFlash() {
|
|
337
|
+
if (yankTimer) clearTimeout(yankTimer)
|
|
338
|
+
yankTimer = undefined
|
|
339
|
+
yankFlashActive = false
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function shouldFlashYankFor(key: string) {
|
|
343
|
+
if (isVisualMode(vim.mode)) return key === "y"
|
|
344
|
+
return vim.operator === "y"
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function visualCharRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined {
|
|
349
|
+
const anchorOffset = hostOffset(map, anchor, "next")
|
|
350
|
+
const cursorOffset = hostOffset(map, cursor, "previous")
|
|
351
|
+
return hostRange(map, anchorOffset, cursorOffset)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function visualLineRange(map: PromptMap, anchor: CursorPosition, cursor: CursorPosition): HostRange | undefined {
|
|
355
|
+
const startLine = Math.min(anchor.line, cursor.line)
|
|
356
|
+
const endLine = Math.max(anchor.line, cursor.line)
|
|
357
|
+
const start = hostOffset(map, { line: startLine, col: 0 }, "next")
|
|
358
|
+
const end = hostOffset(map, { line: endLine, col: vimLineLength(map.vimText, endLine) }, "previous")
|
|
359
|
+
return hostRange(map, start, end)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function yankedTextRange(map: PromptMap, cursor: CursorPosition, text: string): HostRange | undefined {
|
|
363
|
+
if (!text) return undefined
|
|
364
|
+
if (text.endsWith("\n")) {
|
|
365
|
+
const lineCount = text.split("\n").length - 1
|
|
366
|
+
return visualLineRange(map, cursor, { line: cursor.line + Math.max(0, lineCount - 1), col: 0 })
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const start = vimOffsetFromPosition(map.vimText, cursor)
|
|
370
|
+
return vimOffsetRange(map, start, start + text.length - 1)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function vimOffsetRange(map: PromptMap, left: number, right: number): HostRange | undefined {
|
|
374
|
+
const start = hostFromVimOffset(map, Math.min(left, right), "next")
|
|
375
|
+
const end = hostFromVimOffset(map, Math.max(left, right), "previous")
|
|
376
|
+
return hostRange(map, start, end)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function hostRange(map: PromptMap, left: number, right: number): HostRange | undefined {
|
|
380
|
+
if (!map.hostText) return undefined
|
|
381
|
+
const start = clamp(Math.min(left, right), 0, Math.max(0, map.hostText.length - 1))
|
|
382
|
+
const end = clamp(Math.max(left, right), 0, Math.max(0, map.hostText.length - 1))
|
|
383
|
+
return { start, end }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
type YankAction = Extract<VimeeAction, { type: "yank" }>
|
|
387
|
+
|
|
388
|
+
function yankAction(actions: VimeeAction[]): YankAction | undefined {
|
|
389
|
+
return actions.find((action): action is YankAction => action.type === "yank")
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function setSelection(input: EditBufferLike, start: number, end: number, ctx: PromptContext) {
|
|
393
|
+
setSelectionColors(input, start, end, ctx.api.theme.current.warning, ctx.api.theme.current.background)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function setYankSelection(input: EditBufferLike, start: number, end: number, ctx: PromptContext) {
|
|
397
|
+
setSelectionColors(input, start, end, ctx.api.theme.current.info, ctx.api.theme.current.background)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function setSelectionColors(input: EditBufferLike, start: number, end: number, background: PromptContext["api"]["theme"]["current"]["warning"], foreground: PromptContext["api"]["theme"]["current"]["background"]) {
|
|
401
|
+
input.selectionBg = background
|
|
402
|
+
input.selectionFg = foreground
|
|
403
|
+
|
|
404
|
+
if (input.setSelectionInclusive) {
|
|
405
|
+
input.setSelectionInclusive(start, end)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const exclusiveEnd = end + 1
|
|
410
|
+
if (input.setSelection) {
|
|
411
|
+
input.setSelection(start, exclusiveEnd)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
input.editorView?.setSelection?.(start, exclusiveEnd, background, foreground)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function clearVisualSelection(input: EditBufferLike) {
|
|
419
|
+
if (input.clearSelection) {
|
|
420
|
+
input.clearSelection()
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
input.editorView?.resetSelection?.()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isVisualMode(mode: VimContext["mode"]): mode is "visual" | "visual-line" {
|
|
428
|
+
return mode === "visual" || mode === "visual-line"
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function vimLineLength(text: string, line: number) {
|
|
432
|
+
return text.split("\n")[line]?.length ?? 0
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function vimOffsetFromPosition(text: string, position: CursorPosition) {
|
|
436
|
+
const lines = text.split("\n")
|
|
437
|
+
const line = clamp(position.line, 0, Math.max(0, lines.length - 1))
|
|
438
|
+
let offset = 0
|
|
439
|
+
for (let index = 0; index < line; index++) offset += lines[index].length + 1
|
|
440
|
+
return offset + clamp(position.col, 0, lines[line]?.length ?? 0)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function hostFromVimOffset(map: PromptMap, offset: number, bias: "previous" | "next") {
|
|
444
|
+
const current = clamp(offset, 0, map.vimText.length)
|
|
445
|
+
if (current === map.vimText.length) return map.hostText.length
|
|
446
|
+
|
|
447
|
+
const host = map.vimToHost[current]
|
|
448
|
+
if (host !== undefined) return host
|
|
449
|
+
|
|
450
|
+
if (bias === "previous") {
|
|
451
|
+
for (let previous = current - 1; previous >= 0; previous--) {
|
|
452
|
+
const previousHost = map.vimToHost[previous]
|
|
453
|
+
if (previousHost !== undefined) return previousHost
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (let next = current + 1; next < map.vimToHost.length; next++) {
|
|
458
|
+
const nextHost = map.vimToHost[next]
|
|
459
|
+
if (nextHost !== undefined) return nextHost
|
|
460
|
+
}
|
|
461
|
+
return map.hostText.length
|
|
270
462
|
}
|
|
271
463
|
|
|
272
464
|
function clampNormalCursor(input: EditBufferLike) {
|
|
@@ -278,6 +470,27 @@ function clampNormalCursor(input: EditBufferLike) {
|
|
|
278
470
|
if (cursor.visualRow === eol.visualRow && (cursor.offset === eol.offset || offset === eol.offset)) input.cursorOffset = Math.max(0, offset - 1)
|
|
279
471
|
}
|
|
280
472
|
|
|
473
|
+
function endMotionOffset(text: string, offset: number, count: number) {
|
|
474
|
+
let index = offset
|
|
475
|
+
for (let step = 0; step < count; step++) {
|
|
476
|
+
index++
|
|
477
|
+
while (index < text.length && isWhitespace(text[index])) index++
|
|
478
|
+
const kind = wordKind(text[index])
|
|
479
|
+
if (!kind) return Math.max(0, text.length - 1)
|
|
480
|
+
while (wordKind(text[index + 1]) === kind) index++
|
|
481
|
+
}
|
|
482
|
+
return clamp(index, 0, Math.max(0, text.length - 1))
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function isWhitespace(value: string | undefined) {
|
|
486
|
+
return value === " " || value === "\t" || value === "\n"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function wordKind(value: string | undefined) {
|
|
490
|
+
if (!value || isWhitespace(value)) return undefined
|
|
491
|
+
return /\w/.test(value) ? "word" : "punctuation"
|
|
492
|
+
}
|
|
493
|
+
|
|
281
494
|
function createKeybinds(config: VimConfig, log: VimLog): KeybindMap | undefined {
|
|
282
495
|
const map = createKeybindMap()
|
|
283
496
|
let count = 0
|
|
@@ -338,7 +551,7 @@ function tokenCtrl(token: string) {
|
|
|
338
551
|
}
|
|
339
552
|
|
|
340
553
|
function syncMode(state: VimState, mode: VimContext["mode"]) {
|
|
341
|
-
state.setMode(mode === "insert"
|
|
554
|
+
state.setMode(mode === "insert" || mode === "visual" || mode === "visual-line" ? mode : "normal")
|
|
342
555
|
}
|
|
343
556
|
|
|
344
557
|
function pendingDisplay(ctx: VimContext, keybindPending: boolean) {
|