feat: Improve @-command file path completion with fzf integration (#5650)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
DevMassive 2025-08-07 08:41:04 +09:00 committed by GitHub
parent 4782113ceb
commit 9ac3e8b79e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 49 additions and 2 deletions

7
package-lock.json generated
View File

@ -5540,6 +5540,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/fzf": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz",
"integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==",
"license": "BSD-3-Clause"
},
"node_modules/gcp-metadata": { "node_modules/gcp-metadata": {
"version": "6.1.1", "version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
@ -11889,6 +11895,7 @@
"diff": "^7.0.0", "diff": "^7.0.0",
"dotenv": "^17.1.0", "dotenv": "^17.1.0",
"fdir": "^6.4.6", "fdir": "^6.4.6",
"fzf": "^0.5.2",
"glob": "^10.4.5", "glob": "^10.4.5",
"google-auth-library": "^9.11.0", "google-auth-library": "^9.11.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",

View File

@ -114,8 +114,8 @@ describe('useAtCompletion', () => {
expect(result.current.suggestions.map((s) => s.value)).toEqual([ expect(result.current.suggestions.map((s) => s.value)).toEqual([
'src/', 'src/',
'src/components/', 'src/components/',
'src/components/Button.tsx',
'src/index.js', 'src/index.js',
'src/components/Button.tsx',
]); ]);
}); });

View File

@ -35,6 +35,7 @@
"diff": "^7.0.0", "diff": "^7.0.0",
"dotenv": "^17.1.0", "dotenv": "^17.1.0",
"fdir": "^6.4.6", "fdir": "^6.4.6",
"fzf": "^0.5.2",
"glob": "^10.4.5", "glob": "^10.4.5",
"google-auth-library": "^9.11.0", "google-auth-library": "^9.11.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",

View File

@ -290,6 +290,30 @@ describe('FileSearch', () => {
expect(results).toEqual(['src/file1.js', 'src/file2.js']); // Assuming alphabetical sort expect(results).toEqual(['src/file1.js', 'src/file2.js']); // Assuming alphabetical sort
}); });
it('should use fzf for fuzzy matching when pattern does not contain wildcards', async () => {
tmpDir = await createTmpDir({
src: {
'main.js': '',
'util.ts': '',
'style.css': '',
},
});
const fileSearch = new FileSearch({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
});
await fileSearch.initialize();
const results = await fileSearch.search('sst');
expect(results).toEqual(['src/style.css']);
});
it('should return empty array when no matches are found', async () => { it('should return empty array when no matches are found', async () => {
tmpDir = await createTmpDir({ tmpDir = await createTmpDir({
src: ['file1.js'], src: ['file1.js'],

View File

@ -11,6 +11,7 @@ import picomatch from 'picomatch';
import { Ignore } from './ignore.js'; import { Ignore } from './ignore.js';
import { ResultCache } from './result-cache.js'; import { ResultCache } from './result-cache.js';
import * as cache from './crawlCache.js'; import * as cache from './crawlCache.js';
import { Fzf, FzfResultItem } from 'fzf';
export type FileSearchOptions = { export type FileSearchOptions = {
projectRoot: string; projectRoot: string;
@ -77,6 +78,18 @@ export async function filter(
return results; return results;
} }
/**
* Filters a list of paths based on a given pattern using fzf.
* @param allPaths The list of all paths to filter.
* @param pattern The fzf pattern to filter by.
* @returns The filtered and sorted list of paths.
*/
function filterByFzf(allPaths: string[], pattern: string) {
return new Fzf(allPaths)
.find(pattern)
.map((entry: FzfResultItem) => entry.item);
}
export type SearchOptions = { export type SearchOptions = {
signal?: AbortSignal; signal?: AbortSignal;
maxResults?: number; maxResults?: number;
@ -137,7 +150,9 @@ export class FileSearch {
filteredCandidates = candidates; filteredCandidates = candidates;
} else { } else {
// Apply the user's picomatch pattern filter // Apply the user's picomatch pattern filter
filteredCandidates = await filter(candidates, pattern, options.signal); filteredCandidates = pattern.includes('*')
? await filter(candidates, pattern, options.signal)
: filterByFzf(this.allFiles, pattern);
this.resultCache!.set(pattern, filteredCandidates); this.resultCache!.set(pattern, filteredCandidates);
} }