mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
feat(core): improve @file autocomplete to prioritize filenames (#21064)
This commit is contained in:
@@ -120,8 +120,8 @@ describe('useAtCompletion', () => {
|
||||
|
||||
expect(result.current.suggestions.map((s) => s.value)).toEqual([
|
||||
'src/',
|
||||
'src/components/',
|
||||
'src/index.js',
|
||||
'src/components/',
|
||||
'src/components/Button.tsx',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -421,6 +421,47 @@ describe('FileSearch', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should prioritize filenames closer to the end of the path and shorter paths', async () => {
|
||||
tmpDir = await createTmpDir({
|
||||
src: {
|
||||
'hooks.ts': '',
|
||||
hooks: {
|
||||
'index.ts': '',
|
||||
},
|
||||
utils: {
|
||||
'hooks.tsx': '',
|
||||
},
|
||||
'hooks-dev': {
|
||||
'test.ts': '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const fileSearch = FileSearchFactory.create({
|
||||
projectRoot: tmpDir,
|
||||
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
|
||||
respectGitIgnore: false,
|
||||
respectGeminiIgnore: false,
|
||||
}),
|
||||
ignoreDirs: [],
|
||||
cache: false,
|
||||
cacheTtl: 0,
|
||||
enableRecursiveFileSearch: true,
|
||||
enableFuzzySearch: true,
|
||||
});
|
||||
|
||||
await fileSearch.initialize();
|
||||
const results = await fileSearch.search('hooks');
|
||||
|
||||
// The order should prioritize matches closer to the end and shorter strings.
|
||||
// FZF matches right-to-left.
|
||||
expect(results[0]).toBe('src/hooks/');
|
||||
expect(results[1]).toBe('src/hooks.ts');
|
||||
expect(results[2]).toBe('src/utils/hooks.tsx');
|
||||
expect(results[3]).toBe('src/hooks-dev/');
|
||||
expect(results[4]).toBe('src/hooks/index.ts');
|
||||
expect(results[5]).toBe('src/hooks-dev/test.ts');
|
||||
});
|
||||
it('should return empty array when no matches are found', async () => {
|
||||
tmpDir = await createTmpDir({
|
||||
src: ['file1.js'],
|
||||
|
||||
@@ -13,6 +13,44 @@ import { AsyncFzf, type FzfResultItem } from 'fzf';
|
||||
import { unescapePath } from '../paths.js';
|
||||
import type { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
|
||||
|
||||
// Tiebreaker: Prefers shorter paths.
|
||||
const byLengthAsc = (a: { item: string }, b: { item: string }) =>
|
||||
a.item.length - b.item.length;
|
||||
|
||||
// Tiebreaker: Prefers matches at the start of the filename (basename prefix).
|
||||
const byBasenamePrefix = (
|
||||
a: { item: string; positions: Set<number> },
|
||||
b: { item: string; positions: Set<number> },
|
||||
) => {
|
||||
const getBasenameStart = (p: string) => {
|
||||
const trimmed = p.endsWith('/') ? p.slice(0, -1) : p;
|
||||
return Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\')) + 1;
|
||||
};
|
||||
const aDiff = Math.min(...a.positions) - getBasenameStart(a.item);
|
||||
const bDiff = Math.min(...b.positions) - getBasenameStart(b.item);
|
||||
|
||||
const aIsFilenameMatch = aDiff >= 0;
|
||||
const bIsFilenameMatch = bDiff >= 0;
|
||||
|
||||
if (aIsFilenameMatch && !bIsFilenameMatch) return -1;
|
||||
if (!aIsFilenameMatch && bIsFilenameMatch) return 1;
|
||||
if (aIsFilenameMatch && bIsFilenameMatch) return aDiff - bDiff;
|
||||
|
||||
return 0; // Both are directory matches, let subsequent tiebreakers decide.
|
||||
};
|
||||
|
||||
// Tiebreaker: Prefers matches closer to the end of the path.
|
||||
const byMatchPosFromEnd = (
|
||||
a: { item: string; positions: Set<number> },
|
||||
b: { item: string; positions: Set<number> },
|
||||
) => {
|
||||
const maxPosA = Math.max(-1, ...a.positions);
|
||||
const maxPosB = Math.max(-1, ...b.positions);
|
||||
const distA = a.item.length - maxPosA;
|
||||
const distB = b.item.length - maxPosB;
|
||||
return distA - distB;
|
||||
};
|
||||
|
||||
export interface FileSearchOptions {
|
||||
projectRoot: string;
|
||||
ignoreDirs: string[];
|
||||
@@ -192,6 +230,8 @@ class RecursiveFileSearch implements FileSearch {
|
||||
// files, because the v2 algorithm is just too slow in those cases.
|
||||
this.fzf = new AsyncFzf(this.allFiles, {
|
||||
fuzzy: this.allFiles.length > 20000 ? 'v1' : 'v2',
|
||||
forward: false,
|
||||
tiebreakers: [byBasenamePrefix, byMatchPosFromEnd, byLengthAsc],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user