@oh-my-pi/pi-tui 14.8.0 → 14.8.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/package.json +3 -3
- package/src/autocomplete.ts +38 -0
- package/src/components/editor.ts +60 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "14.8.
|
|
4
|
+
"version": "14.8.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "14.8.
|
|
41
|
-
"@oh-my-pi/pi-utils": "14.8.
|
|
40
|
+
"@oh-my-pi/pi-natives": "14.8.1",
|
|
41
|
+
"@oh-my-pi/pi-utils": "14.8.1",
|
|
42
42
|
"lru-cache": "11.3.6",
|
|
43
43
|
"marked": "^18.0.3"
|
|
44
44
|
},
|
package/src/autocomplete.ts
CHANGED
|
@@ -193,6 +193,9 @@ export interface AutocompleteProvider {
|
|
|
193
193
|
|
|
194
194
|
/** Get inline hint text to show as dim ghost text after the cursor */
|
|
195
195
|
getInlineHint?(lines: string[], cursorLine: number, cursorCol: number): string | null;
|
|
196
|
+
/** Synchronously try to complete a slash command at the start of a line (no async I/O). */
|
|
197
|
+
/** Returns matched items and the full prefix, or null if not applicable. */
|
|
198
|
+
trySyncSlashCompletion?(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
// Combined provider that handles both slash commands and file paths.
|
|
@@ -777,4 +780,39 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
777
780
|
|
|
778
781
|
return command.getInlineHint(argumentText);
|
|
779
782
|
}
|
|
783
|
+
trySyncSlashCompletion(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
|
|
784
|
+
if (!textBeforeCursor.startsWith("/")) return null;
|
|
785
|
+
if (textBeforeCursor.length <= 1) return null; // Bare "/" alone, don't auto-complete
|
|
786
|
+
if (textBeforeCursor.includes(" ")) return null; // Only complete command name, not args
|
|
787
|
+
|
|
788
|
+
const prefix = textBeforeCursor.slice(1);
|
|
789
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
790
|
+
|
|
791
|
+
const matches = this.#commands
|
|
792
|
+
.filter(cmd => {
|
|
793
|
+
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
794
|
+
if (!name) return false;
|
|
795
|
+
if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
|
|
796
|
+
const desc = cmd.description?.toLowerCase();
|
|
797
|
+
return desc ? fuzzyMatch(lowerPrefix, desc) : false;
|
|
798
|
+
})
|
|
799
|
+
.map(cmd => {
|
|
800
|
+
const name = "name" in cmd ? cmd.name : cmd.value;
|
|
801
|
+
const lowerName = name?.toLowerCase() ?? "";
|
|
802
|
+
const lowerDesc = cmd.description?.toLowerCase() ?? "";
|
|
803
|
+
const nameScore = fuzzyMatch(lowerPrefix, lowerName) ? fuzzyScore(lowerPrefix, lowerName) : 0;
|
|
804
|
+
const descScore = fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
|
|
805
|
+
return {
|
|
806
|
+
value: name,
|
|
807
|
+
label: "name" in cmd ? cmd.name : cmd.label,
|
|
808
|
+
score: Math.max(nameScore, descScore),
|
|
809
|
+
...(cmd.description && { description: cmd.description }),
|
|
810
|
+
} as AutocompleteItem & { score: number };
|
|
811
|
+
})
|
|
812
|
+
.sort((a, b) => b.score - a.score)
|
|
813
|
+
.map(({ score: _, ...rest }) => rest);
|
|
814
|
+
|
|
815
|
+
if (matches.length === 0) return null;
|
|
816
|
+
return { items: matches, prefix: textBeforeCursor };
|
|
817
|
+
}
|
|
780
818
|
}
|
package/src/components/editor.ts
CHANGED
|
@@ -1135,6 +1135,38 @@ export class Editor implements Component, Focusable {
|
|
|
1135
1135
|
return;
|
|
1136
1136
|
}
|
|
1137
1137
|
|
|
1138
|
+
// Synchronous slash command completion for the race condition where
|
|
1139
|
+
// async autocomplete hasn't resolved yet (user types /q quickly + Enter).
|
|
1140
|
+
// Match the existing selected-item behavior when autocomplete IS showing.
|
|
1141
|
+
if (!this.#autocompleteState) {
|
|
1142
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1143
|
+
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1144
|
+
if (
|
|
1145
|
+
textBeforeCursor.startsWith("/") &&
|
|
1146
|
+
this.#isInSubmittedSlashCommandContext() &&
|
|
1147
|
+
this.#autocompleteProvider?.trySyncSlashCompletion
|
|
1148
|
+
) {
|
|
1149
|
+
const syncResult = this.#autocompleteProvider.trySyncSlashCompletion(textBeforeCursor);
|
|
1150
|
+
if (syncResult && syncResult.items.length > 0) {
|
|
1151
|
+
// Invalidate any pending async autocomplete so its stale results are discarded
|
|
1152
|
+
this.#autocompleteRequestId += 1;
|
|
1153
|
+
// Apply the best match and submit the completed command
|
|
1154
|
+
const selected = syncResult.items[0]!;
|
|
1155
|
+
const result = this.#autocompleteProvider.applyCompletion(
|
|
1156
|
+
this.#state.lines,
|
|
1157
|
+
this.#state.cursorLine,
|
|
1158
|
+
this.#state.cursorCol,
|
|
1159
|
+
selected,
|
|
1160
|
+
syncResult.prefix,
|
|
1161
|
+
);
|
|
1162
|
+
this.#state.lines = result.lines;
|
|
1163
|
+
this.#state.cursorLine = result.cursorLine;
|
|
1164
|
+
this.#setCursorCol(result.cursorCol);
|
|
1165
|
+
result.onApplied?.();
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1138
1170
|
this.#submitValue();
|
|
1139
1171
|
}
|
|
1140
1172
|
// Backspace (including Shift+Backspace)
|
|
@@ -1466,7 +1498,7 @@ export class Editor implements Component, Focusable {
|
|
|
1466
1498
|
// Check if we should trigger or update autocomplete
|
|
1467
1499
|
if (!this.#autocompleteState) {
|
|
1468
1500
|
// Auto-trigger for "/" at the start of a line (slash commands)
|
|
1469
|
-
if (char === "/" && this.#
|
|
1501
|
+
if (char === "/" && this.#isAtStartOfSubmittedMessage()) {
|
|
1470
1502
|
this.#tryTriggerAutocomplete();
|
|
1471
1503
|
}
|
|
1472
1504
|
// Auto-trigger for "@" file reference (fuzzy search)
|
|
@@ -1488,7 +1520,7 @@ export class Editor implements Component, Focusable {
|
|
|
1488
1520
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1489
1521
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1490
1522
|
// Check if we're in a slash command (with or without space for arguments)
|
|
1491
|
-
if (
|
|
1523
|
+
if (this.#isInSubmittedSlashCommandContext()) {
|
|
1492
1524
|
this.#tryTriggerAutocomplete();
|
|
1493
1525
|
}
|
|
1494
1526
|
// Check if we're in an @ file reference context
|
|
@@ -1661,7 +1693,7 @@ export class Editor implements Component, Focusable {
|
|
|
1661
1693
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1662
1694
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1663
1695
|
// Slash command context
|
|
1664
|
-
if (
|
|
1696
|
+
if (this.#isInSubmittedSlashCommandContext()) {
|
|
1665
1697
|
this.#tryTriggerAutocomplete();
|
|
1666
1698
|
}
|
|
1667
1699
|
// @ file reference context
|
|
@@ -1817,7 +1849,7 @@ export class Editor implements Component, Focusable {
|
|
|
1817
1849
|
} else {
|
|
1818
1850
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1819
1851
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1820
|
-
if (
|
|
1852
|
+
if (this.#isInSubmittedSlashCommandContext()) {
|
|
1821
1853
|
this.#tryTriggerAutocomplete();
|
|
1822
1854
|
} else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
|
|
1823
1855
|
this.#tryTriggerAutocomplete();
|
|
@@ -2131,7 +2163,7 @@ export class Editor implements Component, Focusable {
|
|
|
2131
2163
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2132
2164
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2133
2165
|
// Slash command context
|
|
2134
|
-
if (
|
|
2166
|
+
if (this.#isInSubmittedSlashCommandContext()) {
|
|
2135
2167
|
this.#tryTriggerAutocomplete();
|
|
2136
2168
|
}
|
|
2137
2169
|
// @ file reference context
|
|
@@ -2327,13 +2359,27 @@ export class Editor implements Component, Focusable {
|
|
|
2327
2359
|
this.#setCursorCol(moveWordRight(currentLine, this.#state.cursorCol));
|
|
2328
2360
|
}
|
|
2329
2361
|
|
|
2330
|
-
|
|
2331
|
-
|
|
2362
|
+
#hasOnlyWhitespaceBeforeCursorLine(): boolean {
|
|
2363
|
+
for (let i = 0; i < this.#state.cursorLine; i++) {
|
|
2364
|
+
if ((this.#state.lines[i] || "").trim() !== "") {
|
|
2365
|
+
return false;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return true;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// Slash commands execute only when the submitted prompt starts with the command.
|
|
2372
|
+
#isAtStartOfSubmittedMessage(): boolean {
|
|
2332
2373
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2333
2374
|
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2334
2375
|
|
|
2335
|
-
|
|
2336
|
-
|
|
2376
|
+
return this.#hasOnlyWhitespaceBeforeCursorLine() && (beforeCursor.trim() === "" || beforeCursor.trim() === "/");
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
#isInSubmittedSlashCommandContext(): boolean {
|
|
2380
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2381
|
+
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2382
|
+
return this.#hasOnlyWhitespaceBeforeCursorLine() && beforeCursor.trimStart().startsWith("/");
|
|
2337
2383
|
}
|
|
2338
2384
|
|
|
2339
2385
|
#isSlashCommandNameAutocompleteSelection(): boolean {
|
|
@@ -2343,7 +2389,9 @@ export class Editor implements Component, Focusable {
|
|
|
2343
2389
|
|
|
2344
2390
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2345
2391
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol).trimStart();
|
|
2346
|
-
return
|
|
2392
|
+
return (
|
|
2393
|
+
this.#isInSubmittedSlashCommandContext() && textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")
|
|
2394
|
+
);
|
|
2347
2395
|
}
|
|
2348
2396
|
|
|
2349
2397
|
#isCompletedSlashCommandAtCursor(): boolean {
|
|
@@ -2353,7 +2401,7 @@ export class Editor implements Component, Focusable {
|
|
|
2353
2401
|
}
|
|
2354
2402
|
|
|
2355
2403
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol).trimStart();
|
|
2356
|
-
return /^\/\S+ $/.test(textBeforeCursor);
|
|
2404
|
+
return this.#isInSubmittedSlashCommandContext() && /^\/\S+ $/.test(textBeforeCursor);
|
|
2357
2405
|
}
|
|
2358
2406
|
|
|
2359
2407
|
// Autocomplete methods
|
|
@@ -2407,7 +2455,7 @@ export class Editor implements Component, Focusable {
|
|
|
2407
2455
|
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2408
2456
|
|
|
2409
2457
|
// Check if we're in a slash command context
|
|
2410
|
-
if (
|
|
2458
|
+
if (this.#isInSubmittedSlashCommandContext() && !beforeCursor.trimStart().includes(" ")) {
|
|
2411
2459
|
this.#handleSlashCommandCompletion();
|
|
2412
2460
|
} else {
|
|
2413
2461
|
this.#forceFileAutocomplete(true);
|