@earendil-works/pi-tui 0.79.0 → 0.79.2
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/dist/autocomplete.d.ts +2 -0
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/editor.d.ts +6 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +87 -39
- package/dist/components/editor.js.map +1 -1
- package/dist/components/markdown.d.ts +2 -1
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +11 -1
- package/dist/components/markdown.js.map +1 -1
- package/dist/fuzzy.d.ts +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +2 -2
- package/dist/fuzzy.js.map +1 -1
- package/dist/tui.d.ts +2 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +72 -16
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +43 -15
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { decodePrintableKey, matchesKey } from "../keys.js";
|
|
|
3
3
|
import { KillRing } from "../kill-ring.js";
|
|
4
4
|
import { CURSOR_MARKER } from "../tui.js";
|
|
5
5
|
import { UndoStack } from "../undo-stack.js";
|
|
6
|
-
import { getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils.js";
|
|
6
|
+
import { cjkBreakRegex, getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, truncateToWidth, visibleWidth, } from "../utils.js";
|
|
7
7
|
import { findWordBackward, findWordForward } from "../word-navigation.js";
|
|
8
8
|
import { SelectList } from "./select-list.js";
|
|
9
9
|
const graphemeSegmenter = getGraphemeSegmenter();
|
|
@@ -140,14 +140,23 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
|
|
|
140
140
|
}
|
|
141
141
|
// Advance.
|
|
142
142
|
currentWidth += gWidth;
|
|
143
|
-
// Record wrap opportunity: whitespace followed by non-whitespace
|
|
144
|
-
//
|
|
145
|
-
//
|
|
143
|
+
// Record wrap opportunity: whitespace followed by non-whitespace
|
|
144
|
+
// (multiple spaces join; the break point is after the last space),
|
|
145
|
+
// or at a boundary where either side is CJK (CJK allows breaking
|
|
146
|
+
// between any adjacent characters).
|
|
146
147
|
const next = segments[i + 1];
|
|
147
148
|
if (isWs && next && (isPasteMarker(next.segment) || !isWhitespaceChar(next.segment))) {
|
|
148
149
|
wrapOppIndex = next.index;
|
|
149
150
|
wrapOppWidth = currentWidth;
|
|
150
151
|
}
|
|
152
|
+
else if (!isWs && next && !isWhitespaceChar(next.segment)) {
|
|
153
|
+
const isCjk = !isPasteMarker(grapheme) && cjkBreakRegex.test(grapheme);
|
|
154
|
+
const nextIsCjk = !isPasteMarker(next.segment) && cjkBreakRegex.test(next.segment);
|
|
155
|
+
if (isCjk || nextIsCjk) {
|
|
156
|
+
wrapOppIndex = next.index;
|
|
157
|
+
wrapOppWidth = currentWidth;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
151
160
|
}
|
|
152
161
|
// Push final chunk.
|
|
153
162
|
chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
|
|
@@ -158,6 +167,17 @@ const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
|
|
|
158
167
|
maxPrimaryColumnWidth: 32,
|
|
159
168
|
};
|
|
160
169
|
const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
|
|
170
|
+
const DEFAULT_AUTOCOMPLETE_TRIGGER_CHARACTERS = ["@", "#"];
|
|
171
|
+
function escapeCharacterClass(value) {
|
|
172
|
+
return value.replace(/[\\^$.*+?()[\]{}|-]/g, "\\$&");
|
|
173
|
+
}
|
|
174
|
+
function buildTriggerPattern(triggerCharacters) {
|
|
175
|
+
return new RegExp(`(?:^|[\\s])[${triggerCharacters.map(escapeCharacterClass).join("")}][^\\s]*$`);
|
|
176
|
+
}
|
|
177
|
+
function buildDebouncePattern(triggerCharacters) {
|
|
178
|
+
const escapedWithoutAt = triggerCharacters.filter((character) => character !== "@").map(escapeCharacterClass);
|
|
179
|
+
return new RegExp(`(?:^|[ \\t])(?:@(?:"[^"]*|[^\\s]*)|[${escapedWithoutAt.join("")}][^\\s]*)$`);
|
|
180
|
+
}
|
|
161
181
|
export class Editor {
|
|
162
182
|
state = {
|
|
163
183
|
lines: [""],
|
|
@@ -177,6 +197,9 @@ export class Editor {
|
|
|
177
197
|
borderColor;
|
|
178
198
|
// Autocomplete support
|
|
179
199
|
autocompleteProvider;
|
|
200
|
+
autocompleteTriggerCharacters = [...DEFAULT_AUTOCOMPLETE_TRIGGER_CHARACTERS];
|
|
201
|
+
autocompleteTriggerPattern = buildTriggerPattern(this.autocompleteTriggerCharacters);
|
|
202
|
+
autocompleteDebouncePattern = buildDebouncePattern(this.autocompleteTriggerCharacters);
|
|
180
203
|
autocompleteList;
|
|
181
204
|
autocompleteState = null;
|
|
182
205
|
autocompletePrefix = "";
|
|
@@ -195,6 +218,7 @@ export class Editor {
|
|
|
195
218
|
// Prompt history for up/down navigation
|
|
196
219
|
history = [];
|
|
197
220
|
historyIndex = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
|
|
221
|
+
historyDraft = null;
|
|
198
222
|
// Kill ring for Emacs-style kill/yank operations
|
|
199
223
|
killRing = new KillRing();
|
|
200
224
|
lastAction = null;
|
|
@@ -253,6 +277,7 @@ export class Editor {
|
|
|
253
277
|
setAutocompleteProvider(provider) {
|
|
254
278
|
this.cancelAutocomplete();
|
|
255
279
|
this.autocompleteProvider = provider;
|
|
280
|
+
this.setAutocompleteTriggerCharacters(provider.triggerCharacters ?? []);
|
|
256
281
|
}
|
|
257
282
|
/**
|
|
258
283
|
* Add a prompt to history for up/down arrow navigation.
|
|
@@ -271,9 +296,6 @@ export class Editor {
|
|
|
271
296
|
this.history.pop();
|
|
272
297
|
}
|
|
273
298
|
}
|
|
274
|
-
isEditorEmpty() {
|
|
275
|
-
return this.state.lines.length === 1 && this.state.lines[0] === "";
|
|
276
|
-
}
|
|
277
299
|
isOnFirstVisualLine() {
|
|
278
300
|
const visualLines = this.buildVisualLineMap(this.lastWidth);
|
|
279
301
|
const currentVisualLine = this.findCurrentVisualLine(visualLines);
|
|
@@ -294,16 +316,32 @@ export class Editor {
|
|
|
294
316
|
// Capture state when first entering history browsing mode
|
|
295
317
|
if (this.historyIndex === -1 && newIndex >= 0) {
|
|
296
318
|
this.pushUndoSnapshot();
|
|
319
|
+
this.historyDraft = structuredClone(this.state);
|
|
297
320
|
}
|
|
298
321
|
this.historyIndex = newIndex;
|
|
299
322
|
if (this.historyIndex === -1) {
|
|
300
|
-
|
|
301
|
-
this.
|
|
323
|
+
const draft = this.historyDraft;
|
|
324
|
+
this.historyDraft = null;
|
|
325
|
+
if (draft) {
|
|
326
|
+
this.state = draft;
|
|
327
|
+
this.preferredVisualCol = null;
|
|
328
|
+
this.snappedFromCursorCol = null;
|
|
329
|
+
this.scrollOffset = 0;
|
|
330
|
+
if (this.onChange)
|
|
331
|
+
this.onChange(this.getText());
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
this.setTextInternal("");
|
|
335
|
+
}
|
|
302
336
|
}
|
|
303
337
|
else {
|
|
304
338
|
this.setTextInternal(this.history[this.historyIndex] || "", direction === -1 ? "start" : "end");
|
|
305
339
|
}
|
|
306
340
|
}
|
|
341
|
+
exitHistoryBrowsing() {
|
|
342
|
+
this.historyIndex = -1;
|
|
343
|
+
this.historyDraft = null;
|
|
344
|
+
}
|
|
307
345
|
/** Internal setText that doesn't reset history state - used by navigateHistory */
|
|
308
346
|
setTextInternal(text, cursorPlacement = "end") {
|
|
309
347
|
const lines = text.split("\n");
|
|
@@ -368,8 +406,10 @@ export class Editor {
|
|
|
368
406
|
result.push(horizontal.repeat(width));
|
|
369
407
|
}
|
|
370
408
|
// Render each visible layout line
|
|
371
|
-
// Emit hardware cursor marker
|
|
372
|
-
|
|
409
|
+
// Emit hardware cursor marker when focused so TUI can position the
|
|
410
|
+
// hardware cursor for IME candidate-window placement even while
|
|
411
|
+
// autocomplete (e.g. slash-command menu) is visible.
|
|
412
|
+
const emitCursorMarker = this.focused;
|
|
373
413
|
for (const layoutLine of visibleLines) {
|
|
374
414
|
let displayText = layoutLine.text;
|
|
375
415
|
let lineVisibleWidth = visibleWidth(layoutLine.text);
|
|
@@ -616,10 +656,7 @@ export class Editor {
|
|
|
616
656
|
}
|
|
617
657
|
// Arrow key navigation (with history support)
|
|
618
658
|
if (kb.matches(data, "tui.editor.cursorUp")) {
|
|
619
|
-
if (this.
|
|
620
|
-
this.navigateHistory(-1);
|
|
621
|
-
}
|
|
622
|
-
else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) {
|
|
659
|
+
if (this.isOnFirstVisualLine() && this.history.length > 0) {
|
|
623
660
|
this.navigateHistory(-1);
|
|
624
661
|
}
|
|
625
662
|
else if (this.isOnFirstVisualLine()) {
|
|
@@ -795,7 +832,7 @@ export class Editor {
|
|
|
795
832
|
setText(text) {
|
|
796
833
|
this.cancelAutocomplete();
|
|
797
834
|
this.lastAction = null;
|
|
798
|
-
this.
|
|
835
|
+
this.exitHistoryBrowsing();
|
|
799
836
|
const normalized = this.normalizeText(text);
|
|
800
837
|
// Push undo snapshot if content differs (makes programmatic changes undoable)
|
|
801
838
|
if (this.getText() !== normalized) {
|
|
@@ -814,7 +851,7 @@ export class Editor {
|
|
|
814
851
|
this.cancelAutocomplete();
|
|
815
852
|
this.pushUndoSnapshot();
|
|
816
853
|
this.lastAction = null;
|
|
817
|
-
this.
|
|
854
|
+
this.exitHistoryBrowsing();
|
|
818
855
|
this.insertTextAtCursorInternal(text);
|
|
819
856
|
}
|
|
820
857
|
/**
|
|
@@ -867,7 +904,7 @@ export class Editor {
|
|
|
867
904
|
}
|
|
868
905
|
// All the editor methods from before...
|
|
869
906
|
insertCharacter(char, skipUndoCoalescing) {
|
|
870
|
-
this.
|
|
907
|
+
this.exitHistoryBrowsing();
|
|
871
908
|
// Undo coalescing (fish-style):
|
|
872
909
|
// - Consecutive word chars coalesce into one undo unit
|
|
873
910
|
// - Space captures state before itself (so undo removes space+following word together)
|
|
@@ -893,8 +930,8 @@ export class Editor {
|
|
|
893
930
|
if (char === "/" && this.isAtStartOfMessage()) {
|
|
894
931
|
this.tryTriggerAutocomplete();
|
|
895
932
|
}
|
|
896
|
-
// Auto-trigger for symbol-based completion like
|
|
897
|
-
else if (char
|
|
933
|
+
// Auto-trigger for symbol-based completion like @, #, or provider triggers at token boundaries
|
|
934
|
+
else if (this.autocompleteTriggerCharacters.includes(char)) {
|
|
898
935
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
899
936
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
900
937
|
const charBeforeSymbol = textBeforeCursor[textBeforeCursor.length - 2];
|
|
@@ -910,8 +947,8 @@ export class Editor {
|
|
|
910
947
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
911
948
|
this.tryTriggerAutocomplete();
|
|
912
949
|
}
|
|
913
|
-
// Check if we're in a symbol-based completion context like
|
|
914
|
-
else if (
|
|
950
|
+
// Check if we're in a symbol-based completion context like @, #, or provider triggers
|
|
951
|
+
else if (this.autocompleteTriggerPattern.test(textBeforeCursor)) {
|
|
915
952
|
this.tryTriggerAutocomplete();
|
|
916
953
|
}
|
|
917
954
|
}
|
|
@@ -922,7 +959,7 @@ export class Editor {
|
|
|
922
959
|
}
|
|
923
960
|
handlePaste(pastedText) {
|
|
924
961
|
this.cancelAutocomplete();
|
|
925
|
-
this.
|
|
962
|
+
this.exitHistoryBrowsing();
|
|
926
963
|
this.lastAction = null;
|
|
927
964
|
this.pushUndoSnapshot();
|
|
928
965
|
// Some terminals (e.g. tmux popups with extended-keys-format=csi-u) re-encode
|
|
@@ -980,7 +1017,7 @@ export class Editor {
|
|
|
980
1017
|
}
|
|
981
1018
|
addNewLine() {
|
|
982
1019
|
this.cancelAutocomplete();
|
|
983
|
-
this.
|
|
1020
|
+
this.exitHistoryBrowsing();
|
|
984
1021
|
this.lastAction = null;
|
|
985
1022
|
this.pushUndoSnapshot();
|
|
986
1023
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
@@ -1014,7 +1051,7 @@ export class Editor {
|
|
|
1014
1051
|
this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
1015
1052
|
this.pastes.clear();
|
|
1016
1053
|
this.pasteCounter = 0;
|
|
1017
|
-
this.
|
|
1054
|
+
this.exitHistoryBrowsing();
|
|
1018
1055
|
this.scrollOffset = 0;
|
|
1019
1056
|
this.undoStack.clear();
|
|
1020
1057
|
this.lastAction = null;
|
|
@@ -1024,7 +1061,7 @@ export class Editor {
|
|
|
1024
1061
|
this.onSubmit(result);
|
|
1025
1062
|
}
|
|
1026
1063
|
handleBackspace() {
|
|
1027
|
-
this.
|
|
1064
|
+
this.exitHistoryBrowsing();
|
|
1028
1065
|
this.lastAction = null;
|
|
1029
1066
|
if (this.state.cursorCol > 0) {
|
|
1030
1067
|
this.pushUndoSnapshot();
|
|
@@ -1065,8 +1102,8 @@ export class Editor {
|
|
|
1065
1102
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
1066
1103
|
this.tryTriggerAutocomplete();
|
|
1067
1104
|
}
|
|
1068
|
-
// Symbol-based completion context like
|
|
1069
|
-
else if (
|
|
1105
|
+
// Symbol-based completion context like @, #, or provider triggers
|
|
1106
|
+
else if (this.autocompleteTriggerPattern.test(textBeforeCursor)) {
|
|
1070
1107
|
this.tryTriggerAutocomplete();
|
|
1071
1108
|
}
|
|
1072
1109
|
}
|
|
@@ -1206,7 +1243,7 @@ export class Editor {
|
|
|
1206
1243
|
this.setCursorCol(currentLine.length);
|
|
1207
1244
|
}
|
|
1208
1245
|
deleteToStartOfLine() {
|
|
1209
|
-
this.
|
|
1246
|
+
this.exitHistoryBrowsing();
|
|
1210
1247
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1211
1248
|
if (this.state.cursorCol > 0) {
|
|
1212
1249
|
this.pushUndoSnapshot();
|
|
@@ -1234,7 +1271,7 @@ export class Editor {
|
|
|
1234
1271
|
}
|
|
1235
1272
|
}
|
|
1236
1273
|
deleteToEndOfLine() {
|
|
1237
|
-
this.
|
|
1274
|
+
this.exitHistoryBrowsing();
|
|
1238
1275
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1239
1276
|
if (this.state.cursorCol < currentLine.length) {
|
|
1240
1277
|
this.pushUndoSnapshot();
|
|
@@ -1259,7 +1296,7 @@ export class Editor {
|
|
|
1259
1296
|
}
|
|
1260
1297
|
}
|
|
1261
1298
|
deleteWordBackwards() {
|
|
1262
|
-
this.
|
|
1299
|
+
this.exitHistoryBrowsing();
|
|
1263
1300
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1264
1301
|
// If at start of line, behave like backspace at column 0 (merge with previous line)
|
|
1265
1302
|
if (this.state.cursorCol === 0) {
|
|
@@ -1295,7 +1332,7 @@ export class Editor {
|
|
|
1295
1332
|
}
|
|
1296
1333
|
}
|
|
1297
1334
|
deleteWordForward() {
|
|
1298
|
-
this.
|
|
1335
|
+
this.exitHistoryBrowsing();
|
|
1299
1336
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1300
1337
|
// If at end of line, merge with next line (delete the newline)
|
|
1301
1338
|
if (this.state.cursorCol >= currentLine.length) {
|
|
@@ -1328,7 +1365,7 @@ export class Editor {
|
|
|
1328
1365
|
}
|
|
1329
1366
|
}
|
|
1330
1367
|
handleForwardDelete() {
|
|
1331
|
-
this.
|
|
1368
|
+
this.exitHistoryBrowsing();
|
|
1332
1369
|
this.lastAction = null;
|
|
1333
1370
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1334
1371
|
if (this.state.cursorCol < currentLine.length) {
|
|
@@ -1364,8 +1401,8 @@ export class Editor {
|
|
|
1364
1401
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
1365
1402
|
this.tryTriggerAutocomplete();
|
|
1366
1403
|
}
|
|
1367
|
-
// Symbol-based completion context like
|
|
1368
|
-
else if (
|
|
1404
|
+
// Symbol-based completion context like @, #, or provider triggers
|
|
1405
|
+
else if (this.autocompleteTriggerPattern.test(textBeforeCursor)) {
|
|
1369
1406
|
this.tryTriggerAutocomplete();
|
|
1370
1407
|
}
|
|
1371
1408
|
}
|
|
@@ -1551,7 +1588,7 @@ export class Editor {
|
|
|
1551
1588
|
* Insert text at cursor position (used by yank operations).
|
|
1552
1589
|
*/
|
|
1553
1590
|
insertYankedText(text) {
|
|
1554
|
-
this.
|
|
1591
|
+
this.exitHistoryBrowsing();
|
|
1555
1592
|
const lines = text.split("\n");
|
|
1556
1593
|
if (lines.length === 1) {
|
|
1557
1594
|
// Single line - insert at cursor
|
|
@@ -1623,7 +1660,7 @@ export class Editor {
|
|
|
1623
1660
|
this.undoStack.push(this.state);
|
|
1624
1661
|
}
|
|
1625
1662
|
undo() {
|
|
1626
|
-
this.
|
|
1663
|
+
this.exitHistoryBrowsing();
|
|
1627
1664
|
const snapshot = this.undoStack.pop();
|
|
1628
1665
|
if (!snapshot)
|
|
1629
1666
|
return;
|
|
@@ -1784,14 +1821,25 @@ export class Editor {
|
|
|
1784
1821
|
})();
|
|
1785
1822
|
await this.autocompleteRequestTask;
|
|
1786
1823
|
}
|
|
1824
|
+
setAutocompleteTriggerCharacters(triggerCharacters) {
|
|
1825
|
+
const next = [...DEFAULT_AUTOCOMPLETE_TRIGGER_CHARACTERS];
|
|
1826
|
+
for (const character of triggerCharacters) {
|
|
1827
|
+
if (character.length !== 1 || character === "/" || isWhitespaceChar(character) || next.includes(character)) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
next.push(character);
|
|
1831
|
+
}
|
|
1832
|
+
this.autocompleteTriggerCharacters = next;
|
|
1833
|
+
this.autocompleteTriggerPattern = buildTriggerPattern(next);
|
|
1834
|
+
this.autocompleteDebouncePattern = buildDebouncePattern(next);
|
|
1835
|
+
}
|
|
1787
1836
|
getAutocompleteDebounceMs(options) {
|
|
1788
1837
|
if (options.explicitTab || options.force) {
|
|
1789
1838
|
return 0;
|
|
1790
1839
|
}
|
|
1791
1840
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1792
1841
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1793
|
-
|
|
1794
|
-
return isSymbolAutocompleteContext ? ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS : 0;
|
|
1842
|
+
return this.autocompleteDebouncePattern.test(textBeforeCursor) ? ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS : 0;
|
|
1795
1843
|
}
|
|
1796
1844
|
async runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options) {
|
|
1797
1845
|
if (!this.autocompleteProvider)
|