@gajae-code/tui 0.15.3 → 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 +11 -0
- package/dist/types/autocomplete.d.ts +14 -1
- package/package.json +3 -3
- package/src/autocomplete.ts +136 -65
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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.
|
|
10
|
+
## [0.15.4] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Path autocomplete matches decomposed (NFD) file names against composed (NFC) input. Composer keystrokes are NFC-normalized while macOS volumes commonly return Hangul and other composed scripts in NFD, so `@한` found nothing even though `한글.txt` existed; both the directory-listing prefix match and the fuzzy filter now compare NFC forms while completion values keep the on-disk name. The native fuzzy finder applies the same normalization to queries and candidates.
|
|
15
|
+
|
|
5
16
|
## [0.15.3] - 2026-08-27
|
|
6
17
|
|
|
7
18
|
### 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
|
@@ -18,6 +18,10 @@ async function fuzzyFindNative(): Promise<NativeFuzzyFind> {
|
|
|
18
18
|
|
|
19
19
|
const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
|
|
20
20
|
|
|
21
|
+
function isAbsolutePathLike(value: string): boolean {
|
|
22
|
+
return path.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
function buildAutocompleteFuzzyDiscoveryProfile(
|
|
22
26
|
query: string,
|
|
23
27
|
basePath: string,
|
|
@@ -117,9 +121,30 @@ function buildCompletionValue(
|
|
|
117
121
|
return `${openQuote}${path}${closeQuote}`;
|
|
118
122
|
}
|
|
119
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
|
+
|
|
120
143
|
/**
|
|
121
144
|
* Check if query is a subsequence of target (fuzzy match).
|
|
122
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 "한글".
|
|
123
148
|
*/
|
|
124
149
|
function fuzzyMatch(query: string, target: string): boolean {
|
|
125
150
|
if (query.length === 0) return true;
|
|
@@ -127,7 +152,7 @@ function fuzzyMatch(query: string, target: string): boolean {
|
|
|
127
152
|
|
|
128
153
|
let qi = 0;
|
|
129
154
|
for (let ti = 0; ti < target.length && qi < query.length; ti++) {
|
|
130
|
-
if (query[qi]
|
|
155
|
+
if (fuzzyCharMatches(query[qi] as string, target[ti] as string)) qi++;
|
|
131
156
|
}
|
|
132
157
|
return qi === query.length;
|
|
133
158
|
}
|
|
@@ -148,7 +173,7 @@ function fuzzyScore(query: string, target: string): number {
|
|
|
148
173
|
let gaps = 0;
|
|
149
174
|
let lastMatchIdx = -1;
|
|
150
175
|
for (let ti = 0; ti < target.length && qi < query.length; ti++) {
|
|
151
|
-
if (query[qi]
|
|
176
|
+
if (fuzzyCharMatches(query[qi] as string, target[ti] as string)) {
|
|
152
177
|
if (lastMatchIdx >= 0 && ti - lastMatchIdx > 1) gaps++;
|
|
153
178
|
lastMatchIdx = ti;
|
|
154
179
|
qi++;
|
|
@@ -159,6 +184,8 @@ function fuzzyScore(query: string, target: string): number {
|
|
|
159
184
|
// Base score 40 for subsequence, minus penalty for gaps
|
|
160
185
|
return Math.max(1, 40 - gaps * 5);
|
|
161
186
|
}
|
|
187
|
+
|
|
188
|
+
export { fuzzyMatch as autocompleteFuzzyMatch, fuzzyScore as autocompleteFuzzyScore };
|
|
162
189
|
export function getSlashCommandMatchRank(query: string, commandName: string): number {
|
|
163
190
|
const normalizedQuery = normalizeSlashCommandText(query);
|
|
164
191
|
if (normalizedQuery.length === 0) return 4;
|
|
@@ -310,7 +337,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
310
337
|
// Intentionally separate from pi-natives cache: this cache is a local,
|
|
311
338
|
// per-directory readdir fast-path for prefix completions. Global fuzzy
|
|
312
339
|
// discovery continues to use native fuzzyFind + shared scan cache.
|
|
313
|
-
#dirCache: Map<string, { entries: fs.Dirent[]; timestamp: number }> = new Map();
|
|
340
|
+
#dirCache: Map<string, { entries: fs.Dirent[]; resolvedDir: string; timestamp: number }> = new Map();
|
|
314
341
|
readonly #DIR_CACHE_TTL = 2000; // 2 seconds
|
|
315
342
|
|
|
316
343
|
constructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = getProjectDir()) {
|
|
@@ -324,22 +351,32 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
324
351
|
|
|
325
352
|
#getSlashCommandNameSuggestions(prefix: string): AutocompleteItem[] {
|
|
326
353
|
const lowerPrefix = prefix.toLowerCase();
|
|
354
|
+
const normalizedPrefix = normalizeFuzzyText(prefix);
|
|
355
|
+
if (prefix.length > 0 && normalizedPrefix.length === 0) return [];
|
|
327
356
|
|
|
328
357
|
return this.#commands
|
|
329
358
|
.filter(cmd => {
|
|
330
359
|
const name = this.#getCommandName(cmd);
|
|
331
360
|
if (!name) return false;
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
const
|
|
335
|
-
|
|
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;
|
|
336
367
|
})
|
|
337
368
|
.map((cmd, index) => {
|
|
338
369
|
const name = this.#getCommandName(cmd);
|
|
339
370
|
const lowerName = name?.toLowerCase() ?? "";
|
|
340
|
-
const lowerDesc = cmd.description
|
|
341
|
-
const
|
|
342
|
-
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;
|
|
343
380
|
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
|
344
381
|
const desc = cmd.description ?? "";
|
|
345
382
|
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
|
@@ -349,7 +386,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
349
386
|
label: "name" in cmd ? cmd.name : cmd.label,
|
|
350
387
|
score: Math.max(nameScore, descScore),
|
|
351
388
|
priority,
|
|
352
|
-
matchRank:
|
|
389
|
+
matchRank: /^[\x00-\x7F]*$/.test(`${prefix}${name}`)
|
|
390
|
+
? getSlashCommandMatchRank(lowerPrefix, lowerName)
|
|
391
|
+
: 4,
|
|
353
392
|
index,
|
|
354
393
|
...(fullDesc && { description: fullDesc }),
|
|
355
394
|
} as AutocompleteItem & { score: number; priority: number; matchRank: number; index: number };
|
|
@@ -589,7 +628,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
589
628
|
async #resolveScopedFuzzyQuery(
|
|
590
629
|
rawQuery: string,
|
|
591
630
|
): Promise<{ baseDir: string; query: string; displayBase: string } | null> {
|
|
592
|
-
const slashIndex = rawQuery.lastIndexOf("/");
|
|
631
|
+
const slashIndex = Math.max(rawQuery.lastIndexOf("/"), rawQuery.lastIndexOf("\\"));
|
|
593
632
|
if (slashIndex === -1) {
|
|
594
633
|
return null;
|
|
595
634
|
}
|
|
@@ -606,15 +645,16 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
606
645
|
baseDir = path.join(this.#basePath, displayBase);
|
|
607
646
|
}
|
|
608
647
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
} catch {
|
|
648
|
+
const resolvedDir = await this.#resolveDirectoryPath(baseDir);
|
|
649
|
+
if (!resolvedDir) {
|
|
614
650
|
return null;
|
|
615
651
|
}
|
|
616
652
|
|
|
617
|
-
return {
|
|
653
|
+
return {
|
|
654
|
+
baseDir: resolvedDir,
|
|
655
|
+
query,
|
|
656
|
+
displayBase: this.#formatResolvedDirectoryPrefix(displayBase, resolvedDir),
|
|
657
|
+
};
|
|
618
658
|
}
|
|
619
659
|
|
|
620
660
|
#scopedPathForDisplay(displayBase: string, relativePath: string): string {
|
|
@@ -624,16 +664,75 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
624
664
|
return `${displayBase}${relativePath}`;
|
|
625
665
|
}
|
|
626
666
|
|
|
627
|
-
async #
|
|
667
|
+
async #resolveDirectoryPath(candidate: string): Promise<string | null> {
|
|
668
|
+
const absoluteCandidate = path.resolve(candidate);
|
|
669
|
+
const root = path.parse(absoluteCandidate).root;
|
|
670
|
+
const relativePath = path.relative(root, absoluteCandidate);
|
|
671
|
+
const components = relativePath ? relativePath.split(path.sep).filter(Boolean) : [];
|
|
672
|
+
let resolvedPath = root;
|
|
673
|
+
|
|
674
|
+
for (const component of components) {
|
|
675
|
+
let entries: fs.Dirent[];
|
|
676
|
+
try {
|
|
677
|
+
entries = await fs.promises.readdir(resolvedPath, { withFileTypes: true });
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const exactEntry = entries.find(entry => entry.name === component);
|
|
684
|
+
const matchingEntries = exactEntry
|
|
685
|
+
? [exactEntry]
|
|
686
|
+
: entries.filter(entry => entry.name.normalize("NFC") === component.normalize("NFC"));
|
|
687
|
+
// An exact spelling is unambiguous even when a Linux directory contains
|
|
688
|
+
// both NFC and NFD variants. A normalized-only match must not guess
|
|
689
|
+
// between siblings, since doing so can expose the wrong subtree.
|
|
690
|
+
if (matchingEntries.length !== 1) return null;
|
|
691
|
+
|
|
692
|
+
const entry = matchingEntries[0];
|
|
693
|
+
if (!entry) return null;
|
|
694
|
+
let isDirectory = entry.isDirectory();
|
|
695
|
+
if (!isDirectory && entry.isSymbolicLink()) {
|
|
696
|
+
isDirectory = (await fs.promises.stat(path.join(resolvedPath, entry.name))).isDirectory();
|
|
697
|
+
}
|
|
698
|
+
if (!isDirectory) return null;
|
|
699
|
+
resolvedPath = path.join(resolvedPath, entry.name);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return resolvedPath;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
#formatResolvedDirectoryPrefix(rawPrefix: string, resolvedDir: string): string {
|
|
706
|
+
const normalizedResolvedDir = resolvedDir.replaceAll(path.sep, "/");
|
|
707
|
+
if (rawPrefix === "~" || rawPrefix.startsWith("~/")) {
|
|
708
|
+
const homeRelative = path.relative(path.resolve(os.homedir()), resolvedDir).replaceAll(path.sep, "/");
|
|
709
|
+
return homeRelative ? `~/${homeRelative}/` : "~/";
|
|
710
|
+
}
|
|
711
|
+
if (isAbsolutePathLike(rawPrefix)) {
|
|
712
|
+
return normalizedResolvedDir === "/" ? "/" : `${normalizedResolvedDir}/`;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const relative = path.relative(path.resolve(this.#basePath), resolvedDir).replaceAll(path.sep, "/");
|
|
716
|
+
if (rawPrefix.startsWith("./")) {
|
|
717
|
+
return relative ? `./${relative}/` : "./";
|
|
718
|
+
}
|
|
719
|
+
return relative ? `${relative}/` : "";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async #getCachedDirEntries(searchDir: string): Promise<{ entries: fs.Dirent[]; resolvedDir: string }> {
|
|
628
723
|
const now = Date.now();
|
|
629
724
|
const cached = this.#dirCache.get(searchDir);
|
|
630
725
|
|
|
631
726
|
if (cached && now - cached.timestamp < this.#DIR_CACHE_TTL) {
|
|
632
|
-
return cached.entries;
|
|
727
|
+
return { entries: cached.entries, resolvedDir: cached.resolvedDir };
|
|
633
728
|
}
|
|
634
729
|
|
|
635
|
-
const
|
|
636
|
-
|
|
730
|
+
const resolvedDir = await this.#resolveDirectoryPath(searchDir);
|
|
731
|
+
if (!resolvedDir) {
|
|
732
|
+
throw new Error(`Directory not found: ${searchDir}`);
|
|
733
|
+
}
|
|
734
|
+
const entries = await fs.promises.readdir(resolvedDir, { withFileTypes: true });
|
|
735
|
+
this.#dirCache.set(searchDir, { entries, resolvedDir, timestamp: now });
|
|
637
736
|
|
|
638
737
|
if (this.#dirCache.size > 100) {
|
|
639
738
|
const sortedKeys = [...this.#dirCache.entries()]
|
|
@@ -645,7 +744,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
645
744
|
}
|
|
646
745
|
}
|
|
647
746
|
|
|
648
|
-
return entries;
|
|
747
|
+
return { entries, resolvedDir };
|
|
649
748
|
}
|
|
650
749
|
|
|
651
750
|
invalidateDirCache(dir?: string): void {
|
|
@@ -680,7 +779,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
680
779
|
|
|
681
780
|
if (isRootPrefix) {
|
|
682
781
|
// Complete from specified position
|
|
683
|
-
if (rawPrefix.startsWith("~") || expandedPrefix
|
|
782
|
+
if (rawPrefix.startsWith("~") || isAbsolutePathLike(expandedPrefix)) {
|
|
684
783
|
searchDir = expandedPrefix;
|
|
685
784
|
} else {
|
|
686
785
|
searchDir = path.join(this.#basePath, expandedPrefix);
|
|
@@ -688,7 +787,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
688
787
|
searchPrefix = "";
|
|
689
788
|
} else if (rawPrefix.endsWith("/")) {
|
|
690
789
|
// If prefix ends with /, show contents of that directory
|
|
691
|
-
if (rawPrefix.startsWith("~") || expandedPrefix
|
|
790
|
+
if (rawPrefix.startsWith("~") || isAbsolutePathLike(expandedPrefix)) {
|
|
692
791
|
searchDir = expandedPrefix;
|
|
693
792
|
} else {
|
|
694
793
|
searchDir = path.join(this.#basePath, expandedPrefix);
|
|
@@ -698,7 +797,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
698
797
|
// Split into directory and file prefix
|
|
699
798
|
const dir = path.dirname(expandedPrefix);
|
|
700
799
|
const file = path.basename(expandedPrefix);
|
|
701
|
-
if (rawPrefix.startsWith("~") || expandedPrefix
|
|
800
|
+
if (rawPrefix.startsWith("~") || isAbsolutePathLike(expandedPrefix)) {
|
|
702
801
|
searchDir = dir;
|
|
703
802
|
} else {
|
|
704
803
|
searchDir = path.join(this.#basePath, dir);
|
|
@@ -706,11 +805,13 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
706
805
|
searchPrefix = file;
|
|
707
806
|
}
|
|
708
807
|
|
|
709
|
-
const entries = await this.#getCachedDirEntries(searchDir);
|
|
808
|
+
const { entries, resolvedDir } = await this.#getCachedDirEntries(searchDir);
|
|
710
809
|
const suggestions: AutocompleteItem[] = [];
|
|
810
|
+
const lowerSearchPrefix = searchPrefix.normalize("NFC").toLowerCase();
|
|
811
|
+
const displayDirectoryPrefix = this.#formatResolvedDirectoryPrefix(rawPrefix, resolvedDir);
|
|
711
812
|
|
|
712
813
|
for (const entry of entries) {
|
|
713
|
-
if (!entry.name.toLowerCase().startsWith(
|
|
814
|
+
if (!entry.name.normalize("NFC").toLowerCase().startsWith(lowerSearchPrefix)) {
|
|
714
815
|
continue;
|
|
715
816
|
}
|
|
716
817
|
// Skip .git directory
|
|
@@ -722,7 +823,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
722
823
|
let isDirectory = entry.isDirectory();
|
|
723
824
|
if (!isDirectory && entry.isSymbolicLink()) {
|
|
724
825
|
try {
|
|
725
|
-
const fullPath = path.join(
|
|
826
|
+
const fullPath = path.join(resolvedDir, entry.name);
|
|
726
827
|
isDirectory = (await fs.promises.stat(fullPath)).isDirectory();
|
|
727
828
|
} catch {
|
|
728
829
|
// Broken symlink, file deleted between readdir and stat, or permission error
|
|
@@ -730,41 +831,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
730
831
|
}
|
|
731
832
|
}
|
|
732
833
|
|
|
733
|
-
let relativePath: string;
|
|
734
834
|
const name = entry.name;
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
if (displayPrefix.endsWith("/")) {
|
|
738
|
-
// If prefix ends with /, append entry to the prefix
|
|
739
|
-
relativePath = displayPrefix + name;
|
|
740
|
-
} else if (displayPrefix.includes("/")) {
|
|
741
|
-
// Preserve ~/ format for home directory paths
|
|
742
|
-
if (displayPrefix.startsWith("~/")) {
|
|
743
|
-
const homeRelativeDir = displayPrefix.slice(2); // Remove ~/
|
|
744
|
-
const dir = path.dirname(homeRelativeDir);
|
|
745
|
-
relativePath = `~/${dir === "." ? name : path.join(dir, name)}`;
|
|
746
|
-
} else if (displayPrefix.startsWith("/")) {
|
|
747
|
-
// Absolute path - construct properly
|
|
748
|
-
const dir = path.dirname(displayPrefix);
|
|
749
|
-
if (dir === "/") {
|
|
750
|
-
relativePath = `/${name}`;
|
|
751
|
-
} else {
|
|
752
|
-
relativePath = `${dir}/${name}`;
|
|
753
|
-
}
|
|
754
|
-
} else {
|
|
755
|
-
relativePath = path.join(path.dirname(displayPrefix), name);
|
|
756
|
-
if (displayPrefix.startsWith("./") && !relativePath.startsWith("./")) {
|
|
757
|
-
relativePath = `./${relativePath}`;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
} else {
|
|
761
|
-
// For standalone entries, preserve ~/ if original prefix was ~/
|
|
762
|
-
if (displayPrefix.startsWith("~")) {
|
|
763
|
-
relativePath = `~/${name}`;
|
|
764
|
-
} else {
|
|
765
|
-
relativePath = name;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
835
|
+
const relativePath = `${displayDirectoryPrefix}${name}`;
|
|
768
836
|
|
|
769
837
|
const pathValue = isDirectory ? `${relativePath}/` : relativePath;
|
|
770
838
|
const value = buildCompletionValue(pathValue, {
|
|
@@ -798,17 +866,20 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
798
866
|
async #getFuzzyFileSuggestions(query: string, options: { isQuotedPrefix: boolean }): Promise<AutocompleteItem[]> {
|
|
799
867
|
try {
|
|
800
868
|
const scopedQuery = await this.#resolveScopedFuzzyQuery(query);
|
|
869
|
+
if (query.includes("/") && !scopedQuery) {
|
|
870
|
+
return [];
|
|
871
|
+
}
|
|
801
872
|
const searchPath = scopedQuery?.baseDir ?? this.#basePath;
|
|
802
873
|
const fuzzyQuery = scopedQuery?.query ?? query;
|
|
803
874
|
const result = await (await fuzzyFindNative())(buildAutocompleteFuzzyDiscoveryProfile(fuzzyQuery, searchPath));
|
|
804
|
-
const
|
|
875
|
+
const normalizedQuery = normalizeFuzzyText(fuzzyQuery);
|
|
805
876
|
const filteredMatches = result.matches.filter(entry => {
|
|
806
877
|
const p = entry.path.endsWith("/") ? entry.path.slice(0, -1) : entry.path;
|
|
807
878
|
const normalized = p.replaceAll("\\", "/");
|
|
808
879
|
if (/(^|\/)\.git(\/|$)/.test(normalized)) {
|
|
809
880
|
return false;
|
|
810
881
|
}
|
|
811
|
-
return
|
|
882
|
+
return normalizedQuery.length === 0 || fuzzyMatch(normalizedQuery, normalizeFuzzyText(normalized));
|
|
812
883
|
});
|
|
813
884
|
const topEntries = filteredMatches.slice(0, 20);
|
|
814
885
|
const suggestions: AutocompleteItem[] = [];
|