diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts index 08631634..efe15c64 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts @@ -90,7 +90,7 @@ describe('handleAtCommand', () => { // Mock FileDiscoveryService mockFileDiscoveryService = { initialize: vi.fn(), - shouldGitIgnoreFile: vi.fn(() => false), + shouldIgnoreFile: vi.fn(() => false), filterFiles: vi.fn((files) => files), getIgnoreInfo: vi.fn(() => ({ gitIgnored: [] })), isGitRepository: vi.fn(() => true), @@ -171,7 +171,7 @@ describe('handleAtCommand', () => { 125, ); expect(mockReadManyFilesExecute).toHaveBeenCalledWith( - { paths: [filePath], respectGitIgnore: true }, + { paths: [filePath], respect_git_ignore: true }, abortController.signal, ); expect(mockAddItem).toHaveBeenCalledWith( @@ -217,7 +217,7 @@ describe('handleAtCommand', () => { 126, ); expect(mockReadManyFilesExecute).toHaveBeenCalledWith( - { paths: [resolvedGlob], respectGitIgnore: true }, + { paths: [resolvedGlob], respect_git_ignore: true }, abortController.signal, ); expect(mockOnDebugMessage).toHaveBeenCalledWith( @@ -318,7 +318,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); expect(mockReadManyFilesExecute).toHaveBeenCalledWith( - { paths: [unescapedPath], respectGitIgnore: true }, + { paths: [unescapedPath], respect_git_ignore: true }, abortController.signal, ); }); @@ -347,7 +347,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); expect(mockReadManyFilesExecute).toHaveBeenCalledWith( - { paths: [file1, file2], respectGitIgnore: true }, + { paths: [file1, file2], respect_git_ignore: true }, abortController.signal, ); expect(result.processedQuery).toEqual([ @@ -389,7 +389,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); expect(mockReadManyFilesExecute).toHaveBeenCalledWith( - { paths: [file1, file2], respectGitIgnore: true }, + { paths: [file1, file2], respect_git_ignore: true }, abortController.signal, ); expect(result.processedQuery).toEqual([ @@ -454,7 +454,7 @@ describe('handleAtCommand', () => { }); expect(mockReadManyFilesExecute).toHaveBeenCalledWith( - { paths: [file1, resolvedFile2], respectGitIgnore: true }, + { paths: [file1, resolvedFile2], respect_git_ignore: true }, abortController.signal, ); expect(result.processedQuery).toEqual([ @@ -556,7 +556,7 @@ describe('handleAtCommand', () => { // If the mock is simpler, it might use queryPath if stat(queryPath) succeeds. // The most important part is that *some* version of the path that leads to the content is used. // Let's assume it uses the path from the query if stat confirms it exists (even if different case on disk) - { paths: [queryPath], respectGitIgnore: true }, + { paths: [queryPath], respect_git_ignore: true }, abortController.signal, ); expect(mockAddItem).toHaveBeenCalledWith( @@ -582,8 +582,9 @@ describe('handleAtCommand', () => { const query = `@${gitIgnoredFile}`; // Mock the file discovery service to report this file as git-ignored - mockFileDiscoveryService.shouldGitIgnoreFile.mockImplementation( - (path: string) => path === gitIgnoredFile, + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string, options?: { respectGitIgnore?: boolean }) => + path === gitIgnoredFile && options?.respectGitIgnore !== false, ); const result = await handleAtCommand({ @@ -595,8 +596,9 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(mockFileDiscoveryService.shouldGitIgnoreFile).toHaveBeenCalledWith( + expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith( gitIgnoredFile, + { respectGitIgnore: true }, ); expect(mockOnDebugMessage).toHaveBeenCalledWith( `Path ${gitIgnoredFile} is git-ignored and will be skipped.`, @@ -614,7 +616,7 @@ describe('handleAtCommand', () => { const query = `@${validFile}`; const fileContent = 'console.log("Hello world");'; - mockFileDiscoveryService.shouldGitIgnoreFile.mockReturnValue(false); + mockFileDiscoveryService.shouldIgnoreFile.mockReturnValue(false); mockReadManyFilesExecute.mockResolvedValue({ llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`], returnDisplay: 'Read 1 file.', @@ -629,11 +631,12 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(mockFileDiscoveryService.shouldGitIgnoreFile).toHaveBeenCalledWith( + expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith( validFile, + { respectGitIgnore: true }, ); expect(mockReadManyFilesExecute).toHaveBeenCalledWith( - { paths: [validFile], respectGitIgnore: true }, + { paths: [validFile], respect_git_ignore: true }, abortController.signal, ); expect(result.processedQuery).toEqual([ @@ -652,8 +655,9 @@ describe('handleAtCommand', () => { const query = `@${validFile} @${gitIgnoredFile}`; const fileContent = '# Project README'; - mockFileDiscoveryService.shouldGitIgnoreFile.mockImplementation( - (path: string) => path === gitIgnoredFile, + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string, options?: { respectGitIgnore?: boolean }) => + path === gitIgnoredFile && options?.respectGitIgnore !== false, ); mockReadManyFilesExecute.mockResolvedValue({ llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`], @@ -669,11 +673,13 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(mockFileDiscoveryService.shouldGitIgnoreFile).toHaveBeenCalledWith( + expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith( validFile, + { respectGitIgnore: true }, ); - expect(mockFileDiscoveryService.shouldGitIgnoreFile).toHaveBeenCalledWith( + expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith( gitIgnoredFile, + { respectGitIgnore: true }, ); expect(mockOnDebugMessage).toHaveBeenCalledWith( `Path ${gitIgnoredFile} is git-ignored and will be skipped.`, @@ -682,7 +688,7 @@ describe('handleAtCommand', () => { 'Ignored 1 git-ignored files: .env', ); expect(mockReadManyFilesExecute).toHaveBeenCalledWith( - { paths: [validFile], respectGitIgnore: true }, + { paths: [validFile], respect_git_ignore: true }, abortController.signal, ); expect(result.processedQuery).toEqual([ @@ -699,7 +705,7 @@ describe('handleAtCommand', () => { const gitFile = '.git/config'; const query = `@${gitFile}`; - mockFileDiscoveryService.shouldGitIgnoreFile.mockReturnValue(true); + mockFileDiscoveryService.shouldIgnoreFile.mockReturnValue(true); const result = await handleAtCommand({ query, @@ -710,8 +716,9 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(mockFileDiscoveryService.shouldGitIgnoreFile).toHaveBeenCalledWith( + expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith( gitFile, + { respectGitIgnore: true }, ); expect(mockOnDebugMessage).toHaveBeenCalledWith( `Path ${gitFile} is git-ignored and will be skipped.`, diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index 80393ef2..7fe68f10 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -181,12 +181,10 @@ export async function handleAtCommand({ return { processedQuery: null, shouldProceed: false }; } - // Check if path should be ignored by git - if (fileDiscovery.shouldGitIgnoreFile(pathName)) { - const reason = respectGitIgnore - ? 'git-ignored and will be skipped' - : 'ignored by custom patterns'; - onDebugMessage(`Path ${pathName} is ${reason}.`); + // Check if path should be ignored based on filtering options + if (fileDiscovery.shouldIgnoreFile(pathName, { respectGitIgnore })) { + const reason = respectGitIgnore ? 'git-ignored' : 'custom-ignored'; + onDebugMessage(`Path ${pathName} is ${reason} and will be skipped.`); ignoredPaths.push(pathName); continue; } @@ -349,7 +347,7 @@ export async function handleAtCommand({ const toolArgs = { paths: pathSpecsToRead, - respectGitIgnore, // Use configuration setting + respect_git_ignore: respectGitIgnore, // Use configuration setting }; let toolCallDisplay: IndividualToolCallDisplay; diff --git a/packages/cli/src/ui/hooks/useCompletion.integration.test.ts b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts index 6a178fb1..f5864a58 100644 --- a/packages/cli/src/ui/hooks/useCompletion.integration.test.ts +++ b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts @@ -41,7 +41,10 @@ describe('useCompletion git-aware filtering integration', () => { beforeEach(() => { mockFileDiscoveryService = { shouldGitIgnoreFile: vi.fn(), + shouldGeminiIgnoreFile: vi.fn(), + shouldIgnoreFile: vi.fn(), filterFiles: vi.fn(), + getGeminiIgnorePatterns: vi.fn(() => []), }; mockConfig = { @@ -68,6 +71,14 @@ describe('useCompletion git-aware filtering integration', () => { mockFileDiscoveryService.shouldGitIgnoreFile.mockImplementation( (path: string) => path.includes('dist'), ); + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string, options) => { + if (options?.respectGitIgnore !== false) { + return mockFileDiscoveryService.shouldGitIgnoreFile(path); + } + return false; + }, + ); const { result } = renderHook(() => useCompletion('@d', testCwd, true, slashCommands, mockConfig), @@ -102,6 +113,14 @@ describe('useCompletion git-aware filtering integration', () => { path.includes('dist') || path.includes('.env'), ); + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string, options) => { + if (options?.respectGitIgnore !== false) { + return mockFileDiscoveryService.shouldGitIgnoreFile(path); + } + return false; + }, + ); const { result } = renderHook(() => useCompletion('@', testCwd, true, slashCommands, mockConfig), @@ -153,6 +172,14 @@ describe('useCompletion git-aware filtering integration', () => { mockFileDiscoveryService.shouldGitIgnoreFile.mockImplementation( (path: string) => path.includes('node_modules') || path.includes('temp'), ); + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string, options) => { + if (options?.respectGitIgnore !== false) { + return mockFileDiscoveryService.shouldGitIgnoreFile(path); + } + return false; + }, + ); const { result } = renderHook(() => useCompletion('@t', testCwd, true, slashCommands, mockConfig), @@ -261,6 +288,14 @@ describe('useCompletion git-aware filtering integration', () => { mockFileDiscoveryService.shouldGitIgnoreFile.mockImplementation( (path: string) => path.includes('.log'), ); + mockFileDiscoveryService.shouldIgnoreFile.mockImplementation( + (path: string, options) => { + if (options?.respectGitIgnore !== false) { + return mockFileDiscoveryService.shouldGitIgnoreFile(path); + } + return false; + }, + ); const { result } = renderHook(() => useCompletion('@src/comp', testCwd, true, slashCommands, mockConfig), diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts index 27a1c708..fd826c92 100644 --- a/packages/cli/src/ui/hooks/useCompletion.ts +++ b/packages/cli/src/ui/hooks/useCompletion.ts @@ -217,7 +217,11 @@ export function useCompletion( const findFilesRecursively = async ( startDir: string, searchPrefix: string, - fileDiscovery: { shouldGitIgnoreFile: (path: string) => boolean } | null, + fileDiscovery: FileDiscoveryService | null, + filterOptions: { + respectGitIgnore?: boolean; + respectGeminiIgnore?: boolean; + }, currentRelativePath = '', depth = 0, maxDepth = 10, // Limit recursion depth @@ -245,10 +249,10 @@ export function useCompletion( continue; } - // Check if this entry should be ignored by git-aware filtering + // Check if this entry should be ignored by filtering options if ( fileDiscovery && - fileDiscovery.shouldGitIgnoreFile(entryPathFromRoot) + fileDiscovery.shouldIgnoreFile(entryPathFromRoot, filterOptions) ) { continue; } @@ -272,6 +276,7 @@ export function useCompletion( path.join(startDir, entry.name), searchPrefix, // Pass original searchPrefix for recursive calls fileDiscovery, + filterOptions, entryPathRelative, depth + 1, maxDepth, @@ -290,6 +295,10 @@ export function useCompletion( const findFilesWithGlob = async ( searchPrefix: string, fileDiscoveryService: FileDiscoveryService, + filterOptions: { + respectGitIgnore?: boolean; + respectGeminiIgnore?: boolean; + }, maxResults = 50, ): Promise => { const globPattern = `**/${searchPrefix}*`; @@ -309,7 +318,10 @@ export function useCompletion( }) .filter((s) => { if (fileDiscoveryService) { - return !fileDiscoveryService.shouldGitIgnoreFile(s.label); // relative path + return !fileDiscoveryService.shouldIgnoreFile( + s.label, + filterOptions, + ); // relative path } return true; }) @@ -325,6 +337,10 @@ export function useCompletion( const fileDiscoveryService = config ? config.getFileService() : null; const enableRecursiveSearch = config?.getEnableRecursiveFileSearch() ?? true; + const filterOptions = { + respectGitIgnore: config?.getFileFilteringRespectGitIgnore() ?? true, + respectGeminiIgnore: true, + }; try { // If there's no slash, or it's the root, do a recursive search from cwd @@ -337,12 +353,14 @@ export function useCompletion( fetchedSuggestions = await findFilesWithGlob( prefix, fileDiscoveryService, + filterOptions, ); } else { fetchedSuggestions = await findFilesRecursively( cwd, prefix, fileDiscoveryService, + filterOptions, ); } } else { @@ -367,7 +385,7 @@ export function useCompletion( ); if ( fileDiscoveryService && - fileDiscoveryService.shouldGitIgnoreFile(relativePath) + fileDiscoveryService.shouldIgnoreFile(relativePath, filterOptions) ) { continue; } diff --git a/packages/core/src/services/fileDiscoveryService.ts b/packages/core/src/services/fileDiscoveryService.ts index 984f3f53..22092813 100644 --- a/packages/core/src/services/fileDiscoveryService.ts +++ b/packages/core/src/services/fileDiscoveryService.ts @@ -84,6 +84,24 @@ export class FileDiscoveryService { return false; } + /** + * Unified method to check if a file should be ignored based on filtering options + */ + shouldIgnoreFile( + filePath: string, + options: FilterFilesOptions = {}, + ): boolean { + const { respectGitIgnore = true, respectGeminiIgnore = true } = options; + + if (respectGitIgnore && this.shouldGitIgnoreFile(filePath)) { + return true; + } + if (respectGeminiIgnore && this.shouldGeminiIgnoreFile(filePath)) { + return true; + } + return false; + } + /** * Returns loaded patterns from .geminiignore */