@xzzpig/pi-starline 0.1.0 → 0.2.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 +31 -27
- package/extensions/starline/config.ts +0 -17
- package/extensions/starline/index.ts +12 -0
- package/extensions/starline/mouse/capabilities.ts +36 -36
- package/extensions/starline/mouse/editor-caret.ts +3 -3
- package/extensions/starline/mouse/index.ts +163 -272
- package/extensions/starline/mouse/selection-state.ts +17 -39
- package/extensions/starline/mouse/transcript-copy.ts +1 -1
- package/extensions/starline/prototype-patch-registry.ts +1 -2
- package/extensions/starline/ui.ts +1 -1
- package/package.json +58 -59
- package/extensions/starline/mouse/word-select.ts +0 -145
|
@@ -5,32 +5,22 @@
|
|
|
5
5
|
* what it would do to it. `installMouse` probes what the running Pi build
|
|
6
6
|
* exposes, logs once if something is missing, and installs each feature gated
|
|
7
7
|
* on exactly its own declared requirement (`capabilities.ts`). Today that is
|
|
8
|
-
* six features across
|
|
8
|
+
* six features across four patches. Two of those methods carry two features
|
|
9
9
|
* each, and in every case the two share a single patch, because the patch
|
|
10
10
|
* registry holds one behaviour per adapter key and a second registration would
|
|
11
11
|
* silently replace the first:
|
|
12
12
|
*
|
|
13
|
-
* `
|
|
14
|
-
* - `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `pathAwareWords`, one patch:
|
|
26
|
-
* - `getWordSelection` — Pi's own double-click word lookup, replaced with
|
|
27
|
-
* `wordRangeAt` (`word-select.ts`, ported from pi issue #7746) so a path or
|
|
28
|
-
* a kebab-case identifier selects whole instead of stopping at `/` or `-`.
|
|
29
|
-
* The installed 0.84.1 build predates #7746, so this substitutes Pi's
|
|
30
|
-
* entire word-selection rule rather than layering path handling on top of
|
|
31
|
-
* it — that is what #7746 itself proposes, not scope creep introduced
|
|
32
|
-
* here. Calls through when `wordRangeAt` declines (column past the end of
|
|
33
|
-
* the line) or when `mouse.pathAwareWords` is off.
|
|
13
|
+
* `selectionHint`, one patch plus a derived hint:
|
|
14
|
+
* - `copyActiveSelectionToClipboard` — the method Pi 0.84.4's Ctrl+X handler
|
|
15
|
+
* reaches (`handleCopyCommand` with `preferSelection`). Wrapped so a
|
|
16
|
+
* selection the clean-copy features recognise copies clean, calling
|
|
17
|
+
* through for everything else.
|
|
18
|
+
* - The hint is derived, not owned: `activeSelectionHintText()` renders the
|
|
19
|
+
* live renderer's state — an active selection that is not being
|
|
20
|
+
* auto-copied (`getCopyOnSelect() === false`) — so select-without-copy is
|
|
21
|
+
* Pi's own `fullscreenCopyOnSelect`, and this module only tells the user
|
|
22
|
+
* what to press. The copy key is `app.message.copy` (default ctrl+x),
|
|
23
|
+
* resolved from Pi's keybinding registry so a rebind shows up in the hint.
|
|
34
24
|
*
|
|
35
25
|
* `clickToExpandTools`, one patch:
|
|
36
26
|
* - `handleSelectionMouseEvent` — watched for a left-button press that landed
|
|
@@ -63,7 +53,7 @@
|
|
|
63
53
|
* interrupt and exit chords cannot be swallowed by it from either direction.
|
|
64
54
|
* Every backspace it does not act on falls through and deletes one character.
|
|
65
55
|
*
|
|
66
|
-
* `editorBufferCopy`, sharing the `
|
|
56
|
+
* `editorBufferCopy`, sharing the `copyActiveSelectionToClipboard` patch:
|
|
67
57
|
* - A selection lying inside the input box is copied from the *draft* rather
|
|
68
58
|
* than from the rendered rows. That is the one place this module builds
|
|
69
59
|
* clipboard bytes itself, and the reason is that the rendered rows are not
|
|
@@ -80,15 +70,10 @@
|
|
|
80
70
|
* was clean before changes.
|
|
81
71
|
*
|
|
82
72
|
* Everywhere else the clipboard gets exactly what Pi's own
|
|
83
|
-
* `
|
|
73
|
+
* `copyActiveSelectionToClipboard` puts there — see `installMouse` for why frame-free
|
|
84
74
|
* selection is not part of this.
|
|
85
75
|
*/
|
|
86
|
-
import {
|
|
87
|
-
getKeybindings,
|
|
88
|
-
matchesKey,
|
|
89
|
-
sliceByColumn,
|
|
90
|
-
stripTerminalSequences,
|
|
91
|
-
} from "@earendil-works/pi-tui";
|
|
76
|
+
import { getKeybindings, sliceByColumn, stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
92
77
|
import type { PolishedTuiConfig } from "../config";
|
|
93
78
|
import { installPrototypePatch } from "../prototype-patch-registry";
|
|
94
79
|
import {
|
|
@@ -107,37 +92,17 @@ import { activeEditor, wheelTarget } from "./editor-mouse";
|
|
|
107
92
|
import { scrollEditorBy } from "./editor-scroll";
|
|
108
93
|
import { editorVisualRowCount } from "./editor-text-cursor";
|
|
109
94
|
import { type BoxLike, scrollContentLinesFor } from "./hit-test";
|
|
110
|
-
import { externalEditorName,
|
|
95
|
+
import { externalEditorName, selectionHintText } from "./selection-state";
|
|
111
96
|
import { type ExpandTarget, expandKeyText, expandTargetAt, keyTextFor } from "./tool-box";
|
|
112
97
|
import { cleanTranscriptRows } from "./transcript-copy";
|
|
113
|
-
import { wordRangeAt } from "./word-select";
|
|
114
98
|
|
|
99
|
+
/** Pi's interrupt chord — refused by the range-delete branch, never consumed. */
|
|
115
100
|
const CTRL_C = "\x03";
|
|
116
101
|
/** Pi's exit chord, and `tui.editor.deleteCharForward`'s second default key. */
|
|
117
102
|
const CTRL_D = "\x04";
|
|
118
103
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
* it. Pi 0.84 pushes the Kitty keyboard protocol (and falls back to xterm
|
|
122
|
-
* modifyOtherKeys) on every capable terminal, and under either protocol the
|
|
123
|
-
* chord arrives as an escape sequence — `\x1b[99;5u` or `\x1b[27;5;99~` —
|
|
124
|
-
* never as the bare `\x03` a plain legacy terminal sends. The pending copy
|
|
125
|
-
* must recognise all three, or ctrl+c falls through to Pi's own binding
|
|
126
|
-
* (`app.clear`, clear the editor) exactly when it was about to copy. Pi's
|
|
127
|
-
* own key matcher is the authority here: it parses every protocol this
|
|
128
|
-
* build negotiates, and its `ctrl+c` arm accepts the raw control character
|
|
129
|
-
* too, so a legacy terminal keeps working unchanged.
|
|
130
|
-
*/
|
|
131
|
-
function isCtrlC(data: unknown): boolean {
|
|
132
|
-
if (typeof data !== "string" || data.length === 0) return false;
|
|
133
|
-
try {
|
|
134
|
-
return matchesKey(data, "ctrl+c");
|
|
135
|
-
} catch {
|
|
136
|
-
// A pi-tui build that has moved `matchesKey` must not break the copy:
|
|
137
|
-
// fall back to the raw byte, which is what this branch has always read.
|
|
138
|
-
return data === CTRL_C;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
104
|
+
/** The binding Pi's selection copy lives on (`app.message.copy`, default ctrl+x). */
|
|
105
|
+
const COPY_KEYBINDING = "app.message.copy";
|
|
141
106
|
|
|
142
107
|
/** Opens the draft in `$EDITOR` — the hint an editor selection's hint carries. */
|
|
143
108
|
const EXTERNAL_EDITOR_KEYBINDING = "app.editor.external";
|
|
@@ -171,11 +136,23 @@ type MouseCapablePrototype = {
|
|
|
171
136
|
row: number,
|
|
172
137
|
selection: SelectionBounds,
|
|
173
138
|
): SelectionColumns;
|
|
174
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Pi 0.84.4's public selection-copy entry (`tui-alt-screen.ts`): true when
|
|
141
|
+
* the active selection was copied, false when there was none. This is what
|
|
142
|
+
* coding-agent's Ctrl+X handler reaches, so a clean copy answering this
|
|
143
|
+
* method answers exactly the copies that key performs.
|
|
144
|
+
*/
|
|
145
|
+
copyActiveSelectionToClipboard(this: unknown): Promise<boolean>;
|
|
146
|
+
/**
|
|
147
|
+
* Pi 0.84.4's select-without-copy state. The hint reads it to decide
|
|
148
|
+
* whether a selection is waiting for the copy key or was already copied on
|
|
149
|
+
* release.
|
|
150
|
+
*/
|
|
151
|
+
getCopyOnSelect(this: unknown): boolean;
|
|
152
|
+
/** Whether the fullscreen viewport has a non-empty active text selection. */
|
|
153
|
+
hasActiveSelection(this: unknown): boolean;
|
|
175
154
|
handleViewportInput(this: unknown, data: string): { consume: boolean } | undefined;
|
|
176
155
|
flash(this: unknown, message: string, durationMs?: number): void;
|
|
177
|
-
getWordSelection(this: unknown, point: SelectionPoint): SelectionBounds | undefined;
|
|
178
|
-
getSelectionSourceLine(this: unknown, point: SelectionPoint): string;
|
|
179
156
|
hasOverlay(this: unknown): boolean;
|
|
180
157
|
/**
|
|
181
158
|
* `TuiAltScreen.routeWheel(event)` (`tui-alt-screen.js:375`). `event` is
|
|
@@ -203,7 +180,7 @@ type MouseCapablePrototype = {
|
|
|
203
180
|
selectionFocus?: unknown;
|
|
204
181
|
/**
|
|
205
182
|
* `TuiBase.terminal` (`tui.js:101`); `rows` is a getter on it, and `write` is
|
|
206
|
-
* how `
|
|
183
|
+
* how `copyActiveSelectionToClipboard` reaches the clipboard — it emits OSC 52
|
|
207
184
|
* through the terminal rather than through any renderer method.
|
|
208
185
|
*/
|
|
209
186
|
terminal?: { rows?: number; write?: (data: string) => void };
|
|
@@ -234,15 +211,72 @@ export type InstallMouseDeps = {
|
|
|
234
211
|
let hasWarned = false;
|
|
235
212
|
|
|
236
213
|
/**
|
|
237
|
-
*
|
|
214
|
+
* The live renderer the hint reads. `installMouse` receives only the
|
|
215
|
+
* prototype, so the instance is captured here from the first patched call
|
|
216
|
+
* that sees one — `handleViewportInput` sees every key and mouse event, and
|
|
217
|
+
* repaints ride Pi's own `requestRender`, so the hint is never more than one
|
|
218
|
+
* input away from the current renderer.
|
|
219
|
+
*/
|
|
220
|
+
let activeReceiver: MouseCapablePrototype | undefined;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Reader for the derived selection hint of whichever `installMouse` call is
|
|
238
224
|
* currently active, mirroring `pasteExpandHintText`'s pattern in
|
|
239
|
-
* `../paste-collapse.ts`. `ui.ts` composes this with the paste hint
|
|
240
|
-
*
|
|
225
|
+
* `../paste-collapse.ts`. `ui.ts` composes this with the paste hint on every
|
|
226
|
+
* render; there is nothing to wire when no mouse install is active.
|
|
227
|
+
*
|
|
228
|
+
* The hint is computed on demand from the live renderer, never cached:
|
|
229
|
+
* "selected, ctrl+x to copy" is true exactly while Pi is not auto-copying
|
|
230
|
+
* and something is actually selected. The character count goes through the
|
|
231
|
+
* same two copy paths the copy key itself would answer, so the hint's
|
|
232
|
+
* promise and the clipboard's bytes stay in agreement.
|
|
241
233
|
*/
|
|
242
|
-
let
|
|
234
|
+
let activeHint:
|
|
235
|
+
| { getConfig: () => PolishedTuiConfig; bufferCopy: boolean; cleanCopy: boolean }
|
|
236
|
+
| undefined;
|
|
243
237
|
|
|
244
238
|
export function activeSelectionHintText(): string | null {
|
|
245
|
-
|
|
239
|
+
if (!activeHint || !activeReceiver) return null;
|
|
240
|
+
const { getConfig, bufferCopy, cleanCopy } = activeHint;
|
|
241
|
+
try {
|
|
242
|
+
// Pi auto-copies on release; a selection that is already on the
|
|
243
|
+
// clipboard needs no hint. `getCopyOnSelect()` is the live value, so a
|
|
244
|
+
// setting flipped at runtime flips the hint with it.
|
|
245
|
+
if (activeReceiver.getCopyOnSelect()) return null;
|
|
246
|
+
if (!activeReceiver.hasActiveSelection()) return null;
|
|
247
|
+
const bounds = activeReceiver.getSelectionBounds();
|
|
248
|
+
if (!bounds) return null;
|
|
249
|
+
const pending = pendingSelectionText(
|
|
250
|
+
activeReceiver,
|
|
251
|
+
getConfig(),
|
|
252
|
+
bounds,
|
|
253
|
+
bufferCopy,
|
|
254
|
+
cleanCopy,
|
|
255
|
+
);
|
|
256
|
+
return selectionHintText(
|
|
257
|
+
pending.text.length,
|
|
258
|
+
copyKeyText(),
|
|
259
|
+
// An editor selection cannot grow past the visible window — there is
|
|
260
|
+
// no drag-scroll — so the hint for one points at the external editor,
|
|
261
|
+
// the way to act on the whole draft. "" (unbound) shows no suffix.
|
|
262
|
+
pending.inEditor ? keyTextFor(EXTERNAL_EDITOR_KEYBINDING) : undefined,
|
|
263
|
+
externalEditorName(),
|
|
264
|
+
);
|
|
265
|
+
} catch {
|
|
266
|
+
// The renderer is best effort; a receiver that has moved on offers no
|
|
267
|
+
// hint rather than breaking the metadata row.
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The rendered name of the copy key, for the hint. `app.message.copy` is what
|
|
274
|
+
* Pi 0.84.4's Ctrl+X path is bound to (see `handleCopyCommand` in
|
|
275
|
+
* interactive-mode.ts), and the registry resolves a user rebind so the hint
|
|
276
|
+
* always quotes the key that really copies.
|
|
277
|
+
*/
|
|
278
|
+
function copyKeyText(): string {
|
|
279
|
+
return keyTextFor(COPY_KEYBINDING);
|
|
246
280
|
}
|
|
247
281
|
|
|
248
282
|
/**
|
|
@@ -283,10 +317,10 @@ function refreshExternalEditorHint(
|
|
|
283
317
|
}
|
|
284
318
|
|
|
285
319
|
/**
|
|
286
|
-
* The exact text
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
320
|
+
* The exact text Pi's selection copy would produce, built the same way it
|
|
321
|
+
* builds it: per row, through the receiver's own `getSelectionColumns`, then
|
|
322
|
+
* `sliceByColumn` and `stripTerminalSequences` (both exported by pi-tui),
|
|
323
|
+
* joined with "\n" — see `copyActiveSelectionToClipboard` in
|
|
290
324
|
* `node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js`. Reusing Pi's
|
|
291
325
|
* own helpers instead of re-deriving the column math is what keeps this exact
|
|
292
326
|
* rather than an estimate. The scroll-view case needs the box behind
|
|
@@ -321,34 +355,6 @@ function selectionText(receiver: MouseCapablePrototype, bounds: SelectionBounds)
|
|
|
321
355
|
return rows.join("\n");
|
|
322
356
|
}
|
|
323
357
|
|
|
324
|
-
/**
|
|
325
|
-
* Runs `copySelectionToClipboard` (Pi's real one, reached through the
|
|
326
|
-
* receiver so the `mouse-copy` patch's re-entrancy guard applies) with its
|
|
327
|
-
* own "Copied!" flash suppressed when `copyNotice` is off.
|
|
328
|
-
*
|
|
329
|
-
* Pi's copy method flashes unconditionally — see
|
|
330
|
-
* `node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js`. Respecting
|
|
331
|
-
* `copyNotice: false` therefore means shadowing `flash` on the receiver for
|
|
332
|
-
* the duration of this one call and restoring it immediately after, the same
|
|
333
|
-
* shadow-and-restore shape `installPasteCollapse` already uses on `handlePaste`.
|
|
334
|
-
* `receiver` here is Pi's own instance (the wrapper runs inside Pi's own
|
|
335
|
-
* method call, not through the extension-facing Proxy), so a plain
|
|
336
|
-
* assignment is safe.
|
|
337
|
-
*/
|
|
338
|
-
function copyWithNotice(receiver: MouseCapablePrototype, showNotice: boolean): void {
|
|
339
|
-
if (showNotice) {
|
|
340
|
-
receiver.copySelectionToClipboard();
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const originalFlash = receiver.flash;
|
|
344
|
-
receiver.flash = () => {};
|
|
345
|
-
try {
|
|
346
|
-
receiver.copySelectionToClipboard();
|
|
347
|
-
} finally {
|
|
348
|
-
receiver.flash = originalFlash;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
358
|
/**
|
|
353
359
|
* Writes the draft's own text to the clipboard for a selection that lies inside
|
|
354
360
|
* the input box, and reports whether it did — `false` means this selection was
|
|
@@ -360,14 +366,13 @@ function copyWithNotice(receiver: MouseCapablePrototype, showNotice: boolean): v
|
|
|
360
366
|
* the frame fills each row out to, and a newline wherever the draft happened to
|
|
361
367
|
* wrap. None of that is in the draft. The bytes go out the same way Pi's own
|
|
362
368
|
* copy sends them — OSC 52 through `terminal.write`, see
|
|
363
|
-
* `
|
|
369
|
+
* `copyActiveSelectionToClipboard` in
|
|
364
370
|
* `node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js` — so nothing
|
|
365
371
|
* downstream can tell the two copies apart.
|
|
366
372
|
*
|
|
367
|
-
* The flash is
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* and not at all on the other.
|
|
373
|
+
* The flash is gated on `copyNotice` at the write itself, exactly once — Pi's
|
|
374
|
+
* own copy flashes unconditionally, so this check is the whole notice path for
|
|
375
|
+
* a clean copy.
|
|
371
376
|
*/
|
|
372
377
|
function editorTextForSelection(
|
|
373
378
|
receiver: MouseCapablePrototype,
|
|
@@ -399,7 +404,7 @@ function copyEditorSelection(receiver: MouseCapablePrototype, config: PolishedTu
|
|
|
399
404
|
const write = receiver.terminal?.write;
|
|
400
405
|
if (typeof write !== "function") return false;
|
|
401
406
|
write.call(receiver.terminal, `\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`);
|
|
402
|
-
receiver.flash("Copied!");
|
|
407
|
+
if (config.mouse.copyNotice) receiver.flash("Copied!");
|
|
403
408
|
return true;
|
|
404
409
|
} catch {
|
|
405
410
|
// Reading the editor and the layout is best effort. Anything this trips
|
|
@@ -500,7 +505,7 @@ function copyTranscriptSelection(
|
|
|
500
505
|
const write = receiver.terminal?.write;
|
|
501
506
|
if (typeof write !== "function") return false;
|
|
502
507
|
write.call(receiver.terminal, `\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`);
|
|
503
|
-
receiver.flash("Copied!");
|
|
508
|
+
if (config.mouse.copyNotice) receiver.flash("Copied!");
|
|
504
509
|
return true;
|
|
505
510
|
} catch {
|
|
506
511
|
// Cleaning reads the layout and the selection, both best effort.
|
|
@@ -524,8 +529,6 @@ function copyTranscriptSelection(
|
|
|
524
529
|
* all. `tui.editor.deleteCharForward` binds `ctrl+d` by default, and ctrl+c and
|
|
525
530
|
* ctrl+d are Pi's interrupt and exit chords; consuming either because a
|
|
526
531
|
* selection happens to be live would break a global key to save a keystroke.
|
|
527
|
-
* The ctrl+c branch above already returns before this is reached, but stating
|
|
528
|
-
* it here means the guarantee does not depend on branch order.
|
|
529
532
|
*/
|
|
530
533
|
function isRangeDeleteKey(data: unknown): boolean {
|
|
531
534
|
if (typeof data !== "string" || data.length === 0) return false;
|
|
@@ -573,11 +576,11 @@ function deleteSelectedRange(receiver: MouseCapablePrototype, config: PolishedTu
|
|
|
573
576
|
}
|
|
574
577
|
|
|
575
578
|
/**
|
|
576
|
-
* The text a copy of this selection would put on the clipboard, for the
|
|
577
|
-
*
|
|
579
|
+
* The text a copy of this selection would put on the clipboard, for the hint's
|
|
580
|
+
* character count.
|
|
578
581
|
*
|
|
579
582
|
* It has to ask the same two paths the copy itself asks, in the same order, or
|
|
580
|
-
* the hint promises a number
|
|
583
|
+
* the hint promises a number the copy does not deliver.
|
|
581
584
|
*/
|
|
582
585
|
function pendingSelectionText(
|
|
583
586
|
receiver: MouseCapablePrototype,
|
|
@@ -588,8 +591,8 @@ function pendingSelectionText(
|
|
|
588
591
|
): { text: string; inEditor: boolean } {
|
|
589
592
|
const buffered = bufferCopy ? editorTextForSelection(receiver, config, bounds) : undefined;
|
|
590
593
|
if (buffered !== undefined) return { text: buffered, inEditor: true };
|
|
591
|
-
// The count the hint shows must be the number
|
|
592
|
-
// a selection the copy would clean is counted after cleaning.
|
|
594
|
+
// The count the hint shows must be the number the copy key actually
|
|
595
|
+
// delivers, so a selection the copy would clean is counted after cleaning.
|
|
593
596
|
if (cleanCopy && config.mouse.transcriptCleanCopy) {
|
|
594
597
|
try {
|
|
595
598
|
const cleaned = transcriptSelectionText(receiver, config, bounds);
|
|
@@ -603,25 +606,24 @@ function pendingSelectionText(
|
|
|
603
606
|
}
|
|
604
607
|
|
|
605
608
|
/**
|
|
606
|
-
* Installs the
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
* Either feature can be off
|
|
623
|
-
*
|
|
624
|
-
* the clipboard is Pi's own bytes, exactly as before.
|
|
609
|
+
* Installs the clean-copy features on Pi 0.84.4's public copy entry, and the
|
|
610
|
+
* derived selection hint.
|
|
611
|
+
*
|
|
612
|
+
* `editorBufferCopy` and `transcriptCleanCopy` sit in front of
|
|
613
|
+
* `copyActiveSelectionToClipboard` — the method coding-agent's Ctrl+X handler
|
|
614
|
+
* reaches (`handleCopyCommand` with `preferSelection`) — so the clean copy
|
|
615
|
+
* answers exactly the copies that key performs. They share one patch rather
|
|
616
|
+
* than taking a key each because `installPrototypePatch` holds exactly one
|
|
617
|
+
* behaviour per adapter key: a second registration under `mouse-copy` would
|
|
618
|
+
* silently replace the first, and two different keys would leave the order
|
|
619
|
+
* they run in — and which of them gets to consume the call — implicit. Here
|
|
620
|
+
* the precedence is written down: the buffer copy answers first, and only
|
|
621
|
+
* when it declines does the clean copy or predecessor see the call.
|
|
622
|
+
*
|
|
623
|
+
* `selectionHint` owns no state at all: it records the config reader and the
|
|
624
|
+
* clean-copy flags so `activeSelectionHintText()` can derive the hint on
|
|
625
|
+
* demand from the live renderer. Either clean-copy feature can be off; with
|
|
626
|
+
* neither available the clipboard is Pi's own bytes, exactly as before.
|
|
625
627
|
*/
|
|
626
628
|
function installCopying(
|
|
627
629
|
prototype: MouseCapablePrototype,
|
|
@@ -631,81 +633,40 @@ function installCopying(
|
|
|
631
633
|
const bufferCopy = features.has("editorBufferCopy");
|
|
632
634
|
const cleanCopy = features.has("transcriptCleanCopy");
|
|
633
635
|
const rangeDelete = features.has("editorClickToCaret");
|
|
634
|
-
const
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
if (state) activeState = state;
|
|
638
|
-
|
|
639
|
-
// Set while this module is driving `copySelectionToClipboard` itself (the
|
|
640
|
-
// ctrl+c path below), so the `mouse-copy` patch performs the copy instead of
|
|
641
|
-
// re-arming the state it is itself in the middle of clearing.
|
|
642
|
-
let performingRealCopy = false;
|
|
636
|
+
const hint = features.has("selectionHint");
|
|
637
|
+
const previousHint = activeHint;
|
|
638
|
+
if (hint) activeHint = { getConfig: deps.getConfig, bufferCopy, cleanCopy };
|
|
643
639
|
|
|
644
640
|
const cleanups: Array<() => void> = [];
|
|
645
641
|
|
|
646
|
-
// Only installed when a feature that answers `
|
|
647
|
-
// on. With just `editorClickToCaret` the method is
|
|
648
|
-
// Pi that has moved it must not make this install
|
|
649
|
-
|
|
642
|
+
// Only installed when a feature that answers `copyActiveSelectionToClipboard`
|
|
643
|
+
// is on. With just `editorClickToCaret` or `selectionHint` the method is
|
|
644
|
+
// not ours to touch, and a Pi that has moved it must not make this install
|
|
645
|
+
// throw.
|
|
646
|
+
if (bufferCopy || cleanCopy) {
|
|
650
647
|
cleanups.push(
|
|
651
648
|
installPrototypePatch(
|
|
652
649
|
prototype,
|
|
653
|
-
"
|
|
650
|
+
"copyActiveSelectionToClipboard",
|
|
654
651
|
"mouse-copy",
|
|
655
652
|
({ predecessor, receiver, args }) => {
|
|
656
653
|
const typedReceiver = receiver as MouseCapablePrototype;
|
|
657
654
|
const config = deps.getConfig();
|
|
658
|
-
if (
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
config.mouse.transcriptCleanCopy &&
|
|
666
|
-
copyTranscriptSelection(typedReceiver, config)
|
|
667
|
-
) {
|
|
668
|
-
return undefined;
|
|
669
|
-
}
|
|
670
|
-
return Reflect.apply(predecessor, receiver, args);
|
|
655
|
+
if (bufferCopy && copyEditorSelection(typedReceiver, config)) return true;
|
|
656
|
+
if (
|
|
657
|
+
cleanCopy &&
|
|
658
|
+
config.mouse.transcriptCleanCopy &&
|
|
659
|
+
copyTranscriptSelection(typedReceiver, config)
|
|
660
|
+
) {
|
|
661
|
+
return true;
|
|
671
662
|
}
|
|
672
|
-
|
|
673
|
-
if (!bounds) {
|
|
674
|
-
// A collapsed or empty selection (e.g. a plain click after a prior
|
|
675
|
-
// drag) still reaches this call — Pi runs it unconditionally on
|
|
676
|
-
// every release and relies on its own `if (!selection) return;`
|
|
677
|
-
// guard. Any stale arm from an earlier selection must not survive
|
|
678
|
-
// this: left in place, it would make a later ctrl+c consume the
|
|
679
|
-
// key for a no-op copy instead of falling through to interrupt.
|
|
680
|
-
if (state.pending) {
|
|
681
|
-
state.clear();
|
|
682
|
-
typedReceiver.requestRender();
|
|
683
|
-
}
|
|
684
|
-
return undefined;
|
|
685
|
-
}
|
|
686
|
-
const pendingText = pendingSelectionText(
|
|
687
|
-
typedReceiver,
|
|
688
|
-
config,
|
|
689
|
-
bounds,
|
|
690
|
-
bufferCopy,
|
|
691
|
-
cleanCopy,
|
|
692
|
-
);
|
|
693
|
-
state.arm(
|
|
694
|
-
pendingText.text.length,
|
|
695
|
-
// An editor selection cannot grow past the visible window —
|
|
696
|
-
// there is no drag-scroll — so the hint for one points at the
|
|
697
|
-
// external editor, the way to act on the whole draft. ""
|
|
698
|
-
// (unbound) simply shows no suffix.
|
|
699
|
-
pendingText.inEditor ? keyTextFor(EXTERNAL_EDITOR_KEYBINDING) : undefined,
|
|
700
|
-
);
|
|
701
|
-
typedReceiver.requestRender();
|
|
702
|
-
return undefined;
|
|
663
|
+
return Reflect.apply(predecessor, receiver, args);
|
|
703
664
|
},
|
|
704
665
|
),
|
|
705
666
|
);
|
|
706
667
|
}
|
|
707
668
|
|
|
708
|
-
if (!
|
|
669
|
+
if (!hint && !rangeDelete) {
|
|
709
670
|
return () => {
|
|
710
671
|
for (const cleanup of cleanups) cleanup();
|
|
711
672
|
};
|
|
@@ -718,58 +679,27 @@ function installCopying(
|
|
|
718
679
|
"mouse-viewport-input",
|
|
719
680
|
({ predecessor, receiver, args }) => {
|
|
720
681
|
const data = args[0];
|
|
682
|
+
const typedReceiver = receiver as MouseCapablePrototype;
|
|
721
683
|
// Keystrokes and mouse events both land here, so this is the one
|
|
722
684
|
// place that sees every draft change; keep the outgrew-the-box
|
|
723
|
-
// hint current with it
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
//
|
|
728
|
-
// must never regress is a bare ctrl+c with nothing pending reaching
|
|
729
|
-
// Pi and interrupting; evaluating its branch before any other means
|
|
730
|
-
// no feature added to this method later can intercept it by being
|
|
731
|
-
// installed in a different order. `isRangeDeleteKey` refuses ctrl+c
|
|
732
|
-
// and ctrl+d outright as well, so the guarantee holds structurally
|
|
733
|
-
// from both directions rather than by ordering alone.
|
|
734
|
-
if (state && isCtrlC(data) && state.pending) {
|
|
735
|
-
const typedReceiver = receiver as MouseCapablePrototype;
|
|
736
|
-
// `state.pending` can be stale: Pi clears its own selection
|
|
737
|
-
// through paths this module never sees (e.g. starting a new
|
|
738
|
-
// drag overwrites `selectionAnchor`/`selectionFocus` directly,
|
|
739
|
-
// with no call to `copySelectionToClipboard`). Re-read the real
|
|
740
|
-
// bounds before deciding: a copy that would be a no-op must not
|
|
741
|
-
// consume ctrl+c, or an in-flight interrupt gets swallowed for
|
|
742
|
-
// nothing.
|
|
743
|
-
if (!typedReceiver.getSelectionBounds()) {
|
|
744
|
-
state.clear();
|
|
745
|
-
typedReceiver.requestRender();
|
|
746
|
-
return Reflect.apply(predecessor, receiver, args);
|
|
747
|
-
}
|
|
748
|
-
performingRealCopy = true;
|
|
749
|
-
try {
|
|
750
|
-
copyWithNotice(typedReceiver, deps.getConfig().mouse.copyNotice);
|
|
751
|
-
} finally {
|
|
752
|
-
performingRealCopy = false;
|
|
753
|
-
}
|
|
754
|
-
state.clear();
|
|
755
|
-
typedReceiver.requestRender();
|
|
756
|
-
return { consume: true };
|
|
757
|
-
}
|
|
758
|
-
// ---- Branch 2: backspace and delete, `editorClickToCaret`'s. ----
|
|
685
|
+
// hint current with it, and keep the selection hint's renderer
|
|
686
|
+
// reference pointed at the live instance.
|
|
687
|
+
activeReceiver = typedReceiver;
|
|
688
|
+
refreshExternalEditorHint(typedReceiver, deps.getConfig());
|
|
689
|
+
// ---- Backspace and delete, `editorClickToCaret`'s. ------------
|
|
759
690
|
//
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
//
|
|
691
|
+
// Range delete consumes the key only when it really removed
|
|
692
|
+
// something; every other backspace falls through and goes on
|
|
693
|
+
// deleting one character, which is the behaviour it must not
|
|
694
|
+
// break. `isRangeDeleteKey` refuses ctrl+c and ctrl+d outright,
|
|
695
|
+
// so Pi's interrupt and exit chords can never be swallowed by
|
|
696
|
+
// this branch.
|
|
765
697
|
if (rangeDelete && isRangeDeleteKey(data)) {
|
|
766
|
-
const typedReceiver = receiver as MouseCapablePrototype;
|
|
767
698
|
if (deleteSelectedRange(typedReceiver, deps.getConfig())) {
|
|
768
699
|
// Pi's `handleInput` returns as soon as a listener consumes,
|
|
769
700
|
// so the focused editor never sees the key and Pi never
|
|
770
701
|
// reaches its own `requestImmediateRender` (`tui.js:620`).
|
|
771
702
|
// Redrawing the changed draft is this branch's job.
|
|
772
|
-
state?.clear();
|
|
773
703
|
typedReceiver.requestRender();
|
|
774
704
|
return { consume: true };
|
|
775
705
|
}
|
|
@@ -781,47 +711,9 @@ function installCopying(
|
|
|
781
711
|
|
|
782
712
|
return () => {
|
|
783
713
|
for (const cleanup of cleanups) cleanup();
|
|
784
|
-
if (activeState === state) activeState = previousState;
|
|
785
714
|
};
|
|
786
715
|
}
|
|
787
716
|
|
|
788
|
-
/**
|
|
789
|
-
* Installs `pathAwareWords` on `getWordSelection`.
|
|
790
|
-
*
|
|
791
|
-
* The receiver's own `getSelectionSourceLine` gives the raw line under the
|
|
792
|
-
* point; `stripTerminalSequences` is applied the same way predecessor itself
|
|
793
|
-
* applies it (see `getWordSelection` in
|
|
794
|
-
* `node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js`), so
|
|
795
|
-
* `wordRangeAt` sees the same plain text Pi's own segmenter would. `col` is
|
|
796
|
-
* the only thing the wrapper changes on the way out — `row`, `scrollView`
|
|
797
|
-
* and any other point field ride through unmodified, which is what keeps a
|
|
798
|
-
* scroll-view selection landing in the right place.
|
|
799
|
-
*/
|
|
800
|
-
function installPathAwareWords(
|
|
801
|
-
prototype: MouseCapablePrototype,
|
|
802
|
-
deps: InstallMouseDeps,
|
|
803
|
-
): () => void {
|
|
804
|
-
return installPrototypePatch(
|
|
805
|
-
prototype,
|
|
806
|
-
"getWordSelection",
|
|
807
|
-
"mouse-word-selection",
|
|
808
|
-
({ predecessor, receiver, args }) => {
|
|
809
|
-
if (!deps.getConfig().mouse.pathAwareWords) {
|
|
810
|
-
return Reflect.apply(predecessor, receiver, args);
|
|
811
|
-
}
|
|
812
|
-
const typedReceiver = receiver as MouseCapablePrototype;
|
|
813
|
-
const point = args[0] as SelectionPoint;
|
|
814
|
-
const line = stripTerminalSequences(typedReceiver.getSelectionSourceLine(point));
|
|
815
|
-
const range = wordRangeAt(line, point.col);
|
|
816
|
-
if (!range) return Reflect.apply(predecessor, receiver, args);
|
|
817
|
-
return {
|
|
818
|
-
start: { ...point, col: range.start },
|
|
819
|
-
end: { ...point, col: range.end, boundary: true },
|
|
820
|
-
};
|
|
821
|
-
},
|
|
822
|
-
);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
717
|
/**
|
|
826
718
|
* A left-button press: not a release, not a drag, not another button.
|
|
827
719
|
*
|
|
@@ -1094,16 +986,13 @@ export function installMouse(prototype: object, deps: InstallMouseDeps): () => v
|
|
|
1094
986
|
// below: `pi-toolbox` publishing which rows it drew a border on, at which
|
|
1095
987
|
// columns, so Starline reads a fact instead of inferring one.
|
|
1096
988
|
if (
|
|
1097
|
-
enabled.has("
|
|
989
|
+
enabled.has("selectionHint") ||
|
|
1098
990
|
enabled.has("editorBufferCopy") ||
|
|
1099
991
|
enabled.has("editorClickToCaret") ||
|
|
1100
992
|
enabled.has("transcriptCleanCopy")
|
|
1101
993
|
) {
|
|
1102
994
|
cleanups.push(installCopying(typedPrototype, deps, enabled));
|
|
1103
995
|
}
|
|
1104
|
-
if (enabled.has("pathAwareWords")) {
|
|
1105
|
-
cleanups.push(installPathAwareWords(typedPrototype, deps));
|
|
1106
|
-
}
|
|
1107
996
|
if (enabled.has("clickToExpandTools") || enabled.has("editorClickToCaret")) {
|
|
1108
997
|
cleanups.push(installSelectionMouse(typedPrototype, deps, enabled));
|
|
1109
998
|
}
|
|
@@ -1114,6 +1003,8 @@ export function installMouse(prototype: object, deps: InstallMouseDeps): () => v
|
|
|
1114
1003
|
if (cleanups.length === 0) return () => {};
|
|
1115
1004
|
return () => {
|
|
1116
1005
|
externalEditorHint = null;
|
|
1006
|
+
activeReceiver = undefined;
|
|
1007
|
+
activeHint = undefined;
|
|
1117
1008
|
for (const cleanup of cleanups) cleanup();
|
|
1118
1009
|
};
|
|
1119
1010
|
}
|