@gajae-code/tui 0.15.4 → 0.15.5
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 +14 -1
- package/package.json +3 -3
- package/src/autocomplete.ts +47 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.15.5] - 2026-08-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `@` fuzzy file search supports Hangul chosung (초성) matching: a bare consonant matches any syllable with that initial, so `@ㅎㄱ` finds `한글.txt`. Literal and full-syllable matches keep ranking above chosung matches.
|
|
5
10
|
## [0.15.4] - 2026-08-29
|
|
6
11
|
|
|
7
12
|
### Fixed
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
export declare function normalizeFuzzyText(value: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Check if query is a subsequence of target (fuzzy match).
|
|
4
|
+
* "wig" matches "skill:wig" because w-i-g appear in order.
|
|
5
|
+
* A bare Hangul consonant also matches a syllable's initial (초성 검색),
|
|
6
|
+
* so "ㅎㄱ" matches "한글".
|
|
7
|
+
*/
|
|
8
|
+
declare function fuzzyMatch(query: string, target: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Score a fuzzy match. Higher = better match.
|
|
11
|
+
* Prioritizes: exact match > starts-with > contains > subsequence
|
|
12
|
+
*/
|
|
13
|
+
declare function fuzzyScore(query: string, target: string): number;
|
|
14
|
+
export { fuzzyMatch as autocompleteFuzzyMatch, fuzzyScore as autocompleteFuzzyScore };
|
|
1
15
|
export declare function getSlashCommandMatchRank(query: string, commandName: string): number;
|
|
2
16
|
export declare function isInsideInlineCodeSpan(text: string): boolean;
|
|
3
17
|
export declare function extractSlashCommandTokenPrefix(text: string): string | null;
|
|
@@ -82,4 +96,3 @@ export declare class CombinedAutocompleteProvider implements AutocompleteProvide
|
|
|
82
96
|
prefix: string;
|
|
83
97
|
} | null;
|
|
84
98
|
}
|
|
85
|
-
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/tui",
|
|
4
|
-
"version": "0.15.
|
|
4
|
+
"version": "0.15.5",
|
|
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.15.
|
|
40
|
-
"@gajae-code/utils": "0.15.
|
|
39
|
+
"@gajae-code/natives": "0.15.5",
|
|
40
|
+
"@gajae-code/utils": "0.15.5",
|
|
41
41
|
"lru-cache": "11.3.6",
|
|
42
42
|
"marked": "18.0.6"
|
|
43
43
|
},
|
package/src/autocomplete.ts
CHANGED
|
@@ -121,9 +121,30 @@ function buildCompletionValue(
|
|
|
121
121
|
return `${openQuote}${path}${closeQuote}`;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
const HANGUL_INITIAL_COMPAT_JAMO = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ";
|
|
125
|
+
|
|
126
|
+
function hangulInitialJamo(char: string): string | undefined {
|
|
127
|
+
const offset = char.charCodeAt(0) - 0xac00;
|
|
128
|
+
if (offset < 0 || offset >= 11172) return undefined;
|
|
129
|
+
return HANGUL_INITIAL_COMPAT_JAMO[Math.floor(offset / 588)];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function fuzzyCharMatches(queryChar: string, targetChar: string): boolean {
|
|
133
|
+
return queryChar === targetChar || hangulInitialJamo(targetChar) === queryChar;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function normalizeFuzzyText(value: string): string {
|
|
137
|
+
return value
|
|
138
|
+
.normalize("NFC")
|
|
139
|
+
.toLowerCase()
|
|
140
|
+
.replace(/[\s/\\._-]+/g, "");
|
|
141
|
+
}
|
|
142
|
+
|
|
124
143
|
/**
|
|
125
144
|
* Check if query is a subsequence of target (fuzzy match).
|
|
126
145
|
* "wig" matches "skill:wig" because w-i-g appear in order.
|
|
146
|
+
* A bare Hangul consonant also matches a syllable's initial (초성 검색),
|
|
147
|
+
* so "ㅎㄱ" matches "한글".
|
|
127
148
|
*/
|
|
128
149
|
function fuzzyMatch(query: string, target: string): boolean {
|
|
129
150
|
if (query.length === 0) return true;
|
|
@@ -131,7 +152,7 @@ function fuzzyMatch(query: string, target: string): boolean {
|
|
|
131
152
|
|
|
132
153
|
let qi = 0;
|
|
133
154
|
for (let ti = 0; ti < target.length && qi < query.length; ti++) {
|
|
134
|
-
if (query[qi]
|
|
155
|
+
if (fuzzyCharMatches(query[qi] as string, target[ti] as string)) qi++;
|
|
135
156
|
}
|
|
136
157
|
return qi === query.length;
|
|
137
158
|
}
|
|
@@ -152,7 +173,7 @@ function fuzzyScore(query: string, target: string): number {
|
|
|
152
173
|
let gaps = 0;
|
|
153
174
|
let lastMatchIdx = -1;
|
|
154
175
|
for (let ti = 0; ti < target.length && qi < query.length; ti++) {
|
|
155
|
-
if (query[qi]
|
|
176
|
+
if (fuzzyCharMatches(query[qi] as string, target[ti] as string)) {
|
|
156
177
|
if (lastMatchIdx >= 0 && ti - lastMatchIdx > 1) gaps++;
|
|
157
178
|
lastMatchIdx = ti;
|
|
158
179
|
qi++;
|
|
@@ -163,6 +184,8 @@ function fuzzyScore(query: string, target: string): number {
|
|
|
163
184
|
// Base score 40 for subsequence, minus penalty for gaps
|
|
164
185
|
return Math.max(1, 40 - gaps * 5);
|
|
165
186
|
}
|
|
187
|
+
|
|
188
|
+
export { fuzzyMatch as autocompleteFuzzyMatch, fuzzyScore as autocompleteFuzzyScore };
|
|
166
189
|
export function getSlashCommandMatchRank(query: string, commandName: string): number {
|
|
167
190
|
const normalizedQuery = normalizeSlashCommandText(query);
|
|
168
191
|
if (normalizedQuery.length === 0) return 4;
|
|
@@ -328,22 +351,32 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
328
351
|
|
|
329
352
|
#getSlashCommandNameSuggestions(prefix: string): AutocompleteItem[] {
|
|
330
353
|
const lowerPrefix = prefix.toLowerCase();
|
|
354
|
+
const normalizedPrefix = normalizeFuzzyText(prefix);
|
|
355
|
+
if (prefix.length > 0 && normalizedPrefix.length === 0) return [];
|
|
331
356
|
|
|
332
357
|
return this.#commands
|
|
333
358
|
.filter(cmd => {
|
|
334
359
|
const name = this.#getCommandName(cmd);
|
|
335
360
|
if (!name) return false;
|
|
336
|
-
|
|
337
|
-
if (
|
|
338
|
-
const
|
|
339
|
-
|
|
361
|
+
const lowerName = name.toLowerCase();
|
|
362
|
+
if (fuzzyMatch(normalizedPrefix, normalizeFuzzyText(name))) return true;
|
|
363
|
+
const isAscii = /^[\x00-\x7F]*$/.test(`${prefix}${name}`);
|
|
364
|
+
if (isAscii && getSlashCommandMatchRank(lowerPrefix, lowerName) < 4) return true;
|
|
365
|
+
const desc = cmd.description;
|
|
366
|
+
return desc ? fuzzyMatch(normalizedPrefix, normalizeFuzzyText(desc)) : false;
|
|
340
367
|
})
|
|
341
368
|
.map((cmd, index) => {
|
|
342
369
|
const name = this.#getCommandName(cmd);
|
|
343
370
|
const lowerName = name?.toLowerCase() ?? "";
|
|
344
|
-
const lowerDesc = cmd.description
|
|
345
|
-
const
|
|
346
|
-
const
|
|
371
|
+
const lowerDesc = cmd.description ?? "";
|
|
372
|
+
const normalizedName = normalizeFuzzyText(name);
|
|
373
|
+
const normalizedDesc = normalizeFuzzyText(lowerDesc);
|
|
374
|
+
const nameScore = fuzzyMatch(normalizedPrefix, normalizedName)
|
|
375
|
+
? fuzzyScore(normalizedPrefix, normalizedName)
|
|
376
|
+
: 0;
|
|
377
|
+
const descScore = fuzzyMatch(normalizedPrefix, normalizedDesc)
|
|
378
|
+
? fuzzyScore(normalizedPrefix, normalizedDesc) * 0.5
|
|
379
|
+
: 0;
|
|
347
380
|
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
348
381
|
const desc = cmd.description ?? "";
|
|
349
382
|
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
@@ -353,7 +386,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
353
386
|
label: "name" in cmd ? cmd.name : cmd.label,
|
|
354
387
|
score: Math.max(nameScore, descScore),
|
|
355
388
|
priority,
|
|
356
|
-
matchRank:
|
|
389
|
+
matchRank: /^[\x00-\x7F]*$/.test(`${prefix}${name}`)
|
|
390
|
+
? getSlashCommandMatchRank(lowerPrefix, lowerName)
|
|
391
|
+
: 4,
|
|
357
392
|
index,
|
|
358
393
|
...(fullDesc && { description: fullDesc }),
|
|
359
394
|
} as AutocompleteItem & { score: number; priority: number; matchRank: number; index: number };
|
|
@@ -837,14 +872,14 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
837
872
|
const searchPath = scopedQuery?.baseDir ?? this.#basePath;
|
|
838
873
|
const fuzzyQuery = scopedQuery?.query ?? query;
|
|
839
874
|
const result = await (await fuzzyFindNative())(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath));
|
|
840
|
-
const
|
|
875
|
+
const normalizedQuery = normalizeFuzzyText(fuzzyQuery);
|
|
841
876
|
const filteredMatches = result.matches.filter(entry => {
|
|
842
877
|
const p = entry.path.endsWith("/") ? entry.path.slice(0, -1) : entry.path;
|
|
843
878
|
const normalized = p.replaceAll("\\", "/");
|
|
844
879
|
if (/(^|\/)\.git(\/|$)/.test(normalized)) {
|
|
845
880
|
return false;
|
|
846
881
|
}
|
|
847
|
-
return
|
|
882
|
+
return normalizedQuery.length === 0 || fuzzyMatch(normalizedQuery, normalizeFuzzyText(normalized));
|
|
848
883
|
});
|
|
849
884
|
const topEntries = filteredMatches.slice(0, 20);
|
|
850
885
|
const suggestions: AutocompleteItem[] = [];
|