pi-vim 0.8.0 → 0.10.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 +10 -14
- package/clipboard-policy.ts +7 -57
- package/index.ts +546 -281
- package/motions.ts +38 -15
- package/package.json +3 -3
- package/settings.ts +92 -0
- package/text-objects.ts +38 -15
- 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,54 @@ 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
|
+
resolveWordTextObjectRange,
|
|
29
|
+
type TextObjectKind,
|
|
30
|
+
type TextObjectRange,
|
|
31
|
+
type WordTextObjectClass,
|
|
32
|
+
} from "./text-objects.js";
|
|
15
33
|
import type {
|
|
16
|
-
Mode,
|
|
17
34
|
CharMotion,
|
|
35
|
+
LastCharMotion,
|
|
36
|
+
Mode,
|
|
18
37
|
PendingMotion,
|
|
19
38
|
PendingOperator,
|
|
20
|
-
LastCharMotion,
|
|
21
39
|
} from "./types.js";
|
|
22
40
|
import {
|
|
23
|
-
NORMAL_KEYS,
|
|
24
41
|
CHAR_MOTION_KEYS,
|
|
25
|
-
ESC_LEFT,
|
|
26
|
-
ESC_RIGHT,
|
|
27
|
-
ESC_UP,
|
|
28
42
|
CTRL_A,
|
|
29
43
|
CTRL_E,
|
|
30
44
|
CTRL_K,
|
|
31
45
|
CTRL_R,
|
|
32
46
|
CTRL_UNDERSCORE,
|
|
33
|
-
NEWLINE,
|
|
34
47
|
ESC_DOWN,
|
|
48
|
+
ESC_LEFT,
|
|
49
|
+
ESC_RIGHT,
|
|
50
|
+
ESC_UP,
|
|
51
|
+
NEWLINE,
|
|
52
|
+
NORMAL_KEYS,
|
|
35
53
|
} from "./types.js";
|
|
36
|
-
import {
|
|
37
|
-
reverseCharMotion,
|
|
38
|
-
findCharMotionTarget,
|
|
39
|
-
findParagraphMotionTarget,
|
|
40
|
-
findFirstNonWhitespaceColumn,
|
|
41
|
-
getLineGraphemes,
|
|
42
|
-
type WordMotionClass,
|
|
43
|
-
} from "./motions.js";
|
|
44
54
|
import {
|
|
45
55
|
WordBoundaryCache,
|
|
46
56
|
type WordMotionDirection,
|
|
47
57
|
type WordMotionTarget,
|
|
48
58
|
} 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
59
|
|
|
64
60
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
65
61
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
@@ -71,12 +67,16 @@ const SOFTWARE_CURSOR_RESETS = ["\x1b[0m", "\x1b[27m"] as const;
|
|
|
71
67
|
const INSERT_CURSOR_SHAPE = "\x1b[5 q";
|
|
72
68
|
const BLOCK_CURSOR_SHAPE = "\x1b[1 q";
|
|
73
69
|
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
70
|
const CLIPBOARD_WRITE_TIMEOUT_MS = PI_NATIVE_CLIPBOARD_TIMEOUT_MS + 500;
|
|
77
71
|
const CLIPBOARD_SPAWN_FAILURE_LIMIT = 3;
|
|
78
72
|
const CLIPBOARD_READ_TIMEOUT_MS = 750;
|
|
79
73
|
const CLIPBOARD_READ_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
74
|
+
const MODE_COLORS = {
|
|
75
|
+
insert: "borderMuted",
|
|
76
|
+
normal: "borderAccent",
|
|
77
|
+
ex: "warning",
|
|
78
|
+
} as const;
|
|
79
|
+
const TOKEN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
80
80
|
|
|
81
81
|
type EditorSnapshot = {
|
|
82
82
|
text: string;
|
|
@@ -101,11 +101,13 @@ type ClipboardWriteFn = (text: string, signal: AbortSignal) => Promise<void>;
|
|
|
101
101
|
type ClipboardReadFn = () => string | null;
|
|
102
102
|
type ClipboardProcess = ReturnType<typeof spawn>;
|
|
103
103
|
|
|
104
|
-
type
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
type ModeColorKey = keyof typeof MODE_COLORS;
|
|
105
|
+
type ModeColorizers = Record<ModeColorKey, (s: string) => string>;
|
|
106
|
+
type ModalEditorOptions = {
|
|
107
|
+
labelColorizers?: ModeColorizers | null;
|
|
108
|
+
borderColorizers?: ModeColorizers | null;
|
|
108
109
|
};
|
|
110
|
+
type ThemeLike = { fg(token: string, text: string): string };
|
|
109
111
|
|
|
110
112
|
type CursorShapeSequence =
|
|
111
113
|
| typeof INSERT_CURSOR_SHAPE
|
|
@@ -120,6 +122,45 @@ type CursorShapeRuntime = {
|
|
|
120
122
|
|
|
121
123
|
type CursorShapeCleanup = () => void;
|
|
122
124
|
|
|
125
|
+
function resolveModeColors(
|
|
126
|
+
colors?: ModeColorSettings,
|
|
127
|
+
): Required<ModeColorSettings> {
|
|
128
|
+
return {
|
|
129
|
+
insert: colors?.insert ?? MODE_COLORS.insert,
|
|
130
|
+
normal: colors?.normal ?? MODE_COLORS.normal,
|
|
131
|
+
ex: colors?.ex ?? MODE_COLORS.ex,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function colorizeWithTheme(
|
|
135
|
+
theme: ThemeLike,
|
|
136
|
+
token: string,
|
|
137
|
+
fallback: string,
|
|
138
|
+
text: string,
|
|
139
|
+
): string {
|
|
140
|
+
const trimmedToken = token.trim();
|
|
141
|
+
if (TOKEN.test(trimmedToken)) {
|
|
142
|
+
try {
|
|
143
|
+
return theme.fg(trimmedToken, text);
|
|
144
|
+
} catch {
|
|
145
|
+
return theme.fg(fallback, text);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return theme.fg(fallback, text);
|
|
149
|
+
}
|
|
150
|
+
function buildModeColorizers(
|
|
151
|
+
theme: ThemeLike,
|
|
152
|
+
colors: Required<ModeColorSettings>,
|
|
153
|
+
transform: (text: string) => string = (text) => text,
|
|
154
|
+
): ModeColorizers {
|
|
155
|
+
const colorizer = (mode: ModeColorKey) => (text: string) =>
|
|
156
|
+
colorizeWithTheme(theme, colors[mode], MODE_COLORS[mode], transform(text));
|
|
157
|
+
return {
|
|
158
|
+
insert: colorizer("insert"),
|
|
159
|
+
normal: colorizer("normal"),
|
|
160
|
+
ex: colorizer("ex"),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
123
164
|
type CursorShapeTuiCandidate = {
|
|
124
165
|
terminal?: { write?: unknown };
|
|
125
166
|
setShowHardwareCursor?: unknown;
|
|
@@ -135,7 +176,10 @@ function getCursorShapeRuntime(tui: unknown): CursorShapeRuntime | null {
|
|
|
135
176
|
|
|
136
177
|
const write = terminal.write;
|
|
137
178
|
const setShowHardwareCursor = candidate.setShowHardwareCursor;
|
|
138
|
-
if (
|
|
179
|
+
if (
|
|
180
|
+
typeof write !== "function" ||
|
|
181
|
+
typeof setShowHardwareCursor !== "function"
|
|
182
|
+
) {
|
|
139
183
|
return null;
|
|
140
184
|
}
|
|
141
185
|
|
|
@@ -178,7 +222,10 @@ function findSoftwareCursorReset(
|
|
|
178
222
|
line: string,
|
|
179
223
|
startIndex: number,
|
|
180
224
|
): { index: number; sequence: (typeof SOFTWARE_CURSOR_RESETS)[number] } | null {
|
|
181
|
-
let firstReset: {
|
|
225
|
+
let firstReset: {
|
|
226
|
+
index: number;
|
|
227
|
+
sequence: (typeof SOFTWARE_CURSOR_RESETS)[number];
|
|
228
|
+
} | null = null;
|
|
182
229
|
|
|
183
230
|
for (const sequence of SOFTWARE_CURSOR_RESETS) {
|
|
184
231
|
const index = line.indexOf(sequence, startIndex);
|
|
@@ -203,9 +250,11 @@ function stripSoftwareCursorAfterMarker(line: string): string {
|
|
|
203
250
|
const reset = findSoftwareCursorReset(line, cursorContentStart);
|
|
204
251
|
if (!reset) return line;
|
|
205
252
|
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
|
|
253
|
+
return (
|
|
254
|
+
line.slice(0, cursorStart) +
|
|
255
|
+
line.slice(cursorContentStart, reset.index) +
|
|
256
|
+
line.slice(reset.index + reset.sequence.length)
|
|
257
|
+
);
|
|
209
258
|
}
|
|
210
259
|
|
|
211
260
|
type ClipboardCircuitBreaker = {
|
|
@@ -236,17 +285,21 @@ function isNodeSpawnErrno(error: unknown): boolean {
|
|
|
236
285
|
if (!(error instanceof Error)) return false;
|
|
237
286
|
|
|
238
287
|
const candidate = error as SpawnErrnoLike;
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
288
|
+
return (
|
|
289
|
+
typeof candidate.code === "string" &&
|
|
290
|
+
candidate.code.length > 0 &&
|
|
291
|
+
typeof candidate.syscall === "string" &&
|
|
292
|
+
candidate.syscall.startsWith("spawn")
|
|
293
|
+
);
|
|
243
294
|
}
|
|
244
295
|
|
|
245
296
|
function isClipboardEnvironmentFailure(error: unknown): boolean {
|
|
246
297
|
return error instanceof ClipboardSpawnError || isNodeSpawnErrno(error);
|
|
247
298
|
}
|
|
248
299
|
|
|
249
|
-
const PI_CODING_AGENT_MODULE_URL = import.meta.resolve(
|
|
300
|
+
const PI_CODING_AGENT_MODULE_URL = import.meta.resolve(
|
|
301
|
+
"@mariozechner/pi-coding-agent",
|
|
302
|
+
);
|
|
250
303
|
const CLIPBOARD_HELPER_SOURCE = `
|
|
251
304
|
import { copyToClipboard } from ${JSON.stringify(PI_CODING_AGENT_MODULE_URL)};
|
|
252
305
|
|
|
@@ -257,10 +310,7 @@ for await (const chunk of process.stdin) {
|
|
|
257
310
|
|
|
258
311
|
try {
|
|
259
312
|
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
|
-
}
|
|
313
|
+
} catch {}
|
|
264
314
|
`;
|
|
265
315
|
|
|
266
316
|
const CLIPBOARD_READ_HELPER_SOURCE = `
|
|
@@ -316,11 +366,14 @@ function killClipboardProcess(child: ClipboardProcess): void {
|
|
|
316
366
|
try {
|
|
317
367
|
child.kill("SIGKILL");
|
|
318
368
|
} catch {
|
|
319
|
-
|
|
369
|
+
return;
|
|
320
370
|
}
|
|
321
371
|
}
|
|
322
372
|
|
|
323
|
-
function writeClipboardInChildProcess(
|
|
373
|
+
function writeClipboardInChildProcess(
|
|
374
|
+
text: string,
|
|
375
|
+
signal: AbortSignal,
|
|
376
|
+
): Promise<void> {
|
|
324
377
|
return new Promise<void>((resolve, reject) => {
|
|
325
378
|
if (signal.aborted) {
|
|
326
379
|
reject(getAbortError(signal));
|
|
@@ -350,12 +403,20 @@ function writeClipboardInChildProcess(text: string, signal: AbortSignal): Promis
|
|
|
350
403
|
}
|
|
351
404
|
|
|
352
405
|
try {
|
|
353
|
-
child = spawn(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
406
|
+
child = spawn(
|
|
407
|
+
process.execPath,
|
|
408
|
+
["--input-type=module", "-e", CLIPBOARD_HELPER_SOURCE],
|
|
409
|
+
{
|
|
410
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
411
|
+
windowsHide: true,
|
|
412
|
+
},
|
|
413
|
+
);
|
|
357
414
|
} catch (error) {
|
|
358
|
-
finish(
|
|
415
|
+
finish(
|
|
416
|
+
new ClipboardSpawnError("clipboard helper spawn failed", {
|
|
417
|
+
cause: error,
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
359
420
|
return;
|
|
360
421
|
}
|
|
361
422
|
|
|
@@ -369,7 +430,11 @@ function writeClipboardInChildProcess(text: string, signal: AbortSignal): Promis
|
|
|
369
430
|
});
|
|
370
431
|
|
|
371
432
|
child.once("error", (error) => {
|
|
372
|
-
finish(
|
|
433
|
+
finish(
|
|
434
|
+
new ClipboardSpawnError("clipboard helper spawn failed", {
|
|
435
|
+
cause: error,
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
373
438
|
});
|
|
374
439
|
|
|
375
440
|
child.once("close", (code) => {
|
|
@@ -393,7 +458,11 @@ function writeClipboardInChildProcess(text: string, signal: AbortSignal): Promis
|
|
|
393
458
|
return;
|
|
394
459
|
}
|
|
395
460
|
|
|
396
|
-
finish(
|
|
461
|
+
finish(
|
|
462
|
+
new ClipboardSpawnError(
|
|
463
|
+
`clipboard helper failed with exit code ${code ?? "null"}`,
|
|
464
|
+
),
|
|
465
|
+
);
|
|
397
466
|
});
|
|
398
467
|
|
|
399
468
|
if (!child.stdin) {
|
|
@@ -436,7 +505,9 @@ class ClipboardMirror {
|
|
|
436
505
|
) {}
|
|
437
506
|
|
|
438
507
|
setWriteFn(writeFn: ClipboardWriteFn): void {
|
|
439
|
-
this.activeController?.abort(
|
|
508
|
+
this.activeController?.abort(
|
|
509
|
+
createClipboardAbortError("clipboard writer replaced"),
|
|
510
|
+
);
|
|
440
511
|
this.writeFn = writeFn;
|
|
441
512
|
resetClipboardCircuitBreaker();
|
|
442
513
|
}
|
|
@@ -446,7 +517,9 @@ class ClipboardMirror {
|
|
|
446
517
|
}
|
|
447
518
|
|
|
448
519
|
hasPendingWrite(): boolean {
|
|
449
|
-
return
|
|
520
|
+
return (
|
|
521
|
+
this.activeText !== null || this.pendingText !== null || this.draining
|
|
522
|
+
);
|
|
450
523
|
}
|
|
451
524
|
|
|
452
525
|
mirror(text: string): void {
|
|
@@ -476,7 +549,6 @@ class ClipboardMirror {
|
|
|
476
549
|
this.circuitBreaker.consecutiveEnvironmentFailures = 0;
|
|
477
550
|
} catch (error) {
|
|
478
551
|
this.recordWriteFailure(error);
|
|
479
|
-
// Clipboard mirroring is best-effort; the register is authoritative.
|
|
480
552
|
} finally {
|
|
481
553
|
if (this.activeController === controller) {
|
|
482
554
|
this.activeController = null;
|
|
@@ -503,13 +575,19 @@ class ClipboardMirror {
|
|
|
503
575
|
}
|
|
504
576
|
|
|
505
577
|
this.circuitBreaker.consecutiveEnvironmentFailures += 1;
|
|
506
|
-
if (
|
|
578
|
+
if (
|
|
579
|
+
this.circuitBreaker.consecutiveEnvironmentFailures >=
|
|
580
|
+
CLIPBOARD_SPAWN_FAILURE_LIMIT
|
|
581
|
+
) {
|
|
507
582
|
this.circuitBreaker.disabled = true;
|
|
508
583
|
this.pendingText = null;
|
|
509
584
|
}
|
|
510
585
|
}
|
|
511
586
|
|
|
512
|
-
private async writeWithTimeout(
|
|
587
|
+
private async writeWithTimeout(
|
|
588
|
+
text: string,
|
|
589
|
+
controller: AbortController,
|
|
590
|
+
): Promise<void> {
|
|
513
591
|
const timeoutError = createClipboardAbortError("clipboard write timed out");
|
|
514
592
|
const timeoutId = setTimeout(() => {
|
|
515
593
|
controller.abort(timeoutError);
|
|
@@ -548,14 +626,18 @@ export class ModalEditor extends CustomEditor {
|
|
|
548
626
|
private readonly redoStack: EditorSnapshot[] = [];
|
|
549
627
|
private currentTransition: TransitionState = "none";
|
|
550
628
|
private onChangeHooked: boolean = false;
|
|
551
|
-
private readonly labelColorizers:
|
|
629
|
+
private readonly labelColorizers: ModeColorizers | null;
|
|
630
|
+
private readonly borderColorizers: ModeColorizers | null;
|
|
552
631
|
private readonly cursorShapeRuntime: CursorShapeRuntime | null;
|
|
553
632
|
private lastCursorShapeSequence: CursorShapeSequence | null = null;
|
|
554
633
|
|
|
555
|
-
// Unnamed register
|
|
556
634
|
private unnamedRegister: string = "";
|
|
557
|
-
private
|
|
558
|
-
private
|
|
635
|
+
private preferRegisterForPut = false;
|
|
636
|
+
private clipboardMirrorPolicy: ClipboardMirrorPolicy =
|
|
637
|
+
DEFAULT_CLIPBOARD_MIRROR_POLICY;
|
|
638
|
+
private readonly clipboardMirror = new ClipboardMirror(
|
|
639
|
+
writeClipboardInChildProcess,
|
|
640
|
+
);
|
|
559
641
|
private clipboardReadFn: ClipboardReadFn = readClipboardInChildProcess;
|
|
560
642
|
private quitFn: () => void = () => {};
|
|
561
643
|
private notifyFn: (message: string) => void = () => {};
|
|
@@ -564,18 +646,21 @@ export class ModalEditor extends CustomEditor {
|
|
|
564
646
|
tui: CustomEditorConstructorArgs[0],
|
|
565
647
|
theme: CustomEditorConstructorArgs[1],
|
|
566
648
|
kb: CustomEditorConstructorArgs[2],
|
|
567
|
-
|
|
649
|
+
opts?: ModalEditorOptions,
|
|
568
650
|
) {
|
|
569
651
|
super(tui, theme, kb);
|
|
570
652
|
this.cursorShapeRuntime = getCursorShapeRuntime(tui);
|
|
571
|
-
this.labelColorizers = labelColorizers ?? null;
|
|
653
|
+
this.labelColorizers = opts?.labelColorizers ?? null;
|
|
654
|
+
this.borderColorizers = opts?.borderColorizers ?? null;
|
|
655
|
+
this.installModeBorderColorizer();
|
|
572
656
|
}
|
|
573
657
|
|
|
574
|
-
// Test seams
|
|
575
658
|
setClipboardFn(fn: (text: string, signal?: AbortSignal) => unknown): void {
|
|
576
|
-
this.clipboardMirror.setWriteFn(
|
|
577
|
-
|
|
578
|
-
|
|
659
|
+
this.clipboardMirror.setWriteFn(
|
|
660
|
+
async (text: string, signal: AbortSignal) => {
|
|
661
|
+
await fn(text, signal);
|
|
662
|
+
},
|
|
663
|
+
);
|
|
579
664
|
}
|
|
580
665
|
setClipboardWriteTimeoutMs(timeoutMs: number): void {
|
|
581
666
|
this.clipboardMirror.setTimeoutMs(timeoutMs);
|
|
@@ -589,12 +674,51 @@ export class ModalEditor extends CustomEditor {
|
|
|
589
674
|
getClipboardMirrorPolicy(): ClipboardMirrorPolicy {
|
|
590
675
|
return this.clipboardMirrorPolicy;
|
|
591
676
|
}
|
|
592
|
-
setQuitFn(fn: () => void): void {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
677
|
+
setQuitFn(fn: () => void): void {
|
|
678
|
+
this.quitFn = fn;
|
|
679
|
+
}
|
|
680
|
+
setNotifyFn(fn: (message: string) => void): void {
|
|
681
|
+
this.notifyFn = fn;
|
|
682
|
+
}
|
|
683
|
+
getRegister(): string {
|
|
684
|
+
return this.unnamedRegister;
|
|
685
|
+
}
|
|
686
|
+
setRegister(text: string): void {
|
|
687
|
+
this.unnamedRegister = text;
|
|
688
|
+
this.preferRegisterForPut = false;
|
|
689
|
+
}
|
|
690
|
+
getMode(): Mode {
|
|
691
|
+
return this.mode;
|
|
692
|
+
}
|
|
693
|
+
getText(): string {
|
|
694
|
+
return this.getLines().join("\n");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private getActiveMode(): Mode | "ex" {
|
|
698
|
+
if (this.pendingExCommand !== null) return "ex";
|
|
699
|
+
return this.mode;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private installModeBorderColorizer(): void {
|
|
703
|
+
if (!this.borderColorizers) return;
|
|
704
|
+
let base = this.borderColor;
|
|
705
|
+
const modeBorderColor = (text: string) =>
|
|
706
|
+
(this.borderColorizers?.[this.getActiveMode()] ?? base)(text);
|
|
707
|
+
// Pi assigns its default border color after extension editor construction.
|
|
708
|
+
// Keep a mode-aware getter installed and treat later assignments as the
|
|
709
|
+
// fallback/base color, otherwise syncBorderColorWithMode is overwritten in
|
|
710
|
+
// real sessions even though direct editor tests pass.
|
|
711
|
+
Object.defineProperty(this, "borderColor", {
|
|
712
|
+
get: () => modeBorderColor,
|
|
713
|
+
set(next: unknown) {
|
|
714
|
+
if (typeof next === "function") base = next as typeof base;
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private setMode(mode: Mode = "insert"): void {
|
|
720
|
+
this.mode = mode;
|
|
721
|
+
}
|
|
598
722
|
|
|
599
723
|
override setText(text: string): void {
|
|
600
724
|
this.clearRedoStack();
|
|
@@ -609,14 +733,20 @@ export class ModalEditor extends CustomEditor {
|
|
|
609
733
|
};
|
|
610
734
|
}
|
|
611
735
|
|
|
612
|
-
private requireRedoRestoreState(
|
|
613
|
-
|
|
614
|
-
|
|
736
|
+
private requireRedoRestoreState(editor: ModalEditorInternals): {
|
|
737
|
+
lines: string[];
|
|
738
|
+
cursorLine?: number;
|
|
739
|
+
cursorCol?: number;
|
|
740
|
+
} {
|
|
615
741
|
const state = editor.state;
|
|
616
742
|
if (!state || !Array.isArray(state.lines)) {
|
|
617
743
|
throw new Error("Redo restore prerequisite: editor state unavailable");
|
|
618
744
|
}
|
|
619
|
-
return state as {
|
|
745
|
+
return state as {
|
|
746
|
+
lines: string[];
|
|
747
|
+
cursorLine?: number;
|
|
748
|
+
cursorCol?: number;
|
|
749
|
+
};
|
|
620
750
|
}
|
|
621
751
|
|
|
622
752
|
private restoreSnapshot(snapshot: EditorSnapshot): void {
|
|
@@ -648,9 +778,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
648
778
|
}
|
|
649
779
|
|
|
650
780
|
private snapshotChanged(a: EditorSnapshot, b: EditorSnapshot): boolean {
|
|
651
|
-
return
|
|
652
|
-
|
|
653
|
-
|
|
781
|
+
return (
|
|
782
|
+
a.text !== b.text ||
|
|
783
|
+
a.cursor.line !== b.cursor.line ||
|
|
784
|
+
a.cursor.col !== b.cursor.col
|
|
785
|
+
);
|
|
654
786
|
}
|
|
655
787
|
|
|
656
788
|
private withTransition<T>(
|
|
@@ -737,9 +869,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
737
869
|
private applySyntheticEdit(mutation: () => void): void {
|
|
738
870
|
const editor = this as unknown as ModalEditorInternals;
|
|
739
871
|
if (!editor.state || !Array.isArray(editor.state.lines)) {
|
|
740
|
-
throw new Error(
|
|
741
|
-
"Synthetic edit prerequisite: editor state unavailable",
|
|
742
|
-
);
|
|
872
|
+
throw new Error("Synthetic edit prerequisite: editor state unavailable");
|
|
743
873
|
}
|
|
744
874
|
|
|
745
875
|
if (typeof editor.pushUndoSnapshot !== "function") {
|
|
@@ -756,9 +886,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
756
886
|
|
|
757
887
|
if (this.getText() === textBefore) return;
|
|
758
888
|
|
|
759
|
-
// Text changed — push undo boundary for pre-mutation state.
|
|
760
|
-
// Briefly swap pre-mutation state in for the snapshot, then
|
|
761
|
-
// restore the post-mutation result.
|
|
762
889
|
const postLines = editor.state.lines.slice();
|
|
763
890
|
const postCursorLine = editor.state.cursorLine;
|
|
764
891
|
const postCursorCol = editor.state.cursorCol;
|
|
@@ -786,8 +913,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
786
913
|
}
|
|
787
914
|
|
|
788
915
|
private clearPendingExCommand(): void {
|
|
789
|
-
const shouldDiscardBracketedPasteTail =
|
|
790
|
-
|
|
916
|
+
const shouldDiscardBracketedPasteTail =
|
|
917
|
+
this.acceptingBracketedPasteInExCommand ||
|
|
918
|
+
this.pendingEscWhileAcceptingBracketedPasteInExCommand;
|
|
791
919
|
|
|
792
920
|
this.pendingExCommand = null;
|
|
793
921
|
this.acceptingBracketedPasteInExCommand = false;
|
|
@@ -871,7 +999,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
871
999
|
}
|
|
872
1000
|
}
|
|
873
1001
|
|
|
874
|
-
private stripBracketedPasteInNormalMode(data: string): {
|
|
1002
|
+
private stripBracketedPasteInNormalMode(data: string): {
|
|
1003
|
+
filtered: string | null;
|
|
1004
|
+
stripped: boolean;
|
|
1005
|
+
} {
|
|
875
1006
|
let chunk = data;
|
|
876
1007
|
let stripped = false;
|
|
877
1008
|
|
|
@@ -894,14 +1025,18 @@ export class ModalEditor extends CustomEditor {
|
|
|
894
1025
|
}
|
|
895
1026
|
|
|
896
1027
|
stripped = true;
|
|
897
|
-
const end = chunk.indexOf(
|
|
1028
|
+
const end = chunk.indexOf(
|
|
1029
|
+
BRACKETED_PASTE_END,
|
|
1030
|
+
start + BRACKETED_PASTE_START.length,
|
|
1031
|
+
);
|
|
898
1032
|
if (end === -1) {
|
|
899
1033
|
this.discardingBracketedPasteInNormalMode = true;
|
|
900
1034
|
const leading = chunk.slice(0, start);
|
|
901
1035
|
return { filtered: leading.length > 0 ? leading : null, stripped };
|
|
902
1036
|
}
|
|
903
1037
|
|
|
904
|
-
chunk =
|
|
1038
|
+
chunk =
|
|
1039
|
+
chunk.slice(0, start) + chunk.slice(end + BRACKETED_PASTE_END.length);
|
|
905
1040
|
if (!chunk) return { filtered: null, stripped };
|
|
906
1041
|
}
|
|
907
1042
|
}
|
|
@@ -954,24 +1089,19 @@ export class ModalEditor extends CustomEditor {
|
|
|
954
1089
|
return;
|
|
955
1090
|
}
|
|
956
1091
|
|
|
957
|
-
if (this.mode
|
|
958
|
-
// Shift+Alt+A: go to end of line (like Esc -> A but stay in insert)
|
|
1092
|
+
if ("insert" === this.mode) {
|
|
959
1093
|
if (matchesKey(data, Key.shiftAlt("a")) || data === "\x1bA") {
|
|
960
1094
|
super.handleInput(CTRL_E);
|
|
961
1095
|
return;
|
|
962
1096
|
}
|
|
963
|
-
// Shift+Alt+I: go to start of line (like Esc -> I but stay in insert)
|
|
964
1097
|
if (matchesKey(data, Key.shiftAlt("i")) || data === "\x1bI") {
|
|
965
1098
|
super.handleInput(CTRL_A);
|
|
966
1099
|
return;
|
|
967
1100
|
}
|
|
968
|
-
// Alt+o: open new line below (stay in insert mode)
|
|
969
1101
|
if (matchesKey(data, Key.alt("o")) || data === "\x1bo") {
|
|
970
1102
|
this.openLineBelow();
|
|
971
1103
|
return;
|
|
972
1104
|
}
|
|
973
|
-
// Alt+Shift+o: open new line above (stay in insert mode)
|
|
974
|
-
// \x1bO is the legacy sequence for Alt+Shift+O (VT100 SS3 prefix in non-Kitty terminals)
|
|
975
1105
|
if (matchesKey(data, Key.shiftAlt("o")) || data === "\x1bO") {
|
|
976
1106
|
this.openLineAbove();
|
|
977
1107
|
return;
|
|
@@ -999,9 +1129,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
999
1129
|
const replacement = data.repeat(count);
|
|
1000
1130
|
const lineStartAbs = this.getAbsoluteIndex(cursor.line, 0);
|
|
1001
1131
|
const text = this.getText();
|
|
1002
|
-
const newText =
|
|
1003
|
-
|
|
1004
|
-
|
|
1132
|
+
const newText =
|
|
1133
|
+
text.slice(0, lineStartAbs) +
|
|
1134
|
+
before +
|
|
1135
|
+
replacement +
|
|
1136
|
+
after +
|
|
1137
|
+
text.slice(lineStartAbs + line.length);
|
|
1138
|
+
const newCursorAbs =
|
|
1139
|
+
lineStartAbs + before.length + data.length * (count - 1);
|
|
1005
1140
|
this.replaceTextInBuffer(newText, newCursorAbs);
|
|
1006
1141
|
return;
|
|
1007
1142
|
}
|
|
@@ -1064,32 +1199,42 @@ export class ModalEditor extends CustomEditor {
|
|
|
1064
1199
|
}
|
|
1065
1200
|
|
|
1066
1201
|
if (
|
|
1067
|
-
this.pendingMotion
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1202
|
+
this.pendingMotion ||
|
|
1203
|
+
this.pendingTextObject ||
|
|
1204
|
+
this.pendingOperator ||
|
|
1205
|
+
this.prefixCount ||
|
|
1206
|
+
this.operatorCount ||
|
|
1207
|
+
this.pendingG ||
|
|
1208
|
+
this.pendingGCount ||
|
|
1209
|
+
this.pendingReplace
|
|
1075
1210
|
) {
|
|
1076
1211
|
this.clearPendingState();
|
|
1077
1212
|
return;
|
|
1078
1213
|
}
|
|
1079
|
-
if (this.mode
|
|
1214
|
+
if ("insert" === this.mode) {
|
|
1080
1215
|
this.clearUnderlyingPasteStateIfActive();
|
|
1081
|
-
this.
|
|
1216
|
+
this.setMode("normal");
|
|
1082
1217
|
} else {
|
|
1083
1218
|
super.handleInput("\x1b"); // pass escape to abort agent
|
|
1084
1219
|
}
|
|
1085
1220
|
}
|
|
1086
1221
|
|
|
1087
1222
|
private isEnterLikeInput(data: string): boolean {
|
|
1088
|
-
return
|
|
1223
|
+
return (
|
|
1224
|
+
data === "\r" ||
|
|
1225
|
+
data === "\n" ||
|
|
1226
|
+
matchesKey(data, "enter") ||
|
|
1227
|
+
matchesKey(data, "return")
|
|
1228
|
+
);
|
|
1089
1229
|
}
|
|
1090
1230
|
|
|
1091
1231
|
private isBackspaceLikeInput(data: string): boolean {
|
|
1092
|
-
return
|
|
1232
|
+
return (
|
|
1233
|
+
data === "\x7f" ||
|
|
1234
|
+
data === "\x08" ||
|
|
1235
|
+
matchesKey(data, "backspace") ||
|
|
1236
|
+
matchesKey(data, "ctrl+h")
|
|
1237
|
+
);
|
|
1093
1238
|
}
|
|
1094
1239
|
|
|
1095
1240
|
private deleteLastPendingExCommandGrapheme(): void {
|
|
@@ -1112,10 +1257,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1112
1257
|
|
|
1113
1258
|
private handlePendingExCommandControlChunk(data: string): boolean {
|
|
1114
1259
|
if (
|
|
1115
|
-
!data.includes("\r")
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1260
|
+
!data.includes("\r") &&
|
|
1261
|
+
!data.includes("\n") &&
|
|
1262
|
+
!data.includes("\x7f") &&
|
|
1263
|
+
!data.includes("\x08")
|
|
1119
1264
|
) {
|
|
1120
1265
|
return false;
|
|
1121
1266
|
}
|
|
@@ -1212,7 +1357,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
1212
1357
|
if (data.length === 0) return false;
|
|
1213
1358
|
for (const char of data) {
|
|
1214
1359
|
const codePoint = char.codePointAt(0);
|
|
1215
|
-
if (codePoint === undefined || codePoint < 32 || codePoint === 127)
|
|
1360
|
+
if (codePoint === undefined || codePoint < 32 || codePoint === 127)
|
|
1361
|
+
return false;
|
|
1216
1362
|
}
|
|
1217
1363
|
return true;
|
|
1218
1364
|
}
|
|
@@ -1249,9 +1395,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1249
1395
|
|
|
1250
1396
|
if (prefix === null && operator === null) return defaultValue;
|
|
1251
1397
|
|
|
1252
|
-
const total =
|
|
1253
|
-
|
|
1254
|
-
|
|
1398
|
+
const total =
|
|
1399
|
+
prefix !== null && operator !== null
|
|
1400
|
+
? prefix * operator
|
|
1401
|
+
: (prefix ?? operator ?? defaultValue);
|
|
1255
1402
|
|
|
1256
1403
|
if (!Number.isFinite(total) || total <= 0) return defaultValue;
|
|
1257
1404
|
return Math.min(MAX_COUNT, total);
|
|
@@ -1286,7 +1433,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1286
1433
|
} else if (this.pendingOperator === "c") {
|
|
1287
1434
|
this.deleteWithCharMotion(pendingMotion, data);
|
|
1288
1435
|
this.pendingOperator = null;
|
|
1289
|
-
this.
|
|
1436
|
+
this.setMode();
|
|
1290
1437
|
} else if (this.pendingOperator === "y") {
|
|
1291
1438
|
this.yankWithCharMotion(pendingMotion, data);
|
|
1292
1439
|
this.pendingOperator = null;
|
|
@@ -1315,7 +1462,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1315
1462
|
if (data === "w" || data === "W") {
|
|
1316
1463
|
const semanticClass: WordTextObjectClass = data === "W" ? "WORD" : "word";
|
|
1317
1464
|
const count = this.takeTotalCount(1);
|
|
1318
|
-
const range = this.getWordObjectRange(
|
|
1465
|
+
const range = this.getWordObjectRange(
|
|
1466
|
+
pendingTextObject,
|
|
1467
|
+
count,
|
|
1468
|
+
semanticClass,
|
|
1469
|
+
);
|
|
1319
1470
|
if (!range || !this.pendingOperator) {
|
|
1320
1471
|
this.pendingOperator = null;
|
|
1321
1472
|
return;
|
|
@@ -1353,7 +1504,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1353
1504
|
if (range.endAbs === range.startAbs) {
|
|
1354
1505
|
if (pendingOperator === "c") {
|
|
1355
1506
|
this.moveCursorToAbsoluteIndex(range.startAbs);
|
|
1356
|
-
this.
|
|
1507
|
+
this.setMode();
|
|
1357
1508
|
}
|
|
1358
1509
|
return;
|
|
1359
1510
|
}
|
|
@@ -1365,7 +1516,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1365
1516
|
|
|
1366
1517
|
if (pendingOperator === "c") {
|
|
1367
1518
|
this.deleteRangeByAbsolute(range.startAbs, range.endAbs);
|
|
1368
|
-
this.
|
|
1519
|
+
this.setMode();
|
|
1369
1520
|
return;
|
|
1370
1521
|
}
|
|
1371
1522
|
|
|
@@ -1395,7 +1546,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
1395
1546
|
}
|
|
1396
1547
|
|
|
1397
1548
|
if (data === "j" || data === "k") {
|
|
1398
|
-
const hasDualCount =
|
|
1549
|
+
const hasDualCount =
|
|
1550
|
+
this.prefixCount.length > 0 && this.operatorCount.length > 0;
|
|
1399
1551
|
const count = this.takeTotalCount(1);
|
|
1400
1552
|
const delta = hasDualCount ? Math.max(0, count - 1) : count;
|
|
1401
1553
|
this.deleteLinewiseByDelta(data === "j" ? delta : -delta);
|
|
@@ -1426,20 +1578,18 @@ export class ModalEditor extends CustomEditor {
|
|
|
1426
1578
|
return;
|
|
1427
1579
|
}
|
|
1428
1580
|
|
|
1429
|
-
const hasCount =
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1581
|
+
const hasCount =
|
|
1582
|
+
this.prefixCount.length > 0 || this.operatorCount.length > 0;
|
|
1583
|
+
const supportsCountedWordMotion =
|
|
1584
|
+
data === "w" ||
|
|
1585
|
+
data === "e" ||
|
|
1586
|
+
data === "b" ||
|
|
1587
|
+
data === "W" ||
|
|
1588
|
+
data === "E" ||
|
|
1589
|
+
data === "B";
|
|
1438
1590
|
const supportsCountedTextObject = data === "i" || data === "a";
|
|
1439
1591
|
|
|
1440
1592
|
if (hasCount && !supportsCountedWordMotion && !supportsCountedTextObject) {
|
|
1441
|
-
// Counted forms beyond dd, d{count}j/k, d{count}{f/F/t/T}, and
|
|
1442
|
-
// d{count}{w/e/b/W/E/B}/{i/a}w are out of scope.
|
|
1443
1593
|
this.cancelPendingOperator(data);
|
|
1444
1594
|
return;
|
|
1445
1595
|
}
|
|
@@ -1455,7 +1605,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
1455
1605
|
return;
|
|
1456
1606
|
}
|
|
1457
1607
|
|
|
1458
|
-
// Invalid motion: cancel operator to avoid sticky surprising deletes.
|
|
1459
1608
|
this.cancelPendingOperator(data);
|
|
1460
1609
|
}
|
|
1461
1610
|
|
|
@@ -1480,7 +1629,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1480
1629
|
|
|
1481
1630
|
this.cutLine();
|
|
1482
1631
|
this.pendingOperator = null;
|
|
1483
|
-
this.
|
|
1632
|
+
this.setMode();
|
|
1484
1633
|
return;
|
|
1485
1634
|
}
|
|
1486
1635
|
|
|
@@ -1501,7 +1650,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1501
1650
|
this.replaceTextInBuffer(newText, cursorAbs);
|
|
1502
1651
|
}
|
|
1503
1652
|
this.pendingOperator = null;
|
|
1504
|
-
this.
|
|
1653
|
+
this.setMode();
|
|
1505
1654
|
return;
|
|
1506
1655
|
}
|
|
1507
1656
|
|
|
@@ -1510,15 +1659,15 @@ export class ModalEditor extends CustomEditor {
|
|
|
1510
1659
|
return;
|
|
1511
1660
|
}
|
|
1512
1661
|
|
|
1513
|
-
const hasCount =
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1662
|
+
const hasCount =
|
|
1663
|
+
this.prefixCount.length > 0 || this.operatorCount.length > 0;
|
|
1664
|
+
const supportsCountedWordMotion =
|
|
1665
|
+
data === "w" ||
|
|
1666
|
+
data === "e" ||
|
|
1667
|
+
data === "b" ||
|
|
1668
|
+
data === "W" ||
|
|
1669
|
+
data === "E" ||
|
|
1670
|
+
data === "B";
|
|
1522
1671
|
const supportsCountedTextObject = data === "i" || data === "a";
|
|
1523
1672
|
|
|
1524
1673
|
if (hasCount && !supportsCountedWordMotion && !supportsCountedTextObject) {
|
|
@@ -1532,16 +1681,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
1532
1681
|
}
|
|
1533
1682
|
|
|
1534
1683
|
const motionCount = supportsCountedWordMotion ? this.takeTotalCount(1) : 1;
|
|
1535
|
-
const effectiveMotion =
|
|
1536
|
-
? "E"
|
|
1537
|
-
: data;
|
|
1684
|
+
const effectiveMotion =
|
|
1685
|
+
data === "W" && this.isCursorOnNonWhitespace() ? "E" : data;
|
|
1538
1686
|
if (this.deleteWithMotion(effectiveMotion, motionCount)) {
|
|
1539
1687
|
this.pendingOperator = null;
|
|
1540
|
-
this.
|
|
1688
|
+
this.setMode();
|
|
1541
1689
|
return;
|
|
1542
1690
|
}
|
|
1543
1691
|
|
|
1544
|
-
// Invalid motion: cancel operator to avoid sticky surprising changes.
|
|
1545
1692
|
this.cancelPendingOperator(data);
|
|
1546
1693
|
}
|
|
1547
1694
|
|
|
@@ -1601,43 +1748,34 @@ export class ModalEditor extends CustomEditor {
|
|
|
1601
1748
|
return;
|
|
1602
1749
|
}
|
|
1603
1750
|
|
|
1604
|
-
const supportsCountedStandaloneEdit =
|
|
1605
|
-
data === "x"
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
data === "
|
|
1628
|
-
|
|
1629
|
-
|| data === "b"
|
|
1630
|
-
|| data === "W"
|
|
1631
|
-
|| data === "E"
|
|
1632
|
-
|| data === "B"
|
|
1633
|
-
);
|
|
1751
|
+
const supportsCountedStandaloneEdit =
|
|
1752
|
+
data === "x" ||
|
|
1753
|
+
data === "r" ||
|
|
1754
|
+
data === "s" ||
|
|
1755
|
+
data === "S" ||
|
|
1756
|
+
data === "D" ||
|
|
1757
|
+
data === "C" ||
|
|
1758
|
+
data === "p" ||
|
|
1759
|
+
data === "P" ||
|
|
1760
|
+
data === "Y" ||
|
|
1761
|
+
data === "J" ||
|
|
1762
|
+
data === "u" ||
|
|
1763
|
+
data === CTRL_UNDERSCORE ||
|
|
1764
|
+
matchesKey(data, "ctrl+_") ||
|
|
1765
|
+
data === CTRL_R ||
|
|
1766
|
+
matchesKey(data, "ctrl+r");
|
|
1767
|
+
const supportsCountedCharMotion =
|
|
1768
|
+
CHAR_MOTION_KEYS.has(data) || data === ";" || data === ",";
|
|
1769
|
+
const supportsCountedWordMotion =
|
|
1770
|
+
data === "w" ||
|
|
1771
|
+
data === "e" ||
|
|
1772
|
+
data === "b" ||
|
|
1773
|
+
data === "W" ||
|
|
1774
|
+
data === "E" ||
|
|
1775
|
+
data === "B";
|
|
1634
1776
|
const supportsCountedParagraphMotion = data === "{" || data === "}";
|
|
1635
|
-
const supportsCountedNav =
|
|
1636
|
-
data === "h"
|
|
1637
|
-
|| data === "j"
|
|
1638
|
-
|| data === "k"
|
|
1639
|
-
|| data === "l"
|
|
1640
|
-
);
|
|
1777
|
+
const supportsCountedNav =
|
|
1778
|
+
data === "h" || data === "j" || data === "k" || data === "l";
|
|
1641
1779
|
const supportsCountedUnderscore = data === "_";
|
|
1642
1780
|
|
|
1643
1781
|
if (supportsCountedNav) {
|
|
@@ -1660,13 +1798,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
1660
1798
|
}
|
|
1661
1799
|
|
|
1662
1800
|
if (
|
|
1663
|
-
!supportsCountedStandaloneEdit
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1801
|
+
!supportsCountedStandaloneEdit &&
|
|
1802
|
+
!supportsCountedCharMotion &&
|
|
1803
|
+
!supportsCountedWordMotion &&
|
|
1804
|
+
!supportsCountedParagraphMotion &&
|
|
1805
|
+
!supportsCountedUnderscore
|
|
1668
1806
|
) {
|
|
1669
|
-
// Unsupported prefixed forms: drop count and keep processing this key.
|
|
1670
1807
|
this.prefixCount = "";
|
|
1671
1808
|
this.operatorCount = "";
|
|
1672
1809
|
}
|
|
@@ -1738,7 +1875,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1738
1875
|
}
|
|
1739
1876
|
|
|
1740
1877
|
if (data === ";" && this.lastCharMotion) {
|
|
1741
|
-
this.executeCharMotion(
|
|
1878
|
+
this.executeCharMotion(
|
|
1879
|
+
this.lastCharMotion.motion,
|
|
1880
|
+
this.lastCharMotion.char,
|
|
1881
|
+
false,
|
|
1882
|
+
);
|
|
1742
1883
|
return;
|
|
1743
1884
|
}
|
|
1744
1885
|
if (data === "," && this.lastCharMotion) {
|
|
@@ -1750,7 +1891,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1750
1891
|
return;
|
|
1751
1892
|
}
|
|
1752
1893
|
|
|
1753
|
-
if (
|
|
1894
|
+
if (
|
|
1895
|
+
data === "u" ||
|
|
1896
|
+
data === CTRL_UNDERSCORE ||
|
|
1897
|
+
matchesKey(data, "ctrl+_")
|
|
1898
|
+
) {
|
|
1754
1899
|
this.performUndo();
|
|
1755
1900
|
return;
|
|
1756
1901
|
}
|
|
@@ -1810,7 +1955,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
1810
1955
|
return;
|
|
1811
1956
|
}
|
|
1812
1957
|
|
|
1813
|
-
// Pass control sequences (ctrl+c, etc.) to super, ignore printable chars
|
|
1814
1958
|
if (this.isPrintableChunk(data)) return;
|
|
1815
1959
|
super.handleInput(data);
|
|
1816
1960
|
}
|
|
@@ -1830,29 +1974,29 @@ export class ModalEditor extends CustomEditor {
|
|
|
1830
1974
|
const seq = NORMAL_KEYS[key];
|
|
1831
1975
|
switch (key) {
|
|
1832
1976
|
case "i":
|
|
1833
|
-
this.
|
|
1977
|
+
this.setMode();
|
|
1834
1978
|
break;
|
|
1835
1979
|
case "a":
|
|
1836
|
-
this.
|
|
1980
|
+
this.setMode();
|
|
1837
1981
|
if (!this.isCursorAtOrPastEol()) {
|
|
1838
1982
|
super.handleInput(ESC_RIGHT);
|
|
1839
1983
|
}
|
|
1840
1984
|
break;
|
|
1841
1985
|
case "A":
|
|
1842
|
-
this.
|
|
1986
|
+
this.setMode();
|
|
1843
1987
|
super.handleInput(CTRL_E);
|
|
1844
1988
|
break;
|
|
1845
1989
|
case "I":
|
|
1846
|
-
this.
|
|
1990
|
+
this.setMode();
|
|
1847
1991
|
this.moveCursorToFirstNonWhitespace();
|
|
1848
1992
|
break;
|
|
1849
1993
|
case "o":
|
|
1850
1994
|
this.openLineBelow();
|
|
1851
|
-
this.
|
|
1995
|
+
this.setMode();
|
|
1852
1996
|
break;
|
|
1853
1997
|
case "O":
|
|
1854
1998
|
this.openLineAbove();
|
|
1855
|
-
this.
|
|
1999
|
+
this.setMode();
|
|
1856
2000
|
break;
|
|
1857
2001
|
case "D":
|
|
1858
2002
|
this.takeTotalCount(1);
|
|
@@ -1861,16 +2005,16 @@ export class ModalEditor extends CustomEditor {
|
|
|
1861
2005
|
case "C":
|
|
1862
2006
|
this.takeTotalCount(1);
|
|
1863
2007
|
this.cutToEndOfLine();
|
|
1864
|
-
this.
|
|
2008
|
+
this.setMode();
|
|
1865
2009
|
break;
|
|
1866
2010
|
case "S":
|
|
1867
2011
|
this.takeTotalCount(1);
|
|
1868
2012
|
this.cutCurrentLineContent();
|
|
1869
|
-
this.
|
|
2013
|
+
this.setMode();
|
|
1870
2014
|
break;
|
|
1871
2015
|
case "s":
|
|
1872
2016
|
this.cutCharUnderCursor();
|
|
1873
|
-
this.
|
|
2017
|
+
this.setMode();
|
|
1874
2018
|
break;
|
|
1875
2019
|
case "x":
|
|
1876
2020
|
this.cutCharUnderCursor();
|
|
@@ -1886,11 +2030,22 @@ export class ModalEditor extends CustomEditor {
|
|
|
1886
2030
|
}
|
|
1887
2031
|
}
|
|
1888
2032
|
|
|
1889
|
-
private executeCharMotion(
|
|
2033
|
+
private executeCharMotion(
|
|
2034
|
+
motion: CharMotion,
|
|
2035
|
+
targetChar: string,
|
|
2036
|
+
saveMotion: boolean = true,
|
|
2037
|
+
): void {
|
|
1890
2038
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
1891
2039
|
const col = this.getCursor().col;
|
|
1892
2040
|
const count = this.takeTotalCount(1);
|
|
1893
|
-
const targetCol = findCharMotionTarget(
|
|
2041
|
+
const targetCol = findCharMotionTarget(
|
|
2042
|
+
line,
|
|
2043
|
+
col,
|
|
2044
|
+
motion,
|
|
2045
|
+
targetChar,
|
|
2046
|
+
!saveMotion,
|
|
2047
|
+
count,
|
|
2048
|
+
);
|
|
1894
2049
|
|
|
1895
2050
|
if (targetCol !== null && saveMotion) {
|
|
1896
2051
|
this.lastCharMotion = { motion, char: targetChar };
|
|
@@ -1905,7 +2060,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
1905
2060
|
const lines = this.getLines();
|
|
1906
2061
|
const fromLine = this.getCursor().line;
|
|
1907
2062
|
const count = this.takeTotalCount(1);
|
|
1908
|
-
const targetLine = findParagraphMotionTarget(
|
|
2063
|
+
const targetLine = findParagraphMotionTarget(
|
|
2064
|
+
lines,
|
|
2065
|
+
fromLine,
|
|
2066
|
+
direction,
|
|
2067
|
+
count,
|
|
2068
|
+
);
|
|
1909
2069
|
this.moveCursorToLineStart(targetLine);
|
|
1910
2070
|
}
|
|
1911
2071
|
|
|
@@ -1920,7 +2080,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1920
2080
|
|
|
1921
2081
|
const state = editor.state;
|
|
1922
2082
|
if (!state || !Array.isArray(state.lines)) return false;
|
|
1923
|
-
if (
|
|
2083
|
+
if (
|
|
2084
|
+
!Number.isInteger(state.cursorLine) ||
|
|
2085
|
+
!Number.isInteger(state.cursorCol)
|
|
2086
|
+
)
|
|
2087
|
+
return false;
|
|
1924
2088
|
|
|
1925
2089
|
const cursorLine = state.cursorLine as number;
|
|
1926
2090
|
const cursorCol = state.cursorCol as number;
|
|
@@ -1929,8 +2093,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
1929
2093
|
|
|
1930
2094
|
const target = cursorCol + delta;
|
|
1931
2095
|
|
|
1932
|
-
// Only short-circuit line-local movement when each grapheme is one code
|
|
1933
|
-
// unit; otherwise let the base editor keep cursor boundaries valid.
|
|
1934
2096
|
if (target < 0 || target > line.length) return false;
|
|
1935
2097
|
|
|
1936
2098
|
state.cursorCol = target;
|
|
@@ -1970,7 +2132,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1970
2132
|
}
|
|
1971
2133
|
|
|
1972
2134
|
const currentLine = state.cursorLine ?? 0;
|
|
1973
|
-
const targetLine = Math.max(
|
|
2135
|
+
const targetLine = Math.max(
|
|
2136
|
+
0,
|
|
2137
|
+
Math.min(currentLine + delta, state.lines.length - 1),
|
|
2138
|
+
);
|
|
1974
2139
|
if (targetLine === currentLine) return;
|
|
1975
2140
|
|
|
1976
2141
|
const preferredCol = editor.preferredVisualCol ?? state.cursorCol ?? 0;
|
|
@@ -2074,9 +2239,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
2074
2239
|
if (normalize) {
|
|
2075
2240
|
const trimmedRight = right.trimStart();
|
|
2076
2241
|
const leftLastChar = left[left.length - 1];
|
|
2077
|
-
const leftEndsWithSpace =
|
|
2242
|
+
const leftEndsWithSpace =
|
|
2243
|
+
leftLastChar !== undefined && /\s/.test(leftLastChar);
|
|
2078
2244
|
const needsSeparator = !leftEndsWithSpace && trimmedRight.length > 0;
|
|
2079
|
-
joined = needsSeparator
|
|
2245
|
+
joined = needsSeparator
|
|
2246
|
+
? `${left} ${trimmedRight}`
|
|
2247
|
+
: left + trimmedRight;
|
|
2080
2248
|
joinPoint = left.length;
|
|
2081
2249
|
} else {
|
|
2082
2250
|
joined = left + right;
|
|
@@ -2170,25 +2338,43 @@ export class ModalEditor extends CustomEditor {
|
|
|
2170
2338
|
} else if (target === "start") {
|
|
2171
2339
|
const startType = this.charType(text[next], semanticClass);
|
|
2172
2340
|
if (startType !== "space") {
|
|
2173
|
-
while (
|
|
2341
|
+
while (
|
|
2342
|
+
next < len &&
|
|
2343
|
+
this.charType(text[next], semanticClass) === startType
|
|
2344
|
+
)
|
|
2345
|
+
next++;
|
|
2174
2346
|
}
|
|
2175
|
-
while (
|
|
2347
|
+
while (
|
|
2348
|
+
next < len &&
|
|
2349
|
+
this.charType(text[next], semanticClass) === "space"
|
|
2350
|
+
)
|
|
2351
|
+
next++;
|
|
2176
2352
|
} else {
|
|
2177
2353
|
if (next < len - 1) next++;
|
|
2178
|
-
while (
|
|
2354
|
+
while (
|
|
2355
|
+
next < len &&
|
|
2356
|
+
this.charType(text[next], semanticClass) === "space"
|
|
2357
|
+
)
|
|
2358
|
+
next++;
|
|
2179
2359
|
if (next >= len) {
|
|
2180
2360
|
next = len;
|
|
2181
2361
|
} else {
|
|
2182
2362
|
const t = this.charType(text[next], semanticClass);
|
|
2183
|
-
while (
|
|
2363
|
+
while (
|
|
2364
|
+
next < len - 1 &&
|
|
2365
|
+
this.charType(text[next + 1], semanticClass) === t
|
|
2366
|
+
)
|
|
2367
|
+
next++;
|
|
2184
2368
|
}
|
|
2185
2369
|
}
|
|
2186
2370
|
} else {
|
|
2187
2371
|
if (next >= len) next = len - 1;
|
|
2188
2372
|
if (next > 0) next--;
|
|
2189
|
-
while (next > 0 && this.charType(text[next], semanticClass) === "space")
|
|
2373
|
+
while (next > 0 && this.charType(text[next], semanticClass) === "space")
|
|
2374
|
+
next--;
|
|
2190
2375
|
const t = this.charType(text[next], semanticClass);
|
|
2191
|
-
while (next > 0 && this.charType(text[next - 1], semanticClass) === t)
|
|
2376
|
+
while (next > 0 && this.charType(text[next - 1], semanticClass) === t)
|
|
2377
|
+
next--;
|
|
2192
2378
|
}
|
|
2193
2379
|
|
|
2194
2380
|
if (next === i) break;
|
|
@@ -2277,7 +2463,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2277
2463
|
semanticClass: WordMotionClass = "word",
|
|
2278
2464
|
): boolean {
|
|
2279
2465
|
const col = this.getCursor().col;
|
|
2280
|
-
const targetCol = this.tryFindWordTargetLineLocal(
|
|
2466
|
+
const targetCol = this.tryFindWordTargetLineLocal(
|
|
2467
|
+
direction,
|
|
2468
|
+
target,
|
|
2469
|
+
semanticClass,
|
|
2470
|
+
);
|
|
2281
2471
|
if (targetCol === null || targetCol === col) return false;
|
|
2282
2472
|
|
|
2283
2473
|
this.moveCursorToCol(targetCol);
|
|
@@ -2293,7 +2483,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2293
2483
|
const lineIndex = cursor.line;
|
|
2294
2484
|
const col = cursor.col;
|
|
2295
2485
|
const lineSnapshot = this.getLines()[lineIndex] ?? "";
|
|
2296
|
-
const direction: WordMotionDirection =
|
|
2486
|
+
const direction: WordMotionDirection =
|
|
2487
|
+
motion === "b" ? "backward" : "forward";
|
|
2297
2488
|
const target: WordMotionTarget = motion === "e" ? "end" : "start";
|
|
2298
2489
|
const steps = Math.max(1, Math.min(MAX_COUNT, count));
|
|
2299
2490
|
|
|
@@ -2360,10 +2551,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2360
2551
|
return true;
|
|
2361
2552
|
}
|
|
2362
2553
|
|
|
2363
|
-
private writeToRegister(
|
|
2554
|
+
private writeToRegister(
|
|
2555
|
+
text: string,
|
|
2556
|
+
source: RegisterWriteSource = "mutation",
|
|
2557
|
+
): void {
|
|
2364
2558
|
this.unnamedRegister = text;
|
|
2365
|
-
|
|
2366
|
-
|
|
2559
|
+
const shouldMirror = text !== "" && this.shouldMirrorRegisterWrite(source);
|
|
2560
|
+
this.preferRegisterForPut = text !== "" && !shouldMirror;
|
|
2561
|
+
if (!shouldMirror) return;
|
|
2367
2562
|
|
|
2368
2563
|
this.clipboardMirror.mirror(text);
|
|
2369
2564
|
}
|
|
@@ -2375,7 +2570,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
2375
2570
|
}
|
|
2376
2571
|
|
|
2377
2572
|
private hasMultiCodeUnitGraphemes(line: string): boolean {
|
|
2378
|
-
return getLineGraphemes(line).some(
|
|
2573
|
+
return getLineGraphemes(line).some(
|
|
2574
|
+
(segment) => segment.end - segment.start > 1,
|
|
2575
|
+
);
|
|
2379
2576
|
}
|
|
2380
2577
|
|
|
2381
2578
|
private getGraphemeRangeAtCol(
|
|
@@ -2386,7 +2583,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
2386
2583
|
): { start: number; end: number } | null {
|
|
2387
2584
|
const clampedCol = Math.max(0, Math.min(col, line.length));
|
|
2388
2585
|
const segments = getLineGraphemes(line);
|
|
2389
|
-
const startIndex = segments.findIndex(
|
|
2586
|
+
const startIndex = segments.findIndex(
|
|
2587
|
+
(segment) => clampedCol < segment.end,
|
|
2588
|
+
);
|
|
2390
2589
|
if (startIndex === -1) return null;
|
|
2391
2590
|
|
|
2392
2591
|
let endIndex = startIndex + Math.max(1, count) - 1;
|
|
@@ -2427,7 +2626,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2427
2626
|
const text = this.getText();
|
|
2428
2627
|
this.writeToRegister(line.slice(range.start, range.end));
|
|
2429
2628
|
this.replaceTextInBuffer(
|
|
2430
|
-
text.slice(0, lineStartAbs + range.start) +
|
|
2629
|
+
text.slice(0, lineStartAbs + range.start) +
|
|
2630
|
+
text.slice(lineStartAbs + range.end),
|
|
2431
2631
|
lineStartAbs + range.start,
|
|
2432
2632
|
);
|
|
2433
2633
|
}
|
|
@@ -2438,7 +2638,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2438
2638
|
const { line, col } = this.getCurrentLineAndCol();
|
|
2439
2639
|
|
|
2440
2640
|
const hasNextLine = cursorLine < lines.length - 1;
|
|
2441
|
-
const deleted =
|
|
2641
|
+
const deleted =
|
|
2642
|
+
col < line.length ? line.slice(col) : hasNextLine ? "\n" : "";
|
|
2442
2643
|
|
|
2443
2644
|
this.writeToRegister(deleted);
|
|
2444
2645
|
super.handleInput(CTRL_K);
|
|
@@ -2461,7 +2662,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2461
2662
|
this.cutCurrentLineContent();
|
|
2462
2663
|
}
|
|
2463
2664
|
|
|
2464
|
-
private getNormalizedLineRange(
|
|
2665
|
+
private getNormalizedLineRange(
|
|
2666
|
+
startLine: number,
|
|
2667
|
+
endLine: number,
|
|
2668
|
+
): { start: number; end: number } {
|
|
2465
2669
|
const lines = this.getLines();
|
|
2466
2670
|
const last = Math.max(0, lines.length - 1);
|
|
2467
2671
|
const clampedStart = Math.max(0, Math.min(startLine, last));
|
|
@@ -2478,7 +2682,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2478
2682
|
return `${lines.slice(start, end + 1).join("\n")}\n`;
|
|
2479
2683
|
}
|
|
2480
2684
|
|
|
2481
|
-
private getLineDeleteAbsoluteRange(
|
|
2685
|
+
private getLineDeleteAbsoluteRange(
|
|
2686
|
+
startLine: number,
|
|
2687
|
+
endLine: number,
|
|
2688
|
+
): { startAbs: number; endAbs: number } {
|
|
2482
2689
|
const lines = this.getLines();
|
|
2483
2690
|
const text = this.getText();
|
|
2484
2691
|
const { start, end } = this.getNormalizedLineRange(startLine, endLine);
|
|
@@ -2505,7 +2712,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2505
2712
|
if (lines.length === 0) return;
|
|
2506
2713
|
|
|
2507
2714
|
const payload = this.getLinewisePayload(startLine, endLine);
|
|
2508
|
-
const { startAbs, endAbs } = this.getLineDeleteAbsoluteRange(
|
|
2715
|
+
const { startAbs, endAbs } = this.getLineDeleteAbsoluteRange(
|
|
2716
|
+
startLine,
|
|
2717
|
+
endLine,
|
|
2718
|
+
);
|
|
2509
2719
|
|
|
2510
2720
|
this.writeToRegister(payload);
|
|
2511
2721
|
|
|
@@ -2514,7 +2724,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2514
2724
|
const newText = text.slice(0, startAbs) + text.slice(endAbs);
|
|
2515
2725
|
this.replaceTextInBuffer(newText, startAbs);
|
|
2516
2726
|
|
|
2517
|
-
// Ensure cursor is at column 0 of the landing line
|
|
2518
2727
|
super.handleInput(CTRL_A);
|
|
2519
2728
|
}
|
|
2520
2729
|
}
|
|
@@ -2547,7 +2756,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2547
2756
|
const col = cursor.col;
|
|
2548
2757
|
|
|
2549
2758
|
if (motion === "$") {
|
|
2550
|
-
// Match D/C behavior exactly, including newline kill at EOL.
|
|
2551
2759
|
this.cutToEndOfLine();
|
|
2552
2760
|
return true;
|
|
2553
2761
|
}
|
|
@@ -2558,7 +2766,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2558
2766
|
}
|
|
2559
2767
|
|
|
2560
2768
|
if (motion === "^") {
|
|
2561
|
-
this.deleteRange(
|
|
2769
|
+
this.deleteRange(
|
|
2770
|
+
col,
|
|
2771
|
+
findFirstNonWhitespaceColumn(this.getLines()[cursor.line] ?? ""),
|
|
2772
|
+
false,
|
|
2773
|
+
);
|
|
2562
2774
|
return true;
|
|
2563
2775
|
}
|
|
2564
2776
|
|
|
@@ -2588,7 +2800,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2588
2800
|
count,
|
|
2589
2801
|
wordMotion.semanticClass,
|
|
2590
2802
|
);
|
|
2591
|
-
this.deleteRangeByAbsolute(
|
|
2803
|
+
this.deleteRangeByAbsolute(
|
|
2804
|
+
currentAbs,
|
|
2805
|
+
targetAbs,
|
|
2806
|
+
wordMotion.motion === "e",
|
|
2807
|
+
);
|
|
2592
2808
|
return true;
|
|
2593
2809
|
}
|
|
2594
2810
|
|
|
@@ -2599,7 +2815,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2599
2815
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
2600
2816
|
const col = this.getCursor().col;
|
|
2601
2817
|
const count = this.takeTotalCount(1);
|
|
2602
|
-
const targetCol = findCharMotionTarget(
|
|
2818
|
+
const targetCol = findCharMotionTarget(
|
|
2819
|
+
line,
|
|
2820
|
+
col,
|
|
2821
|
+
motion,
|
|
2822
|
+
targetChar,
|
|
2823
|
+
false,
|
|
2824
|
+
count,
|
|
2825
|
+
);
|
|
2603
2826
|
|
|
2604
2827
|
if (targetCol === null) return;
|
|
2605
2828
|
|
|
@@ -2628,7 +2851,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2628
2851
|
}
|
|
2629
2852
|
|
|
2630
2853
|
if (data === "j" || data === "k") {
|
|
2631
|
-
const hasDualCount =
|
|
2854
|
+
const hasDualCount =
|
|
2855
|
+
this.prefixCount.length > 0 && this.operatorCount.length > 0;
|
|
2632
2856
|
const count = this.takeTotalCount(1);
|
|
2633
2857
|
const delta = hasDualCount ? Math.max(0, count - 1) : count;
|
|
2634
2858
|
this.yankLinewiseByDelta(data === "j" ? delta : -delta);
|
|
@@ -2665,7 +2889,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2665
2889
|
}
|
|
2666
2890
|
|
|
2667
2891
|
if (this.hasPendingCount()) {
|
|
2668
|
-
// Counted forms beyond yy, y{count}j/k, and y{count}{f/F/t/T} are out of scope.
|
|
2669
2892
|
this.cancelPendingOperator(data);
|
|
2670
2893
|
return;
|
|
2671
2894
|
}
|
|
@@ -2723,7 +2946,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2723
2946
|
1,
|
|
2724
2947
|
wordMotion.semanticClass,
|
|
2725
2948
|
);
|
|
2726
|
-
this.yankRangeByAbsolute(
|
|
2949
|
+
this.yankRangeByAbsolute(
|
|
2950
|
+
currentAbs,
|
|
2951
|
+
targetAbs,
|
|
2952
|
+
wordMotion.motion === "e",
|
|
2953
|
+
);
|
|
2727
2954
|
return true;
|
|
2728
2955
|
}
|
|
2729
2956
|
|
|
@@ -2734,7 +2961,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2734
2961
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
2735
2962
|
const col = this.getCursor().col;
|
|
2736
2963
|
const count = this.takeTotalCount(1);
|
|
2737
|
-
const targetCol = findCharMotionTarget(
|
|
2964
|
+
const targetCol = findCharMotionTarget(
|
|
2965
|
+
line,
|
|
2966
|
+
col,
|
|
2967
|
+
motion,
|
|
2968
|
+
targetChar,
|
|
2969
|
+
false,
|
|
2970
|
+
count,
|
|
2971
|
+
);
|
|
2738
2972
|
|
|
2739
2973
|
if (targetCol === null) return;
|
|
2740
2974
|
|
|
@@ -2749,17 +2983,24 @@ export class ModalEditor extends CustomEditor {
|
|
|
2749
2983
|
let end = Math.min(rawEnd, line.length);
|
|
2750
2984
|
|
|
2751
2985
|
if (inclusive) {
|
|
2752
|
-
const targetRange = this.getGraphemeRangeAtCol(
|
|
2986
|
+
const targetRange = this.getGraphemeRangeAtCol(
|
|
2987
|
+
line,
|
|
2988
|
+
Math.max(col, targetCol),
|
|
2989
|
+
1,
|
|
2990
|
+
);
|
|
2753
2991
|
end = targetRange?.end ?? end;
|
|
2754
2992
|
}
|
|
2755
2993
|
|
|
2756
2994
|
if (end <= start) return;
|
|
2757
2995
|
|
|
2758
|
-
// Yank only — no cursor movement, no text mutation
|
|
2759
2996
|
this.writeToRegister(line.slice(start, end), "yank");
|
|
2760
2997
|
}
|
|
2761
2998
|
|
|
2762
|
-
private yankRangeByAbsolute(
|
|
2999
|
+
private yankRangeByAbsolute(
|
|
3000
|
+
currentAbs: number,
|
|
3001
|
+
targetAbs: number,
|
|
3002
|
+
inclusive: boolean = false,
|
|
3003
|
+
): void {
|
|
2763
3004
|
const text = this.getText();
|
|
2764
3005
|
const start = Math.min(currentAbs, targetAbs);
|
|
2765
3006
|
const rawEnd = Math.max(currentAbs, targetAbs) + (inclusive ? 1 : 0);
|
|
@@ -2768,7 +3009,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2768
3009
|
this.writeToRegister(text.slice(start, end), "yank");
|
|
2769
3010
|
}
|
|
2770
3011
|
|
|
2771
|
-
private getCursorFromAbsoluteIndex(
|
|
3012
|
+
private getCursorFromAbsoluteIndex(
|
|
3013
|
+
text: string,
|
|
3014
|
+
abs: number,
|
|
3015
|
+
): { line: number; col: number } {
|
|
2772
3016
|
const lines = text.length === 0 ? [""] : text.split("\n");
|
|
2773
3017
|
let remaining = Math.max(0, Math.min(abs, text.length));
|
|
2774
3018
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
@@ -2809,7 +3053,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2809
3053
|
editor.tui?.requestRender?.();
|
|
2810
3054
|
}
|
|
2811
3055
|
|
|
2812
|
-
private deleteRangeByAbsolute(
|
|
3056
|
+
private deleteRangeByAbsolute(
|
|
3057
|
+
currentAbs: number,
|
|
3058
|
+
targetAbs: number,
|
|
3059
|
+
inclusive: boolean = false,
|
|
3060
|
+
): void {
|
|
2813
3061
|
const text = this.getText();
|
|
2814
3062
|
const start = Math.min(currentAbs, targetAbs);
|
|
2815
3063
|
const rawEnd = Math.max(currentAbs, targetAbs) + (inclusive ? 1 : 0);
|
|
@@ -2845,7 +3093,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
2845
3093
|
private static readonly PUT_SIZE_LIMIT = 512 * 1024; // 512 KB safety cap
|
|
2846
3094
|
|
|
2847
3095
|
private getPasteRegisterText(): string {
|
|
2848
|
-
if (this.clipboardMirror.hasPendingWrite()) {
|
|
3096
|
+
if (this.preferRegisterForPut || this.clipboardMirror.hasPendingWrite()) {
|
|
2849
3097
|
return this.unnamedRegister;
|
|
2850
3098
|
}
|
|
2851
3099
|
|
|
@@ -2861,12 +3109,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2861
3109
|
const count = this.takeTotalCount(1);
|
|
2862
3110
|
const text = this.getPasteRegisterText();
|
|
2863
3111
|
if (!text) return;
|
|
2864
|
-
const safeCount = Math.min(
|
|
3112
|
+
const safeCount = Math.min(
|
|
3113
|
+
count,
|
|
3114
|
+
Math.max(1, Math.floor(ModalEditor.PUT_SIZE_LIMIT / text.length)),
|
|
3115
|
+
);
|
|
2865
3116
|
|
|
2866
3117
|
if (text.endsWith("\n")) {
|
|
2867
3118
|
const content = text.slice(0, -1);
|
|
2868
3119
|
for (let i = 0; i < safeCount; i++) {
|
|
2869
|
-
// Line-wise: insert new line below and fill it
|
|
2870
3120
|
super.handleInput(CTRL_E);
|
|
2871
3121
|
super.handleInput(NEWLINE);
|
|
2872
3122
|
for (const char of content) {
|
|
@@ -2876,7 +3126,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2876
3126
|
return;
|
|
2877
3127
|
}
|
|
2878
3128
|
|
|
2879
|
-
// Character-wise: insert after cursor
|
|
2880
3129
|
if (!this.isCursorAtOrPastEol()) {
|
|
2881
3130
|
super.handleInput(ESC_RIGHT);
|
|
2882
3131
|
}
|
|
@@ -2891,12 +3140,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2891
3140
|
const count = this.takeTotalCount(1);
|
|
2892
3141
|
const text = this.getPasteRegisterText();
|
|
2893
3142
|
if (!text) return;
|
|
2894
|
-
const safeCount = Math.min(
|
|
3143
|
+
const safeCount = Math.min(
|
|
3144
|
+
count,
|
|
3145
|
+
Math.max(1, Math.floor(ModalEditor.PUT_SIZE_LIMIT / text.length)),
|
|
3146
|
+
);
|
|
2895
3147
|
|
|
2896
3148
|
if (text.endsWith("\n")) {
|
|
2897
3149
|
const content = text.slice(0, -1);
|
|
2898
3150
|
for (let i = 0; i < safeCount; i++) {
|
|
2899
|
-
// Line-wise: insert new line above and fill it
|
|
2900
3151
|
super.handleInput(CTRL_A);
|
|
2901
3152
|
super.handleInput(NEWLINE);
|
|
2902
3153
|
super.handleInput(ESC_UP);
|
|
@@ -2907,7 +3158,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2907
3158
|
return;
|
|
2908
3159
|
}
|
|
2909
3160
|
|
|
2910
|
-
// Character-wise: insert before cursor (just type it)
|
|
2911
3161
|
for (let i = 0; i < safeCount; i++) {
|
|
2912
3162
|
for (const char of text) {
|
|
2913
3163
|
super.handleInput(char === "\n" ? NEWLINE : char);
|
|
@@ -2915,7 +3165,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2915
3165
|
}
|
|
2916
3166
|
}
|
|
2917
3167
|
|
|
2918
|
-
private deleteRange(
|
|
3168
|
+
private deleteRange(
|
|
3169
|
+
col: number,
|
|
3170
|
+
targetCol: number,
|
|
3171
|
+
inclusive: boolean,
|
|
3172
|
+
): void {
|
|
2919
3173
|
const cursor = this.getCursor();
|
|
2920
3174
|
const line = this.getLines()[cursor.line] ?? "";
|
|
2921
3175
|
const lineStartAbs = this.getAbsoluteIndex(cursor.line, 0);
|
|
@@ -2924,7 +3178,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2924
3178
|
let end = Math.min(rawEnd, line.length);
|
|
2925
3179
|
|
|
2926
3180
|
if (inclusive) {
|
|
2927
|
-
const targetRange = this.getGraphemeRangeAtCol(
|
|
3181
|
+
const targetRange = this.getGraphemeRangeAtCol(
|
|
3182
|
+
line,
|
|
3183
|
+
Math.max(col, targetCol),
|
|
3184
|
+
1,
|
|
3185
|
+
);
|
|
2928
3186
|
end = targetRange?.end ?? end;
|
|
2929
3187
|
}
|
|
2930
3188
|
|
|
@@ -2973,7 +3231,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
2973
3231
|
}
|
|
2974
3232
|
|
|
2975
3233
|
private getDesiredCursorShapeSequence(): CursorShapeSequence {
|
|
2976
|
-
return this.mode
|
|
3234
|
+
return "insert" === this.mode && this.pendingExCommand === null
|
|
2977
3235
|
? INSERT_CURSOR_SHAPE
|
|
2978
3236
|
: BLOCK_CURSOR_SHAPE;
|
|
2979
3237
|
}
|
|
@@ -3021,7 +3279,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
3021
3279
|
const last = lines.length - 1;
|
|
3022
3280
|
const lastLine = lines[last];
|
|
3023
3281
|
if (lastLine && visibleWidth(lastLine) >= visibleWidth(rawLabel)) {
|
|
3024
|
-
lines[last] =
|
|
3282
|
+
lines[last] =
|
|
3283
|
+
truncateToWidth(lastLine, width - visibleWidth(rawLabel), "") + label;
|
|
3025
3284
|
} else {
|
|
3026
3285
|
lines[last] = label;
|
|
3027
3286
|
}
|
|
@@ -3029,13 +3288,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
3029
3288
|
}
|
|
3030
3289
|
|
|
3031
3290
|
private getModeLabelColorizer(): ((s: string) => string) | null {
|
|
3032
|
-
|
|
3033
|
-
if (this.pendingExCommand !== null) return this.labelColorizers.ex;
|
|
3034
|
-
return this.mode === "insert" ? this.labelColorizers.insert : this.labelColorizers.normal;
|
|
3291
|
+
return this.labelColorizers?.[this.getActiveMode()] ?? null;
|
|
3035
3292
|
}
|
|
3036
3293
|
|
|
3037
3294
|
private getModeLabel(): string {
|
|
3038
|
-
if (this.mode
|
|
3295
|
+
if ("insert" === this.mode) return " INSERT ";
|
|
3039
3296
|
if (this.pendingExCommand !== null) return ` EX ${this.pendingExCommand}_ `;
|
|
3040
3297
|
|
|
3041
3298
|
const prefixCount = this.prefixCount;
|
|
@@ -3068,21 +3325,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
3068
3325
|
|
|
3069
3326
|
pi.on("session_start", (_event, ctx) => {
|
|
3070
3327
|
const piVimSettings = readPiVimSettings(ctx.cwd);
|
|
3071
|
-
const clipboardMirrorPolicy = resolveClipboardMirrorPolicy(
|
|
3328
|
+
const clipboardMirrorPolicy = resolveClipboardMirrorPolicy(
|
|
3329
|
+
piVimSettings.clipboardMirror,
|
|
3330
|
+
);
|
|
3072
3331
|
if (clipboardMirrorPolicy.warning && ctx.hasUI) {
|
|
3073
3332
|
ctx.ui.notify(clipboardMirrorPolicy.warning, "warning");
|
|
3074
3333
|
}
|
|
3075
3334
|
|
|
3076
3335
|
const t = ctx.ui.theme;
|
|
3336
|
+
const modeColors = resolveModeColors(piVimSettings.modeColors);
|
|
3077
3337
|
const reverseVideo = (s: string) => `\x1b[7m${s}\x1b[27m`;
|
|
3078
|
-
const
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3338
|
+
const labelColorizers = t
|
|
3339
|
+
? buildModeColorizers(t, modeColors, reverseVideo)
|
|
3340
|
+
: null;
|
|
3341
|
+
const borderColorizers =
|
|
3342
|
+
t && piVimSettings.syncBorderColorWithMode === true
|
|
3343
|
+
? buildModeColorizers(t, modeColors)
|
|
3344
|
+
: null;
|
|
3083
3345
|
ctx.ui.setEditorComponent((tui, theme, kb) => {
|
|
3084
3346
|
cursorShapeCleanup = enableCursorShapeSupport(tui);
|
|
3085
|
-
const editor = new ModalEditor(tui, theme, kb,
|
|
3347
|
+
const editor = new ModalEditor(tui, theme, kb, {
|
|
3348
|
+
labelColorizers,
|
|
3349
|
+
borderColorizers,
|
|
3350
|
+
});
|
|
3086
3351
|
editor.setClipboardMirrorPolicy(clipboardMirrorPolicy.policy);
|
|
3087
3352
|
editor.setQuitFn(() => ctx.shutdown());
|
|
3088
3353
|
editor.setNotifyFn((message) => ctx.ui.notify(message, "warning"));
|