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/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 ModeLabelColorizers = {
105
- insert: (s: string) => string;
106
- normal: (s: string) => string;
107
- ex: (s: string) => string;
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 (typeof write !== "function" || typeof setShowHardwareCursor !== "function") {
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: { index: number; sequence: (typeof SOFTWARE_CURSOR_RESETS)[number] } | null = null;
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 line.slice(0, cursorStart)
207
- + line.slice(cursorContentStart, reset.index)
208
- + line.slice(reset.index + reset.sequence.length);
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 typeof candidate.code === "string"
240
- && candidate.code.length > 0
241
- && typeof candidate.syscall === "string"
242
- && candidate.syscall.startsWith("spawn");
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("@mariozechner/pi-coding-agent");
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
- // Best effort only; clipboard mirroring must not affect editing.
369
+ return;
320
370
  }
321
371
  }
322
372
 
323
- function writeClipboardInChildProcess(text: string, signal: AbortSignal): Promise<void> {
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(process.execPath, ["--input-type=module", "-e", CLIPBOARD_HELPER_SOURCE], {
354
- stdio: ["pipe", "pipe", "ignore"],
355
- windowsHide: true,
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(new ClipboardSpawnError("clipboard helper spawn failed", { cause: error }));
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(new ClipboardSpawnError("clipboard helper spawn failed", { cause: error }));
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(new ClipboardSpawnError(`clipboard helper failed with exit code ${code ?? "null"}`));
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(createClipboardAbortError("clipboard writer replaced"));
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 this.activeText !== null || this.pendingText !== null || this.draining;
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 (this.circuitBreaker.consecutiveEnvironmentFailures >= CLIPBOARD_SPAWN_FAILURE_LIMIT) {
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(text: string, controller: AbortController): Promise<void> {
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: ModeLabelColorizers | null;
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 clipboardMirrorPolicy: ClipboardMirrorPolicy = DEFAULT_CLIPBOARD_MIRROR_POLICY;
558
- private readonly clipboardMirror = new ClipboardMirror(writeClipboardInChildProcess);
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
- labelColorizers?: ModeLabelColorizers | null,
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(async (text: string, signal: AbortSignal) => {
577
- await fn(text, signal);
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 { this.quitFn = fn; }
593
- setNotifyFn(fn: (message: string) => void): void { this.notifyFn = fn; }
594
- getRegister(): string { return this.unnamedRegister; }
595
- setRegister(text: string): void { this.unnamedRegister = text; }
596
- getMode(): Mode { return this.mode; }
597
- getText(): string { return this.getLines().join("\n"); }
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
- editor: ModalEditorInternals,
614
- ): { lines: string[]; cursorLine?: number; cursorCol?: number } {
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 { lines: string[]; cursorLine?: number; cursorCol?: number };
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 a.text !== b.text
652
- || a.cursor.line !== b.cursor.line
653
- || a.cursor.col !== b.cursor.col;
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 = this.acceptingBracketedPasteInExCommand
790
- || this.pendingEscWhileAcceptingBracketedPasteInExCommand;
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): { filtered: string | null; stripped: boolean } {
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(BRACKETED_PASTE_END, start + BRACKETED_PASTE_START.length);
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 = chunk.slice(0, start) + chunk.slice(end + BRACKETED_PASTE_END.length);
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 === "insert") {
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 = text.slice(0, lineStartAbs) + before + replacement + after
1003
- + text.slice(lineStartAbs + line.length);
1004
- const newCursorAbs = lineStartAbs + before.length + data.length * (count - 1);
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
- || this.pendingTextObject
1069
- || this.pendingOperator
1070
- || this.prefixCount
1071
- || this.operatorCount
1072
- || this.pendingG
1073
- || this.pendingGCount
1074
- || this.pendingReplace
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 === "insert") {
1214
+ if ("insert" === this.mode) {
1080
1215
  this.clearUnderlyingPasteStateIfActive();
1081
- this.mode = "normal";
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 data === "\r" || data === "\n" || matchesKey(data, "enter") || matchesKey(data, "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 data === "\x7f" || data === "\x08" || matchesKey(data, "backspace") || matchesKey(data, "ctrl+h");
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
- && !data.includes("\n")
1117
- && !data.includes("\x7f")
1118
- && !data.includes("\x08")
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) return false;
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 = prefix !== null && operator !== null
1253
- ? prefix * operator
1254
- : prefix ?? operator ?? defaultValue;
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.mode = "insert";
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(pendingTextObject, count, semanticClass);
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.mode = "insert";
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.mode = "insert";
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 = this.prefixCount.length > 0 && this.operatorCount.length > 0;
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 = this.prefixCount.length > 0 || this.operatorCount.length > 0;
1430
- const supportsCountedWordMotion = (
1431
- data === "w"
1432
- || data === "e"
1433
- || data === "b"
1434
- || data === "W"
1435
- || data === "E"
1436
- || data === "B"
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.mode = "insert";
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.mode = "insert";
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 = this.prefixCount.length > 0 || this.operatorCount.length > 0;
1514
- const supportsCountedWordMotion = (
1515
- data === "w"
1516
- || data === "e"
1517
- || data === "b"
1518
- || data === "W"
1519
- || data === "E"
1520
- || data === "B"
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 = data === "W" && this.isCursorOnNonWhitespace()
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.mode = "insert";
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
- || data === "r"
1607
- || data === "s"
1608
- || data === "S"
1609
- || data === "D"
1610
- || data === "C"
1611
- || data === "p"
1612
- || data === "P"
1613
- || data === "Y"
1614
- || data === "J"
1615
- || data === "u"
1616
- || data === CTRL_UNDERSCORE
1617
- || matchesKey(data, "ctrl+_")
1618
- || data === CTRL_R
1619
- || matchesKey(data, "ctrl+r")
1620
- );
1621
- const supportsCountedCharMotion = (
1622
- CHAR_MOTION_KEYS.has(data)
1623
- || data === ";"
1624
- || data === ","
1625
- );
1626
- const supportsCountedWordMotion = (
1627
- data === "w"
1628
- || data === "e"
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
- && !supportsCountedCharMotion
1665
- && !supportsCountedWordMotion
1666
- && !supportsCountedParagraphMotion
1667
- && !supportsCountedUnderscore
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(this.lastCharMotion.motion, this.lastCharMotion.char, false);
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 (data === "u" || data === CTRL_UNDERSCORE || matchesKey(data, "ctrl+_")) {
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.mode = "insert";
1977
+ this.setMode();
1834
1978
  break;
1835
1979
  case "a":
1836
- this.mode = "insert";
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.mode = "insert";
1986
+ this.setMode();
1843
1987
  super.handleInput(CTRL_E);
1844
1988
  break;
1845
1989
  case "I":
1846
- this.mode = "insert";
1990
+ this.setMode();
1847
1991
  this.moveCursorToFirstNonWhitespace();
1848
1992
  break;
1849
1993
  case "o":
1850
1994
  this.openLineBelow();
1851
- this.mode = "insert";
1995
+ this.setMode();
1852
1996
  break;
1853
1997
  case "O":
1854
1998
  this.openLineAbove();
1855
- this.mode = "insert";
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.mode = "insert";
2008
+ this.setMode();
1865
2009
  break;
1866
2010
  case "S":
1867
2011
  this.takeTotalCount(1);
1868
2012
  this.cutCurrentLineContent();
1869
- this.mode = "insert";
2013
+ this.setMode();
1870
2014
  break;
1871
2015
  case "s":
1872
2016
  this.cutCharUnderCursor();
1873
- this.mode = "insert";
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(motion: CharMotion, targetChar: string, saveMotion: boolean = true): void {
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(line, col, motion, targetChar, !saveMotion, count);
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(lines, fromLine, direction, count);
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 (!Number.isInteger(state.cursorLine) || !Number.isInteger(state.cursorCol)) return false;
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(0, Math.min(currentLine + delta, state.lines.length - 1));
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 = leftLastChar !== undefined && /\s/.test(leftLastChar);
2242
+ const leftEndsWithSpace =
2243
+ leftLastChar !== undefined && /\s/.test(leftLastChar);
2078
2244
  const needsSeparator = !leftEndsWithSpace && trimmedRight.length > 0;
2079
- joined = needsSeparator ? `${left} ${trimmedRight}` : left + trimmedRight;
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 (next < len && this.charType(text[next], semanticClass) === startType) next++;
2341
+ while (
2342
+ next < len &&
2343
+ this.charType(text[next], semanticClass) === startType
2344
+ )
2345
+ next++;
2174
2346
  }
2175
- while (next < len && this.charType(text[next], semanticClass) === "space") next++;
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 (next < len && this.charType(text[next], semanticClass) === "space") next++;
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 (next < len - 1 && this.charType(text[next + 1], semanticClass) === t) next++;
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") next--;
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) next--;
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(direction, target, semanticClass);
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 = motion === "b" ? "backward" : "forward";
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(text: string, source: RegisterWriteSource = "mutation"): void {
2554
+ private writeToRegister(
2555
+ text: string,
2556
+ source: RegisterWriteSource = "mutation",
2557
+ ): void {
2364
2558
  this.unnamedRegister = text;
2365
- if (!text) return;
2366
- if (!this.shouldMirrorRegisterWrite(source)) return;
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((segment) => segment.end - segment.start > 1);
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((segment) => clampedCol < segment.end);
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) + text.slice(lineStartAbs + range.end),
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 = col < line.length ? line.slice(col) : hasNextLine ? "\n" : "";
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(startLine: number, endLine: number): { start: number; end: number } {
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(startLine: number, endLine: number): { startAbs: number; endAbs: number } {
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(startLine, endLine);
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(col, findFirstNonWhitespaceColumn(this.getLines()[cursor.line] ?? ""), false);
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(currentAbs, targetAbs, wordMotion.motion === "e");
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(line, col, motion, targetChar, false, count);
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 = this.prefixCount.length > 0 && this.operatorCount.length > 0;
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(currentAbs, targetAbs, wordMotion.motion === "e");
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(line, col, motion, targetChar, false, count);
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(line, Math.max(col, targetCol), 1);
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(currentAbs: number, targetAbs: number, inclusive: boolean = false): void {
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(text: string, abs: number): { line: number; col: number } {
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(currentAbs: number, targetAbs: number, inclusive: boolean = false): void {
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(count, Math.max(1, Math.floor(ModalEditor.PUT_SIZE_LIMIT / text.length)));
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(count, Math.max(1, Math.floor(ModalEditor.PUT_SIZE_LIMIT / text.length)));
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(col: number, targetCol: number, inclusive: boolean): void {
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(line, Math.max(col, targetCol), 1);
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 === "insert" && this.pendingExCommand === null
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] = truncateToWidth(lastLine, width - visibleWidth(rawLabel), "") + label;
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
- if (!this.labelColorizers) return null;
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 === "insert") return " INSERT ";
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(piVimSettings.clipboardMirror);
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 colorizers = t ? {
3079
- insert: (s: string) => t.fg("borderMuted", reverseVideo(s)),
3080
- normal: (s: string) => t.fg("borderAccent", reverseVideo(s)),
3081
- ex: (s: string) => t.fg("warning", reverseVideo(s)),
3082
- } : null;
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, colorizers);
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"));