summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/hooks')
-rw-r--r--packages/cli/src/ui/hooks/atCommandProcessor.test.ts387
-rw-r--r--packages/cli/src/ui/hooks/atCommandProcessor.ts69
-rw-r--r--packages/cli/src/ui/hooks/useCompletion.integration.test.ts95
-rw-r--r--packages/cli/src/ui/hooks/useCompletion.test.ts4
-rw-r--r--packages/cli/src/ui/hooks/useCompletion.ts7
5 files changed, 494 insertions, 68 deletions
diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts
index efe15c64..6e272b24 100644
--- a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts
+++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts
@@ -21,6 +21,11 @@ const mockConfig = {
isSandboxed: vi.fn(() => false),
getFileService: vi.fn(),
getFileFilteringRespectGitIgnore: vi.fn(() => true),
+ getFileFilteringRespectGeminiIgnore: vi.fn(() => true),
+ getFileFilteringOptions: vi.fn(() => ({
+ respectGitIgnore: true,
+ respectGeminiIgnore: true,
+ })),
getEnableRecursiveFileSearch: vi.fn(() => true),
} as unknown as Config;
@@ -171,7 +176,13 @@ describe('handleAtCommand', () => {
125,
);
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
- { paths: [filePath], respect_git_ignore: true },
+ {
+ paths: [filePath],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
expect(mockAddItem).toHaveBeenCalledWith(
@@ -217,7 +228,13 @@ describe('handleAtCommand', () => {
126,
);
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
- { paths: [resolvedGlob], respect_git_ignore: true },
+ {
+ paths: [resolvedGlob],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
expect(mockOnDebugMessage).toHaveBeenCalledWith(
@@ -318,7 +335,13 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
- { paths: [unescapedPath], respect_git_ignore: true },
+ {
+ paths: [unescapedPath],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
});
@@ -347,7 +370,13 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
- { paths: [file1, file2], respect_git_ignore: true },
+ {
+ paths: [file1, file2],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
expect(result.processedQuery).toEqual([
@@ -389,7 +418,13 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
- { paths: [file1, file2], respect_git_ignore: true },
+ {
+ paths: [file1, file2],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
expect(result.processedQuery).toEqual([
@@ -454,7 +489,13 @@ describe('handleAtCommand', () => {
});
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
- { paths: [file1, resolvedFile2], respect_git_ignore: true },
+ {
+ paths: [file1, resolvedFile2],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
expect(result.processedQuery).toEqual([
@@ -556,7 +597,13 @@ 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], respect_git_ignore: true },
+ {
+ paths: [queryPath],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
expect(mockAddItem).toHaveBeenCalledWith(
@@ -583,8 +630,18 @@ describe('handleAtCommand', () => {
// Mock the file discovery service to report this file as git-ignored
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
- (path: string, options?: { respectGitIgnore?: boolean }) =>
- path === gitIgnoredFile && options?.respectGitIgnore !== false,
+ (
+ path: string,
+ options?: {
+ respectGitIgnore?: boolean;
+ respectGeminiIgnore?: boolean;
+ },
+ ) => {
+ if (path !== gitIgnoredFile) return false;
+ if (options?.respectGitIgnore) return true;
+ if (options?.respectGeminiIgnore) return false;
+ return false;
+ },
);
const result = await handleAtCommand({
@@ -596,15 +653,24 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});
+ // Should be called twice - once for git ignore check and once for gemini ignore check
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
+ 2,
+ );
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
gitIgnoredFile,
- { respectGitIgnore: true },
+ { respectGitIgnore: true, respectGeminiIgnore: false },
);
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ gitIgnoredFile,
+ { respectGitIgnore: false, respectGeminiIgnore: true },
+ );
+
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
);
expect(mockOnDebugMessage).toHaveBeenCalledWith(
- 'Ignored 1 git-ignored files: node_modules/package.json',
+ 'Ignored 1 files:\nGit-ignored: node_modules/package.json',
);
expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
expect(result.processedQuery).toEqual([{ text: query }]);
@@ -616,7 +682,15 @@ describe('handleAtCommand', () => {
const query = `@${validFile}`;
const fileContent = 'console.log("Hello world");';
- mockFileDiscoveryService.shouldIgnoreFile.mockReturnValue(false);
+ mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
+ (
+ _path: string,
+ _options?: {
+ respectGitIgnore?: boolean;
+ respectGeminiIgnore?: boolean;
+ },
+ ) => false,
+ );
mockReadManyFilesExecute.mockResolvedValue({
llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
returnDisplay: 'Read 1 file.',
@@ -631,12 +705,26 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});
+ // Should be called twice - once for git ignore check and once for gemini ignore check
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
+ 2,
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ validFile,
+ { respectGitIgnore: true, respectGeminiIgnore: false },
+ );
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
validFile,
- { respectGitIgnore: true },
+ { respectGitIgnore: false, respectGeminiIgnore: true },
);
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
- { paths: [validFile], respect_git_ignore: true },
+ {
+ paths: [validFile],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
expect(result.processedQuery).toEqual([
@@ -656,8 +744,21 @@ describe('handleAtCommand', () => {
const fileContent = '# Project README';
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
- (path: string, options?: { respectGitIgnore?: boolean }) =>
- path === gitIgnoredFile && options?.respectGitIgnore !== false,
+ (
+ path: string,
+ options?: {
+ respectGitIgnore?: boolean;
+ respectGeminiIgnore?: boolean;
+ },
+ ) => {
+ if (path === gitIgnoredFile && options?.respectGitIgnore) {
+ return true;
+ }
+ if (options?.respectGeminiIgnore) {
+ return false;
+ }
+ return false;
+ },
);
mockReadManyFilesExecute.mockResolvedValue({
llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
@@ -673,22 +774,40 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});
+ // Should be called twice for each file - once for git ignore check and once for gemini ignore check
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
+ 4,
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ validFile,
+ { respectGitIgnore: true, respectGeminiIgnore: false },
+ );
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
validFile,
- { respectGitIgnore: true },
+ { respectGitIgnore: false, respectGeminiIgnore: true },
);
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
gitIgnoredFile,
- { respectGitIgnore: true },
+ { respectGitIgnore: true, respectGeminiIgnore: false },
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ gitIgnoredFile,
+ { respectGitIgnore: false, respectGeminiIgnore: true },
);
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
);
expect(mockOnDebugMessage).toHaveBeenCalledWith(
- 'Ignored 1 git-ignored files: .env',
+ 'Ignored 1 files:\nGit-ignored: .env',
);
expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
- { paths: [validFile], respect_git_ignore: true },
+ {
+ paths: [validFile],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
abortController.signal,
);
expect(result.processedQuery).toEqual([
@@ -705,7 +824,16 @@ describe('handleAtCommand', () => {
const gitFile = '.git/config';
const query = `@${gitFile}`;
- mockFileDiscoveryService.shouldIgnoreFile.mockReturnValue(true);
+ // Mock to return true for git ignore check, false for gemini ignore check
+ mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
+ (
+ _path: string,
+ options?: {
+ respectGitIgnore?: boolean;
+ respectGeminiIgnore?: boolean;
+ },
+ ) => options?.respectGitIgnore === true,
+ );
const result = await handleAtCommand({
query,
@@ -716,13 +844,24 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});
+ // Should be called twice - once for git ignore check and once for gemini ignore check
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
+ 2,
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ gitFile,
+ { respectGitIgnore: true, respectGeminiIgnore: false },
+ );
expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
gitFile,
- { respectGitIgnore: true },
+ { respectGitIgnore: false, respectGeminiIgnore: true },
);
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${gitFile} is git-ignored and will be skipped.`,
);
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ 'Ignored 1 files:\nGit-ignored: .git/config',
+ );
expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
expect(result.processedQuery).toEqual([{ text: query }]);
expect(result.shouldProceed).toBe(true);
@@ -759,4 +898,208 @@ describe('handleAtCommand', () => {
expect(result.shouldProceed).toBe(true);
});
});
+
+ describe('gemini-ignore filtering', () => {
+ it('should skip gemini-ignored files in @ commands', async () => {
+ const geminiIgnoredFile = 'build/output.js';
+ const query = `@${geminiIgnoredFile}`;
+
+ // Mock the file discovery service to report this file as gemini-ignored
+ mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
+ (
+ path: string,
+ options?: {
+ respectGitIgnore?: boolean;
+ respectGeminiIgnore?: boolean;
+ },
+ ) => {
+ if (path !== geminiIgnoredFile) return false;
+ if (options?.respectGeminiIgnore) return true;
+ if (options?.respectGitIgnore) return false;
+ return false;
+ },
+ );
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 204,
+ signal: abortController.signal,
+ });
+
+ // Should be called twice - once for git ignore check and once for gemini ignore check
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
+ 2,
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ geminiIgnoredFile,
+ { respectGitIgnore: true, respectGeminiIgnore: false },
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ geminiIgnoredFile,
+ { respectGitIgnore: false, respectGeminiIgnore: true },
+ );
+
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Path ${geminiIgnoredFile} is gemini-ignored and will be skipped.`,
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ 'No valid file paths found in @ commands to read.',
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ 'Ignored 1 files:\nGemini-ignored: build/output.js',
+ );
+ expect(mockReadManyFilesExecute).not.toHaveBeenCalled();
+ expect(result.processedQuery).toEqual([{ text: query }]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should process non-ignored files when .geminiignore is present', async () => {
+ const validFile = 'src/index.ts';
+ const query = `@${validFile}`;
+ const fileContent = 'console.log("Hello world")';
+
+ mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
+ (
+ _path: string,
+ _options?: {
+ respectGitIgnore?: boolean;
+ respectGeminiIgnore?: boolean;
+ },
+ ) => false,
+ );
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read 1 file.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 205,
+ signal: abortController.signal,
+ });
+
+ // Should be called twice - once for git ignore check and once for gemini ignore check
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
+ 2,
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ validFile,
+ { respectGitIgnore: true, respectGeminiIgnore: false },
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ validFile,
+ { respectGitIgnore: false, respectGeminiIgnore: true },
+ );
+
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ {
+ paths: [validFile],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
+ abortController.signal,
+ );
+ expect(result.processedQuery).toEqual([
+ { text: `@${validFile}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${validFile}:\n` },
+ { text: fileContent },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+
+ it('should handle mixed gemini-ignored and valid files', async () => {
+ const validFile = 'src/main.ts';
+ const geminiIgnoredFile = 'dist/bundle.js';
+ const query = `@${validFile} @${geminiIgnoredFile}`;
+ const fileContent = '// Main application entry';
+
+ mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
+ (
+ path: string,
+ options?: {
+ respectGitIgnore?: boolean;
+ respectGeminiIgnore?: boolean;
+ },
+ ) => {
+ if (path === geminiIgnoredFile && options?.respectGeminiIgnore) {
+ return true;
+ }
+ if (options?.respectGitIgnore) {
+ return false;
+ }
+ return false;
+ },
+ );
+ mockReadManyFilesExecute.mockResolvedValue({
+ llmContent: [`--- ${validFile} ---\n\n${fileContent}\n\n`],
+ returnDisplay: 'Read 1 file.',
+ });
+
+ const result = await handleAtCommand({
+ query,
+ config: mockConfig,
+ addItem: mockAddItem,
+ onDebugMessage: mockOnDebugMessage,
+ messageId: 206,
+ signal: abortController.signal,
+ });
+
+ // Should be called twice for each file - once for git ignore check and once for gemini ignore check
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledTimes(
+ 4,
+ );
+
+ // Verify both files were checked against both ignore types
+ [validFile, geminiIgnoredFile].forEach((file) => {
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ file,
+ { respectGitIgnore: true, respectGeminiIgnore: false },
+ );
+ expect(mockFileDiscoveryService.shouldIgnoreFile).toHaveBeenCalledWith(
+ file,
+ { respectGitIgnore: false, respectGeminiIgnore: true },
+ );
+ });
+
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Path ${validFile} resolved to file: ${validFile}`,
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ `Path ${geminiIgnoredFile} is gemini-ignored and will be skipped.`,
+ );
+ expect(mockOnDebugMessage).toHaveBeenCalledWith(
+ 'Ignored 1 files:\nGemini-ignored: dist/bundle.js',
+ );
+
+ expect(mockReadManyFilesExecute).toHaveBeenCalledWith(
+ {
+ paths: [validFile],
+ file_filtering_options: {
+ respect_git_ignore: true,
+ respect_gemini_ignore: true,
+ },
+ },
+ abortController.signal,
+ );
+
+ expect(result.processedQuery).toEqual([
+ { text: `@${validFile} @${geminiIgnoredFile}` },
+ { text: '\n--- Content from referenced files ---' },
+ { text: `\nContent from @${validFile}:\n` },
+ { text: fileContent },
+ { text: '\n--- End of content ---' },
+ ]);
+ expect(result.shouldProceed).toBe(true);
+ });
+ });
});
diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts
index 7fe68f10..983abc62 100644
--- a/packages/cli/src/ui/hooks/atCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts
@@ -136,12 +136,17 @@ export async function handleAtCommand({
// Get centralized file discovery service
const fileDiscovery = config.getFileService();
- const respectGitIgnore = config.getFileFilteringRespectGitIgnore();
+
+ const respectFileIgnore = config.getFileFilteringOptions();
const pathSpecsToRead: string[] = [];
const atPathToResolvedSpecMap = new Map<string, string>();
const contentLabelsForDisplay: string[] = [];
- const ignoredPaths: string[] = [];
+ const ignoredByReason: Record<string, string[]> = {
+ git: [],
+ gemini: [],
+ both: [],
+ };
const toolRegistry = await config.getToolRegistry();
const readManyFilesTool = toolRegistry.getTool('read_many_files');
@@ -182,10 +187,31 @@ export async function handleAtCommand({
}
// 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);
+
+ const gitIgnored =
+ respectFileIgnore.respectGitIgnore &&
+ fileDiscovery.shouldIgnoreFile(pathName, {
+ respectGitIgnore: true,
+ respectGeminiIgnore: false,
+ });
+ const geminiIgnored =
+ respectFileIgnore.respectGeminiIgnore &&
+ fileDiscovery.shouldIgnoreFile(pathName, {
+ respectGitIgnore: false,
+ respectGeminiIgnore: true,
+ });
+
+ if (gitIgnored || geminiIgnored) {
+ const reason =
+ gitIgnored && geminiIgnored ? 'both' : gitIgnored ? 'git' : 'gemini';
+ ignoredByReason[reason].push(pathName);
+ const reasonText =
+ reason === 'both'
+ ? 'ignored by both git and gemini'
+ : reason === 'git'
+ ? 'git-ignored'
+ : 'gemini-ignored';
+ onDebugMessage(`Path ${pathName} is ${reasonText} and will be skipped.`);
continue;
}
@@ -319,11 +345,26 @@ export async function handleAtCommand({
initialQueryText = initialQueryText.trim();
// Inform user about ignored paths
- if (ignoredPaths.length > 0) {
- const ignoreType = respectGitIgnore ? 'git-ignored' : 'custom-ignored';
- onDebugMessage(
- `Ignored ${ignoredPaths.length} ${ignoreType} files: ${ignoredPaths.join(', ')}`,
- );
+ const totalIgnored =
+ ignoredByReason.git.length +
+ ignoredByReason.gemini.length +
+ ignoredByReason.both.length;
+
+ if (totalIgnored > 0) {
+ const messages = [];
+ if (ignoredByReason.git.length) {
+ messages.push(`Git-ignored: ${ignoredByReason.git.join(', ')}`);
+ }
+ if (ignoredByReason.gemini.length) {
+ messages.push(`Gemini-ignored: ${ignoredByReason.gemini.join(', ')}`);
+ }
+ if (ignoredByReason.both.length) {
+ messages.push(`Ignored by both: ${ignoredByReason.both.join(', ')}`);
+ }
+
+ const message = `Ignored ${totalIgnored} files:\n${messages.join('\n')}`;
+ console.log(message);
+ onDebugMessage(message);
}
// Fallback for lone "@" or completely invalid @-commands resulting in empty initialQueryText
@@ -347,7 +388,11 @@ export async function handleAtCommand({
const toolArgs = {
paths: pathSpecsToRead,
- respect_git_ignore: respectGitIgnore, // Use configuration setting
+ file_filtering_options: {
+ respect_git_ignore: respectFileIgnore.respectGitIgnore,
+ respect_gemini_ignore: respectFileIgnore.respectGeminiIgnore,
+ },
+ // 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 37075e3c..f6f0944b 100644
--- a/packages/cli/src/ui/hooks/useCompletion.integration.test.ts
+++ b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts
@@ -14,7 +14,10 @@ import { CommandContext, SlashCommand } from '../commands/types.js';
import { Config, FileDiscoveryService } from '@google/gemini-cli-core';
interface MockConfig {
- getFileFilteringRespectGitIgnore: () => boolean;
+ getFileFilteringOptions: () => {
+ respectGitIgnore: boolean;
+ respectGeminiIgnore: boolean;
+ };
getEnableRecursiveFileSearch: () => boolean;
getFileService: () => FileDiscoveryService | null;
}
@@ -118,12 +121,16 @@ describe('useCompletion git-aware filtering integration', () => {
projectRoot: '',
gitIgnoreFilter: null,
geminiIgnoreFilter: null,
+ isFileIgnored: vi.fn(),
} as unknown as Mocked<FileDiscoveryService>;
mockConfig = {
- getFileFilteringRespectGitIgnore: vi.fn(() => true),
- getFileService: vi.fn().mockReturnValue(mockFileDiscoveryService),
+ getFileFilteringOptions: vi.fn(() => ({
+ respectGitIgnore: true,
+ respectGeminiIgnore: true,
+ })),
getEnableRecursiveFileSearch: vi.fn(() => true),
+ getFileService: vi.fn(() => mockFileDiscoveryService),
};
vi.mocked(FileDiscoveryService).mockImplementation(
@@ -186,7 +193,7 @@ describe('useCompletion git-aware filtering integration', () => {
{ name: '.env', isDirectory: () => false },
] as unknown as Awaited<ReturnType<typeof fs.readdir>>);
- // Mock git ignore service to ignore certain files
+ // Mock ignore service to ignore certain files
mockFileDiscoveryService.shouldGitIgnoreFile.mockImplementation(
(path: string) =>
path.includes('node_modules') ||
@@ -195,8 +202,17 @@ describe('useCompletion git-aware filtering integration', () => {
);
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
(path: string, options) => {
- if (options?.respectGitIgnore !== false) {
- return mockFileDiscoveryService.shouldGitIgnoreFile(path);
+ if (
+ options?.respectGitIgnore &&
+ mockFileDiscoveryService.shouldGitIgnoreFile(path)
+ ) {
+ return true;
+ }
+ if (
+ options?.respectGeminiIgnore &&
+ mockFileDiscoveryService.shouldGeminiIgnoreFile
+ ) {
+ return mockFileDiscoveryService.shouldGeminiIgnoreFile(path);
}
return false;
},
@@ -231,38 +247,54 @@ describe('useCompletion git-aware filtering integration', () => {
it('should handle recursive search with git-aware filtering', async () => {
// Mock the recursive file search scenario
vi.mocked(fs.readdir).mockImplementation(
- async (dirPath: string | Buffer | URL) => {
- if (dirPath === testCwd) {
- return [
- { name: 'src', isDirectory: () => true },
- { name: 'node_modules', isDirectory: () => true },
- { name: 'temp', isDirectory: () => true },
- ] as Array<{ name: string; isDirectory: () => boolean }>;
- }
- if (dirPath.endsWith('/src')) {
- return [
- { name: 'index.ts', isDirectory: () => false },
- { name: 'components', isDirectory: () => true },
- ] as Array<{ name: string; isDirectory: () => boolean }>;
+ async (
+ dirPath: string | Buffer | URL,
+ options?: { withFileTypes?: boolean },
+ ) => {
+ const path = dirPath.toString();
+ if (options?.withFileTypes) {
+ if (path === testCwd) {
+ return [
+ { name: 'data', isDirectory: () => true },
+ { name: 'dist', isDirectory: () => true },
+ { name: 'node_modules', isDirectory: () => true },
+ { name: 'README.md', isDirectory: () => false },
+ { name: '.env', isDirectory: () => false },
+ ] as unknown as Awaited<ReturnType<typeof fs.readdir>>;
+ }
+ if (path.endsWith('/src')) {
+ return [
+ { name: 'index.ts', isDirectory: () => false },
+ { name: 'components', isDirectory: () => true },
+ ] as unknown as Awaited<ReturnType<typeof fs.readdir>>;
+ }
+ if (path.endsWith('/temp')) {
+ return [
+ { name: 'temp.log', isDirectory: () => false },
+ ] as unknown as Awaited<ReturnType<typeof fs.readdir>>;
+ }
}
- if (dirPath.endsWith('/temp')) {
- return [{ name: 'temp.log', isDirectory: () => false }] as Array<{
- name: string;
- isDirectory: () => boolean;
- }>;
- }
- return [] as Array<{ name: string; isDirectory: () => boolean }>;
+ return [];
},
);
- // Mock git ignore service
+ // Mock ignore service
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);
+ if (
+ options?.respectGitIgnore &&
+ mockFileDiscoveryService.shouldGitIgnoreFile(path)
+ ) {
+ return true;
+ }
+ if (
+ options?.respectGeminiIgnore &&
+ mockFileDiscoveryService.shouldGeminiIgnoreFile
+ ) {
+ return mockFileDiscoveryService.shouldGeminiIgnoreFile(path);
}
return false;
},
@@ -405,9 +437,12 @@ describe('useCompletion git-aware filtering integration', () => {
);
mockFileDiscoveryService.shouldIgnoreFile.mockImplementation(
(path: string, options) => {
- if (options?.respectGitIgnore !== false) {
+ if (options?.respectGitIgnore) {
return mockFileDiscoveryService.shouldGitIgnoreFile(path);
}
+ if (options?.respectGeminiIgnore) {
+ return mockFileDiscoveryService.shouldGeminiIgnoreFile(path);
+ }
return false;
},
);
diff --git a/packages/cli/src/ui/hooks/useCompletion.test.ts b/packages/cli/src/ui/hooks/useCompletion.test.ts
index 267bce13..f4227c1a 100644
--- a/packages/cli/src/ui/hooks/useCompletion.test.ts
+++ b/packages/cli/src/ui/hooks/useCompletion.test.ts
@@ -55,6 +55,10 @@ describe('useCompletion', () => {
getFileFilteringRespectGitIgnore: vi.fn(() => true),
getFileService: vi.fn().mockReturnValue(mockFileDiscoveryService),
getEnableRecursiveFileSearch: vi.fn(() => true),
+ getFileFilteringOptions: vi.fn(() => ({
+ respectGitIgnore: true,
+ respectGeminiIgnore: true,
+ })),
} as unknown as Mocked<Config>;
mockCommandContext = {} as CommandContext;
diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts
index 81acc992..69d8bfb9 100644
--- a/packages/cli/src/ui/hooks/useCompletion.ts
+++ b/packages/cli/src/ui/hooks/useCompletion.ts
@@ -15,6 +15,7 @@ import {
getErrorMessage,
Config,
FileDiscoveryService,
+ DEFAULT_FILE_FILTERING_OPTIONS,
} from '@google/gemini-cli-core';
import {
MAX_SUGGESTIONS_TO_SHOW,
@@ -415,10 +416,8 @@ export function useCompletion(
const fileDiscoveryService = config ? config.getFileService() : null;
const enableRecursiveSearch =
config?.getEnableRecursiveFileSearch() ?? true;
- const filterOptions = {
- respectGitIgnore: config?.getFileFilteringRespectGitIgnore() ?? true,
- respectGeminiIgnore: true,
- };
+ const filterOptions =
+ config?.getFileFilteringOptions() ?? DEFAULT_FILE_FILTERING_OPTIONS;
try {
// If there's no slash, or it's the root, do a recursive search from cwd