@gajae-code/tui 0.11.2 → 0.11.3
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 +5 -0
- package/dist/types/autocomplete.d.ts +1 -0
- package/package.json +3 -3
- package/src/autocomplete.ts +31 -1
- package/src/components/editor.ts +76 -36
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.3] - 2026-07-19
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
- Suppressed slash-command and skill autocomplete inside line-local single-backtick code spans while preserving path completion and ordinary slash matching outside literals (#2619).
|
|
9
|
+
|
|
5
10
|
## [0.11.0] - 2026-07-15
|
|
6
11
|
### Fixed
|
|
7
12
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function getSlashCommandMatchRank(query: string, commandName: string): number;
|
|
2
|
+
export declare function isInsideInlineCodeSpan(text: string): boolean;
|
|
2
3
|
export declare function extractSlashCommandTokenPrefix(text: string): string | null;
|
|
3
4
|
export interface AutocompleteItem {
|
|
4
5
|
value: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/tui",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.3",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"fmt": "biome format --write ."
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@gajae-code/natives": "0.11.
|
|
40
|
-
"@gajae-code/utils": "0.11.
|
|
39
|
+
"@gajae-code/natives": "0.11.3",
|
|
40
|
+
"@gajae-code/utils": "0.11.3",
|
|
41
41
|
"lru-cache": "11.3.6",
|
|
42
42
|
"marked": "18.0.6"
|
|
43
43
|
},
|
package/src/autocomplete.ts
CHANGED
|
@@ -190,10 +190,38 @@ function normalizeSlashCommandText(value: string): string {
|
|
|
190
190
|
.replace(/\s+/g, " ");
|
|
191
191
|
}
|
|
192
192
|
const NON_COMMAND_SLASH_PREFIX_PRECEDERS = new Set(["/", "\\", ":", ".", "~"]);
|
|
193
|
+
function findOpenInlineCodeSpanStart(text: string): number | null {
|
|
194
|
+
let openDelimiter: number | null = null;
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < text.length; ) {
|
|
197
|
+
if (text[i] !== "`") {
|
|
198
|
+
i += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let runEnd = i + 1;
|
|
203
|
+
while (text[runEnd] === "`") runEnd += 1;
|
|
204
|
+
if (runEnd - i !== 1) {
|
|
205
|
+
i = runEnd;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let backslashCount = 0;
|
|
210
|
+
for (let j = i - 1; j >= 0 && text[j] === "\\"; j -= 1) backslashCount += 1;
|
|
211
|
+
if (backslashCount % 2 === 0) openDelimiter = openDelimiter === null ? i : null;
|
|
212
|
+
i = runEnd;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return openDelimiter;
|
|
216
|
+
}
|
|
217
|
+
export function isInsideInlineCodeSpan(text: string): boolean {
|
|
218
|
+
return findOpenInlineCodeSpanStart(text) !== null;
|
|
219
|
+
}
|
|
193
220
|
|
|
194
221
|
export function extractSlashCommandTokenPrefix(text: string): string | null {
|
|
195
222
|
const slashIndex = text.lastIndexOf("/");
|
|
196
223
|
if (slashIndex === -1) return null;
|
|
224
|
+
if (isInsideInlineCodeSpan(text.slice(0, slashIndex + 1))) return null;
|
|
197
225
|
|
|
198
226
|
const token = text.slice(slashIndex);
|
|
199
227
|
if (/[\s]/.test(token)) return null;
|
|
@@ -556,7 +584,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
556
584
|
}
|
|
557
585
|
|
|
558
586
|
const lastDelimiterIndex = findLastDelimiter(text);
|
|
559
|
-
const
|
|
587
|
+
const inlineCodeStart = findOpenInlineCodeSpanStart(text);
|
|
588
|
+
const prefixStart = Math.max(lastDelimiterIndex, inlineCodeStart ?? -1);
|
|
589
|
+
const pathPrefix = prefixStart === -1 ? text : text.slice(prefixStart + 1);
|
|
560
590
|
|
|
561
591
|
// For forced extraction (Tab key), always return something
|
|
562
592
|
if (forceExtract) {
|
package/src/components/editor.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type AutocompleteProvider,
|
|
4
4
|
type CombinedAutocompleteProvider,
|
|
5
5
|
extractSlashCommandTokenPrefix,
|
|
6
|
+
isInsideInlineCodeSpan,
|
|
6
7
|
} from "../autocomplete";
|
|
7
8
|
import { BracketedPasteHandler } from "../bracketed-paste";
|
|
8
9
|
import { getKeybindings, type KeybindingsManager } from "../keybindings";
|
|
@@ -459,6 +460,7 @@ export class Editor implements Component, Focusable {
|
|
|
459
460
|
#autocompleteState: "regular" | "force" | null = null;
|
|
460
461
|
#autocompletePrefix: string = "";
|
|
461
462
|
#autocompleteRequestId: number = 0;
|
|
463
|
+
#autocompleteOrigin?: { docVersion: number; cursorLine: number; cursorCol: number };
|
|
462
464
|
#autocompleteMaxVisible: number = 5;
|
|
463
465
|
onAutocompleteUpdate?: () => void;
|
|
464
466
|
|
|
@@ -1201,6 +1203,11 @@ export class Editor implements Component, Focusable {
|
|
|
1201
1203
|
|
|
1202
1204
|
// If Tab was pressed, always apply the selection
|
|
1203
1205
|
if (kb.matches(data, "tui.input.tab")) {
|
|
1206
|
+
if (!this.#isAutocompleteSelectionCurrent()) {
|
|
1207
|
+
this.#cancelAutocomplete();
|
|
1208
|
+
this.#handleTabCompletion();
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1204
1211
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
1205
1212
|
if (selected && this.#autocompleteProvider) {
|
|
1206
1213
|
const shouldChainSlashCommandAutocomplete = this.#isSlashCommandNameAutocompleteSelection();
|
|
@@ -1234,36 +1241,38 @@ export class Editor implements Component, Focusable {
|
|
|
1234
1241
|
}
|
|
1235
1242
|
|
|
1236
1243
|
// If Enter was pressed on a slash command, apply completion and submit
|
|
1237
|
-
if (
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1244
|
+
if (
|
|
1245
|
+
(kb.matches(data, "tui.input.submit") || data === "\n") &&
|
|
1246
|
+
this.#autocompleteState === "regular" &&
|
|
1247
|
+
this.#autocompletePrefix.startsWith("/")
|
|
1248
|
+
) {
|
|
1249
|
+
const selected = this.#autocompleteList.getSelectedItem();
|
|
1250
|
+
if (!this.#isAutocompleteSelectionCurrent()) {
|
|
1243
1251
|
this.#cancelAutocomplete();
|
|
1244
|
-
} else {
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
this.#autocompletePrefix,
|
|
1253
|
-
);
|
|
1252
|
+
} else if (selected && this.#autocompleteProvider) {
|
|
1253
|
+
const result = this.#autocompleteProvider.applyCompletion(
|
|
1254
|
+
this.#state.lines,
|
|
1255
|
+
this.#state.cursorLine,
|
|
1256
|
+
this.#state.cursorCol,
|
|
1257
|
+
selected,
|
|
1258
|
+
this.#autocompletePrefix,
|
|
1259
|
+
);
|
|
1254
1260
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1261
|
+
this.#state.lines = result.lines;
|
|
1262
|
+
this.#bumpDocumentVersion();
|
|
1263
|
+
this.#state.cursorLine = result.cursorLine;
|
|
1264
|
+
this.#setCursorCol(result.cursorCol);
|
|
1265
|
+
result.onApplied?.();
|
|
1261
1266
|
this.#cancelAutocomplete();
|
|
1262
1267
|
}
|
|
1263
1268
|
// Don't return - fall through to submission logic
|
|
1264
1269
|
}
|
|
1265
1270
|
// If Enter was pressed on a file path, apply completion
|
|
1266
1271
|
else if (kb.matches(data, "tui.input.submit") || data === "\n") {
|
|
1272
|
+
if (!this.#isAutocompleteSelectionCurrent()) {
|
|
1273
|
+
this.#cancelAutocomplete();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1267
1276
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
1268
1277
|
if (selected && this.#autocompleteProvider) {
|
|
1269
1278
|
const result = this.#autocompleteProvider.applyCompletion(
|
|
@@ -1863,9 +1872,17 @@ export class Editor implements Component, Focusable {
|
|
|
1863
1872
|
|
|
1864
1873
|
// Check if we should trigger or update autocomplete
|
|
1865
1874
|
if (!this.#autocompleteState) {
|
|
1866
|
-
// Auto-trigger
|
|
1867
|
-
if (char === "/"
|
|
1868
|
-
this.#
|
|
1875
|
+
// Auto-trigger slash commands, or path-only completion inside inline code.
|
|
1876
|
+
if (char === "/") {
|
|
1877
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1878
|
+
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1879
|
+
if (this.#isInSubmittedSlashCommandContext()) {
|
|
1880
|
+
this.#tryTriggerAutocomplete();
|
|
1881
|
+
} else if (isInsideInlineCodeSpan(textBeforeCursor)) {
|
|
1882
|
+
this.#forceFileAutocomplete();
|
|
1883
|
+
} else if (this.#isInSlashTokenContext()) {
|
|
1884
|
+
this.#tryTriggerAutocomplete();
|
|
1885
|
+
}
|
|
1869
1886
|
}
|
|
1870
1887
|
// Auto-trigger for "@" file reference (fuzzy search)
|
|
1871
1888
|
else if (char === "@") {
|
|
@@ -2771,14 +2788,6 @@ export class Editor implements Component, Focusable {
|
|
|
2771
2788
|
return true;
|
|
2772
2789
|
}
|
|
2773
2790
|
|
|
2774
|
-
// Slash commands execute only when the submitted prompt starts with the command.
|
|
2775
|
-
#isAtStartOfSubmittedMessage(): boolean {
|
|
2776
|
-
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2777
|
-
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2778
|
-
|
|
2779
|
-
return this.#hasOnlyWhitespaceBeforeCursorLine() && (beforeCursor.trim() === "" || beforeCursor.trim() === "/");
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
2791
|
#isInSubmittedSlashCommandContext(): boolean {
|
|
2783
2792
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2784
2793
|
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
@@ -2794,6 +2803,30 @@ export class Editor implements Component, Focusable {
|
|
|
2794
2803
|
#isInSlashTokenContext(): boolean {
|
|
2795
2804
|
return this.#getSlashTokenBeforeCursor() !== null;
|
|
2796
2805
|
}
|
|
2806
|
+
#isAutocompleteSelectionCurrent(): boolean {
|
|
2807
|
+
if (!this.#isAutocompleteOriginCurrent()) return false;
|
|
2808
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2809
|
+
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2810
|
+
if (!textBeforeCursor.endsWith(this.#autocompletePrefix)) return false;
|
|
2811
|
+
if (this.#autocompleteState !== "regular" || !this.#autocompletePrefix.startsWith("/")) return true;
|
|
2812
|
+
return extractSlashCommandTokenPrefix(textBeforeCursor) === this.#autocompletePrefix;
|
|
2813
|
+
}
|
|
2814
|
+
#captureAutocompleteOrigin(): { docVersion: number; cursorLine: number; cursorCol: number } {
|
|
2815
|
+
return {
|
|
2816
|
+
docVersion: this.#docVersion,
|
|
2817
|
+
cursorLine: this.#state.cursorLine,
|
|
2818
|
+
cursorCol: this.#state.cursorCol,
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
#isAutocompleteOriginCurrent(origin = this.#autocompleteOrigin): boolean {
|
|
2823
|
+
return (
|
|
2824
|
+
origin !== undefined &&
|
|
2825
|
+
origin.docVersion === this.#docVersion &&
|
|
2826
|
+
origin.cursorLine === this.#state.cursorLine &&
|
|
2827
|
+
origin.cursorCol === this.#state.cursorCol
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2797
2830
|
|
|
2798
2831
|
#isSlashCommandNameAutocompleteSelection(): boolean {
|
|
2799
2832
|
if (this.#autocompleteState !== "regular") {
|
|
@@ -2835,6 +2868,7 @@ export class Editor implements Component, Focusable {
|
|
|
2835
2868
|
}
|
|
2836
2869
|
}
|
|
2837
2870
|
|
|
2871
|
+
const origin = this.#captureAutocompleteOrigin();
|
|
2838
2872
|
const requestId = ++this.#autocompleteRequestId;
|
|
2839
2873
|
|
|
2840
2874
|
const suggestions = await this.#autocompleteProvider.getSuggestions(
|
|
@@ -2842,12 +2876,13 @@ export class Editor implements Component, Focusable {
|
|
|
2842
2876
|
this.#state.cursorLine,
|
|
2843
2877
|
this.#state.cursorCol,
|
|
2844
2878
|
);
|
|
2845
|
-
if (requestId !== this.#autocompleteRequestId) return;
|
|
2879
|
+
if (requestId !== this.#autocompleteRequestId || !this.#isAutocompleteOriginCurrent(origin)) return;
|
|
2846
2880
|
|
|
2847
2881
|
if (suggestions && Array.isArray(suggestions.items) && suggestions.items.length > 0) {
|
|
2848
2882
|
this.#autocompletePrefix = suggestions.prefix;
|
|
2849
2883
|
this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
2850
2884
|
this.#autocompleteState = "regular";
|
|
2885
|
+
this.#autocompleteOrigin = origin;
|
|
2851
2886
|
this.onAutocompleteUpdate?.();
|
|
2852
2887
|
} else {
|
|
2853
2888
|
this.#cancelAutocomplete();
|
|
@@ -2907,13 +2942,14 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
2907
2942
|
return;
|
|
2908
2943
|
}
|
|
2909
2944
|
|
|
2945
|
+
const origin = this.#captureAutocompleteOrigin();
|
|
2910
2946
|
const requestId = ++this.#autocompleteRequestId;
|
|
2911
2947
|
const suggestions = await provider.getForceFileSuggestions(
|
|
2912
2948
|
this.#state.lines,
|
|
2913
2949
|
this.#state.cursorLine,
|
|
2914
2950
|
this.#state.cursorCol,
|
|
2915
2951
|
);
|
|
2916
|
-
if (requestId !== this.#autocompleteRequestId) return;
|
|
2952
|
+
if (requestId !== this.#autocompleteRequestId || !this.#isAutocompleteOriginCurrent(origin)) return;
|
|
2917
2953
|
|
|
2918
2954
|
if (suggestions && Array.isArray(suggestions.items) && suggestions.items.length > 0) {
|
|
2919
2955
|
// If there's exactly one suggestion and this was an explicit Tab press, apply it immediately
|
|
@@ -2941,6 +2977,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
2941
2977
|
this.#autocompletePrefix = suggestions.prefix;
|
|
2942
2978
|
this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
2943
2979
|
this.#autocompleteState = "force";
|
|
2980
|
+
this.#autocompleteOrigin = origin;
|
|
2944
2981
|
this.onAutocompleteUpdate?.();
|
|
2945
2982
|
} else {
|
|
2946
2983
|
this.#cancelAutocomplete();
|
|
@@ -2955,6 +2992,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
2955
2992
|
this.#autocompleteRequestId += 1;
|
|
2956
2993
|
this.#autocompleteState = null;
|
|
2957
2994
|
this.#autocompleteList = undefined;
|
|
2995
|
+
this.#autocompleteOrigin = undefined;
|
|
2958
2996
|
this.#autocompletePrefix = "";
|
|
2959
2997
|
if (notifyCancel && wasAutocompleting) {
|
|
2960
2998
|
this.onAutocompleteCancel?.();
|
|
@@ -2974,6 +3012,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
2974
3012
|
return;
|
|
2975
3013
|
}
|
|
2976
3014
|
|
|
3015
|
+
const origin = this.#captureAutocompleteOrigin();
|
|
2977
3016
|
const requestId = ++this.#autocompleteRequestId;
|
|
2978
3017
|
|
|
2979
3018
|
const suggestions = await this.#autocompleteProvider.getSuggestions(
|
|
@@ -2981,12 +3020,13 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
2981
3020
|
this.#state.cursorLine,
|
|
2982
3021
|
this.#state.cursorCol,
|
|
2983
3022
|
);
|
|
2984
|
-
if (requestId !== this.#autocompleteRequestId) return;
|
|
3023
|
+
if (requestId !== this.#autocompleteRequestId || !this.#isAutocompleteOriginCurrent(origin)) return;
|
|
2985
3024
|
|
|
2986
3025
|
if (suggestions && Array.isArray(suggestions.items) && suggestions.items.length > 0) {
|
|
2987
3026
|
this.#autocompletePrefix = suggestions.prefix;
|
|
2988
3027
|
// Always create new SelectList to ensure update
|
|
2989
3028
|
this.#autocompleteList = this.#createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
3029
|
+
this.#autocompleteOrigin = origin;
|
|
2990
3030
|
this.onAutocompleteUpdate?.();
|
|
2991
3031
|
} else {
|
|
2992
3032
|
this.#cancelAutocomplete();
|