@earendil-works/pi-tui 0.79.0 → 0.79.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/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 +74 -35
- package/dist/components/editor.js.map +1 -1
- 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
|
@@ -158,6 +158,17 @@ const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
|
|
|
158
158
|
maxPrimaryColumnWidth: 32,
|
|
159
159
|
};
|
|
160
160
|
const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
|
|
161
|
+
const DEFAULT_AUTOCOMPLETE_TRIGGER_CHARACTERS = ["@", "#"];
|
|
162
|
+
function escapeCharacterClass(value) {
|
|
163
|
+
return value.replace(/[\\^$.*+?()[\]{}|-]/g, "\\$&");
|
|
164
|
+
}
|
|
165
|
+
function buildTriggerPattern(triggerCharacters) {
|
|
166
|
+
return new RegExp(`(?:^|[\\s])[${triggerCharacters.map(escapeCharacterClass).join("")}][^\\s]*$`);
|
|
167
|
+
}
|
|
168
|
+
function buildDebouncePattern(triggerCharacters) {
|
|
169
|
+
const escapedWithoutAt = triggerCharacters.filter((character) => character !== "@").map(escapeCharacterClass);
|
|
170
|
+
return new RegExp(`(?:^|[ \\t])(?:@(?:"[^"]*|[^\\s]*)|[${escapedWithoutAt.join("")}][^\\s]*)$`);
|
|
171
|
+
}
|
|
161
172
|
export class Editor {
|
|
162
173
|
state = {
|
|
163
174
|
lines: [""],
|
|
@@ -177,6 +188,9 @@ export class Editor {
|
|
|
177
188
|
borderColor;
|
|
178
189
|
// Autocomplete support
|
|
179
190
|
autocompleteProvider;
|
|
191
|
+
autocompleteTriggerCharacters = [...DEFAULT_AUTOCOMPLETE_TRIGGER_CHARACTERS];
|
|
192
|
+
autocompleteTriggerPattern = buildTriggerPattern(this.autocompleteTriggerCharacters);
|
|
193
|
+
autocompleteDebouncePattern = buildDebouncePattern(this.autocompleteTriggerCharacters);
|
|
180
194
|
autocompleteList;
|
|
181
195
|
autocompleteState = null;
|
|
182
196
|
autocompletePrefix = "";
|
|
@@ -195,6 +209,7 @@ export class Editor {
|
|
|
195
209
|
// Prompt history for up/down navigation
|
|
196
210
|
history = [];
|
|
197
211
|
historyIndex = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
|
|
212
|
+
historyDraft = null;
|
|
198
213
|
// Kill ring for Emacs-style kill/yank operations
|
|
199
214
|
killRing = new KillRing();
|
|
200
215
|
lastAction = null;
|
|
@@ -253,6 +268,7 @@ export class Editor {
|
|
|
253
268
|
setAutocompleteProvider(provider) {
|
|
254
269
|
this.cancelAutocomplete();
|
|
255
270
|
this.autocompleteProvider = provider;
|
|
271
|
+
this.setAutocompleteTriggerCharacters(provider.triggerCharacters ?? []);
|
|
256
272
|
}
|
|
257
273
|
/**
|
|
258
274
|
* Add a prompt to history for up/down arrow navigation.
|
|
@@ -271,9 +287,6 @@ export class Editor {
|
|
|
271
287
|
this.history.pop();
|
|
272
288
|
}
|
|
273
289
|
}
|
|
274
|
-
isEditorEmpty() {
|
|
275
|
-
return this.state.lines.length === 1 && this.state.lines[0] === "";
|
|
276
|
-
}
|
|
277
290
|
isOnFirstVisualLine() {
|
|
278
291
|
const visualLines = this.buildVisualLineMap(this.lastWidth);
|
|
279
292
|
const currentVisualLine = this.findCurrentVisualLine(visualLines);
|
|
@@ -294,16 +307,32 @@ export class Editor {
|
|
|
294
307
|
// Capture state when first entering history browsing mode
|
|
295
308
|
if (this.historyIndex === -1 && newIndex >= 0) {
|
|
296
309
|
this.pushUndoSnapshot();
|
|
310
|
+
this.historyDraft = structuredClone(this.state);
|
|
297
311
|
}
|
|
298
312
|
this.historyIndex = newIndex;
|
|
299
313
|
if (this.historyIndex === -1) {
|
|
300
|
-
|
|
301
|
-
this.
|
|
314
|
+
const draft = this.historyDraft;
|
|
315
|
+
this.historyDraft = null;
|
|
316
|
+
if (draft) {
|
|
317
|
+
this.state = draft;
|
|
318
|
+
this.preferredVisualCol = null;
|
|
319
|
+
this.snappedFromCursorCol = null;
|
|
320
|
+
this.scrollOffset = 0;
|
|
321
|
+
if (this.onChange)
|
|
322
|
+
this.onChange(this.getText());
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
this.setTextInternal("");
|
|
326
|
+
}
|
|
302
327
|
}
|
|
303
328
|
else {
|
|
304
329
|
this.setTextInternal(this.history[this.historyIndex] || "", direction === -1 ? "start" : "end");
|
|
305
330
|
}
|
|
306
331
|
}
|
|
332
|
+
exitHistoryBrowsing() {
|
|
333
|
+
this.historyIndex = -1;
|
|
334
|
+
this.historyDraft = null;
|
|
335
|
+
}
|
|
307
336
|
/** Internal setText that doesn't reset history state - used by navigateHistory */
|
|
308
337
|
setTextInternal(text, cursorPlacement = "end") {
|
|
309
338
|
const lines = text.split("\n");
|
|
@@ -368,8 +397,10 @@ export class Editor {
|
|
|
368
397
|
result.push(horizontal.repeat(width));
|
|
369
398
|
}
|
|
370
399
|
// Render each visible layout line
|
|
371
|
-
// Emit hardware cursor marker
|
|
372
|
-
|
|
400
|
+
// Emit hardware cursor marker when focused so TUI can position the
|
|
401
|
+
// hardware cursor for IME candidate-window placement even while
|
|
402
|
+
// autocomplete (e.g. slash-command menu) is visible.
|
|
403
|
+
const emitCursorMarker = this.focused;
|
|
373
404
|
for (const layoutLine of visibleLines) {
|
|
374
405
|
let displayText = layoutLine.text;
|
|
375
406
|
let lineVisibleWidth = visibleWidth(layoutLine.text);
|
|
@@ -616,10 +647,7 @@ export class Editor {
|
|
|
616
647
|
}
|
|
617
648
|
// Arrow key navigation (with history support)
|
|
618
649
|
if (kb.matches(data, "tui.editor.cursorUp")) {
|
|
619
|
-
if (this.
|
|
620
|
-
this.navigateHistory(-1);
|
|
621
|
-
}
|
|
622
|
-
else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) {
|
|
650
|
+
if (this.isOnFirstVisualLine() && this.history.length > 0) {
|
|
623
651
|
this.navigateHistory(-1);
|
|
624
652
|
}
|
|
625
653
|
else if (this.isOnFirstVisualLine()) {
|
|
@@ -795,7 +823,7 @@ export class Editor {
|
|
|
795
823
|
setText(text) {
|
|
796
824
|
this.cancelAutocomplete();
|
|
797
825
|
this.lastAction = null;
|
|
798
|
-
this.
|
|
826
|
+
this.exitHistoryBrowsing();
|
|
799
827
|
const normalized = this.normalizeText(text);
|
|
800
828
|
// Push undo snapshot if content differs (makes programmatic changes undoable)
|
|
801
829
|
if (this.getText() !== normalized) {
|
|
@@ -814,7 +842,7 @@ export class Editor {
|
|
|
814
842
|
this.cancelAutocomplete();
|
|
815
843
|
this.pushUndoSnapshot();
|
|
816
844
|
this.lastAction = null;
|
|
817
|
-
this.
|
|
845
|
+
this.exitHistoryBrowsing();
|
|
818
846
|
this.insertTextAtCursorInternal(text);
|
|
819
847
|
}
|
|
820
848
|
/**
|
|
@@ -867,7 +895,7 @@ export class Editor {
|
|
|
867
895
|
}
|
|
868
896
|
// All the editor methods from before...
|
|
869
897
|
insertCharacter(char, skipUndoCoalescing) {
|
|
870
|
-
this.
|
|
898
|
+
this.exitHistoryBrowsing();
|
|
871
899
|
// Undo coalescing (fish-style):
|
|
872
900
|
// - Consecutive word chars coalesce into one undo unit
|
|
873
901
|
// - Space captures state before itself (so undo removes space+following word together)
|
|
@@ -893,8 +921,8 @@ export class Editor {
|
|
|
893
921
|
if (char === "/" && this.isAtStartOfMessage()) {
|
|
894
922
|
this.tryTriggerAutocomplete();
|
|
895
923
|
}
|
|
896
|
-
// Auto-trigger for symbol-based completion like
|
|
897
|
-
else if (char
|
|
924
|
+
// Auto-trigger for symbol-based completion like @, #, or provider triggers at token boundaries
|
|
925
|
+
else if (this.autocompleteTriggerCharacters.includes(char)) {
|
|
898
926
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
899
927
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
900
928
|
const charBeforeSymbol = textBeforeCursor[textBeforeCursor.length - 2];
|
|
@@ -910,8 +938,8 @@ export class Editor {
|
|
|
910
938
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
911
939
|
this.tryTriggerAutocomplete();
|
|
912
940
|
}
|
|
913
|
-
// Check if we're in a symbol-based completion context like
|
|
914
|
-
else if (
|
|
941
|
+
// Check if we're in a symbol-based completion context like @, #, or provider triggers
|
|
942
|
+
else if (this.autocompleteTriggerPattern.test(textBeforeCursor)) {
|
|
915
943
|
this.tryTriggerAutocomplete();
|
|
916
944
|
}
|
|
917
945
|
}
|
|
@@ -922,7 +950,7 @@ export class Editor {
|
|
|
922
950
|
}
|
|
923
951
|
handlePaste(pastedText) {
|
|
924
952
|
this.cancelAutocomplete();
|
|
925
|
-
this.
|
|
953
|
+
this.exitHistoryBrowsing();
|
|
926
954
|
this.lastAction = null;
|
|
927
955
|
this.pushUndoSnapshot();
|
|
928
956
|
// Some terminals (e.g. tmux popups with extended-keys-format=csi-u) re-encode
|
|
@@ -980,7 +1008,7 @@ export class Editor {
|
|
|
980
1008
|
}
|
|
981
1009
|
addNewLine() {
|
|
982
1010
|
this.cancelAutocomplete();
|
|
983
|
-
this.
|
|
1011
|
+
this.exitHistoryBrowsing();
|
|
984
1012
|
this.lastAction = null;
|
|
985
1013
|
this.pushUndoSnapshot();
|
|
986
1014
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
@@ -1014,7 +1042,7 @@ export class Editor {
|
|
|
1014
1042
|
this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
1015
1043
|
this.pastes.clear();
|
|
1016
1044
|
this.pasteCounter = 0;
|
|
1017
|
-
this.
|
|
1045
|
+
this.exitHistoryBrowsing();
|
|
1018
1046
|
this.scrollOffset = 0;
|
|
1019
1047
|
this.undoStack.clear();
|
|
1020
1048
|
this.lastAction = null;
|
|
@@ -1024,7 +1052,7 @@ export class Editor {
|
|
|
1024
1052
|
this.onSubmit(result);
|
|
1025
1053
|
}
|
|
1026
1054
|
handleBackspace() {
|
|
1027
|
-
this.
|
|
1055
|
+
this.exitHistoryBrowsing();
|
|
1028
1056
|
this.lastAction = null;
|
|
1029
1057
|
if (this.state.cursorCol > 0) {
|
|
1030
1058
|
this.pushUndoSnapshot();
|
|
@@ -1065,8 +1093,8 @@ export class Editor {
|
|
|
1065
1093
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
1066
1094
|
this.tryTriggerAutocomplete();
|
|
1067
1095
|
}
|
|
1068
|
-
// Symbol-based completion context like
|
|
1069
|
-
else if (
|
|
1096
|
+
// Symbol-based completion context like @, #, or provider triggers
|
|
1097
|
+
else if (this.autocompleteTriggerPattern.test(textBeforeCursor)) {
|
|
1070
1098
|
this.tryTriggerAutocomplete();
|
|
1071
1099
|
}
|
|
1072
1100
|
}
|
|
@@ -1206,7 +1234,7 @@ export class Editor {
|
|
|
1206
1234
|
this.setCursorCol(currentLine.length);
|
|
1207
1235
|
}
|
|
1208
1236
|
deleteToStartOfLine() {
|
|
1209
|
-
this.
|
|
1237
|
+
this.exitHistoryBrowsing();
|
|
1210
1238
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1211
1239
|
if (this.state.cursorCol > 0) {
|
|
1212
1240
|
this.pushUndoSnapshot();
|
|
@@ -1234,7 +1262,7 @@ export class Editor {
|
|
|
1234
1262
|
}
|
|
1235
1263
|
}
|
|
1236
1264
|
deleteToEndOfLine() {
|
|
1237
|
-
this.
|
|
1265
|
+
this.exitHistoryBrowsing();
|
|
1238
1266
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1239
1267
|
if (this.state.cursorCol < currentLine.length) {
|
|
1240
1268
|
this.pushUndoSnapshot();
|
|
@@ -1259,7 +1287,7 @@ export class Editor {
|
|
|
1259
1287
|
}
|
|
1260
1288
|
}
|
|
1261
1289
|
deleteWordBackwards() {
|
|
1262
|
-
this.
|
|
1290
|
+
this.exitHistoryBrowsing();
|
|
1263
1291
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1264
1292
|
// If at start of line, behave like backspace at column 0 (merge with previous line)
|
|
1265
1293
|
if (this.state.cursorCol === 0) {
|
|
@@ -1295,7 +1323,7 @@ export class Editor {
|
|
|
1295
1323
|
}
|
|
1296
1324
|
}
|
|
1297
1325
|
deleteWordForward() {
|
|
1298
|
-
this.
|
|
1326
|
+
this.exitHistoryBrowsing();
|
|
1299
1327
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1300
1328
|
// If at end of line, merge with next line (delete the newline)
|
|
1301
1329
|
if (this.state.cursorCol >= currentLine.length) {
|
|
@@ -1328,7 +1356,7 @@ export class Editor {
|
|
|
1328
1356
|
}
|
|
1329
1357
|
}
|
|
1330
1358
|
handleForwardDelete() {
|
|
1331
|
-
this.
|
|
1359
|
+
this.exitHistoryBrowsing();
|
|
1332
1360
|
this.lastAction = null;
|
|
1333
1361
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1334
1362
|
if (this.state.cursorCol < currentLine.length) {
|
|
@@ -1364,8 +1392,8 @@ export class Editor {
|
|
|
1364
1392
|
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
1365
1393
|
this.tryTriggerAutocomplete();
|
|
1366
1394
|
}
|
|
1367
|
-
// Symbol-based completion context like
|
|
1368
|
-
else if (
|
|
1395
|
+
// Symbol-based completion context like @, #, or provider triggers
|
|
1396
|
+
else if (this.autocompleteTriggerPattern.test(textBeforeCursor)) {
|
|
1369
1397
|
this.tryTriggerAutocomplete();
|
|
1370
1398
|
}
|
|
1371
1399
|
}
|
|
@@ -1551,7 +1579,7 @@ export class Editor {
|
|
|
1551
1579
|
* Insert text at cursor position (used by yank operations).
|
|
1552
1580
|
*/
|
|
1553
1581
|
insertYankedText(text) {
|
|
1554
|
-
this.
|
|
1582
|
+
this.exitHistoryBrowsing();
|
|
1555
1583
|
const lines = text.split("\n");
|
|
1556
1584
|
if (lines.length === 1) {
|
|
1557
1585
|
// Single line - insert at cursor
|
|
@@ -1623,7 +1651,7 @@ export class Editor {
|
|
|
1623
1651
|
this.undoStack.push(this.state);
|
|
1624
1652
|
}
|
|
1625
1653
|
undo() {
|
|
1626
|
-
this.
|
|
1654
|
+
this.exitHistoryBrowsing();
|
|
1627
1655
|
const snapshot = this.undoStack.pop();
|
|
1628
1656
|
if (!snapshot)
|
|
1629
1657
|
return;
|
|
@@ -1784,14 +1812,25 @@ export class Editor {
|
|
|
1784
1812
|
})();
|
|
1785
1813
|
await this.autocompleteRequestTask;
|
|
1786
1814
|
}
|
|
1815
|
+
setAutocompleteTriggerCharacters(triggerCharacters) {
|
|
1816
|
+
const next = [...DEFAULT_AUTOCOMPLETE_TRIGGER_CHARACTERS];
|
|
1817
|
+
for (const character of triggerCharacters) {
|
|
1818
|
+
if (character.length !== 1 || character === "/" || isWhitespaceChar(character) || next.includes(character)) {
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
next.push(character);
|
|
1822
|
+
}
|
|
1823
|
+
this.autocompleteTriggerCharacters = next;
|
|
1824
|
+
this.autocompleteTriggerPattern = buildTriggerPattern(next);
|
|
1825
|
+
this.autocompleteDebouncePattern = buildDebouncePattern(next);
|
|
1826
|
+
}
|
|
1787
1827
|
getAutocompleteDebounceMs(options) {
|
|
1788
1828
|
if (options.explicitTab || options.force) {
|
|
1789
1829
|
return 0;
|
|
1790
1830
|
}
|
|
1791
1831
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1792
1832
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1793
|
-
|
|
1794
|
-
return isSymbolAutocompleteContext ? ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS : 0;
|
|
1833
|
+
return this.autocompleteDebouncePattern.test(textBeforeCursor) ? ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS : 0;
|
|
1795
1834
|
}
|
|
1796
1835
|
async runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options) {
|
|
1797
1836
|
if (!this.autocompleteProvider)
|