Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 73 additions & 28 deletions src/core/ignore/RooIgnoreController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ export class RooIgnoreController {
private ignoreInstance: Ignore
private disposables: vscode.Disposable[] = []
rooIgnoreContent: string | undefined
private gitIgnoreContent: string | undefined
private usingGitIgnoreFallback: boolean = false

constructor(cwd: string) {
this.cwd = cwd
this.ignoreInstance = ignore()
this.rooIgnoreContent = undefined
// Set up file watcher for .rooignore
this.gitIgnoreContent = undefined
// Set up file watcher for .rooignore and .gitignore
this.setupFileWatcher()
}

Expand All @@ -31,52 +34,85 @@ export class RooIgnoreController {
* Must be called after construction and before using the controller
*/
async initialize(): Promise<void> {
await this.loadRooIgnore()
await this.loadIgnorePatterns()
}

/**
* Set up the file watcher for .rooignore changes
* Set up the file watcher for .rooignore and .gitignore changes
*/
private setupFileWatcher(): void {
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
const gitignorePattern = new vscode.RelativePattern(this.cwd, ".gitignore")
const gitignoreWatcher = vscode.workspace.createFileSystemWatcher(gitignorePattern)

// Watch for .rooignore changes and updates
this.disposables.push(
rooignoreWatcher.onDidChange(() => {
this.loadIgnorePatterns()
}),
rooignoreWatcher.onDidCreate(() => {
this.loadIgnorePatterns()
}),
rooignoreWatcher.onDidDelete(() => {
this.loadIgnorePatterns()
}),
)

// Watch for .gitignore changes and updates
this.disposables.push(
fileWatcher.onDidChange(() => {
this.loadRooIgnore()
gitignoreWatcher.onDidChange(() => {
this.loadIgnorePatterns()
}),
fileWatcher.onDidCreate(() => {
this.loadRooIgnore()
gitignoreWatcher.onDidCreate(() => {
this.loadIgnorePatterns()
}),
fileWatcher.onDidDelete(() => {
this.loadRooIgnore()
gitignoreWatcher.onDidDelete(() => {
this.loadIgnorePatterns()
}),
)

// Add fileWatcher itself to disposables
this.disposables.push(fileWatcher)
// Add fileWatchers themselves to disposables
this.disposables.push(rooignoreWatcher, gitignoreWatcher)
}

/**
* Load custom patterns from .rooignore if it exists
* Load ignore patterns from .rooignore and .gitignore files
* .rooignore takes precedence, but .gitignore is used as fallback
*/
private async loadRooIgnore(): Promise<void> {
private async loadIgnorePatterns(): Promise<void> {
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.usingGitIgnoreFallback = false

const rooIgnorePath = path.join(this.cwd, ".rooignore")
const gitIgnorePath = path.join(this.cwd, ".gitignore")

// Check for .rooignore first
if (await fileExistsAtPath(rooIgnorePath)) {
const content = await fs.readFile(rooIgnorePath, "utf8")
this.rooIgnoreContent = content
this.ignoreInstance.add(content)
this.ignoreInstance.add(".rooignore")
} else {
this.rooIgnoreContent = undefined

// Fallback to .gitignore if .rooignore doesn't exist
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic to load .gitignore if .rooignore is missing looks solid. Consider adding dedicated tests to verify behavior when only .gitignore exists, ensuring the fallback (usingGitIgnoreFallback flag) works as expected.

if (await fileExistsAtPath(gitIgnorePath)) {
const content = await fs.readFile(gitIgnorePath, "utf8")
this.gitIgnoreContent = content
this.ignoreInstance.add(content)
this.ignoreInstance.add(".gitignore")
this.usingGitIgnoreFallback = true
} else {
this.gitIgnoreContent = undefined
}
}
} catch (error) {
// Should never happen: reading file failed even though it exists
console.error("Unexpected error loading .rooignore:", error)
console.error("Unexpected error loading ignore patterns:", error)
}
}

Expand All @@ -87,8 +123,8 @@ 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) {
// Always allow access if no ignore patterns are loaded
if (!this.rooIgnoreContent && !this.usingGitIgnoreFallback) {
return true
}
try {
Expand Down Expand Up @@ -121,8 +157,8 @@ 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) {
// Always allow if no ignore patterns are loaded
if (!this.rooIgnoreContent && !this.usingGitIgnoreFallback) {
return undefined
}

Expand Down Expand Up @@ -200,14 +236,23 @@ export class RooIgnoreController {
}

/**
* Get formatted instructions about the .rooignore file for the LLM
* @returns Formatted instructions or undefined if .rooignore doesn't exist
* Get formatted instructions about the ignore file for the LLM
* @returns Formatted instructions or undefined if no ignore patterns exist
*/
getInstructions(): string | undefined {
if (!this.rooIgnoreContent) {
return undefined
if (this.rooIgnoreContent) {
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`
} else if (this.usingGitIgnoreFallback && this.gitIgnoreContent) {
return `# .gitignore (fallback)\n\n(The following is provided by a root-level .gitignore file that is being used as fallback ignore patterns since no .rooignore file exists. 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`
}
return undefined
}

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`
/**
* Check if the controller is using .gitignore as fallback
* @returns true if using .gitignore patterns because .rooignore doesn't exist
*/
isUsingGitIgnoreFallback(): boolean {
return this.usingGitIgnoreFallback
}
}
2 changes: 1 addition & 1 deletion src/core/ignore/__tests__/RooIgnoreController.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describe("RooIgnoreController", () => {
await controller.initialize()

// Verify error was logged
expect(consoleSpy).toHaveBeenCalledWith("Unexpected error loading .rooignore:", expect.any(Error))
expect(consoleSpy).toHaveBeenCalledWith("Unexpected error loading ignore patterns:", expect.any(Error))

// Cleanup
consoleSpy.mockRestore()
Expand Down
13 changes: 12 additions & 1 deletion src/services/code-index/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class CodeIndexManager {
private _orchestrator: CodeIndexOrchestrator | undefined
private _searchService: CodeIndexSearchService | undefined
private _cacheManager: CacheManager | undefined
private _rooIgnoreController: RooIgnoreController | undefined

// Flag to prevent race conditions during error recovery
private _isRecoveringFromError = false
Expand Down Expand Up @@ -250,12 +251,15 @@ export class CodeIndexManager {
if (this._orchestrator) {
this.stopWatcher()
}
if (this._rooIgnoreController) {
this._rooIgnoreController.dispose()
}
this._stateManager.dispose()
}

/**
* Clears all index data by stopping the watcher, clearing the Qdrant collection,
* and deleting the cache file.
* and deleting the cache file. Also recreates services to ensure ignore patterns are reloaded.
*/
public async clearIndexData(): Promise<void> {
if (!this.isFeatureEnabled) {
Expand All @@ -264,6 +268,10 @@ export class CodeIndexManager {
this.assertInitialized()
await this._orchestrator!.clearIndexData()
await this._cacheManager!.clearCacheFile()

// Recreate services to ensure ignore patterns are properly reloaded
// This fixes the issue where .rooignore is ignored after clearing index data
await this._recreateServices()
}

// --- Private Helpers ---
Expand Down Expand Up @@ -332,6 +340,9 @@ export class CodeIndexManager {
const rooIgnoreController = new RooIgnoreController(workspacePath)
await rooIgnoreController.initialize()

// Store reference to the RooIgnoreController for cleanup
this._rooIgnoreController = rooIgnoreController

// (Re)Create shared service instances
const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices(
this.context,
Expand Down
12 changes: 7 additions & 5 deletions src/services/code-index/processors/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,25 @@ export class DirectoryScanner implements IDirectoryScanner {

// Initialize RooIgnoreController if not provided
const ignoreController = new RooIgnoreController(directoryPath)

await ignoreController.initialize()

// Filter paths using .rooignore
// Filter paths using .rooignore/.gitignore patterns
const allowedPaths = ignoreController.filterPaths(filePaths)

// Filter by supported extensions, ignore patterns, and excluded directories
// Filter by supported extensions and excluded directories
// Note: We removed the duplicate this.ignoreInstance.ignores() check here because:
// 1. The RooIgnoreController already handles both .gitignore and .rooignore patterns
// 2. The listFiles() function already applies .gitignore filtering via ripgrep
// 3. This prevents double-filtering and ensures consistent ignore behavior
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
Expand Down
Loading