opencode-vim 0.0.8 → 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
|
|
|
@@ -63,10 +68,23 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
const hostEnd = vimeeKey === "e" ? endMotionOffset(map.hostText, offset, vim.count || 1) : undefined
|
|
71
|
+
const shouldFlashYank = shouldFlashYankFor(vimeeKey)
|
|
72
|
+
const visualYankRange = visualYankRangeFor(map)
|
|
66
73
|
const result = processKeystroke(vimeeKey, vim, buffer, event.ctrl, false, keybinds)
|
|
67
74
|
vim = result.newCtx
|
|
68
75
|
if (hostEnd !== undefined && result.actions.every((action) => action.type === "cursor-move") && hostEnd > hostOffset(map, vim.cursor, "previous")) vim = { ...vim, cursor: hostPosition(map, hostEnd) }
|
|
69
|
-
|
|
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)
|
|
70
88
|
syncMode(state, vim.mode)
|
|
71
89
|
const keybindPending = keybinds?.isPending() ?? false
|
|
72
90
|
if (wasPending && !keybindPending && pendingBefore && state.mode() === "insert") flushPendingInsert(ctx, pendingBefore, offset)
|
|
@@ -78,6 +96,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
78
96
|
},
|
|
79
97
|
cleanup() {
|
|
80
98
|
if (timer) clearTimeout(timer)
|
|
99
|
+
if (yankTimer) clearTimeout(yankTimer)
|
|
81
100
|
},
|
|
82
101
|
}
|
|
83
102
|
|
|
@@ -115,7 +134,7 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
115
134
|
return next
|
|
116
135
|
}
|
|
117
136
|
|
|
118
|
-
function applyActions(actions: HostAction[], ctx: PromptContext, map: PromptMap) {
|
|
137
|
+
function applyActions(actions: HostAction[], ctx: PromptContext, map: PromptMap, clampFinalCursor = true) {
|
|
119
138
|
const ref = ctx.prompt()
|
|
120
139
|
const input = focusedInput(ctx)
|
|
121
140
|
let currentMap = map
|
|
@@ -146,7 +165,8 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
146
165
|
}
|
|
147
166
|
}
|
|
148
167
|
|
|
149
|
-
setCursor(input, currentMap, vim.cursor)
|
|
168
|
+
setCursor(input, currentMap, vim.cursor, clampFinalCursor)
|
|
169
|
+
syncVisualSelection(input, currentMap, ctx)
|
|
150
170
|
}
|
|
151
171
|
|
|
152
172
|
function handleInsertMode(event: KeyEvent, key: string, ctx: PromptContext, map: PromptMap, cursor: CursorPosition, offset: number) {
|
|
@@ -255,20 +275,190 @@ export function createVimeeAdapter(state: VimState, config: VimConfig, log: VimL
|
|
|
255
275
|
return hostOffset(map, position, vim.mode === "insert" ? "next" : "previous")
|
|
256
276
|
}
|
|
257
277
|
|
|
258
|
-
function setCursor(input: EditBufferLike | undefined, map: PromptMap, position: CursorPosition) {
|
|
278
|
+
function setCursor(input: EditBufferLike | undefined, map: PromptMap, position: CursorPosition, clampCursor = true) {
|
|
259
279
|
if (!input) return
|
|
260
280
|
input.cursorOffset = cursorOffset(map, position)
|
|
261
|
-
if (vim.mode !== "insert") clampNormalCursor(input)
|
|
281
|
+
if (clampCursor && vim.mode !== "insert") clampNormalCursor(input)
|
|
262
282
|
}
|
|
263
283
|
|
|
264
284
|
function appendVisualLine(input: EditBufferLike | undefined, map: PromptMap) {
|
|
265
285
|
if (!input?.gotoVisualLineEnd) return false
|
|
286
|
+
clearVisualSelection(input)
|
|
266
287
|
input.gotoVisualLineEnd()
|
|
267
288
|
vim = { ...vim, cursor: hostPosition(map, input.cursorOffset ?? map.hostText.length), mode: "insert", phase: "idle", count: 0, operator: null, statusMessage: "-- INSERT --" }
|
|
268
289
|
nativeInsertUndoSaved = false
|
|
269
290
|
syncMode(state, "insert")
|
|
270
291
|
return true
|
|
271
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
|
|
272
462
|
}
|
|
273
463
|
|
|
274
464
|
function clampNormalCursor(input: EditBufferLike) {
|
|
@@ -361,7 +551,7 @@ function tokenCtrl(token: string) {
|
|
|
361
551
|
}
|
|
362
552
|
|
|
363
553
|
function syncMode(state: VimState, mode: VimContext["mode"]) {
|
|
364
|
-
state.setMode(mode === "insert"
|
|
554
|
+
state.setMode(mode === "insert" || mode === "visual" || mode === "visual-line" ? mode : "normal")
|
|
365
555
|
}
|
|
366
556
|
|
|
367
557
|
function pendingDisplay(ctx: VimContext, keybindPending: boolean) {
|