Improve the performance of filename completion over large repositories. (#938)
This commit is contained in:
parent
9072a4e5ee
commit
f2ab6d08c4
|
@ -32,6 +32,7 @@ export default tseslint.config(
|
||||||
'eslint.config.js',
|
'eslint.config.js',
|
||||||
'packages/cli/dist/**',
|
'packages/cli/dist/**',
|
||||||
'packages/core/dist/**',
|
'packages/core/dist/**',
|
||||||
|
'packages/server/dist/**',
|
||||||
'eslint-rules/*',
|
'eslint-rules/*',
|
||||||
'bundle/**',
|
'bundle/**',
|
||||||
],
|
],
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gemini-cli/core": "file:../core",
|
"@gemini-cli/core": "file:../core",
|
||||||
|
"command-exists": "^1.2.9",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
@ -48,18 +49,17 @@
|
||||||
"string-width": "^7.1.0",
|
"string-width": "^7.1.0",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
"strip-json-comments": "^3.1.1",
|
"strip-json-comments": "^3.1.1",
|
||||||
"command-exists": "^1.2.9",
|
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
|
"@types/command-exists": "^1.2.3",
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
"@types/dotenv": "^6.1.1",
|
"@types/dotenv": "^6.1.1",
|
||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.11.24",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.1",
|
||||||
"@types/shell-quote": "^1.7.5",
|
"@types/shell-quote": "^1.7.5",
|
||||||
"@types/yargs": "^17.0.32",
|
"@types/yargs": "^17.0.32",
|
||||||
"@types/command-exists": "^1.2.3",
|
|
||||||
"ink-testing-library": "^4.0.0",
|
"ink-testing-library": "^4.0.0",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|
|
@ -42,6 +42,7 @@ describe('useCompletion git-aware filtering integration', () => {
|
||||||
shouldIgnoreFile: vi.fn(),
|
shouldIgnoreFile: vi.fn(),
|
||||||
filterFiles: vi.fn(),
|
filterFiles: vi.fn(),
|
||||||
getIgnoreInfo: vi.fn(() => ({ gitIgnored: [], customIgnored: [] })),
|
getIgnoreInfo: vi.fn(() => ({ gitIgnored: [], customIgnored: [] })),
|
||||||
|
glob: vi.fn().mockResolvedValue([]),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
|
@ -225,4 +226,27 @@ describe('useCompletion git-aware filtering integration', () => {
|
||||||
{ label: 'component.tsx', value: 'component.tsx' },
|
{ label: 'component.tsx', value: 'component.tsx' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use glob for top-level @ completions when available', async () => {
|
||||||
|
const globResults = [`${testCwd}/src/index.ts`, `${testCwd}/README.md`];
|
||||||
|
mockFileDiscoveryService.glob.mockResolvedValue(globResults);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useCompletion('@s', testCwd, true, slashCommands, mockConfig),
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFileDiscoveryService.glob).toHaveBeenCalledWith('**/s*', {
|
||||||
|
cwd: testCwd,
|
||||||
|
dot: true,
|
||||||
|
});
|
||||||
|
expect(fs.readdir).not.toHaveBeenCalled(); // Ensure glob is used instead of readdir
|
||||||
|
expect(result.current.suggestions).toEqual([
|
||||||
|
{ label: 'README.md', value: 'README.md' },
|
||||||
|
{ label: 'src/index.ts', value: 'src/index.ts' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
unescapePath,
|
unescapePath,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
Config,
|
Config,
|
||||||
|
FileDiscoveryService,
|
||||||
} from '@gemini-cli/core';
|
} from '@gemini-cli/core';
|
||||||
import {
|
import {
|
||||||
MAX_SUGGESTIONS_TO_SHOW,
|
MAX_SUGGESTIONS_TO_SHOW,
|
||||||
|
@ -251,21 +252,53 @@ export function useCompletion(
|
||||||
return foundSuggestions.slice(0, maxResults);
|
return foundSuggestions.slice(0, maxResults);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findFilesWithGlob = async (
|
||||||
|
searchPrefix: string,
|
||||||
|
fileDiscoveryService: FileDiscoveryService,
|
||||||
|
maxResults = 50,
|
||||||
|
): Promise<Suggestion[]> => {
|
||||||
|
const globPattern = `**/${searchPrefix}*`;
|
||||||
|
const files = await fileDiscoveryService.glob(globPattern, {
|
||||||
|
cwd,
|
||||||
|
dot: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const suggestions: Suggestion[] = files
|
||||||
|
.map((file: string) => {
|
||||||
|
const relativePath = path.relative(cwd, file);
|
||||||
|
return {
|
||||||
|
label: relativePath,
|
||||||
|
value: escapePath(relativePath),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.slice(0, maxResults);
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchSuggestions = async () => {
|
const fetchSuggestions = async () => {
|
||||||
setIsLoadingSuggestions(true);
|
setIsLoadingSuggestions(true);
|
||||||
let fetchedSuggestions: Suggestion[] = [];
|
let fetchedSuggestions: Suggestion[] = [];
|
||||||
|
|
||||||
// Get centralized file discovery service if config is available
|
const fileDiscoveryService = config
|
||||||
const fileDiscovery = config ? await config.getFileService() : null;
|
? await config.getFileService()
|
||||||
|
: null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// If there's no slash, or it's the root, do a recursive search from cwd
|
// If there's no slash, or it's the root, do a recursive search from cwd
|
||||||
if (partialPath.indexOf('/') === -1 && prefix) {
|
if (partialPath.indexOf('/') === -1 && prefix) {
|
||||||
|
if (fileDiscoveryService) {
|
||||||
|
fetchedSuggestions = await findFilesWithGlob(
|
||||||
|
prefix,
|
||||||
|
fileDiscoveryService,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
fetchedSuggestions = await findFilesRecursively(
|
fetchedSuggestions = await findFilesRecursively(
|
||||||
cwd,
|
cwd,
|
||||||
prefix,
|
prefix,
|
||||||
fileDiscovery,
|
fileDiscoveryService,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Original behavior: list files in the specific directory
|
// Original behavior: list files in the specific directory
|
||||||
const lowerPrefix = prefix.toLowerCase();
|
const lowerPrefix = prefix.toLowerCase();
|
||||||
|
@ -282,7 +315,10 @@ export function useCompletion(
|
||||||
cwd,
|
cwd,
|
||||||
path.join(baseDirAbsolute, entry.name),
|
path.join(baseDirAbsolute, entry.name),
|
||||||
);
|
);
|
||||||
if (fileDiscovery && fileDiscovery.shouldIgnoreFile(relativePath)) {
|
if (
|
||||||
|
fileDiscoveryService &&
|
||||||
|
fileDiscoveryService.shouldIgnoreFile(relativePath)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { GitIgnoreParser, GitIgnoreFilter } from '../utils/gitIgnoreParser.js';
|
import { GitIgnoreParser, GitIgnoreFilter } from '../utils/gitIgnoreParser.js';
|
||||||
import { isGitRepository } from '../utils/gitUtils.js';
|
import { isGitRepository } from '../utils/gitUtils.js';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import fg from 'fast-glob';
|
||||||
|
|
||||||
export interface FileDiscoveryOptions {
|
export interface FileDiscoveryOptions {
|
||||||
respectGitIgnore?: boolean;
|
respectGitIgnore?: boolean;
|
||||||
|
@ -32,6 +33,17 @@ export class FileDiscoveryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async glob(
|
||||||
|
pattern: string | string[],
|
||||||
|
options: fg.Options = {},
|
||||||
|
): Promise<string[]> {
|
||||||
|
const files = await fg(pattern, {
|
||||||
|
...options,
|
||||||
|
caseSensitiveMatch: false,
|
||||||
|
});
|
||||||
|
return this.filterFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters a list of file paths based on git ignore rules
|
* Filters a list of file paths based on git ignore rules
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import ignore, { Ignore } from 'ignore';
|
import ignore, { type Ignore } from 'ignore';
|
||||||
import { isGitRepository } from './gitUtils.js';
|
import { isGitRepository } from './gitUtils.js';
|
||||||
|
|
||||||
export interface GitIgnoreFilter {
|
export interface GitIgnoreFilter {
|
||||||
|
|
Loading…
Reference in New Issue