pi-vim 0.9.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 +3 -1
- package/clipboard-policy.ts +7 -57
- package/index.ts +537 -277
- 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,15 +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
635
|
private preferRegisterForPut = false;
|
|
558
|
-
private clipboardMirrorPolicy: ClipboardMirrorPolicy =
|
|
559
|
-
|
|
636
|
+
private clipboardMirrorPolicy: ClipboardMirrorPolicy =
|
|
637
|
+
DEFAULT_CLIPBOARD_MIRROR_POLICY;
|
|
638
|
+
private readonly clipboardMirror = new ClipboardMirror(
|
|
639
|
+
writeClipboardInChildProcess,
|
|
640
|
+
);
|
|
560
641
|
private clipboardReadFn: ClipboardReadFn = readClipboardInChildProcess;
|
|
561
642
|
private quitFn: () => void = () => {};
|
|
562
643
|
private notifyFn: (message: string) => void = () => {};
|
|
@@ -565,18 +646,21 @@ export class ModalEditor extends CustomEditor {
|
|
|
565
646
|
tui: CustomEditorConstructorArgs[0],
|
|
566
647
|
theme: CustomEditorConstructorArgs[1],
|
|
567
648
|
kb: CustomEditorConstructorArgs[2],
|
|
568
|
-
|
|
649
|
+
opts?: ModalEditorOptions,
|
|
569
650
|
) {
|
|
570
651
|
super(tui, theme, kb);
|
|
571
652
|
this.cursorShapeRuntime = getCursorShapeRuntime(tui);
|
|
572
|
-
this.labelColorizers = labelColorizers ?? null;
|
|
653
|
+
this.labelColorizers = opts?.labelColorizers ?? null;
|
|
654
|
+
this.borderColorizers = opts?.borderColorizers ?? null;
|
|
655
|
+
this.installModeBorderColorizer();
|
|
573
656
|
}
|
|
574
657
|
|
|
575
|
-
// Test seams
|
|
576
658
|
setClipboardFn(fn: (text: string, signal?: AbortSignal) => unknown): void {
|
|
577
|
-
this.clipboardMirror.setWriteFn(
|
|
578
|
-
|
|
579
|
-
|
|
659
|
+
this.clipboardMirror.setWriteFn(
|
|
660
|
+
async (text: string, signal: AbortSignal) => {
|
|
661
|
+
await fn(text, signal);
|
|
662
|
+
},
|
|
663
|
+
);
|
|
580
664
|
}
|
|
581
665
|
setClipboardWriteTimeoutMs(timeoutMs: number): void {
|
|
582
666
|
this.clipboardMirror.setTimeoutMs(timeoutMs);
|
|
@@ -590,15 +674,51 @@ export class ModalEditor extends CustomEditor {
|
|
|
590
674
|
getClipboardMirrorPolicy(): ClipboardMirrorPolicy {
|
|
591
675
|
return this.clipboardMirrorPolicy;
|
|
592
676
|
}
|
|
593
|
-
setQuitFn(fn: () => void): void {
|
|
594
|
-
|
|
595
|
-
|
|
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
|
+
}
|
|
596
686
|
setRegister(text: string): void {
|
|
597
687
|
this.unnamedRegister = text;
|
|
598
688
|
this.preferRegisterForPut = false;
|
|
599
689
|
}
|
|
600
|
-
getMode(): Mode {
|
|
601
|
-
|
|
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
|
+
}
|
|
602
722
|
|
|
603
723
|
override setText(text: string): void {
|
|
604
724
|
this.clearRedoStack();
|
|
@@ -613,14 +733,20 @@ export class ModalEditor extends CustomEditor {
|
|
|
613
733
|
};
|
|
614
734
|
}
|
|
615
735
|
|
|
616
|
-
private requireRedoRestoreState(
|
|
617
|
-
|
|
618
|
-
|
|
736
|
+
private requireRedoRestoreState(editor: ModalEditorInternals): {
|
|
737
|
+
lines: string[];
|
|
738
|
+
cursorLine?: number;
|
|
739
|
+
cursorCol?: number;
|
|
740
|
+
} {
|
|
619
741
|
const state = editor.state;
|
|
620
742
|
if (!state || !Array.isArray(state.lines)) {
|
|
621
743
|
throw new Error("Redo restore prerequisite: editor state unavailable");
|
|
622
744
|
}
|
|
623
|
-
return state as {
|
|
745
|
+
return state as {
|
|
746
|
+
lines: string[];
|
|
747
|
+
cursorLine?: number;
|
|
748
|
+
cursorCol?: number;
|
|
749
|
+
};
|
|
624
750
|
}
|
|
625
751
|
|
|
626
752
|
private restoreSnapshot(snapshot: EditorSnapshot): void {
|
|
@@ -652,9 +778,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
652
778
|
}
|
|
653
779
|
|
|
654
780
|
private snapshotChanged(a: EditorSnapshot, b: EditorSnapshot): boolean {
|
|
655
|
-
return
|
|
656
|
-
|
|
657
|
-
|
|
781
|
+
return (
|
|
782
|
+
a.text !== b.text ||
|
|
783
|
+
a.cursor.line !== b.cursor.line ||
|
|
784
|
+
a.cursor.col !== b.cursor.col
|
|
785
|
+
);
|
|
658
786
|
}
|
|
659
787
|
|
|
660
788
|
private withTransition<T>(
|
|
@@ -741,9 +869,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
741
869
|
private applySyntheticEdit(mutation: () => void): void {
|
|
742
870
|
const editor = this as unknown as ModalEditorInternals;
|
|
743
871
|
if (!editor.state || !Array.isArray(editor.state.lines)) {
|
|
744
|
-
throw new Error(
|
|
745
|
-
"Synthetic edit prerequisite: editor state unavailable",
|
|
746
|
-
);
|
|
872
|
+
throw new Error("Synthetic edit prerequisite: editor state unavailable");
|
|
747
873
|
}
|
|
748
874
|
|
|
749
875
|
if (typeof editor.pushUndoSnapshot !== "function") {
|
|
@@ -760,9 +886,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
760
886
|
|
|
761
887
|
if (this.getText() === textBefore) return;
|
|
762
888
|
|
|
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
889
|
const postLines = editor.state.lines.slice();
|
|
767
890
|
const postCursorLine = editor.state.cursorLine;
|
|
768
891
|
const postCursorCol = editor.state.cursorCol;
|
|
@@ -790,8 +913,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
790
913
|
}
|
|
791
914
|
|
|
792
915
|
private clearPendingExCommand(): void {
|
|
793
|
-
const shouldDiscardBracketedPasteTail =
|
|
794
|
-
|
|
916
|
+
const shouldDiscardBracketedPasteTail =
|
|
917
|
+
this.acceptingBracketedPasteInExCommand ||
|
|
918
|
+
this.pendingEscWhileAcceptingBracketedPasteInExCommand;
|
|
795
919
|
|
|
796
920
|
this.pendingExCommand = null;
|
|
797
921
|
this.acceptingBracketedPasteInExCommand = false;
|
|
@@ -875,7 +999,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
875
999
|
}
|
|
876
1000
|
}
|
|
877
1001
|
|
|
878
|
-
private stripBracketedPasteInNormalMode(data: string): {
|
|
1002
|
+
private stripBracketedPasteInNormalMode(data: string): {
|
|
1003
|
+
filtered: string | null;
|
|
1004
|
+
stripped: boolean;
|
|
1005
|
+
} {
|
|
879
1006
|
let chunk = data;
|
|
880
1007
|
let stripped = false;
|
|
881
1008
|
|
|
@@ -898,14 +1025,18 @@ export class ModalEditor extends CustomEditor {
|
|
|
898
1025
|
}
|
|
899
1026
|
|
|
900
1027
|
stripped = true;
|
|
901
|
-
const end = chunk.indexOf(
|
|
1028
|
+
const end = chunk.indexOf(
|
|
1029
|
+
BRACKETED_PASTE_END,
|
|
1030
|
+
start + BRACKETED_PASTE_START.length,
|
|
1031
|
+
);
|
|
902
1032
|
if (end === -1) {
|
|
903
1033
|
this.discardingBracketedPasteInNormalMode = true;
|
|
904
1034
|
const leading = chunk.slice(0, start);
|
|
905
1035
|
return { filtered: leading.length > 0 ? leading : null, stripped };
|
|
906
1036
|
}
|
|
907
1037
|
|
|
908
|
-
chunk =
|
|
1038
|
+
chunk =
|
|
1039
|
+
chunk.slice(0, start) + chunk.slice(end + BRACKETED_PASTE_END.length);
|
|
909
1040
|
if (!chunk) return { filtered: null, stripped };
|
|
910
1041
|
}
|
|
911
1042
|
}
|
|
@@ -958,24 +1089,19 @@ export class ModalEditor extends CustomEditor {
|
|
|
958
1089
|
return;
|
|
959
1090
|
}
|
|
960
1091
|
|
|
961
|
-
if (this.mode
|
|
962
|
-
// Shift+Alt+A: go to end of line (like Esc -> A but stay in insert)
|
|
1092
|
+
if ("insert" === this.mode) {
|
|
963
1093
|
if (matchesKey(data, Key.shiftAlt("a")) || data === "\x1bA") {
|
|
964
1094
|
super.handleInput(CTRL_E);
|
|
965
1095
|
return;
|
|
966
1096
|
}
|
|
967
|
-
// Shift+Alt+I: go to start of line (like Esc -> I but stay in insert)
|
|
968
1097
|
if (matchesKey(data, Key.shiftAlt("i")) || data === "\x1bI") {
|
|
969
1098
|
super.handleInput(CTRL_A);
|
|
970
1099
|
return;
|
|
971
1100
|
}
|
|
972
|
-
// Alt+o: open new line below (stay in insert mode)
|
|
973
1101
|
if (matchesKey(data, Key.alt("o")) || data === "\x1bo") {
|
|
974
1102
|
this.openLineBelow();
|
|
975
1103
|
return;
|
|
976
1104
|
}
|
|
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
1105
|
if (matchesKey(data, Key.shiftAlt("o")) || data === "\x1bO") {
|
|
980
1106
|
this.openLineAbove();
|
|
981
1107
|
return;
|
|
@@ -1003,9 +1129,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
1003
1129
|
const replacement = data.repeat(count);
|
|
1004
1130
|
const lineStartAbs = this.getAbsoluteIndex(cursor.line, 0);
|
|
1005
1131
|
const text = this.getText();
|
|
1006
|
-
const newText =
|
|
1007
|
-
|
|
1008
|
-
|
|
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);
|
|
1009
1140
|
this.replaceTextInBuffer(newText, newCursorAbs);
|
|
1010
1141
|
return;
|
|
1011
1142
|
}
|
|
@@ -1068,32 +1199,42 @@ export class ModalEditor extends CustomEditor {
|
|
|
1068
1199
|
}
|
|
1069
1200
|
|
|
1070
1201
|
if (
|
|
1071
|
-
this.pendingMotion
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1202
|
+
this.pendingMotion ||
|
|
1203
|
+
this.pendingTextObject ||
|
|
1204
|
+
this.pendingOperator ||
|
|
1205
|
+
this.prefixCount ||
|
|
1206
|
+
this.operatorCount ||
|
|
1207
|
+
this.pendingG ||
|
|
1208
|
+
this.pendingGCount ||
|
|
1209
|
+
this.pendingReplace
|
|
1079
1210
|
) {
|
|
1080
1211
|
this.clearPendingState();
|
|
1081
1212
|
return;
|
|
1082
1213
|
}
|
|
1083
|
-
if (this.mode
|
|
1214
|
+
if ("insert" === this.mode) {
|
|
1084
1215
|
this.clearUnderlyingPasteStateIfActive();
|
|
1085
|
-
this.
|
|
1216
|
+
this.setMode("normal");
|
|
1086
1217
|
} else {
|
|
1087
1218
|
super.handleInput("\x1b"); // pass escape to abort agent
|
|
1088
1219
|
}
|
|
1089
1220
|
}
|
|
1090
1221
|
|
|
1091
1222
|
private isEnterLikeInput(data: string): boolean {
|
|
1092
|
-
return
|
|
1223
|
+
return (
|
|
1224
|
+
data === "\r" ||
|
|
1225
|
+
data === "\n" ||
|
|
1226
|
+
matchesKey(data, "enter") ||
|
|
1227
|
+
matchesKey(data, "return")
|
|
1228
|
+
);
|
|
1093
1229
|
}
|
|
1094
1230
|
|
|
1095
1231
|
private isBackspaceLikeInput(data: string): boolean {
|
|
1096
|
-
return
|
|
1232
|
+
return (
|
|
1233
|
+
data === "\x7f" ||
|
|
1234
|
+
data === "\x08" ||
|
|
1235
|
+
matchesKey(data, "backspace") ||
|
|
1236
|
+
matchesKey(data, "ctrl+h")
|
|
1237
|
+
);
|
|
1097
1238
|
}
|
|
1098
1239
|
|
|
1099
1240
|
private deleteLastPendingExCommandGrapheme(): void {
|
|
@@ -1116,10 +1257,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1116
1257
|
|
|
1117
1258
|
private handlePendingExCommandControlChunk(data: string): boolean {
|
|
1118
1259
|
if (
|
|
1119
|
-
!data.includes("\r")
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1260
|
+
!data.includes("\r") &&
|
|
1261
|
+
!data.includes("\n") &&
|
|
1262
|
+
!data.includes("\x7f") &&
|
|
1263
|
+
!data.includes("\x08")
|
|
1123
1264
|
) {
|
|
1124
1265
|
return false;
|
|
1125
1266
|
}
|
|
@@ -1216,7 +1357,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
1216
1357
|
if (data.length === 0) return false;
|
|
1217
1358
|
for (const char of data) {
|
|
1218
1359
|
const codePoint = char.codePointAt(0);
|
|
1219
|
-
if (codePoint === undefined || codePoint < 32 || codePoint === 127)
|
|
1360
|
+
if (codePoint === undefined || codePoint < 32 || codePoint === 127)
|
|
1361
|
+
return false;
|
|
1220
1362
|
}
|
|
1221
1363
|
return true;
|
|
1222
1364
|
}
|
|
@@ -1253,9 +1395,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1253
1395
|
|
|
1254
1396
|
if (prefix === null && operator === null) return defaultValue;
|
|
1255
1397
|
|
|
1256
|
-
const total =
|
|
1257
|
-
|
|
1258
|
-
|
|
1398
|
+
const total =
|
|
1399
|
+
prefix !== null && operator !== null
|
|
1400
|
+
? prefix * operator
|
|
1401
|
+
: (prefix ?? operator ?? defaultValue);
|
|
1259
1402
|
|
|
1260
1403
|
if (!Number.isFinite(total) || total <= 0) return defaultValue;
|
|
1261
1404
|
return Math.min(MAX_COUNT, total);
|
|
@@ -1290,7 +1433,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1290
1433
|
} else if (this.pendingOperator === "c") {
|
|
1291
1434
|
this.deleteWithCharMotion(pendingMotion, data);
|
|
1292
1435
|
this.pendingOperator = null;
|
|
1293
|
-
this.
|
|
1436
|
+
this.setMode();
|
|
1294
1437
|
} else if (this.pendingOperator === "y") {
|
|
1295
1438
|
this.yankWithCharMotion(pendingMotion, data);
|
|
1296
1439
|
this.pendingOperator = null;
|
|
@@ -1319,7 +1462,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1319
1462
|
if (data === "w" || data === "W") {
|
|
1320
1463
|
const semanticClass: WordTextObjectClass = data === "W" ? "WORD" : "word";
|
|
1321
1464
|
const count = this.takeTotalCount(1);
|
|
1322
|
-
const range = this.getWordObjectRange(
|
|
1465
|
+
const range = this.getWordObjectRange(
|
|
1466
|
+
pendingTextObject,
|
|
1467
|
+
count,
|
|
1468
|
+
semanticClass,
|
|
1469
|
+
);
|
|
1323
1470
|
if (!range || !this.pendingOperator) {
|
|
1324
1471
|
this.pendingOperator = null;
|
|
1325
1472
|
return;
|
|
@@ -1357,7 +1504,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1357
1504
|
if (range.endAbs === range.startAbs) {
|
|
1358
1505
|
if (pendingOperator === "c") {
|
|
1359
1506
|
this.moveCursorToAbsoluteIndex(range.startAbs);
|
|
1360
|
-
this.
|
|
1507
|
+
this.setMode();
|
|
1361
1508
|
}
|
|
1362
1509
|
return;
|
|
1363
1510
|
}
|
|
@@ -1369,7 +1516,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1369
1516
|
|
|
1370
1517
|
if (pendingOperator === "c") {
|
|
1371
1518
|
this.deleteRangeByAbsolute(range.startAbs, range.endAbs);
|
|
1372
|
-
this.
|
|
1519
|
+
this.setMode();
|
|
1373
1520
|
return;
|
|
1374
1521
|
}
|
|
1375
1522
|
|
|
@@ -1399,7 +1546,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
1399
1546
|
}
|
|
1400
1547
|
|
|
1401
1548
|
if (data === "j" || data === "k") {
|
|
1402
|
-
const hasDualCount =
|
|
1549
|
+
const hasDualCount =
|
|
1550
|
+
this.prefixCount.length > 0 && this.operatorCount.length > 0;
|
|
1403
1551
|
const count = this.takeTotalCount(1);
|
|
1404
1552
|
const delta = hasDualCount ? Math.max(0, count - 1) : count;
|
|
1405
1553
|
this.deleteLinewiseByDelta(data === "j" ? delta : -delta);
|
|
@@ -1430,20 +1578,18 @@ export class ModalEditor extends CustomEditor {
|
|
|
1430
1578
|
return;
|
|
1431
1579
|
}
|
|
1432
1580
|
|
|
1433
|
-
const hasCount =
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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";
|
|
1442
1590
|
const supportsCountedTextObject = data === "i" || data === "a";
|
|
1443
1591
|
|
|
1444
1592
|
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
1593
|
this.cancelPendingOperator(data);
|
|
1448
1594
|
return;
|
|
1449
1595
|
}
|
|
@@ -1459,7 +1605,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
1459
1605
|
return;
|
|
1460
1606
|
}
|
|
1461
1607
|
|
|
1462
|
-
// Invalid motion: cancel operator to avoid sticky surprising deletes.
|
|
1463
1608
|
this.cancelPendingOperator(data);
|
|
1464
1609
|
}
|
|
1465
1610
|
|
|
@@ -1484,7 +1629,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1484
1629
|
|
|
1485
1630
|
this.cutLine();
|
|
1486
1631
|
this.pendingOperator = null;
|
|
1487
|
-
this.
|
|
1632
|
+
this.setMode();
|
|
1488
1633
|
return;
|
|
1489
1634
|
}
|
|
1490
1635
|
|
|
@@ -1505,7 +1650,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
1505
1650
|
this.replaceTextInBuffer(newText, cursorAbs);
|
|
1506
1651
|
}
|
|
1507
1652
|
this.pendingOperator = null;
|
|
1508
|
-
this.
|
|
1653
|
+
this.setMode();
|
|
1509
1654
|
return;
|
|
1510
1655
|
}
|
|
1511
1656
|
|
|
@@ -1514,15 +1659,15 @@ export class ModalEditor extends CustomEditor {
|
|
|
1514
1659
|
return;
|
|
1515
1660
|
}
|
|
1516
1661
|
|
|
1517
|
-
const hasCount =
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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";
|
|
1526
1671
|
const supportsCountedTextObject = data === "i" || data === "a";
|
|
1527
1672
|
|
|
1528
1673
|
if (hasCount && !supportsCountedWordMotion && !supportsCountedTextObject) {
|
|
@@ -1536,16 +1681,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
1536
1681
|
}
|
|
1537
1682
|
|
|
1538
1683
|
const motionCount = supportsCountedWordMotion ? this.takeTotalCount(1) : 1;
|
|
1539
|
-
const effectiveMotion =
|
|
1540
|
-
? "E"
|
|
1541
|
-
: data;
|
|
1684
|
+
const effectiveMotion =
|
|
1685
|
+
data === "W" && this.isCursorOnNonWhitespace() ? "E" : data;
|
|
1542
1686
|
if (this.deleteWithMotion(effectiveMotion, motionCount)) {
|
|
1543
1687
|
this.pendingOperator = null;
|
|
1544
|
-
this.
|
|
1688
|
+
this.setMode();
|
|
1545
1689
|
return;
|
|
1546
1690
|
}
|
|
1547
1691
|
|
|
1548
|
-
// Invalid motion: cancel operator to avoid sticky surprising changes.
|
|
1549
1692
|
this.cancelPendingOperator(data);
|
|
1550
1693
|
}
|
|
1551
1694
|
|
|
@@ -1605,43 +1748,34 @@ export class ModalEditor extends CustomEditor {
|
|
|
1605
1748
|
return;
|
|
1606
1749
|
}
|
|
1607
1750
|
|
|
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
|
-
);
|
|
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";
|
|
1638
1776
|
const supportsCountedParagraphMotion = data === "{" || data === "}";
|
|
1639
|
-
const supportsCountedNav =
|
|
1640
|
-
data === "h"
|
|
1641
|
-
|| data === "j"
|
|
1642
|
-
|| data === "k"
|
|
1643
|
-
|| data === "l"
|
|
1644
|
-
);
|
|
1777
|
+
const supportsCountedNav =
|
|
1778
|
+
data === "h" || data === "j" || data === "k" || data === "l";
|
|
1645
1779
|
const supportsCountedUnderscore = data === "_";
|
|
1646
1780
|
|
|
1647
1781
|
if (supportsCountedNav) {
|
|
@@ -1664,13 +1798,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
1664
1798
|
}
|
|
1665
1799
|
|
|
1666
1800
|
if (
|
|
1667
|
-
!supportsCountedStandaloneEdit
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1801
|
+
!supportsCountedStandaloneEdit &&
|
|
1802
|
+
!supportsCountedCharMotion &&
|
|
1803
|
+
!supportsCountedWordMotion &&
|
|
1804
|
+
!supportsCountedParagraphMotion &&
|
|
1805
|
+
!supportsCountedUnderscore
|
|
1672
1806
|
) {
|
|
1673
|
-
// Unsupported prefixed forms: drop count and keep processing this key.
|
|
1674
1807
|
this.prefixCount = "";
|
|
1675
1808
|
this.operatorCount = "";
|
|
1676
1809
|
}
|
|
@@ -1742,7 +1875,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1742
1875
|
}
|
|
1743
1876
|
|
|
1744
1877
|
if (data === ";" && this.lastCharMotion) {
|
|
1745
|
-
this.executeCharMotion(
|
|
1878
|
+
this.executeCharMotion(
|
|
1879
|
+
this.lastCharMotion.motion,
|
|
1880
|
+
this.lastCharMotion.char,
|
|
1881
|
+
false,
|
|
1882
|
+
);
|
|
1746
1883
|
return;
|
|
1747
1884
|
}
|
|
1748
1885
|
if (data === "," && this.lastCharMotion) {
|
|
@@ -1754,7 +1891,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1754
1891
|
return;
|
|
1755
1892
|
}
|
|
1756
1893
|
|
|
1757
|
-
if (
|
|
1894
|
+
if (
|
|
1895
|
+
data === "u" ||
|
|
1896
|
+
data === CTRL_UNDERSCORE ||
|
|
1897
|
+
matchesKey(data, "ctrl+_")
|
|
1898
|
+
) {
|
|
1758
1899
|
this.performUndo();
|
|
1759
1900
|
return;
|
|
1760
1901
|
}
|
|
@@ -1814,7 +1955,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
1814
1955
|
return;
|
|
1815
1956
|
}
|
|
1816
1957
|
|
|
1817
|
-
// Pass control sequences (ctrl+c, etc.) to super, ignore printable chars
|
|
1818
1958
|
if (this.isPrintableChunk(data)) return;
|
|
1819
1959
|
super.handleInput(data);
|
|
1820
1960
|
}
|
|
@@ -1834,29 +1974,29 @@ export class ModalEditor extends CustomEditor {
|
|
|
1834
1974
|
const seq = NORMAL_KEYS[key];
|
|
1835
1975
|
switch (key) {
|
|
1836
1976
|
case "i":
|
|
1837
|
-
this.
|
|
1977
|
+
this.setMode();
|
|
1838
1978
|
break;
|
|
1839
1979
|
case "a":
|
|
1840
|
-
this.
|
|
1980
|
+
this.setMode();
|
|
1841
1981
|
if (!this.isCursorAtOrPastEol()) {
|
|
1842
1982
|
super.handleInput(ESC_RIGHT);
|
|
1843
1983
|
}
|
|
1844
1984
|
break;
|
|
1845
1985
|
case "A":
|
|
1846
|
-
this.
|
|
1986
|
+
this.setMode();
|
|
1847
1987
|
super.handleInput(CTRL_E);
|
|
1848
1988
|
break;
|
|
1849
1989
|
case "I":
|
|
1850
|
-
this.
|
|
1990
|
+
this.setMode();
|
|
1851
1991
|
this.moveCursorToFirstNonWhitespace();
|
|
1852
1992
|
break;
|
|
1853
1993
|
case "o":
|
|
1854
1994
|
this.openLineBelow();
|
|
1855
|
-
this.
|
|
1995
|
+
this.setMode();
|
|
1856
1996
|
break;
|
|
1857
1997
|
case "O":
|
|
1858
1998
|
this.openLineAbove();
|
|
1859
|
-
this.
|
|
1999
|
+
this.setMode();
|
|
1860
2000
|
break;
|
|
1861
2001
|
case "D":
|
|
1862
2002
|
this.takeTotalCount(1);
|
|
@@ -1865,16 +2005,16 @@ export class ModalEditor extends CustomEditor {
|
|
|
1865
2005
|
case "C":
|
|
1866
2006
|
this.takeTotalCount(1);
|
|
1867
2007
|
this.cutToEndOfLine();
|
|
1868
|
-
this.
|
|
2008
|
+
this.setMode();
|
|
1869
2009
|
break;
|
|
1870
2010
|
case "S":
|
|
1871
2011
|
this.takeTotalCount(1);
|
|
1872
2012
|
this.cutCurrentLineContent();
|
|
1873
|
-
this.
|
|
2013
|
+
this.setMode();
|
|
1874
2014
|
break;
|
|
1875
2015
|
case "s":
|
|
1876
2016
|
this.cutCharUnderCursor();
|
|
1877
|
-
this.
|
|
2017
|
+
this.setMode();
|
|
1878
2018
|
break;
|
|
1879
2019
|
case "x":
|
|
1880
2020
|
this.cutCharUnderCursor();
|
|
@@ -1890,11 +2030,22 @@ export class ModalEditor extends CustomEditor {
|
|
|
1890
2030
|
}
|
|
1891
2031
|
}
|
|
1892
2032
|
|
|
1893
|
-
private executeCharMotion(
|
|
2033
|
+
private executeCharMotion(
|
|
2034
|
+
motion: CharMotion,
|
|
2035
|
+
targetChar: string,
|
|
2036
|
+
saveMotion: boolean = true,
|
|
2037
|
+
): void {
|
|
1894
2038
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
1895
2039
|
const col = this.getCursor().col;
|
|
1896
2040
|
const count = this.takeTotalCount(1);
|
|
1897
|
-
const targetCol = findCharMotionTarget(
|
|
2041
|
+
const targetCol = findCharMotionTarget(
|
|
2042
|
+
line,
|
|
2043
|
+
col,
|
|
2044
|
+
motion,
|
|
2045
|
+
targetChar,
|
|
2046
|
+
!saveMotion,
|
|
2047
|
+
count,
|
|
2048
|
+
);
|
|
1898
2049
|
|
|
1899
2050
|
if (targetCol !== null && saveMotion) {
|
|
1900
2051
|
this.lastCharMotion = { motion, char: targetChar };
|
|
@@ -1909,7 +2060,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
1909
2060
|
const lines = this.getLines();
|
|
1910
2061
|
const fromLine = this.getCursor().line;
|
|
1911
2062
|
const count = this.takeTotalCount(1);
|
|
1912
|
-
const targetLine = findParagraphMotionTarget(
|
|
2063
|
+
const targetLine = findParagraphMotionTarget(
|
|
2064
|
+
lines,
|
|
2065
|
+
fromLine,
|
|
2066
|
+
direction,
|
|
2067
|
+
count,
|
|
2068
|
+
);
|
|
1913
2069
|
this.moveCursorToLineStart(targetLine);
|
|
1914
2070
|
}
|
|
1915
2071
|
|
|
@@ -1924,7 +2080,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
1924
2080
|
|
|
1925
2081
|
const state = editor.state;
|
|
1926
2082
|
if (!state || !Array.isArray(state.lines)) return false;
|
|
1927
|
-
if (
|
|
2083
|
+
if (
|
|
2084
|
+
!Number.isInteger(state.cursorLine) ||
|
|
2085
|
+
!Number.isInteger(state.cursorCol)
|
|
2086
|
+
)
|
|
2087
|
+
return false;
|
|
1928
2088
|
|
|
1929
2089
|
const cursorLine = state.cursorLine as number;
|
|
1930
2090
|
const cursorCol = state.cursorCol as number;
|
|
@@ -1933,8 +2093,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
1933
2093
|
|
|
1934
2094
|
const target = cursorCol + delta;
|
|
1935
2095
|
|
|
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
2096
|
if (target < 0 || target > line.length) return false;
|
|
1939
2097
|
|
|
1940
2098
|
state.cursorCol = target;
|
|
@@ -1974,7 +2132,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
1974
2132
|
}
|
|
1975
2133
|
|
|
1976
2134
|
const currentLine = state.cursorLine ?? 0;
|
|
1977
|
-
const targetLine = Math.max(
|
|
2135
|
+
const targetLine = Math.max(
|
|
2136
|
+
0,
|
|
2137
|
+
Math.min(currentLine + delta, state.lines.length - 1),
|
|
2138
|
+
);
|
|
1978
2139
|
if (targetLine === currentLine) return;
|
|
1979
2140
|
|
|
1980
2141
|
const preferredCol = editor.preferredVisualCol ?? state.cursorCol ?? 0;
|
|
@@ -2078,9 +2239,12 @@ export class ModalEditor extends CustomEditor {
|
|
|
2078
2239
|
if (normalize) {
|
|
2079
2240
|
const trimmedRight = right.trimStart();
|
|
2080
2241
|
const leftLastChar = left[left.length - 1];
|
|
2081
|
-
const leftEndsWithSpace =
|
|
2242
|
+
const leftEndsWithSpace =
|
|
2243
|
+
leftLastChar !== undefined && /\s/.test(leftLastChar);
|
|
2082
2244
|
const needsSeparator = !leftEndsWithSpace && trimmedRight.length > 0;
|
|
2083
|
-
joined = needsSeparator
|
|
2245
|
+
joined = needsSeparator
|
|
2246
|
+
? `${left} ${trimmedRight}`
|
|
2247
|
+
: left + trimmedRight;
|
|
2084
2248
|
joinPoint = left.length;
|
|
2085
2249
|
} else {
|
|
2086
2250
|
joined = left + right;
|
|
@@ -2174,25 +2338,43 @@ export class ModalEditor extends CustomEditor {
|
|
|
2174
2338
|
} else if (target === "start") {
|
|
2175
2339
|
const startType = this.charType(text[next], semanticClass);
|
|
2176
2340
|
if (startType !== "space") {
|
|
2177
|
-
while (
|
|
2341
|
+
while (
|
|
2342
|
+
next < len &&
|
|
2343
|
+
this.charType(text[next], semanticClass) === startType
|
|
2344
|
+
)
|
|
2345
|
+
next++;
|
|
2178
2346
|
}
|
|
2179
|
-
while (
|
|
2347
|
+
while (
|
|
2348
|
+
next < len &&
|
|
2349
|
+
this.charType(text[next], semanticClass) === "space"
|
|
2350
|
+
)
|
|
2351
|
+
next++;
|
|
2180
2352
|
} else {
|
|
2181
2353
|
if (next < len - 1) next++;
|
|
2182
|
-
while (
|
|
2354
|
+
while (
|
|
2355
|
+
next < len &&
|
|
2356
|
+
this.charType(text[next], semanticClass) === "space"
|
|
2357
|
+
)
|
|
2358
|
+
next++;
|
|
2183
2359
|
if (next >= len) {
|
|
2184
2360
|
next = len;
|
|
2185
2361
|
} else {
|
|
2186
2362
|
const t = this.charType(text[next], semanticClass);
|
|
2187
|
-
while (
|
|
2363
|
+
while (
|
|
2364
|
+
next < len - 1 &&
|
|
2365
|
+
this.charType(text[next + 1], semanticClass) === t
|
|
2366
|
+
)
|
|
2367
|
+
next++;
|
|
2188
2368
|
}
|
|
2189
2369
|
}
|
|
2190
2370
|
} else {
|
|
2191
2371
|
if (next >= len) next = len - 1;
|
|
2192
2372
|
if (next > 0) next--;
|
|
2193
|
-
while (next > 0 && this.charType(text[next], semanticClass) === "space")
|
|
2373
|
+
while (next > 0 && this.charType(text[next], semanticClass) === "space")
|
|
2374
|
+
next--;
|
|
2194
2375
|
const t = this.charType(text[next], semanticClass);
|
|
2195
|
-
while (next > 0 && this.charType(text[next - 1], semanticClass) === t)
|
|
2376
|
+
while (next > 0 && this.charType(text[next - 1], semanticClass) === t)
|
|
2377
|
+
next--;
|
|
2196
2378
|
}
|
|
2197
2379
|
|
|
2198
2380
|
if (next === i) break;
|
|
@@ -2281,7 +2463,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2281
2463
|
semanticClass: WordMotionClass = "word",
|
|
2282
2464
|
): boolean {
|
|
2283
2465
|
const col = this.getCursor().col;
|
|
2284
|
-
const targetCol = this.tryFindWordTargetLineLocal(
|
|
2466
|
+
const targetCol = this.tryFindWordTargetLineLocal(
|
|
2467
|
+
direction,
|
|
2468
|
+
target,
|
|
2469
|
+
semanticClass,
|
|
2470
|
+
);
|
|
2285
2471
|
if (targetCol === null || targetCol === col) return false;
|
|
2286
2472
|
|
|
2287
2473
|
this.moveCursorToCol(targetCol);
|
|
@@ -2297,7 +2483,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2297
2483
|
const lineIndex = cursor.line;
|
|
2298
2484
|
const col = cursor.col;
|
|
2299
2485
|
const lineSnapshot = this.getLines()[lineIndex] ?? "";
|
|
2300
|
-
const direction: WordMotionDirection =
|
|
2486
|
+
const direction: WordMotionDirection =
|
|
2487
|
+
motion === "b" ? "backward" : "forward";
|
|
2301
2488
|
const target: WordMotionTarget = motion === "e" ? "end" : "start";
|
|
2302
2489
|
const steps = Math.max(1, Math.min(MAX_COUNT, count));
|
|
2303
2490
|
|
|
@@ -2364,7 +2551,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2364
2551
|
return true;
|
|
2365
2552
|
}
|
|
2366
2553
|
|
|
2367
|
-
private writeToRegister(
|
|
2554
|
+
private writeToRegister(
|
|
2555
|
+
text: string,
|
|
2556
|
+
source: RegisterWriteSource = "mutation",
|
|
2557
|
+
): void {
|
|
2368
2558
|
this.unnamedRegister = text;
|
|
2369
2559
|
const shouldMirror = text !== "" && this.shouldMirrorRegisterWrite(source);
|
|
2370
2560
|
this.preferRegisterForPut = text !== "" && !shouldMirror;
|
|
@@ -2380,7 +2570,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
2380
2570
|
}
|
|
2381
2571
|
|
|
2382
2572
|
private hasMultiCodeUnitGraphemes(line: string): boolean {
|
|
2383
|
-
return getLineGraphemes(line).some(
|
|
2573
|
+
return getLineGraphemes(line).some(
|
|
2574
|
+
(segment) => segment.end - segment.start > 1,
|
|
2575
|
+
);
|
|
2384
2576
|
}
|
|
2385
2577
|
|
|
2386
2578
|
private getGraphemeRangeAtCol(
|
|
@@ -2391,7 +2583,9 @@ export class ModalEditor extends CustomEditor {
|
|
|
2391
2583
|
): { start: number; end: number } | null {
|
|
2392
2584
|
const clampedCol = Math.max(0, Math.min(col, line.length));
|
|
2393
2585
|
const segments = getLineGraphemes(line);
|
|
2394
|
-
const startIndex = segments.findIndex(
|
|
2586
|
+
const startIndex = segments.findIndex(
|
|
2587
|
+
(segment) => clampedCol < segment.end,
|
|
2588
|
+
);
|
|
2395
2589
|
if (startIndex === -1) return null;
|
|
2396
2590
|
|
|
2397
2591
|
let endIndex = startIndex + Math.max(1, count) - 1;
|
|
@@ -2432,7 +2626,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2432
2626
|
const text = this.getText();
|
|
2433
2627
|
this.writeToRegister(line.slice(range.start, range.end));
|
|
2434
2628
|
this.replaceTextInBuffer(
|
|
2435
|
-
text.slice(0, lineStartAbs + range.start) +
|
|
2629
|
+
text.slice(0, lineStartAbs + range.start) +
|
|
2630
|
+
text.slice(lineStartAbs + range.end),
|
|
2436
2631
|
lineStartAbs + range.start,
|
|
2437
2632
|
);
|
|
2438
2633
|
}
|
|
@@ -2443,7 +2638,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2443
2638
|
const { line, col } = this.getCurrentLineAndCol();
|
|
2444
2639
|
|
|
2445
2640
|
const hasNextLine = cursorLine < lines.length - 1;
|
|
2446
|
-
const deleted =
|
|
2641
|
+
const deleted =
|
|
2642
|
+
col < line.length ? line.slice(col) : hasNextLine ? "\n" : "";
|
|
2447
2643
|
|
|
2448
2644
|
this.writeToRegister(deleted);
|
|
2449
2645
|
super.handleInput(CTRL_K);
|
|
@@ -2466,7 +2662,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2466
2662
|
this.cutCurrentLineContent();
|
|
2467
2663
|
}
|
|
2468
2664
|
|
|
2469
|
-
private getNormalizedLineRange(
|
|
2665
|
+
private getNormalizedLineRange(
|
|
2666
|
+
startLine: number,
|
|
2667
|
+
endLine: number,
|
|
2668
|
+
): { start: number; end: number } {
|
|
2470
2669
|
const lines = this.getLines();
|
|
2471
2670
|
const last = Math.max(0, lines.length - 1);
|
|
2472
2671
|
const clampedStart = Math.max(0, Math.min(startLine, last));
|
|
@@ -2483,7 +2682,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2483
2682
|
return `${lines.slice(start, end + 1).join("\n")}\n`;
|
|
2484
2683
|
}
|
|
2485
2684
|
|
|
2486
|
-
private getLineDeleteAbsoluteRange(
|
|
2685
|
+
private getLineDeleteAbsoluteRange(
|
|
2686
|
+
startLine: number,
|
|
2687
|
+
endLine: number,
|
|
2688
|
+
): { startAbs: number; endAbs: number } {
|
|
2487
2689
|
const lines = this.getLines();
|
|
2488
2690
|
const text = this.getText();
|
|
2489
2691
|
const { start, end } = this.getNormalizedLineRange(startLine, endLine);
|
|
@@ -2510,7 +2712,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2510
2712
|
if (lines.length === 0) return;
|
|
2511
2713
|
|
|
2512
2714
|
const payload = this.getLinewisePayload(startLine, endLine);
|
|
2513
|
-
const { startAbs, endAbs } = this.getLineDeleteAbsoluteRange(
|
|
2715
|
+
const { startAbs, endAbs } = this.getLineDeleteAbsoluteRange(
|
|
2716
|
+
startLine,
|
|
2717
|
+
endLine,
|
|
2718
|
+
);
|
|
2514
2719
|
|
|
2515
2720
|
this.writeToRegister(payload);
|
|
2516
2721
|
|
|
@@ -2519,7 +2724,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2519
2724
|
const newText = text.slice(0, startAbs) + text.slice(endAbs);
|
|
2520
2725
|
this.replaceTextInBuffer(newText, startAbs);
|
|
2521
2726
|
|
|
2522
|
-
// Ensure cursor is at column 0 of the landing line
|
|
2523
2727
|
super.handleInput(CTRL_A);
|
|
2524
2728
|
}
|
|
2525
2729
|
}
|
|
@@ -2552,7 +2756,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2552
2756
|
const col = cursor.col;
|
|
2553
2757
|
|
|
2554
2758
|
if (motion === "$") {
|
|
2555
|
-
// Match D/C behavior exactly, including newline kill at EOL.
|
|
2556
2759
|
this.cutToEndOfLine();
|
|
2557
2760
|
return true;
|
|
2558
2761
|
}
|
|
@@ -2563,7 +2766,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2563
2766
|
}
|
|
2564
2767
|
|
|
2565
2768
|
if (motion === "^") {
|
|
2566
|
-
this.deleteRange(
|
|
2769
|
+
this.deleteRange(
|
|
2770
|
+
col,
|
|
2771
|
+
findFirstNonWhitespaceColumn(this.getLines()[cursor.line] ?? ""),
|
|
2772
|
+
false,
|
|
2773
|
+
);
|
|
2567
2774
|
return true;
|
|
2568
2775
|
}
|
|
2569
2776
|
|
|
@@ -2593,7 +2800,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2593
2800
|
count,
|
|
2594
2801
|
wordMotion.semanticClass,
|
|
2595
2802
|
);
|
|
2596
|
-
this.deleteRangeByAbsolute(
|
|
2803
|
+
this.deleteRangeByAbsolute(
|
|
2804
|
+
currentAbs,
|
|
2805
|
+
targetAbs,
|
|
2806
|
+
wordMotion.motion === "e",
|
|
2807
|
+
);
|
|
2597
2808
|
return true;
|
|
2598
2809
|
}
|
|
2599
2810
|
|
|
@@ -2604,7 +2815,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2604
2815
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
2605
2816
|
const col = this.getCursor().col;
|
|
2606
2817
|
const count = this.takeTotalCount(1);
|
|
2607
|
-
const targetCol = findCharMotionTarget(
|
|
2818
|
+
const targetCol = findCharMotionTarget(
|
|
2819
|
+
line,
|
|
2820
|
+
col,
|
|
2821
|
+
motion,
|
|
2822
|
+
targetChar,
|
|
2823
|
+
false,
|
|
2824
|
+
count,
|
|
2825
|
+
);
|
|
2608
2826
|
|
|
2609
2827
|
if (targetCol === null) return;
|
|
2610
2828
|
|
|
@@ -2633,7 +2851,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
2633
2851
|
}
|
|
2634
2852
|
|
|
2635
2853
|
if (data === "j" || data === "k") {
|
|
2636
|
-
const hasDualCount =
|
|
2854
|
+
const hasDualCount =
|
|
2855
|
+
this.prefixCount.length > 0 && this.operatorCount.length > 0;
|
|
2637
2856
|
const count = this.takeTotalCount(1);
|
|
2638
2857
|
const delta = hasDualCount ? Math.max(0, count - 1) : count;
|
|
2639
2858
|
this.yankLinewiseByDelta(data === "j" ? delta : -delta);
|
|
@@ -2670,7 +2889,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2670
2889
|
}
|
|
2671
2890
|
|
|
2672
2891
|
if (this.hasPendingCount()) {
|
|
2673
|
-
// Counted forms beyond yy, y{count}j/k, and y{count}{f/F/t/T} are out of scope.
|
|
2674
2892
|
this.cancelPendingOperator(data);
|
|
2675
2893
|
return;
|
|
2676
2894
|
}
|
|
@@ -2728,7 +2946,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2728
2946
|
1,
|
|
2729
2947
|
wordMotion.semanticClass,
|
|
2730
2948
|
);
|
|
2731
|
-
this.yankRangeByAbsolute(
|
|
2949
|
+
this.yankRangeByAbsolute(
|
|
2950
|
+
currentAbs,
|
|
2951
|
+
targetAbs,
|
|
2952
|
+
wordMotion.motion === "e",
|
|
2953
|
+
);
|
|
2732
2954
|
return true;
|
|
2733
2955
|
}
|
|
2734
2956
|
|
|
@@ -2739,7 +2961,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2739
2961
|
const line = this.getLines()[this.getCursor().line] ?? "";
|
|
2740
2962
|
const col = this.getCursor().col;
|
|
2741
2963
|
const count = this.takeTotalCount(1);
|
|
2742
|
-
const targetCol = findCharMotionTarget(
|
|
2964
|
+
const targetCol = findCharMotionTarget(
|
|
2965
|
+
line,
|
|
2966
|
+
col,
|
|
2967
|
+
motion,
|
|
2968
|
+
targetChar,
|
|
2969
|
+
false,
|
|
2970
|
+
count,
|
|
2971
|
+
);
|
|
2743
2972
|
|
|
2744
2973
|
if (targetCol === null) return;
|
|
2745
2974
|
|
|
@@ -2754,17 +2983,24 @@ export class ModalEditor extends CustomEditor {
|
|
|
2754
2983
|
let end = Math.min(rawEnd, line.length);
|
|
2755
2984
|
|
|
2756
2985
|
if (inclusive) {
|
|
2757
|
-
const targetRange = this.getGraphemeRangeAtCol(
|
|
2986
|
+
const targetRange = this.getGraphemeRangeAtCol(
|
|
2987
|
+
line,
|
|
2988
|
+
Math.max(col, targetCol),
|
|
2989
|
+
1,
|
|
2990
|
+
);
|
|
2758
2991
|
end = targetRange?.end ?? end;
|
|
2759
2992
|
}
|
|
2760
2993
|
|
|
2761
2994
|
if (end <= start) return;
|
|
2762
2995
|
|
|
2763
|
-
// Yank only — no cursor movement, no text mutation
|
|
2764
2996
|
this.writeToRegister(line.slice(start, end), "yank");
|
|
2765
2997
|
}
|
|
2766
2998
|
|
|
2767
|
-
private yankRangeByAbsolute(
|
|
2999
|
+
private yankRangeByAbsolute(
|
|
3000
|
+
currentAbs: number,
|
|
3001
|
+
targetAbs: number,
|
|
3002
|
+
inclusive: boolean = false,
|
|
3003
|
+
): void {
|
|
2768
3004
|
const text = this.getText();
|
|
2769
3005
|
const start = Math.min(currentAbs, targetAbs);
|
|
2770
3006
|
const rawEnd = Math.max(currentAbs, targetAbs) + (inclusive ? 1 : 0);
|
|
@@ -2773,7 +3009,10 @@ export class ModalEditor extends CustomEditor {
|
|
|
2773
3009
|
this.writeToRegister(text.slice(start, end), "yank");
|
|
2774
3010
|
}
|
|
2775
3011
|
|
|
2776
|
-
private getCursorFromAbsoluteIndex(
|
|
3012
|
+
private getCursorFromAbsoluteIndex(
|
|
3013
|
+
text: string,
|
|
3014
|
+
abs: number,
|
|
3015
|
+
): { line: number; col: number } {
|
|
2777
3016
|
const lines = text.length === 0 ? [""] : text.split("\n");
|
|
2778
3017
|
let remaining = Math.max(0, Math.min(abs, text.length));
|
|
2779
3018
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
@@ -2814,7 +3053,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2814
3053
|
editor.tui?.requestRender?.();
|
|
2815
3054
|
}
|
|
2816
3055
|
|
|
2817
|
-
private deleteRangeByAbsolute(
|
|
3056
|
+
private deleteRangeByAbsolute(
|
|
3057
|
+
currentAbs: number,
|
|
3058
|
+
targetAbs: number,
|
|
3059
|
+
inclusive: boolean = false,
|
|
3060
|
+
): void {
|
|
2818
3061
|
const text = this.getText();
|
|
2819
3062
|
const start = Math.min(currentAbs, targetAbs);
|
|
2820
3063
|
const rawEnd = Math.max(currentAbs, targetAbs) + (inclusive ? 1 : 0);
|
|
@@ -2866,12 +3109,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2866
3109
|
const count = this.takeTotalCount(1);
|
|
2867
3110
|
const text = this.getPasteRegisterText();
|
|
2868
3111
|
if (!text) return;
|
|
2869
|
-
const safeCount = Math.min(
|
|
3112
|
+
const safeCount = Math.min(
|
|
3113
|
+
count,
|
|
3114
|
+
Math.max(1, Math.floor(ModalEditor.PUT_SIZE_LIMIT / text.length)),
|
|
3115
|
+
);
|
|
2870
3116
|
|
|
2871
3117
|
if (text.endsWith("\n")) {
|
|
2872
3118
|
const content = text.slice(0, -1);
|
|
2873
3119
|
for (let i = 0; i < safeCount; i++) {
|
|
2874
|
-
// Line-wise: insert new line below and fill it
|
|
2875
3120
|
super.handleInput(CTRL_E);
|
|
2876
3121
|
super.handleInput(NEWLINE);
|
|
2877
3122
|
for (const char of content) {
|
|
@@ -2881,7 +3126,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2881
3126
|
return;
|
|
2882
3127
|
}
|
|
2883
3128
|
|
|
2884
|
-
// Character-wise: insert after cursor
|
|
2885
3129
|
if (!this.isCursorAtOrPastEol()) {
|
|
2886
3130
|
super.handleInput(ESC_RIGHT);
|
|
2887
3131
|
}
|
|
@@ -2896,12 +3140,14 @@ export class ModalEditor extends CustomEditor {
|
|
|
2896
3140
|
const count = this.takeTotalCount(1);
|
|
2897
3141
|
const text = this.getPasteRegisterText();
|
|
2898
3142
|
if (!text) return;
|
|
2899
|
-
const safeCount = Math.min(
|
|
3143
|
+
const safeCount = Math.min(
|
|
3144
|
+
count,
|
|
3145
|
+
Math.max(1, Math.floor(ModalEditor.PUT_SIZE_LIMIT / text.length)),
|
|
3146
|
+
);
|
|
2900
3147
|
|
|
2901
3148
|
if (text.endsWith("\n")) {
|
|
2902
3149
|
const content = text.slice(0, -1);
|
|
2903
3150
|
for (let i = 0; i < safeCount; i++) {
|
|
2904
|
-
// Line-wise: insert new line above and fill it
|
|
2905
3151
|
super.handleInput(CTRL_A);
|
|
2906
3152
|
super.handleInput(NEWLINE);
|
|
2907
3153
|
super.handleInput(ESC_UP);
|
|
@@ -2912,7 +3158,6 @@ export class ModalEditor extends CustomEditor {
|
|
|
2912
3158
|
return;
|
|
2913
3159
|
}
|
|
2914
3160
|
|
|
2915
|
-
// Character-wise: insert before cursor (just type it)
|
|
2916
3161
|
for (let i = 0; i < safeCount; i++) {
|
|
2917
3162
|
for (const char of text) {
|
|
2918
3163
|
super.handleInput(char === "\n" ? NEWLINE : char);
|
|
@@ -2920,7 +3165,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2920
3165
|
}
|
|
2921
3166
|
}
|
|
2922
3167
|
|
|
2923
|
-
private deleteRange(
|
|
3168
|
+
private deleteRange(
|
|
3169
|
+
col: number,
|
|
3170
|
+
targetCol: number,
|
|
3171
|
+
inclusive: boolean,
|
|
3172
|
+
): void {
|
|
2924
3173
|
const cursor = this.getCursor();
|
|
2925
3174
|
const line = this.getLines()[cursor.line] ?? "";
|
|
2926
3175
|
const lineStartAbs = this.getAbsoluteIndex(cursor.line, 0);
|
|
@@ -2929,7 +3178,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
2929
3178
|
let end = Math.min(rawEnd, line.length);
|
|
2930
3179
|
|
|
2931
3180
|
if (inclusive) {
|
|
2932
|
-
const targetRange = this.getGraphemeRangeAtCol(
|
|
3181
|
+
const targetRange = this.getGraphemeRangeAtCol(
|
|
3182
|
+
line,
|
|
3183
|
+
Math.max(col, targetCol),
|
|
3184
|
+
1,
|
|
3185
|
+
);
|
|
2933
3186
|
end = targetRange?.end ?? end;
|
|
2934
3187
|
}
|
|
2935
3188
|
|
|
@@ -2978,7 +3231,7 @@ export class ModalEditor extends CustomEditor {
|
|
|
2978
3231
|
}
|
|
2979
3232
|
|
|
2980
3233
|
private getDesiredCursorShapeSequence(): CursorShapeSequence {
|
|
2981
|
-
return this.mode
|
|
3234
|
+
return "insert" === this.mode && this.pendingExCommand === null
|
|
2982
3235
|
? INSERT_CURSOR_SHAPE
|
|
2983
3236
|
: BLOCK_CURSOR_SHAPE;
|
|
2984
3237
|
}
|
|
@@ -3026,7 +3279,8 @@ export class ModalEditor extends CustomEditor {
|
|
|
3026
3279
|
const last = lines.length - 1;
|
|
3027
3280
|
const lastLine = lines[last];
|
|
3028
3281
|
if (lastLine && visibleWidth(lastLine) >= visibleWidth(rawLabel)) {
|
|
3029
|
-
lines[last] =
|
|
3282
|
+
lines[last] =
|
|
3283
|
+
truncateToWidth(lastLine, width - visibleWidth(rawLabel), "") + label;
|
|
3030
3284
|
} else {
|
|
3031
3285
|
lines[last] = label;
|
|
3032
3286
|
}
|
|
@@ -3034,13 +3288,11 @@ export class ModalEditor extends CustomEditor {
|
|
|
3034
3288
|
}
|
|
3035
3289
|
|
|
3036
3290
|
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;
|
|
3291
|
+
return this.labelColorizers?.[this.getActiveMode()] ?? null;
|
|
3040
3292
|
}
|
|
3041
3293
|
|
|
3042
3294
|
private getModeLabel(): string {
|
|
3043
|
-
if (this.mode
|
|
3295
|
+
if ("insert" === this.mode) return " INSERT ";
|
|
3044
3296
|
if (this.pendingExCommand !== null) return ` EX ${this.pendingExCommand}_ `;
|
|
3045
3297
|
|
|
3046
3298
|
const prefixCount = this.prefixCount;
|
|
@@ -3073,21 +3325,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
3073
3325
|
|
|
3074
3326
|
pi.on("session_start", (_event, ctx) => {
|
|
3075
3327
|
const piVimSettings = readPiVimSettings(ctx.cwd);
|
|
3076
|
-
const clipboardMirrorPolicy = resolveClipboardMirrorPolicy(
|
|
3328
|
+
const clipboardMirrorPolicy = resolveClipboardMirrorPolicy(
|
|
3329
|
+
piVimSettings.clipboardMirror,
|
|
3330
|
+
);
|
|
3077
3331
|
if (clipboardMirrorPolicy.warning && ctx.hasUI) {
|
|
3078
3332
|
ctx.ui.notify(clipboardMirrorPolicy.warning, "warning");
|
|
3079
3333
|
}
|
|
3080
3334
|
|
|
3081
3335
|
const t = ctx.ui.theme;
|
|
3336
|
+
const modeColors = resolveModeColors(piVimSettings.modeColors);
|
|
3082
3337
|
const reverseVideo = (s: string) => `\x1b[7m${s}\x1b[27m`;
|
|
3083
|
-
const
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
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;
|
|
3088
3345
|
ctx.ui.setEditorComponent((tui, theme, kb) => {
|
|
3089
3346
|
cursorShapeCleanup = enableCursorShapeSupport(tui);
|
|
3090
|
-
const editor = new ModalEditor(tui, theme, kb,
|
|
3347
|
+
const editor = new ModalEditor(tui, theme, kb, {
|
|
3348
|
+
labelColorizers,
|
|
3349
|
+
borderColorizers,
|
|
3350
|
+
});
|
|
3091
3351
|
editor.setClipboardMirrorPolicy(clipboardMirrorPolicy.policy);
|
|
3092
3352
|
editor.setQuitFn(() => ctx.shutdown());
|
|
3093
3353
|
editor.setNotifyFn((message) => ctx.ui.notify(message, "warning"));
|