diff --git a/src/core/ignore/RooIgnoreController.ts b/src/core/ignore/RooIgnoreController.ts index fda6c37175..ecc9dc59f3 100644 --- a/src/core/ignore/RooIgnoreController.ts +++ b/src/core/ignore/RooIgnoreController.ts @@ -16,13 +16,15 @@ export class RooIgnoreController { private ignoreInstance: Ignore private disposables: vscode.Disposable[] = [] rooIgnoreContent: string | undefined + private gitIgnoreContent: string | undefined + private currentIgnoreSource: "rooignore" | "gitignore" | "default" | "none" = "none" constructor(cwd: string) { this.cwd = cwd this.ignoreInstance = ignore() this.rooIgnoreContent = undefined - // Set up file watcher for .rooignore - this.setupFileWatcher() + // Set up file watchers for both .rooignore and .gitignore + this.setupFileWatchers() } /** @@ -30,53 +32,131 @@ export class RooIgnoreController { * Must be called after construction and before using the controller */ async initialize(): Promise { - await this.loadRooIgnore() + await this.loadIgnorePatterns() } /** - * Set up the file watcher for .rooignore changes + * Set up file watchers for both .rooignore and .gitignore changes */ - private setupFileWatcher(): void { + private setupFileWatchers(): void { + // Watch .rooignore const rooignorePattern = new vscode.RelativePattern(this.cwd, ".rooignore") - const fileWatcher = vscode.workspace.createFileSystemWatcher(rooignorePattern) + const rooignoreWatcher = vscode.workspace.createFileSystemWatcher(rooignorePattern) - // Watch for changes and updates + // Watch .gitignore + const gitignorePattern = new vscode.RelativePattern(this.cwd, ".gitignore") + const gitignoreWatcher = vscode.workspace.createFileSystemWatcher(gitignorePattern) + + // Watch for changes and updates to both files this.disposables.push( - fileWatcher.onDidChange(() => { - this.loadRooIgnore() + rooignoreWatcher.onDidChange(() => { + this.loadIgnorePatterns() }), - fileWatcher.onDidCreate(() => { - this.loadRooIgnore() + rooignoreWatcher.onDidCreate(() => { + this.loadIgnorePatterns() }), - fileWatcher.onDidDelete(() => { - this.loadRooIgnore() + rooignoreWatcher.onDidDelete(() => { + this.loadIgnorePatterns() + }), + gitignoreWatcher.onDidChange(() => { + this.loadIgnorePatterns() + }), + gitignoreWatcher.onDidCreate(() => { + this.loadIgnorePatterns() + }), + gitignoreWatcher.onDidDelete(() => { + this.loadIgnorePatterns() }), ) - // Add fileWatcher itself to disposables - this.disposables.push(fileWatcher) + // Add fileWatchers themselves to disposables + this.disposables.push(rooignoreWatcher, gitignoreWatcher) + } + + /** + * Load ignore patterns with .gitignore fallback support + */ + private async loadIgnorePatterns(): Promise { + try { + // Reset ignore instance to prevent duplicate patterns + this.ignoreInstance = ignore() + + // Try to load .rooignore first + const rooIgnorePath = path.join(this.cwd, ".rooignore") + const rooIgnoreExists = await fileExistsAtPath(rooIgnorePath) + + if (rooIgnoreExists) { + const content = await fs.readFile(rooIgnorePath, "utf8") + const trimmedContent = content.trim() + + if (trimmedContent) { + // .rooignore exists and has content - use it exclusively + this.rooIgnoreContent = content + this.gitIgnoreContent = undefined + this.currentIgnoreSource = "rooignore" + this.ignoreInstance.add(content) + this.ignoreInstance.add(".rooignore") + return + } else { + // .rooignore exists but is empty - fall back to .gitignore + this.rooIgnoreContent = content + } + } else { + this.rooIgnoreContent = undefined + } + + // Try to load .gitignore as fallback + const gitIgnorePath = path.join(this.cwd, ".gitignore") + const gitIgnoreExists = await fileExistsAtPath(gitIgnorePath) + + if (gitIgnoreExists) { + const content = await fs.readFile(gitIgnorePath, "utf8") + this.gitIgnoreContent = content + this.currentIgnoreSource = "gitignore" + this.ignoreInstance.add(content) + this.ignoreInstance.add(".gitignore") + } else { + // Neither file exists - use default patterns for common directories + this.gitIgnoreContent = undefined + this.currentIgnoreSource = "default" + this.addDefaultIgnorePatterns() + } + } catch (error) { + console.error("Unexpected error loading ignore patterns:", error) + // Fallback to default patterns on error + this.currentIgnoreSource = "default" + this.addDefaultIgnorePatterns() + } + } + + /** + * Add default ignore patterns for common directories that should typically be excluded + */ + private addDefaultIgnorePatterns(): void { + const defaultPatterns = [ + "node_modules/", + "vendor/", + ".git/", + ".svn/", + ".hg/", + "dist/", + "build/", + "out/", + "target/", + "*.log", + ".DS_Store", + "Thumbs.db", + ] + + this.ignoreInstance.add(defaultPatterns) } /** * Load custom patterns from .rooignore if it exists + * @deprecated Use loadIgnorePatterns() instead */ private async loadRooIgnore(): Promise { - try { - // Reset ignore instance to prevent duplicate patterns - this.ignoreInstance = ignore() - const ignorePath = path.join(this.cwd, ".rooignore") - if (await fileExistsAtPath(ignorePath)) { - const content = await fs.readFile(ignorePath, "utf8") - this.rooIgnoreContent = content - this.ignoreInstance.add(content) - this.ignoreInstance.add(".rooignore") - } else { - this.rooIgnoreContent = undefined - } - } catch (error) { - // Should never happen: reading file failed even though it exists - console.error("Unexpected error loading .rooignore:", error) - } + await this.loadIgnorePatterns() } /** @@ -85,16 +165,12 @@ export class RooIgnoreController { * @returns true if file is accessible, false if ignored */ validateAccess(filePath: string): boolean { - // Always allow access if .rooignore does not exist - if (!this.rooIgnoreContent) { - return true - } try { // Normalize path to be relative to cwd and use forward slashes const absolutePath = path.resolve(this.cwd, filePath) const relativePath = path.relative(this.cwd, absolutePath).toPosix() - // Ignore expects paths to be path.relative()'d + // Use the unified ignore instance which now handles .rooignore, .gitignore, or default patterns return !this.ignoreInstance.ignores(relativePath) } catch (error) { // console.error(`Error validating access for ${filePath}:`, error) @@ -109,10 +185,7 @@ export class RooIgnoreController { * @returns path of file that is being accessed if it is being accessed, undefined if command is allowed */ validateCommand(command: string): string | undefined { - // Always allow if no .rooignore exists - if (!this.rooIgnoreContent) { - return undefined - } + // Use unified ignore patterns (rooignore, gitignore, or defaults) // Split command into parts and get the base command const parts = command.trim().split(/\s+/) @@ -188,14 +261,30 @@ export class RooIgnoreController { } /** - * Get formatted instructions about the .rooignore file for the LLM - * @returns Formatted instructions or undefined if .rooignore doesn't exist + * Get the current ignore source being used + * @returns The source of ignore patterns currently in use + */ + getIgnoreSource(): "rooignore" | "gitignore" | "default" | "none" { + return this.currentIgnoreSource + } + + /** + * Get formatted instructions about the ignore patterns for the LLM + * @returns Formatted instructions or undefined if no patterns are active */ getInstructions(): string | undefined { - if (!this.rooIgnoreContent) { - return undefined - } + switch (this.currentIgnoreSource) { + case "rooignore": + return `# .rooignore\n\n(The following is provided by a root-level .rooignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${this.rooIgnoreContent}\n.rooignore` - return `# .rooignore\n\n(The following is provided by a root-level .rooignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${this.rooIgnoreContent}\n.rooignore` + case "gitignore": + return `# .gitignore (fallback)\n\n(The following is provided by a root-level .gitignore file being used as fallback since no .rooignore file was found or it was empty. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${this.gitIgnoreContent}\n.gitignore` + + case "default": + return `# Default ignore patterns\n\n(The following default ignore patterns are being used since neither .rooignore nor .gitignore files were found. These patterns exclude common directories that are typically not needed for code analysis. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked.)\n\nnode_modules/\nvendor/\n.git/\n.svn/\n.hg/\ndist/\nbuild/\nout/\ntarget/\n*.log\n.DS_Store\nThumbs.db` + + default: + return undefined + } } } diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index 18e0752c34..fcb2095d4b 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -236,7 +236,6 @@ export class CodeIndexManager { this._cacheManager!, ) - const ignoreInstance = ignore() const workspacePath = getWorkspacePath() if (!workspacePath) { @@ -244,26 +243,10 @@ export class CodeIndexManager { return } - const ignorePath = path.join(workspacePath, ".gitignore") - try { - const content = await fs.readFile(ignorePath, "utf8") - ignoreInstance.add(content) - ignoreInstance.add(".gitignore") - } catch (error) { - // Should never happen: reading file failed even though it exists - console.error("Unexpected error loading .gitignore:", error) - TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - location: "_recreateServices", - }) - } - - // (Re)Create shared service instances + // (Re)Create shared service instances - ignore patterns are now handled by RooIgnoreController const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices( this.context, this._cacheManager!, - ignoreInstance, ) // Validate embedder configuration before proceeding diff --git a/src/services/code-index/processors/__tests__/scanner.spec.ts b/src/services/code-index/processors/__tests__/scanner.spec.ts index f90a6c8159..7e1f167a80 100644 --- a/src/services/code-index/processors/__tests__/scanner.spec.ts +++ b/src/services/code-index/processors/__tests__/scanner.spec.ts @@ -104,13 +104,7 @@ describe("DirectoryScanner", () => { ignores: vi.fn().mockReturnValue(false), } - scanner = new DirectoryScanner( - mockEmbedder, - mockVectorStore, - mockCodeParser, - mockCacheManager, - mockIgnoreInstance, - ) + scanner = new DirectoryScanner(mockEmbedder, mockVectorStore, mockCodeParser, mockCacheManager) // Mock default implementations - create proper Stats object mockStats = { diff --git a/src/services/code-index/processors/file-watcher.ts b/src/services/code-index/processors/file-watcher.ts index 6dc1cd1835..ce68bd5d7f 100644 --- a/src/services/code-index/processors/file-watcher.ts +++ b/src/services/code-index/processors/file-watcher.ts @@ -31,7 +31,6 @@ import { sanitizeErrorMessage } from "../shared/validation-helpers" * Implementation of the file watcher interface */ export class FileWatcher implements IFileWatcher { - private ignoreInstance?: Ignore private fileWatcher?: vscode.FileSystemWatcher private ignoreController: RooIgnoreController private accumulatedEvents: Map = new Map() @@ -76,19 +75,18 @@ export class FileWatcher implements IFileWatcher { private readonly cacheManager: CacheManager, private embedder?: IEmbedder, private vectorStore?: IVectorStore, - ignoreInstance?: Ignore, ignoreController?: RooIgnoreController, ) { this.ignoreController = ignoreController || new RooIgnoreController(workspacePath) - if (ignoreInstance) { - this.ignoreInstance = ignoreInstance - } } /** * Initializes the file watcher */ async initialize(): Promise { + // Initialize the ignore controller + await this.ignoreController.initialize() + // Create file watcher const filePattern = new vscode.RelativePattern( this.workspacePath, @@ -495,12 +493,8 @@ export class FileWatcher implements IFileWatcher { } } - // Check if file should be ignored - const relativeFilePath = generateRelativeFilePath(filePath, this.workspacePath) - if ( - !this.ignoreController.validateAccess(filePath) || - (this.ignoreInstance && this.ignoreInstance.ignores(relativeFilePath)) - ) { + // Check if file should be ignored using unified ignore controller + if (!this.ignoreController.validateAccess(filePath)) { return { path: filePath, status: "skipped" as const, diff --git a/src/services/code-index/processors/scanner.ts b/src/services/code-index/processors/scanner.ts index 538a1252d7..c15cf9df1b 100644 --- a/src/services/code-index/processors/scanner.ts +++ b/src/services/code-index/processors/scanner.ts @@ -35,7 +35,6 @@ export class DirectoryScanner implements IDirectoryScanner { private readonly qdrantClient: IVectorStore, private readonly codeParser: ICodeParser, private readonly cacheManager: CacheManager, - private readonly ignoreInstance: Ignore, ) {} /** @@ -70,17 +69,16 @@ export class DirectoryScanner implements IDirectoryScanner { // Filter paths using .rooignore const allowedPaths = ignoreController.filterPaths(filePaths) - // Filter by supported extensions, ignore patterns, and excluded directories + // Filter by supported extensions and excluded directories const supportedPaths = allowedPaths.filter((filePath) => { const ext = path.extname(filePath).toLowerCase() - const relativeFilePath = generateRelativeFilePath(filePath, scanWorkspace) // Check if file is in an ignored directory using the shared helper if (isPathInIgnoredDirectory(filePath)) { return false } - return scannerExtensions.includes(ext) && !this.ignoreInstance.ignores(relativeFilePath) + return scannerExtensions.includes(ext) }) // Initialize tracking variables diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index a741aaf72a..92268eade1 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -144,9 +144,8 @@ export class CodeIndexServiceFactory { embedder: IEmbedder, vectorStore: IVectorStore, parser: ICodeParser, - ignoreInstance: Ignore, ): DirectoryScanner { - return new DirectoryScanner(embedder, vectorStore, parser, this.cacheManager, ignoreInstance) + return new DirectoryScanner(embedder, vectorStore, parser, this.cacheManager) } /** @@ -157,9 +156,8 @@ export class CodeIndexServiceFactory { embedder: IEmbedder, vectorStore: IVectorStore, cacheManager: CacheManager, - ignoreInstance: Ignore, ): IFileWatcher { - return new FileWatcher(this.workspacePath, context, cacheManager, embedder, vectorStore, ignoreInstance) + return new FileWatcher(this.workspacePath, context, cacheManager, embedder, vectorStore) } /** @@ -169,7 +167,6 @@ export class CodeIndexServiceFactory { public createServices( context: vscode.ExtensionContext, cacheManager: CacheManager, - ignoreInstance: Ignore, ): { embedder: IEmbedder vectorStore: IVectorStore @@ -184,8 +181,8 @@ export class CodeIndexServiceFactory { const embedder = this.createEmbedder() const vectorStore = this.createVectorStore() const parser = codeParser - const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance) - const fileWatcher = this.createFileWatcher(context, embedder, vectorStore, cacheManager, ignoreInstance) + const scanner = this.createDirectoryScanner(embedder, vectorStore, parser) + const fileWatcher = this.createFileWatcher(context, embedder, vectorStore, cacheManager) return { embedder,