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.
- package/README.md +3 -0
- package/index.ts +115 -10
- 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 (
|
|
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 =
|
|
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 = ++
|
|
228
|
-
|
|
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 =
|
|
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 (
|
|
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
|
|
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