pi-better-paste-markers 0.3.0 → 0.3.1

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 +3 -0
  2. package/index.ts +115 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,6 +21,7 @@ Pastes over 10 lines or 1000 chars render as a three-line block:
21
21
  - Everywhere else (drafts, autocomplete, undo) the block collapses back to the standard single-line `[paste #N ...]` marker.
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
+ - Survives `/new`, `/resume` and fork: pi rebuilds the editor component during a session switch, so blocks are rebuilt from the shared registry (and from paste content captured out of the base editor) instead of degrading to a literal `[paste #N ...]` marker.
24
25
  - 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
26
  - 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.
26
27
 
@@ -42,3 +43,5 @@ The extension replaces the main editor on `session_start` via `ctx.ui.setEditorC
42
43
  - `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).
43
44
 
44
45
  Paste records are kept after a block is deleted so that undoing the deletion restores a block that still collapses and expands correctly.
46
+
47
+ The registry is shared across editor instances: during `/new`, `/resume` or a fork, pi tears the custom editor down (`resetExtensionUI()`), rebuilds it, and carries the draft over with `setText(editor.getText())` — i.e. collapsed markers. Blocks are therefore rebuilt on `setText`, from the shared registry or from content captured out of the base editor (whose `pastes` map pi clears on the same path).
package/index.ts CHANGED
@@ -72,7 +72,68 @@ interface ThemeBackgroundProvider {
72
72
  /** Theme key used as the paste-block background (also used for user messages). */
73
73
  const BLOCK_BG = "userMessageBg";
74
74
 
75
+ /**
76
+ * Paste content captured from the base editor right before it drops its
77
+ * registry, keyed by its canonical single-line marker.
78
+ *
79
+ * Session switches (`/new`, `/resume`, fork) tear our editor down
80
+ * (`resetExtensionUI()` -> `setCustomEditorComponent(undefined)`), so a paste
81
+ * made while the session is rebuilt lands in the base `CustomEditor`. pi then
82
+ * rebuilds our editor with `setText(editor.getText())` — the collapsed marker —
83
+ * and the base `setText` clears its `pastes` map. The content would be lost and
84
+ * Enter would submit a literal `[paste #N ...]`. Capturing it in `getText()`
85
+ * (which pi calls just before the rebuild) keeps it recoverable.
86
+ */
87
+ const recoveredPastes = new Map<string, PasteRecord>();
88
+
89
+ /**
90
+ * Block registry shared by every editor instance of this session.
91
+ *
92
+ * pi rebuilds the editor component more than once around a session switch and
93
+ * carries the draft across with `setText(editor.getText())`, i.e. collapsed
94
+ * markers. Keeping the registry per instance lost the content on the second
95
+ * rebuild (the new instance saw no records and submitted the marker verbatim),
96
+ * so it lives here instead.
97
+ */
98
+ const sharedPastes = new Map<number, PasteRecord>();
99
+ let sharedPasteCounter = 0;
100
+
101
+ let baseEditorCaptureInstalled = false;
102
+
103
+ /** Snapshot paste content of a base editor before its registry gets cleared. */
104
+ function installBaseEditorCapture(): void {
105
+ if (baseEditorCaptureInstalled) return;
106
+ baseEditorCaptureInstalled = true;
107
+
108
+ const proto = CustomEditor.prototype as unknown as { getText(): string };
109
+ const originalGetText = proto.getText;
110
+
111
+ proto.getText = function (this: unknown): string {
112
+ const text = originalGetText.call(this);
113
+ const registry = (this as { pastes?: Map<number, string> }).pastes;
114
+ if (!registry || registry.size === 0 || !text.includes("[paste #")) return text;
115
+
116
+ for (const [id, content] of registry) {
117
+ const lines = content.split("\n");
118
+ const marker =
119
+ lines.length > 10
120
+ ? `[paste #${id} +${lines.length} lines]`
121
+ : `[paste #${id} ${content.length} chars]`;
122
+ if (!text.includes(marker) || recoveredPastes.has(marker)) continue;
123
+ recoveredPastes.set(marker, {
124
+ content,
125
+ lines: lines.length,
126
+ chars: content.length,
127
+ first: lines[0] ?? "",
128
+ last: lines[lines.length - 1] ?? "",
129
+ });
130
+ }
131
+ return text;
132
+ };
133
+ }
134
+
75
135
  export default function betterPasteMarkers(pi: ExtensionAPI): void {
136
+ installBaseEditorCapture();
76
137
  pi.on("session_start", (_event, ctx) => {
77
138
  const getAppTheme = () => ctx.ui.theme as unknown as ThemeBackgroundProvider;
78
139
  ctx.ui.setEditorComponent(
@@ -82,9 +143,6 @@ export default function betterPasteMarkers(pi: ExtensionAPI): void {
82
143
  }
83
144
 
84
145
  export class BetterPasteEditor extends CustomEditor {
85
- /** id -> record; ids come from our own counter, so they never collide. */
86
- private pasteRecords = new Map<number, PasteRecord>();
87
- private ownPasteCounter = 0;
88
146
  private editorKeybindings: KeybindingsManager;
89
147
  /** Resolves the current app theme so the block background follows theme switches. */
90
148
  private getAppTheme?: () => ThemeBackgroundProvider;
@@ -111,7 +169,7 @@ export class BetterPasteEditor extends CustomEditor {
111
169
  }
112
170
 
113
171
  private tryDeleteBlock(data: string): boolean {
114
- if (this.pasteRecords.size === 0 || this.isShowingAutocomplete()) return false;
172
+ if (sharedPastes.size === 0 || this.isShowingAutocomplete()) return false;
115
173
 
116
174
  const kb = this.editorKeybindings;
117
175
  let direction: DeleteDirection | null = null;
@@ -151,7 +209,7 @@ export class BetterPasteEditor extends CustomEditor {
151
209
  const middle = MIDDLE_RE.exec(lines[i + 1] ?? "");
152
210
  if (!middle) continue;
153
211
  const id = Number(middle[1]);
154
- const record = this.pasteRecords.get(id);
212
+ const record = sharedPastes.get(id);
155
213
  if (!record) continue;
156
214
  const opening = `[${record.first}`;
157
215
  const closing = `${record.last}]`;
@@ -224,8 +282,8 @@ export class BetterPasteEditor extends CustomEditor {
224
282
  this.lastAction = null;
225
283
  this.pushUndoSnapshot();
226
284
 
227
- const id = ++this.ownPasteCounter;
228
- this.pasteRecords.set(id, {
285
+ const id = ++sharedPasteCounter;
286
+ sharedPastes.set(id, {
229
287
  content: filteredText,
230
288
  lines: pastedLines.length,
231
289
  chars: filteredText.length,
@@ -233,7 +291,7 @@ export class BetterPasteEditor extends CustomEditor {
233
291
  last: previewTail(pastedLines[pastedLines.length - 1] ?? ""),
234
292
  });
235
293
 
236
- const record = this.pasteRecords.get(id)!;
294
+ const record = sharedPastes.get(id)!;
237
295
  const block = [
238
296
  `[${record.first}`,
239
297
  `paste #${id} ${filteredText.length} chars / ${pastedLines.length} lines`,
@@ -272,7 +330,7 @@ export class BetterPasteEditor extends CustomEditor {
272
330
 
273
331
  /** Collapse every intact 3-line block back to its canonical single-line marker. */
274
332
  private collapse(text: string): string {
275
- if (this.pasteRecords.size === 0) return text;
333
+ if (sharedPastes.size === 0) return text;
276
334
  const lines = text.split("\n");
277
335
  const blocks = this.findBlocks(lines);
278
336
  if (blocks.length === 0) return text;
@@ -295,7 +353,7 @@ export class BetterPasteEditor extends CustomEditor {
295
353
  /** Replace collapsed canonical markers with their original content. */
296
354
  private expand(text: string): string {
297
355
  let result = text;
298
- for (const [id, rec] of this.pasteRecords) {
356
+ for (const [id, rec] of sharedPastes) {
299
357
  result = result
300
358
  .split(`[paste #${id} ${rec.chars} chars]`)
301
359
  .join(rec.content)
@@ -305,6 +363,53 @@ export class BetterPasteEditor extends CustomEditor {
305
363
  return result;
306
364
  }
307
365
 
366
+ /**
367
+ * Rebuild blocks from collapsed markers handed back by pi.
368
+ *
369
+ * pi rebuilds the editor component around session switches with
370
+ * `setText(editor.getText())`, i.e. canonical inline markers. Without this
371
+ * the block would return as literal text that submits verbatim. Content
372
+ * comes from the shared registry (a previous instance's paste) or from
373
+ * `recoveredPastes` (a paste that landed in the base editor during the
374
+ * switch).
375
+ */
376
+ override setText(text: string): void {
377
+ super.setText(this.restoreBlocks(text));
378
+ recoveredPastes.clear();
379
+ }
380
+
381
+ private restoreBlocks(text: string): string {
382
+ if (!text.includes("[paste #")) return text;
383
+
384
+ let result = text;
385
+ for (const [id, record] of sharedPastes) {
386
+ result = this.substituteMarker(result, id, record);
387
+ }
388
+ for (const [marker, record] of recoveredPastes) {
389
+ const id = Number(/\[paste #(\d+)/.exec(marker)?.[1]);
390
+ if (!Number.isFinite(id)) continue;
391
+ sharedPastes.set(id, record);
392
+ sharedPasteCounter = Math.max(sharedPasteCounter, id);
393
+ result = this.substituteMarker(result, id, record);
394
+ }
395
+ return result;
396
+ }
397
+
398
+ /** Replace one paste's canonical marker(s) with its three-line block. */
399
+ private substituteMarker(text: string, id: number, record: PasteRecord): string {
400
+ const linesMarker = `[paste #${id} +${record.lines} lines]`;
401
+ const charsMarker = `[paste #${id} ${record.chars} chars]`;
402
+ const marker = text.includes(linesMarker) ? linesMarker : text.includes(charsMarker) ? charsMarker : null;
403
+ if (!marker) return text;
404
+
405
+ const first = previewHead(record.first);
406
+ const last = previewTail(record.last);
407
+ sharedPastes.set(id, { ...record, first, last });
408
+ return text
409
+ .split(marker)
410
+ .join([`[${first}`, `paste #${id} ${record.chars} chars / ${record.lines} lines`, `${last}]`].join("\n"));
411
+ }
412
+
308
413
  override getText(): string {
309
414
  return this.collapse(super.getText());
310
415
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-paste-markers",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",