@oh-my-pi/pi-tui 18.1.22 → 18.2.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/CHANGELOG.md +32 -0
- package/THIRD-PARTY-NOTICES.txt +54 -25
- package/dist/types/autocomplete.d.ts +2 -1
- package/dist/types/components/editor.d.ts +16 -0
- package/dist/types/components/image.d.ts +49 -7
- package/dist/types/terminal-capabilities.d.ts +24 -0
- package/dist/types/terminal-multiplexer.d.ts +17 -1
- package/dist/types/terminal.d.ts +17 -2
- package/package.json +3 -3
- package/src/autocomplete.ts +48 -19
- package/src/components/editor.ts +95 -54
- package/src/components/image.ts +244 -45
- package/src/terminal-capabilities.ts +104 -38
- package/src/terminal-multiplexer.ts +31 -10
- package/src/terminal.ts +29 -12
- package/src/tui.ts +404 -136
package/src/components/editor.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getProjectDir
|
|
1
|
+
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
2
|
+
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
2
3
|
import {
|
|
3
4
|
type AutocompleteItem,
|
|
4
5
|
type AutocompleteProvider,
|
|
@@ -630,6 +631,7 @@ export class Editor implements Component, Focusable {
|
|
|
630
631
|
onSubmit?: (text: string) => void | Promise<void>;
|
|
631
632
|
onAltEnter?: (text: string) => void;
|
|
632
633
|
onChange?: (text: string) => void;
|
|
634
|
+
#textRevision = 0;
|
|
633
635
|
/** Called for a "marker-sized" paste — the point where the editor would otherwise collapse it
|
|
634
636
|
* into a `[Paste #N]` token (> 10 lines or > 1000 characters). Return `true` to intercept:
|
|
635
637
|
* the editor inserts nothing and records no undo state, leaving insertion to the host (e.g. a
|
|
@@ -962,9 +964,7 @@ export class Editor implements Component, Focusable {
|
|
|
962
964
|
this.#state.cursorLine = this.#state.lines.length - 1;
|
|
963
965
|
this.#setCursorCol(this.#state.lines[this.#state.cursorLine]?.length || 0);
|
|
964
966
|
}
|
|
965
|
-
|
|
966
|
-
this.onChange(this.getText());
|
|
967
|
-
}
|
|
967
|
+
this.#notifyChange();
|
|
968
968
|
}
|
|
969
969
|
|
|
970
970
|
invalidate(): void {
|
|
@@ -1661,9 +1661,7 @@ export class Editor implements Component, Focusable {
|
|
|
1661
1661
|
this.#cancelAutocomplete();
|
|
1662
1662
|
this.onAutocompleteUpdate?.();
|
|
1663
1663
|
|
|
1664
|
-
|
|
1665
|
-
this.onChange(this.getText());
|
|
1666
|
-
}
|
|
1664
|
+
this.#notifyChange();
|
|
1667
1665
|
|
|
1668
1666
|
result.onApplied?.();
|
|
1669
1667
|
|
|
@@ -1721,7 +1719,14 @@ export class Editor implements Component, Focusable {
|
|
|
1721
1719
|
} else {
|
|
1722
1720
|
if (selected && this.#autocompleteProvider) {
|
|
1723
1721
|
const shouldChainSlashCommandAutocomplete = this.#isSlashCommandNameAutocompleteSelection();
|
|
1724
|
-
|
|
1722
|
+
// Directory chaining exists so an @ mention can be browsed deeper
|
|
1723
|
+
// without retyping the path. It must not apply to a slash
|
|
1724
|
+
// command's directory argument: there the accepted value is the
|
|
1725
|
+
// whole argument, so chaining reopens the popup on the directory's
|
|
1726
|
+
// children, the command never submits, and every further Enter
|
|
1727
|
+
// descends another level (#12107).
|
|
1728
|
+
const shouldChainDirectoryCompletion =
|
|
1729
|
+
this.#autocompletePrefix.startsWith("@") && isDirectoryCompletionValue(selected.value);
|
|
1725
1730
|
const result = this.#autocompleteProvider.applyCompletion(
|
|
1726
1731
|
this.#state.lines,
|
|
1727
1732
|
this.#state.cursorLine,
|
|
@@ -1737,9 +1742,7 @@ export class Editor implements Component, Focusable {
|
|
|
1737
1742
|
this.#cancelAutocomplete();
|
|
1738
1743
|
this.onAutocompleteUpdate?.();
|
|
1739
1744
|
|
|
1740
|
-
|
|
1741
|
-
this.onChange(this.getText());
|
|
1742
|
-
}
|
|
1745
|
+
this.#notifyChange();
|
|
1743
1746
|
|
|
1744
1747
|
result.onApplied?.();
|
|
1745
1748
|
if (shouldChainDirectoryCompletion) {
|
|
@@ -2168,7 +2171,7 @@ export class Editor implements Component, Focusable {
|
|
|
2168
2171
|
#afterVimEdit(): void {
|
|
2169
2172
|
this.#historyIndex = -1;
|
|
2170
2173
|
this.#resetKillSequence();
|
|
2171
|
-
this
|
|
2174
|
+
this.#notifyChange();
|
|
2172
2175
|
}
|
|
2173
2176
|
|
|
2174
2177
|
/**
|
|
@@ -2389,6 +2392,16 @@ export class Editor implements Component, Focusable {
|
|
|
2389
2392
|
return this.#state.lines.join("\n");
|
|
2390
2393
|
}
|
|
2391
2394
|
|
|
2395
|
+
/** Monotonic buffer-content revision for render caches. Cursor-only movement does not advance it. */
|
|
2396
|
+
get textRevision(): number {
|
|
2397
|
+
return this.#textRevision;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
#notifyChange(text?: string): void {
|
|
2401
|
+
this.#textRevision++;
|
|
2402
|
+
this.onChange?.(text ?? this.getText());
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2392
2405
|
/** Whether the buffer text equals `value`, without `getText()`'s full join —
|
|
2393
2406
|
* O(1) for the hot per-keystroke probes against short single-line values. */
|
|
2394
2407
|
textEquals(value: string): boolean {
|
|
@@ -2437,6 +2450,24 @@ export class Editor implements Component, Focusable {
|
|
|
2437
2450
|
});
|
|
2438
2451
|
}
|
|
2439
2452
|
|
|
2453
|
+
/** Collapse the typed span `[start, end)` on `line` into the atom `label` (expanding back
|
|
2454
|
+
* to `expansion` on submit). A cursor at or past the span keeps its position relative to
|
|
2455
|
+
* the span end; a cursor inside it lands after the label. */
|
|
2456
|
+
collapseToAtom(line: number, start: number, end: number, label: string, expansion: string): void {
|
|
2457
|
+
const text = this.#state.lines[line];
|
|
2458
|
+
if (text === undefined || start < 0 || end > text.length || start >= end) return;
|
|
2459
|
+
this.#resetKillSequence();
|
|
2460
|
+
this.#recordUndoState();
|
|
2461
|
+
this.registerAtom(label, expansion);
|
|
2462
|
+
this.#state.lines[line] = text.slice(0, start) + label + text.slice(end);
|
|
2463
|
+
if (this.#state.cursorLine === line && this.#state.cursorCol > start) {
|
|
2464
|
+
const col = this.#state.cursorCol;
|
|
2465
|
+
this.#setCursorCol(col >= end ? col - (end - start) + label.length : start + label.length);
|
|
2466
|
+
}
|
|
2467
|
+
this.#lastAction = null;
|
|
2468
|
+
this.#notifyChange();
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2440
2471
|
/** Drop every registered atom expansion (draft cleared or replaced by the host). */
|
|
2441
2472
|
clearAtoms(): void {
|
|
2442
2473
|
this.#atoms.clear();
|
|
@@ -2474,6 +2505,29 @@ export class Editor implements Component, Focusable {
|
|
|
2474
2505
|
this.#moveToMessageEnd();
|
|
2475
2506
|
}
|
|
2476
2507
|
|
|
2508
|
+
/** The `tui.editor.deleteCharForward` operation, callable by hosts that resolve the chord
|
|
2509
|
+
* themselves rather than redispatching the raw key (see CustomEditor's exit-chord overlap).
|
|
2510
|
+
* Mirrors the transient state the key dispatch tears down before this action so the two
|
|
2511
|
+
* cannot diverge: a pending character jump is cancelled by any other key, and an open
|
|
2512
|
+
* spelling-assist popup is dismissed by anything that is not one of its accept keys (its
|
|
2513
|
+
* debounced refresh skips assist mode, so a surviving list would hang around forever).
|
|
2514
|
+
* While Vim owns the buffer (Normal or Visual) the operation is Vim's `x` — deleting the
|
|
2515
|
+
* selection and returning to Normal in Visual mode, the grapheme under the cursor
|
|
2516
|
+
* otherwise. Only Insert mode and Vim-off editors delete straight through. */
|
|
2517
|
+
deleteCharForward(): void {
|
|
2518
|
+
this.#jumpMode = null;
|
|
2519
|
+
if (this.#autocompleteState === "assist") {
|
|
2520
|
+
this.#cancelAutocomplete();
|
|
2521
|
+
this.onAutocompleteUpdate?.();
|
|
2522
|
+
}
|
|
2523
|
+
const vim = this.#vim;
|
|
2524
|
+
if (vim !== null && vim.mode !== "insert") {
|
|
2525
|
+
this.#runVimKey("x", vim);
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
this.#handleForwardDelete();
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2477
2531
|
/**
|
|
2478
2532
|
* Undo the last meaningful edit while ignoring transient text that is still present at the cursor.
|
|
2479
2533
|
* Used for command-like autocomplete actions whose typed trigger should not count as the edit being undone.
|
|
@@ -2517,9 +2571,7 @@ export class Editor implements Component, Focusable {
|
|
|
2517
2571
|
}
|
|
2518
2572
|
|
|
2519
2573
|
if (this.#undoStack.length === 0) {
|
|
2520
|
-
|
|
2521
|
-
this.onChange(this.getText());
|
|
2522
|
-
}
|
|
2574
|
+
this.#notifyChange();
|
|
2523
2575
|
return;
|
|
2524
2576
|
}
|
|
2525
2577
|
|
|
@@ -2566,9 +2618,7 @@ export class Editor implements Component, Focusable {
|
|
|
2566
2618
|
line.slice(0, this.#state.cursorCol - removable) + line.slice(this.#state.cursorCol);
|
|
2567
2619
|
this.#setCursorCol(this.#state.cursorCol - removable);
|
|
2568
2620
|
this.#lastAction = null;
|
|
2569
|
-
|
|
2570
|
-
this.onChange(this.getText());
|
|
2571
|
-
}
|
|
2621
|
+
this.#notifyChange();
|
|
2572
2622
|
}
|
|
2573
2623
|
|
|
2574
2624
|
/** Code units of the current volatile speech-to-text preview (see {@link setVolatileText}). */
|
|
@@ -2585,7 +2635,7 @@ export class Editor implements Component, Focusable {
|
|
|
2585
2635
|
if (text) this.#insertTextAtCursor(text);
|
|
2586
2636
|
});
|
|
2587
2637
|
this.#volatileTextLen = text.length;
|
|
2588
|
-
if (!text
|
|
2638
|
+
if (!text) this.#notifyChange();
|
|
2589
2639
|
}
|
|
2590
2640
|
|
|
2591
2641
|
/** Remove the current volatile preview without committing it. */
|
|
@@ -2593,7 +2643,7 @@ export class Editor implements Component, Focusable {
|
|
|
2593
2643
|
if (this.#volatileTextLen === 0) return;
|
|
2594
2644
|
this.#withUndoSuspended(() => this.#deleteCharsBeforeCursor(this.#volatileTextLen));
|
|
2595
2645
|
this.#volatileTextLen = 0;
|
|
2596
|
-
|
|
2646
|
+
this.#notifyChange();
|
|
2597
2647
|
}
|
|
2598
2648
|
|
|
2599
2649
|
/** Drop any volatile preview, then insert `text` as a single undoable edit. */
|
|
@@ -2602,7 +2652,7 @@ export class Editor implements Component, Focusable {
|
|
|
2602
2652
|
this.#withUndoSuspended(() => this.#deleteCharsBeforeCursor(this.#volatileTextLen));
|
|
2603
2653
|
this.#volatileTextLen = 0;
|
|
2604
2654
|
if (text) this.#insertTextAtCursor(text);
|
|
2605
|
-
else
|
|
2655
|
+
else this.#notifyChange();
|
|
2606
2656
|
}
|
|
2607
2657
|
|
|
2608
2658
|
/** Delete `count` UTF-16 code units immediately before the cursor, crossing line
|
|
@@ -2662,7 +2712,7 @@ export class Editor implements Component, Focusable {
|
|
|
2662
2712
|
const after = line.slice(this.#state.cursorCol);
|
|
2663
2713
|
this.#state.lines[this.#state.cursorLine] = before + replacement.insert + after;
|
|
2664
2714
|
this.#setCursorCol(before.length + replacement.insert.length);
|
|
2665
|
-
this
|
|
2715
|
+
this.#notifyChange();
|
|
2666
2716
|
if (this.#autocompleteState) {
|
|
2667
2717
|
this.#cancelAutocomplete();
|
|
2668
2718
|
this.onAutocompleteUpdate?.();
|
|
@@ -2688,9 +2738,7 @@ export class Editor implements Component, Focusable {
|
|
|
2688
2738
|
this.#state.lines[this.#state.cursorLine] = before + char + after;
|
|
2689
2739
|
this.#setCursorCol(this.#state.cursorCol + char.length);
|
|
2690
2740
|
|
|
2691
|
-
|
|
2692
|
-
this.onChange(this.getText());
|
|
2693
|
-
}
|
|
2741
|
+
this.#notifyChange();
|
|
2694
2742
|
|
|
2695
2743
|
// Synchronous inline replacement (e.g. emoji shortcodes `:joy:` → 😂).
|
|
2696
2744
|
// Runs before autocomplete trigger so the popup doesn't briefly chase a
|
|
@@ -2743,6 +2791,10 @@ export class Editor implements Component, Focusable {
|
|
|
2743
2791
|
else if (char === "#") {
|
|
2744
2792
|
this.#tryTriggerAutocomplete();
|
|
2745
2793
|
}
|
|
2794
|
+
// Argument providers may expose candidates only after a separator.
|
|
2795
|
+
else if (char === " " && this.#isInSubmittedSlashCommandContext()) {
|
|
2796
|
+
this.#tryTriggerAutocomplete();
|
|
2797
|
+
}
|
|
2746
2798
|
// Also auto-trigger when typing letters/path chars in a completable context
|
|
2747
2799
|
else if (/[a-zA-Z0-9.\-_/]/.test(char)) {
|
|
2748
2800
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
@@ -2899,9 +2951,7 @@ export class Editor implements Component, Focusable {
|
|
|
2899
2951
|
this.#state.cursorLine++;
|
|
2900
2952
|
this.#setCursorCol(0);
|
|
2901
2953
|
|
|
2902
|
-
|
|
2903
|
-
this.onChange(this.getText());
|
|
2904
|
-
}
|
|
2954
|
+
this.#notifyChange();
|
|
2905
2955
|
}
|
|
2906
2956
|
|
|
2907
2957
|
#shouldSubmitOnBackslashEnter(data: string, kb: KeybindingsManager): boolean {
|
|
@@ -2926,7 +2976,7 @@ export class Editor implements Component, Focusable {
|
|
|
2926
2976
|
this.#scrollOffset = 0;
|
|
2927
2977
|
this.#undoStack.length = 0;
|
|
2928
2978
|
|
|
2929
|
-
|
|
2979
|
+
this.#notifyChange("");
|
|
2930
2980
|
if (this.onSubmit) this.onSubmit(result);
|
|
2931
2981
|
}
|
|
2932
2982
|
|
|
@@ -3029,9 +3079,7 @@ export class Editor implements Component, Focusable {
|
|
|
3029
3079
|
this.#setCursorCol(previousLine.length);
|
|
3030
3080
|
}
|
|
3031
3081
|
|
|
3032
|
-
|
|
3033
|
-
this.onChange(this.getText());
|
|
3034
|
-
}
|
|
3082
|
+
this.#notifyChange();
|
|
3035
3083
|
|
|
3036
3084
|
// Update or re-trigger autocomplete after backspace
|
|
3037
3085
|
if (this.#autocompleteState) {
|
|
@@ -3205,9 +3253,7 @@ export class Editor implements Component, Focusable {
|
|
|
3205
3253
|
this.#preferredVisualCol = null;
|
|
3206
3254
|
Object.assign(this.#state, snapshot);
|
|
3207
3255
|
|
|
3208
|
-
|
|
3209
|
-
this.onChange(this.getText());
|
|
3210
|
-
}
|
|
3256
|
+
this.#notifyChange();
|
|
3211
3257
|
|
|
3212
3258
|
if (this.#autocompleteState) {
|
|
3213
3259
|
this.#debouncedUpdateAutocomplete();
|
|
@@ -3295,9 +3341,7 @@ export class Editor implements Component, Focusable {
|
|
|
3295
3341
|
this.#setCursorCol((lines[lines.length - 1] || "").length);
|
|
3296
3342
|
}
|
|
3297
3343
|
|
|
3298
|
-
|
|
3299
|
-
this.onChange(this.getText());
|
|
3300
|
-
}
|
|
3344
|
+
this.#notifyChange();
|
|
3301
3345
|
this.#retriggerAutocompleteAtCursor();
|
|
3302
3346
|
}
|
|
3303
3347
|
|
|
@@ -3402,9 +3446,7 @@ export class Editor implements Component, Focusable {
|
|
|
3402
3446
|
|
|
3403
3447
|
this.#recordKill(deletedText, "backward");
|
|
3404
3448
|
|
|
3405
|
-
|
|
3406
|
-
this.onChange(this.getText());
|
|
3407
|
-
}
|
|
3449
|
+
this.#notifyChange();
|
|
3408
3450
|
this.#retriggerAutocompleteAtCursor();
|
|
3409
3451
|
}
|
|
3410
3452
|
|
|
@@ -3434,9 +3476,7 @@ export class Editor implements Component, Focusable {
|
|
|
3434
3476
|
|
|
3435
3477
|
this.#recordKill(deletedText, "forward");
|
|
3436
3478
|
|
|
3437
|
-
|
|
3438
|
-
this.onChange(this.getText());
|
|
3439
|
-
}
|
|
3479
|
+
this.#notifyChange();
|
|
3440
3480
|
this.#retriggerAutocompleteAtCursor();
|
|
3441
3481
|
}
|
|
3442
3482
|
|
|
@@ -3469,9 +3509,7 @@ export class Editor implements Component, Focusable {
|
|
|
3469
3509
|
this.#recordKill(deletedText, "backward");
|
|
3470
3510
|
}
|
|
3471
3511
|
|
|
3472
|
-
|
|
3473
|
-
this.onChange(this.getText());
|
|
3474
|
-
}
|
|
3512
|
+
this.#notifyChange();
|
|
3475
3513
|
this.#retriggerAutocompleteAtCursor();
|
|
3476
3514
|
}
|
|
3477
3515
|
|
|
@@ -3501,9 +3539,7 @@ export class Editor implements Component, Focusable {
|
|
|
3501
3539
|
this.#recordKill(deletedText, "forward");
|
|
3502
3540
|
}
|
|
3503
3541
|
|
|
3504
|
-
|
|
3505
|
-
this.onChange(this.getText());
|
|
3506
|
-
}
|
|
3542
|
+
this.#notifyChange();
|
|
3507
3543
|
this.#retriggerAutocompleteAtCursor();
|
|
3508
3544
|
}
|
|
3509
3545
|
|
|
@@ -3541,9 +3577,14 @@ export class Editor implements Component, Focusable {
|
|
|
3541
3577
|
this.#state.lines.splice(this.#state.cursorLine + 1, 1);
|
|
3542
3578
|
}
|
|
3543
3579
|
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3580
|
+
// Deleting the final grapheme can leave the cursor one past the end of the line, which
|
|
3581
|
+
// Normal mode never allows (it rests *on* a grapheme). Vim's own `x` clamps via
|
|
3582
|
+
// #applyVimCommands; callers that invoke this operation directly — hosts resolving a
|
|
3583
|
+
// chord themselves, or a key bound to deleteCharForward that Vim does not map — get the
|
|
3584
|
+
// same treatment here so the cursor can't sit off the buffer.
|
|
3585
|
+
this.#clampVimCursor();
|
|
3586
|
+
|
|
3587
|
+
this.#notifyChange();
|
|
3547
3588
|
|
|
3548
3589
|
// Update or re-trigger autocomplete after forward delete
|
|
3549
3590
|
if (this.#autocompleteState) {
|
|
@@ -4023,7 +4064,7 @@ export class Editor implements Component, Focusable {
|
|
|
4023
4064
|
this.#lastAction = null;
|
|
4024
4065
|
this.#cancelAutocomplete();
|
|
4025
4066
|
this.onAutocompleteUpdate?.();
|
|
4026
|
-
this
|
|
4067
|
+
this.#notifyChange();
|
|
4027
4068
|
}
|
|
4028
4069
|
async #handleSlashCommandCompletion(): Promise<void> {
|
|
4029
4070
|
await this.#tryTriggerAutocomplete();
|