pi-vim 0.9.0 → 0.11.0
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 +85 -78
- package/clipboard-policy.ts +7 -57
- package/index.ts +604 -309
- package/motions.ts +38 -15
- package/package.json +3 -3
- package/settings.ts +92 -0
- package/text-objects.ts +130 -82
- package/word-boundary-cache.ts +5 -4
package/index.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
CustomEditor,
|
|
5
|
-
type ExtensionAPI,
|
|
6
|
-
} from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
4
|
import {
|
|
8
5
|
CURSOR_MARKER,
|
|
9
6
|
Key,
|
|
@@ -11,55 +8,55 @@ import {
|
|
|
11
8
|
truncateToWidth,
|
|
12
9
|
visibleWidth,
|
|
13
10
|
} from "@mariozechner/pi-tui";
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
type ClipboardMirrorPolicy,
|
|
13
|
+
DEFAULT_CLIPBOARD_MIRROR_POLICY,
|
|
14
|
+
type RegisterWriteSource,
|
|
15
|
+
resolveClipboardMirrorPolicy,
|
|
16
|
+
} from "./clipboard-policy.js";
|
|
17
|
+
import {
|
|
18
|
+
findCharMotionTarget,
|
|
19
|
+
findFirstNonWhitespaceColumn,
|
|
20
|
+
findParagraphMotionTarget,
|
|
21
|
+
getLineGraphemes,
|
|
22
|
+
reverseCharMotion,
|
|
23
|
+
type WordMotionClass,
|
|
24
|
+
} from "./motions.js";
|
|
25
|
+
import { type ModeColorSettings, readPiVimSettings } from "./settings.js";
|
|
26
|
+
import {
|
|
27
|
+
resolveDelimitedTextObjectRange,
|
|
28
|
+
resolveMatchingPairMotionTarget,
|
|
29
|
+
resolveWordTextObjectRange,
|
|
30
|
+
type TextObjectKind,
|
|
31
|
+
type TextObjectRange,
|
|
32
|
+
type WordTextObjectClass,
|
|
33
|
+
} from "./text-objects.js";
|
|
15
34
|
import type {
|
|
16
|
-
Mode,
|
|
17
35
|
CharMotion,
|
|
36
|
+
LastCharMotion,
|
|
37
|
+
Mode,
|
|
18
38
|
PendingMotion,
|
|
19
39
|
PendingOperator,
|
|
20
|
-
LastCharMotion,
|
|
21
40
|
} from "./types.js";
|
|
22
41
|
import {
|
|
23
|
-
NORMAL_KEYS,
|
|
24
42
|
CHAR_MOTION_KEYS,
|
|
25
|
-
ESC_LEFT,
|
|
26
|
-
ESC_RIGHT,
|
|
27
|
-
ESC_UP,
|
|
28
43
|
CTRL_A,
|
|
29
44
|
CTRL_E,
|
|
30
45
|
CTRL_K,
|
|
31
46
|
CTRL_R,
|
|
32
47
|
CTRL_UNDERSCORE,
|
|
33
|
-
NEWLINE,
|
|
34
48
|
ESC_DOWN,
|
|
49
|
+
ESC_LEFT,
|
|
50
|
+
ESC_RIGHT,
|
|
51
|
+
ESC_UP,
|
|
52
|
+
NEWLINE,
|
|
53
|
+
NORMAL_KEYS,
|
|
35
54
|
} from "./types.js";
|
|
36
|
-
import {
|
|
37
|
-
reverseCharMotion,
|
|
38
|
-
findCharMotionTarget,
|
|
39
|
-
findParagraphMotionTarget,
|
|
40
|
-
findFirstNonWhitespaceColumn,
|
|
41
|
-
getLineGraphemes,
|
|
42
|
-
type WordMotionClass,
|
|
43
|
-
} from "./motions.js";
|
|
44
55
|
import {
|
|
45
56
|
WordBoundaryCache,
|
|
46
57
|
type WordMotionDirection,
|
|
47
58
|
type WordMotionTarget,
|
|
48
59
|
} from "./word-boundary-cache.js";
|
|
49
|
-
import {
|
|
50
|
-
DEFAULT_CLIPBOARD_MIRROR_POLICY,
|
|
51
|
-
readPiVimSettings,
|
|
52
|
-
resolveClipboardMirrorPolicy,
|
|
53
|
-
type ClipboardMirrorPolicy,
|
|
54
|
-
type RegisterWriteSource,
|
|
55
|
-
} from "./clipboard-policy.js";
|
|
56
|
-
import {
|
|
57
|
-
resolveDelimitedTextObjectRange,
|
|
58
|
-
resolveWordTextObjectRange,
|
|
59
|
-
type TextObjectKind,
|
|
60
|
-
type TextObjectRange,
|
|
61
|
-
type WordTextObjectClass,
|
|
62
|
-
} from "./text-objects.js";
|
|
63
60
|
|
|
64
61
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
65
62
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
@@ -71,12 +68,16 @@ const SOFTWARE_CURSOR_RESETS = ["\x1b[0m", "\x1b[27m"] as const;
|
|
|
71
68
|
const INSERT_CURSOR_SHAPE = "\x1b[5 q";
|
|
72
69
|
const BLOCK_CURSOR_SHAPE = "\x1b[1 q";
|
|
73
70
|
const RESET_CURSOR_SHAPE = "\x1b[0 q";
|
|
74
|
-
// Pi emits OSC52 before its native clipboard fallback. Give that 5s fallback
|
|
75
|
-
// a small grace so the parent does not kill the helper and discard stdout.
|
|
76
71
|
const CLIPBOARD_WRITE_TIMEOUT_MS = PI_NATIVE_CLIPBOARD_TIMEOUT_MS + 500;
|
|
77
72
|
const CLIPBOARD_SPAWN_FAILURE_LIMIT = 3;
|
|
78
73
|
const CLIPBOARD_READ_TIMEOUT_MS = 750;
|
|
79
74
|
const CLIPBOARD_READ_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
75
|
+
const MODE_COLORS = {
|
|
76
|
+
insert: "borderMuted",
|
|
77
|
+
normal: "borderAccent",
|
|
78
|
+
ex: "warning",
|
|
79
|
+
} as const;
|
|
80
|
+
const TOKEN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
80
81
|
|
|
81
82
|
type EditorSnapshot = {
|
|
82
83
|
text: string;
|
|
@@ -101,11 +102,13 @@ type ClipboardWriteFn = (text: string, signal: AbortSignal) => Promise<void>;
|
|
|
101
102
|
type ClipboardReadFn = () => string | null;
|
|
102
103
|
type ClipboardProcess = ReturnType<typeof spawn>;
|
|
103
104
|
|
|
104
|
-
type
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
type ModeColorKey = keyof typeof MODE_COLORS;
|
|
106
|
+
type ModeColorizers = Record<ModeColorKey, (s: string) => string>;
|
|
107
|
+
type ModalEditorOptions = {
|
|
108
|
+
labelColorizers?: ModeColorizers | null;
|
|
109
|
+
borderColorizers?: ModeColorizers | null;
|
|
108
110
|
};
|
|
111
|
+
type ThemeLike = { fg(token: string, text: string): string };
|
|
109
112
|
|
|
110
113
|
type CursorShapeSequence =
|
|
111
114
|
| typeof INSERT_CURSOR_SHAPE
|
|
@@ -120,6 +123,45 @@ type CursorShapeRuntime = {
|
|
|
120
123
|
|
|
121
124
|
type CursorShapeCleanup = () => void;
|
|
122
125
|
|
|
126
|
+
function resolveModeColors(
|
|
127
|
+
colors?: ModeColorSettings,
|
|
128
|
+
): Required<ModeColorSettings> {
|
|
129
|
+
return {
|
|
130
|
+
insert: colors?.insert ?? MODE_COLORS.insert,
|
|
131
|
+
normal: colors?.normal ?? MODE_COLORS.normal,
|
|
132
|
+
ex: colors?.ex ?? MODE_COLORS.ex,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function colorizeWithTheme(
|
|
136
|
+
theme: ThemeLike,
|
|
137
|
+
token: string,
|
|
138
|
+
fallback: string,
|
|
139
|
+
text: string,
|
|
140
|
+
): string {
|
|
141
|
+
const trimmedToken = token.trim();
|
|
142
|
+
if (TOKEN.test(trimmedToken)) {
|
|
143
|
+
try {
|
|
144
|
+
return theme.fg(trimmedToken, text);
|
|
145
|
+
} catch {
|
|
146
|
+
return theme.fg(fallback, text);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return theme.fg(fallback, text);
|
|
150
|
+
}
|
|
151
|
+
function buildModeColorizers(
|
|
152
|
+
theme: ThemeLike,
|
|
153
|
+
colors: Required<ModeColorSettings>,
|
|
154
|
+
transform: (text: string) => string = (text) => text,
|
|
155
|
+
): ModeColorizers {
|
|
156
|
+
const colorizer = (mode: ModeColorKey) => (text: string) =>
|
|
157
|
+
colorizeWithTheme(theme, colors[mode], MODE_COLORS[mode], transform(text));
|
|
158
|
+
return {
|
|
159
|
+
insert: colorizer("insert"),
|
|
160
|
+
normal: colorizer("normal"),
|
|
161
|
+
ex: colorizer("ex"),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
type CursorShapeTuiCandidate = {
|
|
124
166
|
terminal?: { write?: unknown };
|
|
125
167
|
setShowHardwareCursor?: unknown;
|
|
@@ -135,7 +177,10 @@ function getCursorShapeRuntime(tui: unknown): CursorShapeRuntime | null {
|
|
|
135
177
|
|
|
136
178
|
const write = terminal.write;
|
|
137
179
|
const setShowHardwareCursor = candidate.setShowHardwareCursor;
|
|
138
|
-
if (
|
|
180
|
+
if (
|
|
181
|
+
typeof write !== "function" ||
|
|
182
|
+
typeof setShowHardwareCursor !== "function"
|
|
183
|
+
) {
|
|
139
184
|
return null;
|
|
140
185
|
}
|
|
141
186
|
|
|
@@ -178,7 +223,10 @@ function findSoftwareCursorReset(
|
|
|
178
223
|
line: string,
|
|
179
224
|
startIndex: number,
|
|
180
225
|
): { index: number; sequence: (typeof SOFTWARE_CURSOR_RESETS)[number] } | null {
|
|
181
|
-
let firstReset: {
|
|
226
|
+
let firstReset: {
|
|
227
|
+
index: number;
|
|
228
|
+
sequence: (typeof SOFTWARE_CURSOR_RESETS)[number];
|
|
229
|
+
} | null = null;
|
|
182
230
|
|
|
183
231
|
for (const sequence of SOFTWARE_CURSOR_RESETS) {
|
|
184
232
|
const index = line.indexOf(sequence, startIndex);
|
|
@@ -203,9 +251,11 @@ function stripSoftwareCursorAfterMarker(line: string): string {
|
|
|
203
251
|
const reset = findSoftwareCursorReset(line, cursorContentStart);
|
|
204
252
|
if (!reset) return line;
|
|
205
253
|
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
|
|
254
|
+
return (
|
|
255
|
+
line.slice(0, cursorStart) +
|
|
256
|
+
line.slice(cursorContentStart, reset.index) +
|
|
257
|
+
line.slice(reset.index + reset.sequence.length)
|
|
258
|
+
);
|
|
209
259
|
}
|
|
210
260
|
|
|
211
261
|
type ClipboardCircuitBreaker = {
|
|
@@ -236,17 +286,21 @@ function isNodeSpawnErrno(error: unknown): boolean {
|
|
|
236
286
|
if (!(error instanceof Error)) return false;
|
|
237
287
|
|
|
238
288
|
const candidate = error as SpawnErrnoLike;
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
289
|
+
return (
|
|
290
|
+
typeof candidate.code === "string" &&
|
|
291
|
+
candidate.code.length > 0 &&
|
|
292
|
+
typeof candidate.syscall === "string" &&
|
|
293
|
+
candidate.syscall.startsWith("spawn")
|
|
294
|
+
);
|
|
243
295
|
}
|
|
244
296
|
|
|
245
297
|
function isClipboardEnvironmentFailure(error: unknown): boolean {
|
|
246
298
|
return error instanceof ClipboardSpawnError || isNodeSpawnErrno(error);
|
|
247
299
|
}
|
|
248
300
|
|
|
249
|
-
const PI_CODING_AGENT_MODULE_URL = import.meta.resolve(
|
|
301
|
+
const PI_CODING_AGENT_MODULE_URL = import.meta.resolve(
|
|
302
|
+
"@mariozechner/pi-coding-agent",
|
|
303
|
+
);
|
|
250
304
|
const CLIPBOARD_HELPER_SOURCE = `
|
|
251
305
|
import { copyToClipboard } from ${JSON.stringify(PI_CODING_AGENT_MODULE_URL)};
|
|
252
306
|
|
|
@@ -257,10 +311,7 @@ for await (const chunk of process.stdin) {
|
|
|
257
311
|
|
|
258
312
|
try {
|
|
259
313
|
await Promise.resolve(copyToClipboard(Buffer.concat(chunks).toString("utf8")));
|
|
260
|
-
} catch {
|
|
261
|
-
// Pi clipboard writes are best-effort. Backend failures must not make the
|
|
262
|
-
// helper exit non-zero and trip the parent spawn/environment breaker.
|
|
263
|
-
}
|
|
314
|
+
} catch {}
|
|
264
315
|
`;
|
|
265
316
|
|
|
266
317
|
const CLIPBOARD_READ_HELPER_SOURCE = `
|
|
@@ -316,11 +367,14 @@ function killClipboardProcess(child: ClipboardProcess): void {
|
|
|
316
367
|
try {
|
|
317
368
|
child.kill("SIGKILL");
|
|
318
369
|
} catch {
|
|
319
|
-
|
|
370
|
+
return;
|
|
320
371
|
}
|
|
321
372
|
}
|
|
322
373
|
|
|
323
|
-
function writeClipboardInChildProcess(
|
|
374
|
+
function writeClipboardInChildProcess(
|
|
375
|
+
text: string,
|
|
376
|
+
signal: AbortSignal,
|
|
377
|
+
): Promise<void> {
|
|
324
378
|
return new Promise<void>((resolve, reject) => {
|
|
325
379
|
if (signal.aborted) {
|
|
326
380
|
reject(getAbortError(signal));
|
|
@@ -350,12 +404,20 @@ function writeClipboardInChildProcess(text: string, signal: AbortSignal): Promis
|
|
|
350
404
|
}
|
|
351
405
|
|
|
352
406
|
try {
|
|
353
|
-
child = spawn(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
407
|
+
child = spawn(
|
|
408
|
+
process.execPath,
|
|
409
|
+
["--input-type=module", "-e", CLIPBOARD_HELPER_SOURCE],
|
|
410
|
+
{
|
|
411
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
412
|
+
windowsHide: true,
|
|
413
|
+
},
|
|
414
|
+
);
|
|
357
415
|
} catch (error) {
|
|
358
|
-
finish(
|
|
416
|
+
finish(
|
|
417
|
+
new ClipboardSpawnError("clipboard helper spawn failed", {
|
|
418
|
+
cause: error,
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
359
421
|
return;
|
|
360
422
|
}
|
|
361
423
|
|
|
@@ -369,7 +431,11 @@ function writeClipboardInChildProcess(text: string, signal: AbortSignal): Promis
|
|
|
369
431
|
});
|
|
370
432
|
|
|
371
433
|
child.once("error", (error) => {
|
|
372
|
-
finish(
|
|
434
|
+
finish(
|
|
435
|
+
new ClipboardSpawnError("clipboard helper spawn failed", {
|
|
436
|
+
cause: error,
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
373
439
|
});
|
|
374
440
|
|
|
375
441
|
child.once("close", (code) => {
|
|
@@ -393,7 +459,11 @@ function writeClipboardInChildProcess(text: string, signal: AbortSignal): Promis
|
|
|
393
459
|
return;
|
|
394
460
|
}
|
|
395
461
|
|
|
396
|
-
finish(
|
|
462
|
+
finish(
|
|
463
|
+
new ClipboardSpawnError(
|
|
464
|
+
`clipboard helper failed with exit code ${code ?? "null"}`,
|
|
465
|
+
),
|
|
466
|
+
);
|
|
397
467
|
});
|
|
398
468
|
|
|
399
469
|
if (!child.stdin) {
|
|
@@ -436,7 +506,9 @@ class ClipboardMirror {
|
|
|
436
506
|
) {}
|
|
437
507
|
|
|
438
508
|
setWriteFn(writeFn: ClipboardWriteFn): void {
|
|
439
|
-
this.activeController?.abort(
|
|
509
|
+
this.activeController?.abort(
|
|
510
|
+
createClipboardAbortError("clipboard writer replaced"),
|
|
511
|
+
);
|
|
440
512
|
this.writeFn = writeFn;
|
|
441
513
|
resetClipboardCircuitBreaker();
|
|
442
514
|
}
|
|
@@ -446,7 +518,9 @@ class ClipboardMirror {
|
|
|
446
518
|
}
|
|
447
519
|
|
|
448
520
|
hasPendingWrite(): boolean {
|
|
449
|
-
return
|
|
521
|
+
return (
|
|
522
|
+
this.activeText !== null || this.pendingText !== null || this.draining
|
|
523
|
+
);
|
|
450
524
|
}
|
|
451
525
|
|
|
452
526
|
mirror(text: string): void {
|
|
@@ -476,7 +550,6 @@ class ClipboardMirror {
|
|
|
476
550
|
this.circuitBreaker.consecutiveEnvironmentFailures = 0;
|
|
477
551
|
} catch (error) {
|
|
478
552
|
this.recordWriteFailure(error);
|
|
479
|
-
// Clipboard mirroring is best-effort; the register is authoritative.
|
|
480
553
|
} finally {
|
|
481
554
|
if (this.activeController === controller) {
|
|
482
555
|
this.activeController = null;
|
|
@@ -503,13 +576,19 @@ class ClipboardMirror {
|
|
|
503
576
|
}
|
|
504
577
|
|
|
505
578
|
this.circuitBreaker.consecutiveEnvironmentFailures += 1;
|
|
506
|
-
if (
|
|
579
|
+
if (
|
|
580
|
+
this.circuitBreaker.consecutiveEnvironmentFailures >=
|
|
581
|
+
CLIPBOARD_SPAWN_FAILURE_LIMIT
|
|
582
|
+
) {
|
|
507
583
|
this.circuitBreaker.disabled = true;
|
|
508
584
|
this.pendingText = null;
|
|
509
585
|
}
|
|
510
586
|
}
|
|
511
587
|
|
|
512
|
-
private async writeWithTimeout(
|
|
588
|
+
private async writeWithTimeout(
|
|
589
|
+
text: string,
|
|
590
|
+
controller: AbortController,
|
|
591
|
+
): Promise<void> {
|
|
513
592
|
const timeoutError = createClipboardAbortError("clipboard write timed out");
|
|
514
593
|
const timeoutId = setTimeout(() => {
|
|
515
594
|
controller.abort(timeoutError);
|
|
@@ -548,15 +627,18 @@ export class ModalEditor extends CustomEditor {
|
|
|
548
627
|
private readonly redoStack: EditorSnapshot[] = [];
|
|
549
628
|
private currentTransition: TransitionState = "none";
|
|
550
629
|
private onChangeHooked: boolean = false;
|
|
551
|
-
private readonly labelColorizers:
|
|
630
|
+
private readonly labelColorizers: ModeColorizers | null;
|
|
631
|
+
private readonly borderColorizers: ModeColorizers | null;
|
|
552
632
|
private readonly cursorShapeRuntime: CursorShapeRuntime | null;
|
|
553
633
|
private lastCursorShapeSequence: CursorShapeSequence | null = null;
|
|
554
634
|
|
|
555
|
-
// Unnamed register
|
|
556
635
|
private unnamedRegister: string = "";
|
|
557
636
|
private preferRegisterForPut = false;
|
|
558
|
-
private clipboardMirrorPolicy: ClipboardMirrorPolicy =
|
|
559
|
-
|
|
637
|
+
private clipboardMirrorPolicy: ClipboardMirrorPolicy =
|
|
638
|
+
DEFAULT_CLIPBOARD_MIRROR_POLICY;
|
|
639
|
+
private readonly clipboardMirror = new ClipboardMirror(
|
|
640
|
+
writeClipboardInChildProcess,
|
|
641
|
+
);
|
|
560
642
|
private clipboardReadFn: ClipboardReadFn = readClipboardInChildProcess;
|
|
561
643
|
private quitFn: () => void = () => {};
|
|
562
644
|
private notifyFn: (message: string) => void = () => {};
|
|
@@ -565,18 +647,21 @@ export class ModalEditor extends CustomEditor {
|
|
|
565
647
|
tui: CustomEditorConstructorArgs[0],
|
|
566
648
|
theme: CustomEditorConstructorArgs[1],
|
|
567
649
|
kb: CustomEditorConstructorArgs[2],
|
|
568
|
-
|
|
650
|
+
opts?: ModalEditorOptions,
|
|
569
651
|
) {
|
|
570
652
|
super(tui, theme, kb);
|
|
571
653
|
this.cursorShapeRuntime = getCursorShapeRuntime(tui);
|
|
572
|
-
this.labelColorizers = labelColorizers ?? null;
|
|
654
|
+
this.labelColorizers = opts?.labelColorizers ?? null;
|
|
655
|
+
this.borderColorizers = opts?.borderColorizers ?? null;
|
|
656
|
+
this.installModeBorderColorizer();
|
|
573
657
|
}
|
|
574
658
|
|
|
575
|
-
// Test seams
|
|
576
659
|
setClipboardFn(fn: (text: string, signal?: AbortSignal) => unknown): void {
|
|
577
|
-
this.clipboardMirror.setWriteFn(
|
|
578
|
-
|
|
579
|
-
|
|
660
|
+
this.clipboardMirror.setWriteFn(
|
|
661
|
+
async (text: string, signal: AbortSignal) => {
|
|
662
|
+
await fn(text, signal);
|
|
663
|
+
},
|
|
664
|
+
);
|
|
580
665
|
}
|
|
581
666
|
setClipboardWriteTimeoutMs(timeoutMs: number): void {
|
|
582
667
|
this.clipboardMirror.setTimeoutMs(timeoutMs);
|
|
@@ -590,15 +675,51 @@ export class ModalEditor extends CustomEditor {
|
|
|
590
675
|
getClipboardMirrorPolicy(): ClipboardMirrorPolicy {
|
|
591
676
|
return this.clipboardMirrorPolicy;
|
|
592
677
|
}
|
|
593
|
-
setQuitFn(fn: () => void): void {
|
|
594
|
-
|
|
595
|
-
|
|
678
|
+
setQuitFn(fn: () => void): void {
|
|
679
|
+
this.quitFn = fn;
|
|
680
|
+
}
|
|
681
|
+
setNotifyFn(fn: (message: string) => void): void {
|
|
682
|
+
this.notifyFn = fn;
|
|
683
|
+
}
|
|
684
|
+
getRegister(): string {
|
|
685
|
+
return this.unnamedRegister;
|
|
686
|
+
}
|
|
596
687
|
setRegister(text: string): void {
|
|
597
688
|
this.unnamedRegister = text;
|
|
598
689
|
this.preferRegisterForPut = false;
|
|
599
690
|
}
|
|
600
|
-
getMode(): Mode {
|
|
601
|
-
|
|
691
|
+
getMode(): Mode {
|
|
692
|
+
return this.mode;
|
|
693
|
+
}
|
|
694
|
+
getText(): string {
|
|
695
|
+
return this.getLines().join("\n");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private getActiveMode(): Mode | "ex" {
|
|
699
|
+
if (this.pendingExCommand !== null) return "ex";
|
|
700
|
+
return this.mode;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private installModeBorderColorizer(): void {
|
|
704
|
+
if (!this.borderColorizers) return;
|
|
705
|
+
let base = this.borderColor;
|
|
706
|
+
const modeBorderColor = (text: string) =>
|
|
707
|
+
(this.borderColorizers?.[this.getActiveMode()] ?? base)(text);
|
|
708
|
+
// Pi assigns its default border color after extension editor construction.
|
|
709
|
+
// Keep a mode-aware getter installed and treat later assignments as the
|
|
710
|
+
// fallback/base color, otherwise syncBorderColorWithMode is overwritten in
|
|
711
|
+
// real sessions even though direct editor tests pass.
|
|
712
|
+
Object.defineProperty(this, "borderColor", {
|
|
713
|
+
get: () => modeBorderColor,
|
|
714
|
+
set(next: unknown) {
|
|
715
|
+
if (typeof next === "function") base = next as typeof base;
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private setMode(mode: Mode = "insert"): void {
|
|
721
|
+
this.mode = mode;
|
|
722
|
+
}
|
|
602
723
|
|
|
603
724
|
override setText(text: string): void {
|
|
604
725
|
this.clearRedoStack();
|
|
@@ -613,14 +734,20 @@ export class ModalEditor extends CustomEditor {
|
|
|
613
734
|
};
|
|
614
735
|
}
|
|
615
736
|
|
|
616
|
-
private requireRedoRestoreState(
|
|
617
|
-
|
|
618
|
-
|
|
737
|
+
private requireRedoRestoreState(editor: ModalEditorInternals): {
|
|
738
|
+
lines: string[];
|
|
739
|
+
cursorLine?: number;
|
|
740
|
+
cursorCol?: number;
|
|
741
|
+
} {
|
|
619
742
|
const state = editor.state;
|
|
620
743
|
if (!state || !Array.isArray(state.lines)) {
|
|
621
744
|
throw new Error("Redo restore prerequisite: editor state unavailable");
|
|
622
745
|
}
|
|
623
|
-
return state as {
|
|
746
|
+
return state as {
|
|
747
|
+
lines: string[];
|
|
748
|
+
cursorLine?: number;
|
|
749
|
+
cursorCol?: number;
|
|
750
|
+
};
|
|
624
751
|
}
|
|
625
752
|
|
|
626
753
|
private restoreSnapshot(snapshot: EditorSnapshot): void {
|
|
@@ -652,9 +779,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
652
779
|
}
|
|
653
780
|
|
|
654
781
|
private snapshotChanged(a: EditorSnapshot, b: EditorSnapshot): boolean {
|
|
655
|
-
return
|
|
656
|
-
|
|
657
|
-
|
|
782
|
+
return (
|
|
783
|
+
a.text !== b.text ||
|
|
784
|
+
a.cursor.line !== b.cursor.line ||
|
|
785
|
+
a.cursor.col !== b.cursor.col
|
|
786
|
+
);
|
|
658
787
|
}
|
|
659
788
|
|
|
660
789
|
private withTransition<T>(
|
|
@@ -741,9 +870,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
741
870
|
private applySyntheticEdit(mutation: () => void): void {
|
|
742
871
|
const editor = this as unknown as ModalEditorInternals;
|
|
743
872
|
if (!editor.state || !Array.isArray(editor.state.lines)) {
|
|
744
|
-
throw new Error(
|
|
745
|
-
"Synthetic edit prerequisite: editor state unavailable",
|
|
746
|
-
);
|
|
873
|
+
throw new Error("Synthetic edit prerequisite: editor state unavailable");
|
|
747
874
|
}
|
|
748
875
|
|
|
749
876
|
if (typeof editor.pushUndoSnapshot !== "function") {
|
|
@@ -760,9 +887,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
760
887
|
|
|
761
888
|
if (this.getText() === textBefore) return;
|
|
762
889
|
|
|
763
|
-
// Text changed — push undo boundary for pre-mutation state.
|
|
764
|
-
// Briefly swap pre-mutation state in for the snapshot, then
|
|
765
|
-
// restore the post-mutation result.
|
|
766
890
|
const postLines = editor.state.lines.slice();
|
|
767
891
|
const postCursorLine = editor.state.cursorLine;
|
|
768
892
|
const postCursorCol = editor.state.cursorCol;
|
|
@@ -790,8 +914,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
790
914
|
}
|
|
791
915
|
|
|
792
916
|
private clearPendingExCommand(): void {
|
|
793
|
-
const shouldDiscardBracketedPasteTail =
|
|
794
|
-
|
|
917
|
+
const shouldDiscardBracketedPasteTail =
|
|
918
|
+
this.acceptingBracketedPasteInExCommand ||
|
|
919
|
+
this.pendingEscWhileAcceptingBracketedPasteInExCommand;
|
|
795
920
|
|
|
796
921
|
this.pendingExCommand = null;
|
|
797
922
|
this.acceptingBracketedPasteInExCommand = false;
|
|
@@ -875,7 +1000,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
875
1000
|
}
|
|
876
1001
|
}
|
|
877
1002
|
|
|
878
|
-
private stripBracketedPasteInNormalMode(data: string): {
|
|
1003
|
+
private stripBracketedPasteInNormalMode(data: string): {
|
|
1004
|
+
filtered: string | null;
|
|
1005
|
+
stripped: boolean;
|
|
1006
|
+
} {
|
|
879
1007
|
let chunk = data;
|
|
880
1008
|
let stripped = false;
|
|
881
1009
|
|
|
@@ -898,14 +1026,18 @@ export class ModalEditor extends CustomEditor {
|
|
|
898
1026
|
}
|
|
899
1027
|
|
|
900
1028
|
stripped = true;
|
|
901
|
-
const end = chunk.indexOf(
|
|
1029
|
+
const end = chunk.indexOf(
|
|
1030
|
+
BRACKETED_PASTE_END,
|
|
1031
|
+
start + BRACKETED_PASTE_START.length,
|
|
1032
|
+
);
|
|
902
1033
|
if (end === -1) {
|
|
903
1034
|
this.discardingBracketedPasteInNormalMode = true;
|
|
904
1035
|
const leading = chunk.slice(0, start);
|
|
905
1036
|
return { filtered: leading.length > 0 ? leading : null, stripped };
|
|
906
1037
|
}
|
|
907
1038
|
|
|
908
|
-
chunk =
|
|
1039
|
+
chunk =
|
|
1040
|
+
chunk.slice(0, start) + chunk.slice(end + BRACKETED_PASTE_END.length);
|
|
909
1041
|
if (!chunk) return { filtered: null, stripped };
|
|
910
1042
|
}
|
|
911
1043
|
}
|
|
@@ -958,24 +1090,19 @@ export class ModalEditor extends CustomEditor {
|
|
|
958
1090
|
return;
|
|
959
1091
|
}
|
|
960
1092
|
|
|
961
|
-
if (this.mode
|
|
962
|
-
// Shift+Alt+A: go to end of line (like Esc -> A but stay in insert)
|
|
1093
|
+
if ("insert" === this.mode) {
|
|
963
1094
|
if (matchesKey(data, Key.shiftAlt("a")) || data === "\x1bA") {
|
|
964
1095
|
super.handleInput(CTRL_E);
|
|
965
1096
|
return;
|
|
966
1097
|
}
|
|
967
|
-
// Shift+Alt+I: go to start of line (like Esc -> I but stay in insert)
|
|
968
1098
|
if (matchesKey(data, Key.shiftAlt("i")) || data === "\x1bI") {
|
|
969
1099
|
super.handleInput(CTRL_A);
|
|
970
1100
|
return;
|
|
971
1101
|
}
|
|
972
|
-
// Alt+o: open new line below (stay in insert mode)
|
|
973
1102
|
if (matchesKey(data, Key.alt("o")) || data === "\x1bo") {
|
|
974
1103
|
this.openLineBelow();
|
|
975
1104
|
return;
|
|
976
1105
|
}
|
|
977
|
-
// Alt+Shift+o: open new line above (stay in insert mode)
|
|
978
|
-
// \x1bO is the legacy sequence for Alt+Shift+O (VT100 SS3 prefix in non-Kitty terminals)
|
|
979
1106
|
if (matchesKey(data, Key.shiftAlt("o")) || data === "\x1bO") {
|
|
980
1107
|
this.openLineAbove();
|
|
981
1108
|
return;
|
|
@@ -1003,9 +1130,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
1003
1130
|
const replacement = data.repeat(count);
|
|
1004
1131
|
const lineStartAbs = this.getAbsoluteIndex(cursor.line, 0);
|
|
1005
1132
|
const text = this.getText();
|
|
1006
|
-
const newText =
|
|
1007
|
-
|
|
1008
|
-
|
|
1133
|
+
const newText =
|
|
1134
|
+
text.slice(0, lineStartAbs) +
|
|
1135
|
+
before +
|
|
1136
|
+
replacement +
|
|
1137
|
+
after +
|
|
1138
|
+
text.slice(lineStartAbs + line.length);
|
|
1139
|
+
const newCursorAbs =
|
|
1140
|
+
lineStartAbs + before.length + data.length * (count - 1);
|
|
1009
1141
|
this.replaceTextInBuffer(newText, newCursorAbs);
|
|
1010
1142
|
return;
|
|
1011
1143
|
}
|
|
@@ -1068,32 +1200,42 @@ export class ModalEditor extends CustomEditor {
|
|
|
1068
1200
|
}
|
|
1069
1201
|
|
|
1070
1202
|
if (
|
|
1071
|
-
this.pendingMotion
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1203
|
+
this.pendingMotion ||
|
|
1204
|
+
this.pendingTextObject ||
|
|
1205
|
+
this.pendingOperator ||
|
|
1206
|
+
this.prefixCount ||
|
|
1207
|
+
this.operatorCount ||
|
|
1208
|
+
this.pendingG ||
|
|
1209
|
+
this.pendingGCount ||
|
|
1210
|
+
this.pendingReplace
|
|
1079
1211
|
) {
|
|
1080
1212
|
this.clearPendingState();
|
|
1081
1213
|
return;
|
|
1082
1214
|
}
|
|
1083
|
-
if (this.mode
|
|
1215
|
+
if ("insert" === this.mode) {
|
|
1084
1216
|
this.clearUnderlyingPasteStateIfActive();
|
|
1085
|
-
this.
|
|
1217
|
+
this.setMode("normal");
|
|
1086
1218
|
} else {
|
|
1087
1219
|
super.handleInput("\x1b"); // pass escape to abort agent
|
|
1088
1220
|
}
|
|
1089
1221
|
}
|
|
1090
1222
|
|
|
1091
1223
|
private isEnterLikeInput(data: string): boolean {
|
|
1092
|
-
return
|
|
1224
|
+
return (
|
|
1225
|
+
data === "\r" ||
|
|
1226
|
+
data === "\n" ||
|
|
1227
|
+
matchesKey(data, "enter") ||
|
|
1228
|
+
matchesKey(data, "return")
|
|
1229
|
+
);
|
|
1093
1230
|
}
|
|
1094
1231
|
|
|
1095
1232
|
private isBackspaceLikeInput(data: string): boolean {
|
|
1096
|
-
return
|
|
1233
|
+
return (
|
|
1234
|
+
data === "\x7f" ||
|
|
1235
|
+
data === "\x08" ||
|
|
1236
|
+
matchesKey(data, "backspace") ||
|
|
1237
|
+
matchesKey(data, "ctrl+h")
|
|
1238
|
+
);
|
|
1097
1239
|
}
|
|
1098
1240
|
|
|
1099
1241
|
private deleteLastPendingExCommandGrapheme(): void {
|
|
@@ -1116,10 +1258,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1116
1258
|
|
|
1117
1259
|
private handlePendingExCommandControlChunk(data: string): boolean {
|
|
1118
1260
|
if (
|
|
1119
|
-
!data.includes("\r")
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1261
|
+
!data.includes("\r") &&
|
|
1262
|
+
!data.includes("\n") &&
|
|
1263
|
+
!data.includes("\x7f") &&
|
|
1264
|
+
!data.includes("\x08")
|
|
1123
1265
|
) {
|
|
1124
1266
|
return false;
|
|
1125
1267
|
}
|
|
@@ -1216,7 +1358,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
1216
1358
|
if (data.length === 0) return false;
|
|
1217
1359
|
for (const char of data) {
|
|
1218
1360
|
const codePoint = char.codePointAt(0);
|
|
1219
|
-
if (codePoint === undefined || codePoint < 32 || codePoint === 127)
|
|
1361
|
+
if (codePoint === undefined || codePoint < 32 || codePoint === 127)
|
|
1362
|
+
return false;
|
|
1220
1363
|
}
|
|
1221
1364
|
return true;
|
|
1222
1365
|
}
|
|
@@ -1253,9 +1396,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1253
1396
|
|
|
1254
1397
|
if (prefix === null && operator === null) return defaultValue;
|
|
1255
1398
|
|
|
1256
|
-
const total =
|
|
1257
|
-
|
|
1258
|
-
|
|
1399
|
+
const total =
|
|
1400
|
+
prefix !== null && operator !== null
|
|
1401
|
+
? prefix * operator
|
|
1402
|
+
: (prefix ?? operator ?? defaultValue);
|
|
1259
1403
|
|
|
1260
1404
|
if (!Number.isFinite(total) || total <= 0) return defaultValue;
|
|
1261
1405
|
return Math.min(MAX_COUNT, total);
|
|
@@ -1265,6 +1409,13 @@ export class ModalEditor extends CustomEditor {
|
|
|
1265
1409
|
return this.prefixCount.length > 0 || this.operatorCount.length > 0;
|
|
1266
1410
|
}
|
|
1267
1411
|
|
|
1412
|
+
private opDigit(data: string): boolean {
|
|
1413
|
+
if (!this.isDigit(data) || (data === "0" && !this.operatorCount))
|
|
1414
|
+
return false;
|
|
1415
|
+
this.operatorCount += data;
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1268
1419
|
private cancelPendingOperator(data: string): void {
|
|
1269
1420
|
this.pendingOperator = null;
|
|
1270
1421
|
this.prefixCount = "";
|
|
@@ -1290,7 +1441,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1290
1441
|
} else if (this.pendingOperator === "c") {
|
|
1291
1442
|
this.deleteWithCharMotion(pendingMotion, data);
|
|
1292
1443
|
this.pendingOperator = null;
|
|
1293
|
-
this.
|
|
1444
|
+
this.setMode();
|
|
1294
1445
|
} else if (this.pendingOperator === "y") {
|
|
1295
1446
|
this.yankWithCharMotion(pendingMotion, data);
|
|
1296
1447
|
this.pendingOperator = null;
|
|
@@ -1319,7 +1470,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1319
1470
|
if (data === "w" || data === "W") {
|
|
1320
1471
|
const semanticClass: WordTextObjectClass = data === "W" ? "WORD" : "word";
|
|
1321
1472
|
const count = this.takeTotalCount(1);
|
|
1322
|
-
const range = this.getWordObjectRange(
|
|
1473
|
+
const range = this.getWordObjectRange(
|
|
1474
|
+
pendingTextObject,
|
|
1475
|
+
count,
|
|
1476
|
+
semanticClass,
|
|
1477
|
+
);
|
|
1323
1478
|
if (!range || !this.pendingOperator) {
|
|
1324
1479
|
this.pendingOperator = null;
|
|
1325
1480
|
return;
|
|
@@ -1357,7 +1512,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1357
1512
|
if (range.endAbs === range.startAbs) {
|
|
1358
1513
|
if (pendingOperator === "c") {
|
|
1359
1514
|
this.moveCursorToAbsoluteIndex(range.startAbs);
|
|
1360
|
-
this.
|
|
1515
|
+
this.setMode();
|
|
1361
1516
|
}
|
|
1362
1517
|
return;
|
|
1363
1518
|
}
|
|
@@ -1369,7 +1524,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1369
1524
|
|
|
1370
1525
|
if (pendingOperator === "c") {
|
|
1371
1526
|
this.deleteRangeByAbsolute(range.startAbs, range.endAbs);
|
|
1372
|
-
this.
|
|
1527
|
+
this.setMode();
|
|
1373
1528
|
return;
|
|
1374
1529
|
}
|
|
1375
1530
|
|
|
@@ -1379,16 +1534,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1379
1534
|
}
|
|
1380
1535
|
|
|
1381
1536
|
private handlePendingDelete(data: string): void {
|
|
1382
|
-
if (this.
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
}
|
|
1388
|
-
} else {
|
|
1389
|
-
this.operatorCount += data;
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1537
|
+
if (this.opDigit(data)) return;
|
|
1538
|
+
|
|
1539
|
+
if (data === "%") {
|
|
1540
|
+
this.applyPercentOp();
|
|
1541
|
+
return;
|
|
1392
1542
|
}
|
|
1393
1543
|
|
|
1394
1544
|
if (data === "d") {
|
|
@@ -1399,7 +1549,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
1399
1549
|
}
|
|
1400
1550
|
|
|
1401
1551
|
if (data === "j" || data === "k") {
|
|
1402
|
-
const hasDualCount =
|
|
1552
|
+
const hasDualCount =
|
|
1553
|
+
this.prefixCount.length > 0 && this.operatorCount.length > 0;
|
|
1403
1554
|
const count = this.takeTotalCount(1);
|
|
1404
1555
|
const delta = hasDualCount ? Math.max(0, count - 1) : count;
|
|
1405
1556
|
this.deleteLinewiseByDelta(data === "j" ? delta : -delta);
|
|
@@ -1430,20 +1581,17 @@ export class ModalEditor extends CustomEditor {
|
|
|
1430
1581
|
return;
|
|
1431
1582
|
}
|
|
1432
1583
|
|
|
1433
|
-
const hasCount = this.
|
|
1434
|
-
const supportsCountedWordMotion =
|
|
1435
|
-
data === "w"
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
);
|
|
1584
|
+
const hasCount = this.hasPendingCount();
|
|
1585
|
+
const supportsCountedWordMotion =
|
|
1586
|
+
data === "w" ||
|
|
1587
|
+
data === "e" ||
|
|
1588
|
+
data === "b" ||
|
|
1589
|
+
data === "W" ||
|
|
1590
|
+
data === "E" ||
|
|
1591
|
+
data === "B";
|
|
1442
1592
|
const supportsCountedTextObject = data === "i" || data === "a";
|
|
1443
1593
|
|
|
1444
1594
|
if (hasCount && !supportsCountedWordMotion && !supportsCountedTextObject) {
|
|
1445
|
-
// Counted forms beyond dd, d{count}j/k, d{count}{f/F/t/T}, and
|
|
1446
|
-
// d{count}{w/e/b/W/E/B}/{i/a}w are out of scope.
|
|
1447
1595
|
this.cancelPendingOperator(data);
|
|
1448
1596
|
return;
|
|
1449
1597
|
}
|
|
@@ -1459,21 +1607,15 @@ export class ModalEditor extends CustomEditor {
|
|
|
1459
1607
|
return;
|
|
1460
1608
|
}
|
|
1461
1609
|
|
|
1462
|
-
// Invalid motion: cancel operator to avoid sticky surprising deletes.
|
|
1463
1610
|
this.cancelPendingOperator(data);
|
|
1464
1611
|
}
|
|
1465
1612
|
|
|
1466
1613
|
private handlePendingChange(data: string): void {
|
|
1467
|
-
if (this.
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
}
|
|
1473
|
-
} else {
|
|
1474
|
-
this.operatorCount += data;
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1614
|
+
if (this.opDigit(data)) return;
|
|
1615
|
+
|
|
1616
|
+
if (data === "%") {
|
|
1617
|
+
this.applyPercentOp();
|
|
1618
|
+
return;
|
|
1477
1619
|
}
|
|
1478
1620
|
|
|
1479
1621
|
if (data === "c") {
|
|
@@ -1484,7 +1626,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1484
1626
|
|
|
1485
1627
|
this.cutLine();
|
|
1486
1628
|
this.pendingOperator = null;
|
|
1487
|
-
this.
|
|
1629
|
+
this.setMode();
|
|
1488
1630
|
return;
|
|
1489
1631
|
}
|
|
1490
1632
|
|
|
@@ -1505,7 +1647,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1505
1647
|
this.replaceTextInBuffer(newText, cursorAbs);
|
|
1506
1648
|
}
|
|
1507
1649
|
this.pendingOperator = null;
|
|
1508
|
-
this.
|
|
1650
|
+
this.setMode();
|
|
1509
1651
|
return;
|
|
1510
1652
|
}
|
|
1511
1653
|
|
|
@@ -1514,15 +1656,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
1514
1656
|
return;
|
|
1515
1657
|
}
|
|
1516
1658
|
|
|
1517
|
-
const hasCount = this.
|
|
1518
|
-
const supportsCountedWordMotion =
|
|
1519
|
-
data === "w"
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
);
|
|
1659
|
+
const hasCount = this.hasPendingCount();
|
|
1660
|
+
const supportsCountedWordMotion =
|
|
1661
|
+
data === "w" ||
|
|
1662
|
+
data === "e" ||
|
|
1663
|
+
data === "b" ||
|
|
1664
|
+
data === "W" ||
|
|
1665
|
+
data === "E" ||
|
|
1666
|
+
data === "B";
|
|
1526
1667
|
const supportsCountedTextObject = data === "i" || data === "a";
|
|
1527
1668
|
|
|
1528
1669
|
if (hasCount && !supportsCountedWordMotion && !supportsCountedTextObject) {
|
|
@@ -1536,16 +1677,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
1536
1677
|
}
|
|
1537
1678
|
|
|
1538
1679
|
const motionCount = supportsCountedWordMotion ? this.takeTotalCount(1) : 1;
|
|
1539
|
-
const effectiveMotion =
|
|
1540
|
-
? "E"
|
|
1541
|
-
: data;
|
|
1680
|
+
const effectiveMotion =
|
|
1681
|
+
data === "W" && this.isCursorOnNonWhitespace() ? "E" : data;
|
|
1542
1682
|
if (this.deleteWithMotion(effectiveMotion, motionCount)) {
|
|
1543
1683
|
this.pendingOperator = null;
|
|
1544
|
-
this.
|
|
1684
|
+
this.setMode();
|
|
1545
1685
|
return;
|
|
1546
1686
|
}
|
|
1547
1687
|
|
|
1548
|
-
// Invalid motion: cancel operator to avoid sticky surprising changes.
|
|
1549
1688
|
this.cancelPendingOperator(data);
|
|
1550
1689
|
}
|
|
1551
1690
|
|
|
@@ -1583,6 +1722,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
1583
1722
|
return;
|
|
1584
1723
|
}
|
|
1585
1724
|
|
|
1725
|
+
if (data === "%") {
|
|
1726
|
+
this.prefixCount = "";
|
|
1727
|
+
this.operatorCount = "";
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1586
1731
|
if (data === "d" || data === "y") {
|
|
1587
1732
|
this.pendingOperator = data;
|
|
1588
1733
|
return;
|
|
@@ -1605,43 +1750,34 @@ export class ModalEditor extends CustomEditor {
|
|
|
1605
1750
|
return;
|
|
1606
1751
|
}
|
|
1607
1752
|
|
|
1608
|
-
const supportsCountedStandaloneEdit =
|
|
1609
|
-
data === "x"
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
data === "
|
|
1632
|
-
|
|
1633
|
-
|| data === "b"
|
|
1634
|
-
|| data === "W"
|
|
1635
|
-
|| data === "E"
|
|
1636
|
-
|| data === "B"
|
|
1637
|
-
);
|
|
1753
|
+
const supportsCountedStandaloneEdit =
|
|
1754
|
+
data === "x" ||
|
|
1755
|
+
data === "r" ||
|
|
1756
|
+
data === "s" ||
|
|
1757
|
+
data === "S" ||
|
|
1758
|
+
data === "D" ||
|
|
1759
|
+
data === "C" ||
|
|
1760
|
+
data === "p" ||
|
|
1761
|
+
data === "P" ||
|
|
1762
|
+
data === "Y" ||
|
|
1763
|
+
data === "J" ||
|
|
1764
|
+
data === "u" ||
|
|
1765
|
+
data === CTRL_UNDERSCORE ||
|
|
1766
|
+
matchesKey(data, "ctrl+_") ||
|
|
1767
|
+
data === CTRL_R ||
|
|
1768
|
+
matchesKey(data, "ctrl+r");
|
|
1769
|
+
const supportsCountedCharMotion =
|
|
1770
|
+
CHAR_MOTION_KEYS.has(data) || data === ";" || data === ",";
|
|
1771
|
+
const supportsCountedWordMotion =
|
|
1772
|
+
data === "w" ||
|
|
1773
|
+
data === "e" ||
|
|
1774
|
+
data === "b" ||
|
|
1775
|
+
data === "W" ||
|
|
1776
|
+
data === "E" ||
|
|
1777
|
+
data === "B";
|
|
1638
1778
|
const supportsCountedParagraphMotion = data === "{" || data === "}";
|
|
1639
|
-
const supportsCountedNav =
|
|
1640
|
-
data === "h"
|
|
1641
|
-
|| data === "j"
|
|
1642
|
-
|| data === "k"
|
|
1643
|
-
|| data === "l"
|
|
1644
|
-
);
|
|
1779
|
+
const supportsCountedNav =
|
|
1780
|
+
data === "h" || data === "j" || data === "k" || data === "l";
|
|
1645
1781
|
const supportsCountedUnderscore = data === "_";
|
|
1646
1782
|
|
|
1647
1783
|
if (supportsCountedNav) {
|
|
@@ -1664,13 +1800,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
1664
1800
|
}
|
|
1665
1801
|
|
|
1666
1802
|
if (
|
|
1667
|
-
!supportsCountedStandaloneEdit
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1803
|
+
!supportsCountedStandaloneEdit &&
|
|
1804
|
+
!supportsCountedCharMotion &&
|
|
1805
|
+
!supportsCountedWordMotion &&
|
|
1806
|
+
!supportsCountedParagraphMotion &&
|
|
1807
|
+
!supportsCountedUnderscore
|
|
1672
1808
|
) {
|
|
1673
|
-
// Unsupported prefixed forms: drop count and keep processing this key.
|
|
1674
1809
|
this.prefixCount = "";
|
|
1675
1810
|
this.operatorCount = "";
|
|
1676
1811
|
}
|
|
@@ -1742,7 +1877,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1742
1877
|
}
|
|
1743
1878
|
|
|
1744
1879
|
if (data === ";" && this.lastCharMotion) {
|
|
1745
|
-
this.executeCharMotion(
|
|
1880
|
+
this.executeCharMotion(
|
|
1881
|
+
this.lastCharMotion.motion,
|
|
1882
|
+
this.lastCharMotion.char,
|
|
1883
|
+
false,
|
|
1884
|
+
);
|
|
1746
1885
|
return;
|
|
1747
1886
|
}
|
|
1748
1887
|
if (data === "," && this.lastCharMotion) {
|
|
@@ -1754,7 +1893,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1754
1893
|
return;
|
|
1755
1894
|
}
|
|
1756
1895
|
|
|
1757
|
-
if (
|
|
1896
|
+
if (
|
|
1897
|
+
data === "u" ||
|
|
1898
|
+
data === CTRL_UNDERSCORE ||
|
|
1899
|
+
matchesKey(data, "ctrl+_")
|
|
1900
|
+
) {
|
|
1758
1901
|
this.performUndo();
|
|
1759
1902
|
return;
|
|
1760
1903
|
}
|
|
@@ -1800,6 +1943,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1800
1943
|
this.moveWord("forward", "start", this.takeTotalCount(1), "WORD");
|
|
1801
1944
|
return;
|
|
1802
1945
|
}
|
|
1946
|
+
if (data === "%") {
|
|
1947
|
+
this.moveToMatchingPairTarget();
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1803
1950
|
if (data === "B") {
|
|
1804
1951
|
this.moveWord("backward", "start", this.takeTotalCount(1), "WORD");
|
|
1805
1952
|
return;
|
|
@@ -1814,7 +1961,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
1814
1961
|
return;
|
|
1815
1962
|
}
|
|
1816
1963
|
|
|
1817
|
-
// Pass control sequences (ctrl+c, etc.) to super, ignore printable chars
|
|
1818
1964
|
if (this.isPrintableChunk(data)) return;
|
|
1819
1965
|
super.handleInput(data);
|
|
1820
1966
|
}
|
|
@@ -1834,29 +1980,29 @@ export class ModalEditor extends CustomEditor {
|
|
|
1834
1980
|
const seq = NORMAL_KEYS[key];
|
|
1835
1981
|
switch (key) {
|
|
1836
1982
|
case "i":
|
|
1837
|
-
this.
|
|
1983
|
+
this.setMode();
|
|
1838
1984
|
break;
|
|
1839
1985
|
case "a":
|
|
1840
|
-
this.
|
|
1986
|
+
this.setMode();
|
|
1841
1987
|
if (!this.isCursorAtOrPastEol()) {
|
|
1842
1988
|
super.handleInput(ESC_RIGHT);
|
|
1843
1989
|
}
|
|
1844
1990
|
break;
|
|
1845
1991
|
case "A":
|
|
1846
|
-
this.
|
|
1992
|
+
this.setMode();
|
|
1847
1993
|
super.handleInput(CTRL_E);
|
|
1848
1994
|
break;
|
|
1849
1995
|
case "I":
|
|
1850
|
-
this.
|
|
1996
|
+
this.setMode();
|
|
1851
1997
|
this.moveCursorToFirstNonWhitespace();
|
|
1852
1998
|
break;
|
|
1853
1999
|
case "o":
|
|
1854
2000
|
this.openLineBelow();
|
|
1855
|
-
this.
|
|
2001
|
+
this.setMode();
|
|
1856
2002
|
break;
|
|
1857
2003
|
case "O":
|
|
1858
2004
|
this.openLineAbove();
|
|
1859
|
-
this.
|
|
2005
|
+
this.setMode();
|
|
1860
2006
|
break;
|
|
1861
2007
|
case "D":
|
|
1862
2008
|
this.takeTotalCount(1);
|
|
@@ -1865,16 +2011,16 @@ export class ModalEditor extends CustomEditor {
|
|
|
1865
2011
|
case "C":
|
|
1866
2012
|
this.takeTotalCount(1);
|
|
1867
2013
|
this.cutToEndOfLine();
|
|
1868
|
-
this.
|
|
2014
|
+
this.setMode();
|
|
1869
2015
|
break;
|
|
1870
2016
|
case "S":
|
|
1871
2017
|
this.takeTotalCount(1);
|
|
1872
2018
|
this.cutCurrentLineContent();
|
|
1873
|
-
this.
|
|
2019
|
+
this.setMode();
|
|
1874
2020
|
break;
|
|
1875
2021
|
case "s":
|
|
1876
2022
|
this.cutCharUnderCursor();
|
|
1877
|
-
this.
|
|
2023
|
+
this.setMode();
|
|
1878
2024
|
break;
|
|
1879
2025
|
case "x":
|
|
1880
2026
|
this.cutCharUnderCursor();
|
|
@@ -1890,11 +2036,22 @@ export class ModalEditor extends CustomEditor {
|
|
|
1890
2036
|
}
|
|
1891
2037
|
}
|
|
1892
2038
|
|
|
1893
|
-
private executeCharMotion(
|
|
2039
|
+
private executeCharMotion(
|
|
2040
|
+
motion: CharMotion,
|
|
2041
|
+
targetChar: string,
|
|
2042
|
+
saveMotion: boolean = true,
|
|
2043
|
+
): void {
|
|
1894
2044
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
1895
2045
|
const col = this.getCursor().col;
|
|
1896
2046
|
const count = this.takeTotalCount(1);
|
|
1897
|
-
const targetCol = findCharMotionTarget(
|
|
2047
|
+
const targetCol = findCharMotionTarget(
|
|
2048
|
+
line,
|
|
2049
|
+
col,
|
|
2050
|
+
motion,
|
|
2051
|
+
targetChar,
|
|
2052
|
+
!saveMotion,
|
|
2053
|
+
count,
|
|
2054
|
+
);
|
|
1898
2055
|
|
|
1899
2056
|
if (targetCol !== null && saveMotion) {
|
|
1900
2057
|
this.lastCharMotion = { motion, char: targetChar };
|
|
@@ -1909,7 +2066,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
1909
2066
|
const lines = this.getLines();
|
|
1910
2067
|
const fromLine = this.getCursor().line;
|
|
1911
2068
|
const count = this.takeTotalCount(1);
|
|
1912
|
-
const targetLine = findParagraphMotionTarget(
|
|
2069
|
+
const targetLine = findParagraphMotionTarget(
|
|
2070
|
+
lines,
|
|
2071
|
+
fromLine,
|
|
2072
|
+
direction,
|
|
2073
|
+
count,
|
|
2074
|
+
);
|
|
1913
2075
|
this.moveCursorToLineStart(targetLine);
|
|
1914
2076
|
}
|
|
1915
2077
|
|
|
@@ -1924,7 +2086,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1924
2086
|
|
|
1925
2087
|
const state = editor.state;
|
|
1926
2088
|
if (!state || !Array.isArray(state.lines)) return false;
|
|
1927
|
-
if (
|
|
2089
|
+
if (
|
|
2090
|
+
!Number.isInteger(state.cursorLine) ||
|
|
2091
|
+
!Number.isInteger(state.cursorCol)
|
|
2092
|
+
)
|
|
2093
|
+
return false;
|
|
1928
2094
|
|
|
1929
2095
|
const cursorLine = state.cursorLine as number;
|
|
1930
2096
|
const cursorCol = state.cursorCol as number;
|
|
@@ -1933,8 +2099,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
1933
2099
|
|
|
1934
2100
|
const target = cursorCol + delta;
|
|
1935
2101
|
|
|
1936
|
-
// Only short-circuit line-local movement when each grapheme is one code
|
|
1937
|
-
// unit; otherwise let the base editor keep cursor boundaries valid.
|
|
1938
2102
|
if (target < 0 || target > line.length) return false;
|
|
1939
2103
|
|
|
1940
2104
|
state.cursorCol = target;
|
|
@@ -1974,7 +2138,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1974
2138
|
}
|
|
1975
2139
|
|
|
1976
2140
|
const currentLine = state.cursorLine ?? 0;
|
|
1977
|
-
const targetLine = Math.max(
|
|
2141
|
+
const targetLine = Math.max(
|
|
2142
|
+
0,
|
|
2143
|
+
Math.min(currentLine + delta, state.lines.length - 1),
|
|
2144
|
+
);
|
|
1978
2145
|
if (targetLine === currentLine) return;
|
|
1979
2146
|
|
|
1980
2147
|
const preferredCol = editor.preferredVisualCol ?? state.cursorCol ?? 0;
|
|
@@ -2078,9 +2245,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
2078
2245
|
if (normalize) {
|
|
2079
2246
|
const trimmedRight = right.trimStart();
|
|
2080
2247
|
const leftLastChar = left[left.length - 1];
|
|
2081
|
-
const leftEndsWithSpace =
|
|
2248
|
+
const leftEndsWithSpace =
|
|
2249
|
+
leftLastChar !== undefined && /\s/.test(leftLastChar);
|
|
2082
2250
|
const needsSeparator = !leftEndsWithSpace && trimmedRight.length > 0;
|
|
2083
|
-
joined = needsSeparator
|
|
2251
|
+
joined = needsSeparator
|
|
2252
|
+
? `${left} ${trimmedRight}`
|
|
2253
|
+
: left + trimmedRight;
|
|
2084
2254
|
joinPoint = left.length;
|
|
2085
2255
|
} else {
|
|
2086
2256
|
joined = left + right;
|
|
@@ -2139,6 +2309,40 @@ export class ModalEditor extends CustomEditor {
|
|
|
2139
2309
|
return this.getAbsoluteIndex(cursor.line, cursor.col);
|
|
2140
2310
|
}
|
|
2141
2311
|
|
|
2312
|
+
private getMatchingPairMotionTarget() {
|
|
2313
|
+
const cursor = this.getCursor();
|
|
2314
|
+
const lineStartAbs = this.getAbsoluteIndex(cursor.line, 0);
|
|
2315
|
+
return resolveMatchingPairMotionTarget(
|
|
2316
|
+
this.getText(),
|
|
2317
|
+
this.getAbsoluteIndexFromCursor(),
|
|
2318
|
+
lineStartAbs,
|
|
2319
|
+
lineStartAbs + (this.getLines()[cursor.line] ?? "").length,
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
private moveToMatchingPairTarget(): void {
|
|
2324
|
+
const target = this.getMatchingPairMotionTarget();
|
|
2325
|
+
if (target) this.moveCursorToAbsoluteIndex(target.targetAbs);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
private applyPercentOp(): void {
|
|
2329
|
+
const op = this.pendingOperator;
|
|
2330
|
+
const counted = this.hasPendingCount();
|
|
2331
|
+
this.clearPendingState();
|
|
2332
|
+
if (!op || counted) return;
|
|
2333
|
+
|
|
2334
|
+
const t = this.getMatchingPairMotionTarget();
|
|
2335
|
+
if (!t) return;
|
|
2336
|
+
|
|
2337
|
+
if (op === "y") {
|
|
2338
|
+
this.yankRangeByAbsolute(t.rangeAnchorAbs, t.targetAbs, true);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
this.deleteRangeByAbsolute(t.rangeAnchorAbs, t.targetAbs, true);
|
|
2343
|
+
if (op === "c") this.mode = "insert";
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2142
2346
|
private getDelimitedTextObjectCursorAbs(): number {
|
|
2143
2347
|
const lines = this.getLines();
|
|
2144
2348
|
const cursor = this.getCursor();
|
|
@@ -2174,25 +2378,43 @@ export class ModalEditor extends CustomEditor {
|
|
|
2174
2378
|
} else if (target === "start") {
|
|
2175
2379
|
const startType = this.charType(text[next], semanticClass);
|
|
2176
2380
|
if (startType !== "space") {
|
|
2177
|
-
while (
|
|
2381
|
+
while (
|
|
2382
|
+
next < len &&
|
|
2383
|
+
this.charType(text[next], semanticClass) === startType
|
|
2384
|
+
)
|
|
2385
|
+
next++;
|
|
2178
2386
|
}
|
|
2179
|
-
while (
|
|
2387
|
+
while (
|
|
2388
|
+
next < len &&
|
|
2389
|
+
this.charType(text[next], semanticClass) === "space"
|
|
2390
|
+
)
|
|
2391
|
+
next++;
|
|
2180
2392
|
} else {
|
|
2181
2393
|
if (next < len - 1) next++;
|
|
2182
|
-
while (
|
|
2394
|
+
while (
|
|
2395
|
+
next < len &&
|
|
2396
|
+
this.charType(text[next], semanticClass) === "space"
|
|
2397
|
+
)
|
|
2398
|
+
next++;
|
|
2183
2399
|
if (next >= len) {
|
|
2184
2400
|
next = len;
|
|
2185
2401
|
} else {
|
|
2186
2402
|
const t = this.charType(text[next], semanticClass);
|
|
2187
|
-
while (
|
|
2403
|
+
while (
|
|
2404
|
+
next < len - 1 &&
|
|
2405
|
+
this.charType(text[next + 1], semanticClass) === t
|
|
2406
|
+
)
|
|
2407
|
+
next++;
|
|
2188
2408
|
}
|
|
2189
2409
|
}
|
|
2190
2410
|
} else {
|
|
2191
2411
|
if (next >= len) next = len - 1;
|
|
2192
2412
|
if (next > 0) next--;
|
|
2193
|
-
while (next > 0 && this.charType(text[next], semanticClass) === "space")
|
|
2413
|
+
while (next > 0 && this.charType(text[next], semanticClass) === "space")
|
|
2414
|
+
next--;
|
|
2194
2415
|
const t = this.charType(text[next], semanticClass);
|
|
2195
|
-
while (next > 0 && this.charType(text[next - 1], semanticClass) === t)
|
|
2416
|
+
while (next > 0 && this.charType(text[next - 1], semanticClass) === t)
|
|
2417
|
+
next--;
|
|
2196
2418
|
}
|
|
2197
2419
|
|
|
2198
2420
|
if (next === i) break;
|
|
@@ -2281,7 +2503,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2281
2503
|
semanticClass: WordMotionClass = "word",
|
|
2282
2504
|
): boolean {
|
|
2283
2505
|
const col = this.getCursor().col;
|
|
2284
|
-
const targetCol = this.tryFindWordTargetLineLocal(
|
|
2506
|
+
const targetCol = this.tryFindWordTargetLineLocal(
|
|
2507
|
+
direction,
|
|
2508
|
+
target,
|
|
2509
|
+
semanticClass,
|
|
2510
|
+
);
|
|
2285
2511
|
if (targetCol === null || targetCol === col) return false;
|
|
2286
2512
|
|
|
2287
2513
|
this.moveCursorToCol(targetCol);
|
|
@@ -2297,7 +2523,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2297
2523
|
const lineIndex = cursor.line;
|
|
2298
2524
|
const col = cursor.col;
|
|
2299
2525
|
const lineSnapshot = this.getLines()[lineIndex] ?? "";
|
|
2300
|
-
const direction: WordMotionDirection =
|
|
2526
|
+
const direction: WordMotionDirection =
|
|
2527
|
+
motion === "b" ? "backward" : "forward";
|
|
2301
2528
|
const target: WordMotionTarget = motion === "e" ? "end" : "start";
|
|
2302
2529
|
const steps = Math.max(1, Math.min(MAX_COUNT, count));
|
|
2303
2530
|
|
|
@@ -2364,7 +2591,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2364
2591
|
return true;
|
|
2365
2592
|
}
|
|
2366
2593
|
|
|
2367
|
-
private writeToRegister(
|
|
2594
|
+
private writeToRegister(
|
|
2595
|
+
text: string,
|
|
2596
|
+
source: RegisterWriteSource = "mutation",
|
|
2597
|
+
): void {
|
|
2368
2598
|
this.unnamedRegister = text;
|
|
2369
2599
|
const shouldMirror = text !== "" && this.shouldMirrorRegisterWrite(source);
|
|
2370
2600
|
this.preferRegisterForPut = text !== "" && !shouldMirror;
|
|
@@ -2380,7 +2610,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
2380
2610
|
}
|
|
2381
2611
|
|
|
2382
2612
|
private hasMultiCodeUnitGraphemes(line: string): boolean {
|
|
2383
|
-
return getLineGraphemes(line).some(
|
|
2613
|
+
return getLineGraphemes(line).some(
|
|
2614
|
+
(segment) => segment.end - segment.start > 1,
|
|
2615
|
+
);
|
|
2384
2616
|
}
|
|
2385
2617
|
|
|
2386
2618
|
private getGraphemeRangeAtCol(
|
|
@@ -2391,7 +2623,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
2391
2623
|
): { start: number; end: number } | null {
|
|
2392
2624
|
const clampedCol = Math.max(0, Math.min(col, line.length));
|
|
2393
2625
|
const segments = getLineGraphemes(line);
|
|
2394
|
-
const startIndex = segments.findIndex(
|
|
2626
|
+
const startIndex = segments.findIndex(
|
|
2627
|
+
(segment) => clampedCol < segment.end,
|
|
2628
|
+
);
|
|
2395
2629
|
if (startIndex === -1) return null;
|
|
2396
2630
|
|
|
2397
2631
|
let endIndex = startIndex + Math.max(1, count) - 1;
|
|
@@ -2432,7 +2666,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2432
2666
|
const text = this.getText();
|
|
2433
2667
|
this.writeToRegister(line.slice(range.start, range.end));
|
|
2434
2668
|
this.replaceTextInBuffer(
|
|
2435
|
-
text.slice(0, lineStartAbs + range.start) +
|
|
2669
|
+
text.slice(0, lineStartAbs + range.start) +
|
|
2670
|
+
text.slice(lineStartAbs + range.end),
|
|
2436
2671
|
lineStartAbs + range.start,
|
|
2437
2672
|
);
|
|
2438
2673
|
}
|
|
@@ -2443,7 +2678,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2443
2678
|
const { line, col } = this.getCurrentLineAndCol();
|
|
2444
2679
|
|
|
2445
2680
|
const hasNextLine = cursorLine < lines.length - 1;
|
|
2446
|
-
const deleted =
|
|
2681
|
+
const deleted =
|
|
2682
|
+
col < line.length ? line.slice(col) : hasNextLine ? "\n" : "";
|
|
2447
2683
|
|
|
2448
2684
|
this.writeToRegister(deleted);
|
|
2449
2685
|
super.handleInput(CTRL_K);
|
|
@@ -2466,7 +2702,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2466
2702
|
this.cutCurrentLineContent();
|
|
2467
2703
|
}
|
|
2468
2704
|
|
|
2469
|
-
private getNormalizedLineRange(
|
|
2705
|
+
private getNormalizedLineRange(
|
|
2706
|
+
startLine: number,
|
|
2707
|
+
endLine: number,
|
|
2708
|
+
): { start: number; end: number } {
|
|
2470
2709
|
const lines = this.getLines();
|
|
2471
2710
|
const last = Math.max(0, lines.length - 1);
|
|
2472
2711
|
const clampedStart = Math.max(0, Math.min(startLine, last));
|
|
@@ -2483,7 +2722,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2483
2722
|
return `${lines.slice(start, end + 1).join("\n")}\n`;
|
|
2484
2723
|
}
|
|
2485
2724
|
|
|
2486
|
-
private getLineDeleteAbsoluteRange(
|
|
2725
|
+
private getLineDeleteAbsoluteRange(
|
|
2726
|
+
startLine: number,
|
|
2727
|
+
endLine: number,
|
|
2728
|
+
): { startAbs: number; endAbs: number } {
|
|
2487
2729
|
const lines = this.getLines();
|
|
2488
2730
|
const text = this.getText();
|
|
2489
2731
|
const { start, end } = this.getNormalizedLineRange(startLine, endLine);
|
|
@@ -2510,7 +2752,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2510
2752
|
if (lines.length === 0) return;
|
|
2511
2753
|
|
|
2512
2754
|
const payload = this.getLinewisePayload(startLine, endLine);
|
|
2513
|
-
const { startAbs, endAbs } = this.getLineDeleteAbsoluteRange(
|
|
2755
|
+
const { startAbs, endAbs } = this.getLineDeleteAbsoluteRange(
|
|
2756
|
+
startLine,
|
|
2757
|
+
endLine,
|
|
2758
|
+
);
|
|
2514
2759
|
|
|
2515
2760
|
this.writeToRegister(payload);
|
|
2516
2761
|
|
|
@@ -2519,7 +2764,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2519
2764
|
const newText = text.slice(0, startAbs) + text.slice(endAbs);
|
|
2520
2765
|
this.replaceTextInBuffer(newText, startAbs);
|
|
2521
2766
|
|
|
2522
|
-
// Ensure cursor is at column 0 of the landing line
|
|
2523
2767
|
super.handleInput(CTRL_A);
|
|
2524
2768
|
}
|
|
2525
2769
|
}
|
|
@@ -2552,7 +2796,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2552
2796
|
const col = cursor.col;
|
|
2553
2797
|
|
|
2554
2798
|
if (motion === "$") {
|
|
2555
|
-
// Match D/C behavior exactly, including newline kill at EOL.
|
|
2556
2799
|
this.cutToEndOfLine();
|
|
2557
2800
|
return true;
|
|
2558
2801
|
}
|
|
@@ -2563,7 +2806,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2563
2806
|
}
|
|
2564
2807
|
|
|
2565
2808
|
if (motion === "^") {
|
|
2566
|
-
this.deleteRange(
|
|
2809
|
+
this.deleteRange(
|
|
2810
|
+
col,
|
|
2811
|
+
findFirstNonWhitespaceColumn(this.getLines()[cursor.line] ?? ""),
|
|
2812
|
+
false,
|
|
2813
|
+
);
|
|
2567
2814
|
return true;
|
|
2568
2815
|
}
|
|
2569
2816
|
|
|
@@ -2593,7 +2840,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2593
2840
|
count,
|
|
2594
2841
|
wordMotion.semanticClass,
|
|
2595
2842
|
);
|
|
2596
|
-
this.deleteRangeByAbsolute(
|
|
2843
|
+
this.deleteRangeByAbsolute(
|
|
2844
|
+
currentAbs,
|
|
2845
|
+
targetAbs,
|
|
2846
|
+
wordMotion.motion === "e",
|
|
2847
|
+
);
|
|
2597
2848
|
return true;
|
|
2598
2849
|
}
|
|
2599
2850
|
|
|
@@ -2604,25 +2855,27 @@ export class ModalEditor extends CustomEditor {
|
|
|
2604
2855
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
2605
2856
|
const col = this.getCursor().col;
|
|
2606
2857
|
const count = this.takeTotalCount(1);
|
|
2607
|
-
const targetCol = findCharMotionTarget(
|
|
2858
|
+
const targetCol = findCharMotionTarget(
|
|
2859
|
+
line,
|
|
2860
|
+
col,
|
|
2861
|
+
motion,
|
|
2862
|
+
targetChar,
|
|
2863
|
+
false,
|
|
2864
|
+
count,
|
|
2865
|
+
);
|
|
2608
2866
|
|
|
2609
2867
|
if (targetCol === null) return;
|
|
2610
2868
|
|
|
2611
2869
|
this.lastCharMotion = { motion, char: targetChar };
|
|
2612
|
-
this.deleteRange(col, targetCol, true);
|
|
2870
|
+
this.deleteRange(col, targetCol, true);
|
|
2613
2871
|
}
|
|
2614
2872
|
|
|
2615
2873
|
private handlePendingYank(data: string): void {
|
|
2616
|
-
if (this.
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
}
|
|
2622
|
-
} else {
|
|
2623
|
-
this.operatorCount += data;
|
|
2624
|
-
return;
|
|
2625
|
-
}
|
|
2874
|
+
if (this.opDigit(data)) return;
|
|
2875
|
+
|
|
2876
|
+
if (data === "%") {
|
|
2877
|
+
this.applyPercentOp();
|
|
2878
|
+
return;
|
|
2626
2879
|
}
|
|
2627
2880
|
|
|
2628
2881
|
if (data === "y") {
|
|
@@ -2633,7 +2886,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2633
2886
|
}
|
|
2634
2887
|
|
|
2635
2888
|
if (data === "j" || data === "k") {
|
|
2636
|
-
const hasDualCount =
|
|
2889
|
+
const hasDualCount =
|
|
2890
|
+
this.prefixCount.length > 0 && this.operatorCount.length > 0;
|
|
2637
2891
|
const count = this.takeTotalCount(1);
|
|
2638
2892
|
const delta = hasDualCount ? Math.max(0, count - 1) : count;
|
|
2639
2893
|
this.yankLinewiseByDelta(data === "j" ? delta : -delta);
|
|
@@ -2670,7 +2924,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2670
2924
|
}
|
|
2671
2925
|
|
|
2672
2926
|
if (this.hasPendingCount()) {
|
|
2673
|
-
// Counted forms beyond yy, y{count}j/k, and y{count}{f/F/t/T} are out of scope.
|
|
2674
2927
|
this.cancelPendingOperator(data);
|
|
2675
2928
|
return;
|
|
2676
2929
|
}
|
|
@@ -2728,7 +2981,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2728
2981
|
1,
|
|
2729
2982
|
wordMotion.semanticClass,
|
|
2730
2983
|
);
|
|
2731
|
-
this.yankRangeByAbsolute(
|
|
2984
|
+
this.yankRangeByAbsolute(
|
|
2985
|
+
currentAbs,
|
|
2986
|
+
targetAbs,
|
|
2987
|
+
wordMotion.motion === "e",
|
|
2988
|
+
);
|
|
2732
2989
|
return true;
|
|
2733
2990
|
}
|
|
2734
2991
|
|
|
@@ -2739,12 +2996,19 @@ export class ModalEditor extends CustomEditor {
|
|
|
2739
2996
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
2740
2997
|
const col = this.getCursor().col;
|
|
2741
2998
|
const count = this.takeTotalCount(1);
|
|
2742
|
-
const targetCol = findCharMotionTarget(
|
|
2999
|
+
const targetCol = findCharMotionTarget(
|
|
3000
|
+
line,
|
|
3001
|
+
col,
|
|
3002
|
+
motion,
|
|
3003
|
+
targetChar,
|
|
3004
|
+
false,
|
|
3005
|
+
count,
|
|
3006
|
+
);
|
|
2743
3007
|
|
|
2744
3008
|
if (targetCol === null) return;
|
|
2745
3009
|
|
|
2746
3010
|
this.lastCharMotion = { motion, char: targetChar };
|
|
2747
|
-
this.yankRange(col, targetCol, true);
|
|
3011
|
+
this.yankRange(col, targetCol, true);
|
|
2748
3012
|
}
|
|
2749
3013
|
|
|
2750
3014
|
private yankRange(col: number, targetCol: number, inclusive: boolean): void {
|
|
@@ -2754,17 +3018,24 @@ export class ModalEditor extends CustomEditor {
|
|
|
2754
3018
|
let end = Math.min(rawEnd, line.length);
|
|
2755
3019
|
|
|
2756
3020
|
if (inclusive) {
|
|
2757
|
-
const targetRange = this.getGraphemeRangeAtCol(
|
|
3021
|
+
const targetRange = this.getGraphemeRangeAtCol(
|
|
3022
|
+
line,
|
|
3023
|
+
Math.max(col, targetCol),
|
|
3024
|
+
1,
|
|
3025
|
+
);
|
|
2758
3026
|
end = targetRange?.end ?? end;
|
|
2759
3027
|
}
|
|
2760
3028
|
|
|
2761
3029
|
if (end <= start) return;
|
|
2762
3030
|
|
|
2763
|
-
// Yank only — no cursor movement, no text mutation
|
|
2764
3031
|
this.writeToRegister(line.slice(start, end), "yank");
|
|
2765
3032
|
}
|
|
2766
3033
|
|
|
2767
|
-
private yankRangeByAbsolute(
|
|
3034
|
+
private yankRangeByAbsolute(
|
|
3035
|
+
currentAbs: number,
|
|
3036
|
+
targetAbs: number,
|
|
3037
|
+
inclusive: boolean = false,
|
|
3038
|
+
): void {
|
|
2768
3039
|
const text = this.getText();
|
|
2769
3040
|
const start = Math.min(currentAbs, targetAbs);
|
|
2770
3041
|
const rawEnd = Math.max(currentAbs, targetAbs) + (inclusive ? 1 : 0);
|
|
@@ -2773,7 +3044,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2773
3044
|
this.writeToRegister(text.slice(start, end), "yank");
|
|
2774
3045
|
}
|
|
2775
3046
|
|
|
2776
|
-
private getCursorFromAbsoluteIndex(
|
|
3047
|
+
private getCursorFromAbsoluteIndex(
|
|
3048
|
+
text: string,
|
|
3049
|
+
abs: number,
|
|
3050
|
+
): { line: number; col: number } {
|
|
2777
3051
|
const lines = text.length === 0 ? [""] : text.split("\n");
|
|
2778
3052
|
let remaining = Math.max(0, Math.min(abs, text.length));
|
|
2779
3053
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
@@ -2814,7 +3088,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2814
3088
|
editor.tui?.requestRender?.();
|
|
2815
3089
|
}
|
|
2816
3090
|
|
|
2817
|
-
private deleteRangeByAbsolute(
|
|
3091
|
+
private deleteRangeByAbsolute(
|
|
3092
|
+
currentAbs: number,
|
|
3093
|
+
targetAbs: number,
|
|
3094
|
+
inclusive: boolean = false,
|
|
3095
|
+
): void {
|
|
2818
3096
|
const text = this.getText();
|
|
2819
3097
|
const start = Math.min(currentAbs, targetAbs);
|
|
2820
3098
|
const rawEnd = Math.max(currentAbs, targetAbs) + (inclusive ? 1 : 0);
|
|
@@ -2866,12 +3144,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2866
3144
|
const count = this.takeTotalCount(1);
|
|
2867
3145
|
const text = this.getPasteRegisterText();
|
|
2868
3146
|
if (!text) return;
|
|
2869
|
-
const safeCount = Math.min(
|
|
3147
|
+
const safeCount = Math.min(
|
|
3148
|
+
count,
|
|
3149
|
+
Math.max(1, Math.floor(ModalEditor.PUT_SIZE_LIMIT / text.length)),
|
|
3150
|
+
);
|
|
2870
3151
|
|
|
2871
3152
|
if (text.endsWith("\n")) {
|
|
2872
3153
|
const content = text.slice(0, -1);
|
|
2873
3154
|
for (let i = 0; i < safeCount; i++) {
|
|
2874
|
-
// Line-wise: insert new line below and fill it
|
|
2875
3155
|
super.handleInput(CTRL_E);
|
|
2876
3156
|
super.handleInput(NEWLINE);
|
|
2877
3157
|
for (const char of content) {
|
|
@@ -2881,7 +3161,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2881
3161
|
return;
|
|
2882
3162
|
}
|
|
2883
3163
|
|
|
2884
|
-
// Character-wise: insert after cursor
|
|
2885
3164
|
if (!this.isCursorAtOrPastEol()) {
|
|
2886
3165
|
super.handleInput(ESC_RIGHT);
|
|
2887
3166
|
}
|
|
@@ -2896,12 +3175,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2896
3175
|
const count = this.takeTotalCount(1);
|
|
2897
3176
|
const text = this.getPasteRegisterText();
|
|
2898
3177
|
if (!text) return;
|
|
2899
|
-
const safeCount = Math.min(
|
|
3178
|
+
const safeCount = Math.min(
|
|
3179
|
+
count,
|
|
3180
|
+
Math.max(1, Math.floor(ModalEditor.PUT_SIZE_LIMIT / text.length)),
|
|
3181
|
+
);
|
|
2900
3182
|
|
|
2901
3183
|
if (text.endsWith("\n")) {
|
|
2902
3184
|
const content = text.slice(0, -1);
|
|
2903
3185
|
for (let i = 0; i < safeCount; i++) {
|
|
2904
|
-
// Line-wise: insert new line above and fill it
|
|
2905
3186
|
super.handleInput(CTRL_A);
|
|
2906
3187
|
super.handleInput(NEWLINE);
|
|
2907
3188
|
super.handleInput(ESC_UP);
|
|
@@ -2912,7 +3193,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2912
3193
|
return;
|
|
2913
3194
|
}
|
|
2914
3195
|
|
|
2915
|
-
// Character-wise: insert before cursor (just type it)
|
|
2916
3196
|
for (let i = 0; i < safeCount; i++) {
|
|
2917
3197
|
for (const char of text) {
|
|
2918
3198
|
super.handleInput(char === "\n" ? NEWLINE : char);
|
|
@@ -2920,7 +3200,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2920
3200
|
}
|
|
2921
3201
|
}
|
|
2922
3202
|
|
|
2923
|
-
private deleteRange(
|
|
3203
|
+
private deleteRange(
|
|
3204
|
+
col: number,
|
|
3205
|
+
targetCol: number,
|
|
3206
|
+
inclusive: boolean,
|
|
3207
|
+
): void {
|
|
2924
3208
|
const cursor = this.getCursor();
|
|
2925
3209
|
const line = this.getLines()[cursor.line] ?? "";
|
|
2926
3210
|
const lineStartAbs = this.getAbsoluteIndex(cursor.line, 0);
|
|
@@ -2929,7 +3213,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2929
3213
|
let end = Math.min(rawEnd, line.length);
|
|
2930
3214
|
|
|
2931
3215
|
if (inclusive) {
|
|
2932
|
-
const targetRange = this.getGraphemeRangeAtCol(
|
|
3216
|
+
const targetRange = this.getGraphemeRangeAtCol(
|
|
3217
|
+
line,
|
|
3218
|
+
Math.max(col, targetCol),
|
|
3219
|
+
1,
|
|
3220
|
+
);
|
|
2933
3221
|
end = targetRange?.end ?? end;
|
|
2934
3222
|
}
|
|
2935
3223
|
|
|
@@ -2978,7 +3266,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
2978
3266
|
}
|
|
2979
3267
|
|
|
2980
3268
|
private getDesiredCursorShapeSequence(): CursorShapeSequence {
|
|
2981
|
-
return this.mode
|
|
3269
|
+
return "insert" === this.mode && this.pendingExCommand === null
|
|
2982
3270
|
? INSERT_CURSOR_SHAPE
|
|
2983
3271
|
: BLOCK_CURSOR_SHAPE;
|
|
2984
3272
|
}
|
|
@@ -3026,7 +3314,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
3026
3314
|
const last = lines.length - 1;
|
|
3027
3315
|
const lastLine = lines[last];
|
|
3028
3316
|
if (lastLine && visibleWidth(lastLine) >= visibleWidth(rawLabel)) {
|
|
3029
|
-
lines[last] =
|
|
3317
|
+
lines[last] =
|
|
3318
|
+
truncateToWidth(lastLine, width - visibleWidth(rawLabel), "") + label;
|
|
3030
3319
|
} else {
|
|
3031
3320
|
lines[last] = label;
|
|
3032
3321
|
}
|
|
@@ -3034,13 +3323,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
3034
3323
|
}
|
|
3035
3324
|
|
|
3036
3325
|
private getModeLabelColorizer(): ((s: string) => string) | null {
|
|
3037
|
-
|
|
3038
|
-
if (this.pendingExCommand !== null) return this.labelColorizers.ex;
|
|
3039
|
-
return this.mode === "insert" ? this.labelColorizers.insert : this.labelColorizers.normal;
|
|
3326
|
+
return this.labelColorizers?.[this.getActiveMode()] ?? null;
|
|
3040
3327
|
}
|
|
3041
3328
|
|
|
3042
3329
|
private getModeLabel(): string {
|
|
3043
|
-
if (this.mode
|
|
3330
|
+
if ("insert" === this.mode) return " INSERT ";
|
|
3044
3331
|
if (this.pendingExCommand !== null) return ` EX ${this.pendingExCommand}_ `;
|
|
3045
3332
|
|
|
3046
3333
|
const prefixCount = this.prefixCount;
|
|
@@ -3073,21 +3360,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
3073
3360
|
|
|
3074
3361
|
pi.on("session_start", (_event, ctx) => {
|
|
3075
3362
|
const piVimSettings = readPiVimSettings(ctx.cwd);
|
|
3076
|
-
const clipboardMirrorPolicy = resolveClipboardMirrorPolicy(
|
|
3363
|
+
const clipboardMirrorPolicy = resolveClipboardMirrorPolicy(
|
|
3364
|
+
piVimSettings.clipboardMirror,
|
|
3365
|
+
);
|
|
3077
3366
|
if (clipboardMirrorPolicy.warning && ctx.hasUI) {
|
|
3078
3367
|
ctx.ui.notify(clipboardMirrorPolicy.warning, "warning");
|
|
3079
3368
|
}
|
|
3080
3369
|
|
|
3081
3370
|
const t = ctx.ui.theme;
|
|
3371
|
+
const modeColors = resolveModeColors(piVimSettings.modeColors);
|
|
3082
3372
|
const reverseVideo = (s: string) => `\x1b[7m${s}\x1b[27m`;
|
|
3083
|
-
const
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3373
|
+
const labelColorizers = t
|
|
3374
|
+
? buildModeColorizers(t, modeColors, reverseVideo)
|
|
3375
|
+
: null;
|
|
3376
|
+
const borderColorizers =
|
|
3377
|
+
t && piVimSettings.syncBorderColorWithMode === true
|
|
3378
|
+
? buildModeColorizers(t, modeColors)
|
|
3379
|
+
: null;
|
|
3088
3380
|
ctx.ui.setEditorComponent((tui, theme, kb) => {
|
|
3089
3381
|
cursorShapeCleanup = enableCursorShapeSupport(tui);
|
|
3090
|
-
const editor = new ModalEditor(tui, theme, kb,
|
|
3382
|
+
const editor = new ModalEditor(tui, theme, kb, {
|
|
3383
|
+
labelColorizers,
|
|
3384
|
+
borderColorizers,
|
|
3385
|
+
});
|
|
3091
3386
|
editor.setClipboardMirrorPolicy(clipboardMirrorPolicy.policy);
|
|
3092
3387
|
editor.setQuitFn(() => ctx.shutdown());
|
|
3093
3388
|
editor.setNotifyFn((message) => ctx.ui.notify(message, "warning"));
|