pi-better-paste-markers 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/index.ts +132 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -22,6 +22,7 @@ Pastes over 10 lines or 1000 chars render as a three-line block:
22
22
  - The block deletes as one unit: a single Backspace/Delete (or Ctrl+W/U/K) that touches it removes the whole block, like the built-in `[paste #N ...]` marker. Text before and after the block on the same line is kept.
23
23
  - Undo restores the whole block and it keeps expanding/submitting correctly.
24
24
  - The input is prefixed with a gray `❯` prompt (the one pi-powerline-footer draws on the default editor is lost when a custom editor component replaces it).
25
+ - The block is painted with the theme's `userMessageBg` background (follows light/dark and theme switches); only the block text is filled, the padding after it stays normal.
25
26
 
26
27
  ## Install
27
28
 
@@ -38,6 +39,6 @@ The extension replaces the main editor on `session_start` via `ctx.ui.setEditorC
38
39
  - `getText()` — collapses blocks back to canonical `[paste #N ...]` markers (drafts, autocomplete snapshots, undo).
39
40
  - `expandPasteMarkers()` — plain Enter submits through the base editor's private `submitValue()`, which expands paste markers with this method; shadowing it makes Enter submit the original pasted content instead of the three-line preview.
40
41
  - `getExpandedText()` — used by the follow-up (Alt+Enter), external-editor and extension-API (`getEditorText`) paths.
41
- - `render()` — prefixes the input with a gray `❯` and keeps the other content lines indented, matching the chrome pi-powerline-footer adds to the default editor.
42
+ - `render()` — prefixes the input with a gray `❯`, keeps the other content lines indented, and paints the paste-block text with `ctx.ui.theme.bg("userMessageBg", …)` (re-applied after the cursor's reset so the highlight survives the cursor).
42
43
 
43
44
  Paste records are kept after a block is deleted so that undoing the deletion restores a block that still collapses and expands correctly.
package/index.ts CHANGED
@@ -64,10 +64,19 @@ interface PasteBlock {
64
64
  /** Which side of the block a deletion key bites from. */
65
65
  type DeleteDirection = "backward" | "forward" | "line";
66
66
 
67
+ /** Minimal slice of pi's app theme we need to paint the block background. */
68
+ interface ThemeBackgroundProvider {
69
+ bg(color: string, text: string): string;
70
+ }
71
+
72
+ /** Theme key used as the paste-block background (also used for user messages). */
73
+ const BLOCK_BG = "userMessageBg";
74
+
67
75
  export default function betterPasteMarkers(pi: ExtensionAPI): void {
68
76
  pi.on("session_start", (_event, ctx) => {
77
+ const getAppTheme = () => ctx.ui.theme as unknown as ThemeBackgroundProvider;
69
78
  ctx.ui.setEditorComponent(
70
- (tui, theme, keybindings) => new BetterPasteEditor(tui, theme, keybindings),
79
+ (tui, theme, keybindings) => new BetterPasteEditor(tui, theme, keybindings, getAppTheme),
71
80
  );
72
81
  });
73
82
  }
@@ -77,10 +86,18 @@ export class BetterPasteEditor extends CustomEditor {
77
86
  private pasteRecords = new Map<number, PasteRecord>();
78
87
  private ownPasteCounter = 0;
79
88
  private editorKeybindings: KeybindingsManager;
80
-
81
- constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
89
+ /** Resolves the current app theme so the block background follows theme switches. */
90
+ private getAppTheme?: () => ThemeBackgroundProvider;
91
+
92
+ constructor(
93
+ tui: TUI,
94
+ theme: EditorTheme,
95
+ keybindings: KeybindingsManager,
96
+ getAppTheme?: () => ThemeBackgroundProvider,
97
+ ) {
82
98
  super(tui, theme, keybindings);
83
99
  this.editorKeybindings = keybindings;
100
+ this.getAppTheme = getAppTheme;
84
101
  }
85
102
 
86
103
  /**
@@ -346,8 +363,58 @@ export class BetterPasteEditor extends CustomEditor {
346
363
  for (let i = bottomBorderIndex + 1; i < lines.length; i++) {
347
364
  result.push(lines[i] ?? "");
348
365
  }
366
+ this.paintBlocks(result, bottomBorderIndex);
349
367
  return result;
350
368
  }
369
+
370
+ /** Block text portions that should carry the background, longest first. */
371
+ private blockSpans(): string[] {
372
+ const lines = this.state.lines;
373
+ const spans: string[] = [];
374
+ for (const block of this.findBlocks()) {
375
+ const first = lines[block.startLine] ?? "";
376
+ const middle = lines[block.startLine + 1] ?? "";
377
+ const last = lines[block.endLine] ?? "";
378
+ if (block.endLine === block.startLine + 2) {
379
+ spans.push(first.slice(block.startCol), middle, last.slice(0, block.endCol));
380
+ } else {
381
+ for (let line = block.startLine; line <= block.endLine; line++) spans.push(lines[line] ?? "");
382
+ }
383
+ }
384
+ return spans.filter((span) => span.length > 0).sort((a, b) => b.length - a.length);
385
+ }
386
+
387
+ /** ANSI prefix for the block background, or null when the theme lacks it. */
388
+ private blockBackground(): string | null {
389
+ const theme = this.getAppTheme?.();
390
+ if (!theme) return null;
391
+ try {
392
+ const wrapped = theme.bg(BLOCK_BG, "");
393
+ const reset = "\x1b[49m";
394
+ const ansi = wrapped.endsWith(reset) ? wrapped.slice(0, -reset.length) : wrapped;
395
+ return ansi || null;
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+
401
+ /** Paint only the block text (not the padding) with the theme background. */
402
+ private paintBlocks(rows: string[], bottomBorderIndex: number): void {
403
+ const spans = this.blockSpans();
404
+ if (spans.length === 0) return;
405
+ const background = this.blockBackground();
406
+ if (!background) return;
407
+ for (let i = 1; i < bottomBorderIndex; i++) {
408
+ const row = rows[i];
409
+ if (row === undefined) continue;
410
+ for (const span of spans) {
411
+ if (stripAnsi(row).includes(span)) {
412
+ rows[i] = paintSpan(row, span, background);
413
+ break;
414
+ }
415
+ }
416
+ }
417
+ }
351
418
  }
352
419
 
353
420
  /** First-line preview: keep the head, add `...` only when something was cut. */
@@ -359,3 +426,65 @@ function previewHead(text: string): string {
359
426
  function previewTail(text: string): string {
360
427
  return text.length <= PREVIEW_MAX_CHARS ? text : `...${text.slice(text.length - (PREVIEW_MAX_CHARS - 3))}`;
361
428
  }
429
+
430
+ /** Length of the escape/APC sequence starting at `index`, or 0 for plain text. */
431
+ function ansiSequenceLength(text: string, index: number): number {
432
+ if (text[index] !== "\x1b") return 0;
433
+ const next = text[index + 1];
434
+ if (next === "[") {
435
+ for (let i = index + 2; i < text.length; i++) {
436
+ const code = text.charCodeAt(i);
437
+ if (code >= 0x40 && code <= 0x7e) return i - index + 1;
438
+ }
439
+ return text.length - index;
440
+ }
441
+ if (next === "_" || next === "]" || next === "P" || next === "^") {
442
+ for (let i = index + 2; i < text.length; i++) {
443
+ if (text[i] === "\x07") return i - index + 1;
444
+ if (text[i] === "\x1b" && text[i + 1] === "\\") return i - index + 2;
445
+ }
446
+ return text.length - index;
447
+ }
448
+ return 2;
449
+ }
450
+
451
+ /** Drop ANSI/APC escape sequences, keeping the visible characters. */
452
+ function stripAnsi(text: string): string {
453
+ let out = "";
454
+ for (let i = 0; i < text.length; ) {
455
+ const esc = ansiSequenceLength(text, i);
456
+ if (esc > 0) {
457
+ i += esc;
458
+ continue;
459
+ }
460
+ out += text[i];
461
+ i++;
462
+ }
463
+ return out;
464
+ }
465
+
466
+ /** Paint only the `span` characters of `row` with `background`, leaving padding untouched. */
467
+ function paintSpan(row: string, span: string, background: string): string {
468
+ const startPlain = stripAnsi(row).indexOf(span);
469
+ if (startPlain < 0) return row;
470
+ const endPlain = startPlain + span.length;
471
+ let plain = 0;
472
+ let start = -1;
473
+ let end = -1;
474
+ for (let i = 0; i < row.length; ) {
475
+ const esc = ansiSequenceLength(row, i);
476
+ if (esc > 0) {
477
+ i += esc;
478
+ continue;
479
+ }
480
+ if (plain === startPlain) start = i;
481
+ // `end` sits right after the last span character, so a cursor escape that
482
+ // follows the text stays outside the painted range.
483
+ if (plain === endPlain - 1) end = i + 1;
484
+ plain++;
485
+ i++;
486
+ }
487
+ if (start < 0 || end < 0) return row;
488
+ const inner = row.slice(start, end).replaceAll("\x1b[0m", `\x1b[0m${background}`);
489
+ return `${row.slice(0, start)}${background}${inner}\x1b[49m${row.slice(end)}`;
490
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-paste-markers",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Richer collapsed paste markers for pi: first/last line preview around the canonical paste marker",
5
5
  "type": "module",
6
6
  "license": "MIT",