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.
- package/README.md +2 -1
- package/index.ts +132 -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
|
|
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
|
-
|
|
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