Skip to content

Commit df040a1

Browse files
committed
feat: implement unified .gitignore/.rooignore handling for code indexing
- Enhanced RooIgnoreController to support .gitignore fallback when .rooignore is missing/empty - Added priority system: .rooignore → .gitignore → default patterns - Removed dual filtering that caused conflicts between ignore systems - Updated DirectoryScanner and FileWatcher to use unified ignore controller - Fixed service factory to remove redundant ignore instance parameter - Updated tests to reflect new constructor signatures Fixes #5655
1 parent e84dd0a commit df040a1

File tree

6 files changed

+142
-87
lines changed

6 files changed

+142
-87
lines changed

src/core/ignore/RooIgnoreController.ts

Lines changed: 129 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,85 +16,161 @@ export class RooIgnoreController {
1616
private ignoreInstance: Ignore
1717
private disposables: vscode.Disposable[] = []
1818
rooIgnoreContent: string | undefined
19+
private gitIgnoreContent: string | undefined
20+
private currentIgnoreSource: "rooignore" | "gitignore" | "default" | "none" = "none"
1921

2022
constructor(cwd: string) {
2123
this.cwd = cwd
2224
this.ignoreInstance = ignore()
2325
this.rooIgnoreContent = undefined
24-
// Set up file watcher for .rooignore
25-
this.setupFileWatcher()
26+
// Set up file watchers for both .rooignore and .gitignore
27+
this.setupFileWatchers()
2628
}
2729

2830
/**
2931
* Initialize the controller by loading custom patterns
3032
* Must be called after construction and before using the controller
3133
*/
3234
async initialize(): Promise<void> {
33-
await this.loadRooIgnore()
35+
await this.loadIgnorePatterns()
3436
}
3537

3638
/**
37-
* Set up the file watcher for .rooignore changes
39+
* Set up file watchers for both .rooignore and .gitignore changes
3840
*/
39-
private setupFileWatcher(): void {
41+
private setupFileWatchers(): void {
42+
// Watch .rooignore
4043
const rooignorePattern = new vscode.RelativePattern(this.cwd, ".rooignore")
41-
const fileWatcher = vscode.workspace.createFileSystemWatcher(rooignorePattern)
44+
const rooignoreWatcher = vscode.workspace.createFileSystemWatcher(rooignorePattern)
4245

43-
// Watch for changes and updates
46+
// Watch .gitignore
47+
const gitignorePattern = new vscode.RelativePattern(this.cwd, ".gitignore")
48+
const gitignoreWatcher = vscode.workspace.createFileSystemWatcher(gitignorePattern)
49+
50+
// Watch for changes and updates to both files
4451
this.disposables.push(
45-
fileWatcher.onDidChange(() => {
46-
this.loadRooIgnore()
52+
rooignoreWatcher.onDidChange(() => {
53+
this.loadIgnorePatterns()
54+
}),
55+
rooignoreWatcher.onDidCreate(() => {
56+
this.loadIgnorePatterns()
57+
}),
58+
rooignoreWatcher.onDidDelete(() => {
59+
this.loadIgnorePatterns()
4760
}),
48-
fileWatcher.onDidCreate(() => {
49-
this.loadRooIgnore()
61+
gitignoreWatcher.onDidChange(() => {
62+
this.loadIgnorePatterns()
5063
}),
51-
fileWatcher.onDidDelete(() => {
52-
this.loadRooIgnore()
64+
gitignoreWatcher.onDidCreate(() => {
65+
this.loadIgnorePatterns()
66+
}),
67+
gitignoreWatcher.onDidDelete(() => {
68+
this.loadIgnorePatterns()
5369
}),
5470
)
5571

56-
// Add fileWatcher itself to disposables
57-
this.disposables.push(fileWatcher)
72+
// Add fileWatchers themselves to disposables
73+
this.disposables.push(rooignoreWatcher, gitignoreWatcher)
5874
}
5975

6076
/**
61-
* Load custom patterns from .rooignore if it exists
77+
* Load ignore patterns with .gitignore fallback support
6278
*/
63-
private async loadRooIgnore(): Promise<void> {
79+
private async loadIgnorePatterns(): Promise<void> {
6480
try {
6581
// Reset ignore instance to prevent duplicate patterns
6682
this.ignoreInstance = ignore()
67-
const ignorePath = path.join(this.cwd, ".rooignore")
68-
if (await fileExistsAtPath(ignorePath)) {
69-
const content = await fs.readFile(ignorePath, "utf8")
70-
this.rooIgnoreContent = content
71-
this.ignoreInstance.add(content)
72-
this.ignoreInstance.add(".rooignore")
83+
84+
// Try to load .rooignore first
85+
const rooIgnorePath = path.join(this.cwd, ".rooignore")
86+
const rooIgnoreExists = await fileExistsAtPath(rooIgnorePath)
87+
88+
if (rooIgnoreExists) {
89+
const content = await fs.readFile(rooIgnorePath, "utf8")
90+
const trimmedContent = content.trim()
91+
92+
if (trimmedContent) {
93+
// .rooignore exists and has content - use it exclusively
94+
this.rooIgnoreContent = content
95+
this.gitIgnoreContent = undefined
96+
this.currentIgnoreSource = "rooignore"
97+
this.ignoreInstance.add(content)
98+
this.ignoreInstance.add(".rooignore")
99+
return
100+
} else {
101+
// .rooignore exists but is empty - fall back to .gitignore
102+
this.rooIgnoreContent = content
103+
}
73104
} else {
74105
this.rooIgnoreContent = undefined
75106
}
107+
108+
// Try to load .gitignore as fallback
109+
const gitIgnorePath = path.join(this.cwd, ".gitignore")
110+
const gitIgnoreExists = await fileExistsAtPath(gitIgnorePath)
111+
112+
if (gitIgnoreExists) {
113+
const content = await fs.readFile(gitIgnorePath, "utf8")
114+
this.gitIgnoreContent = content
115+
this.currentIgnoreSource = "gitignore"
116+
this.ignoreInstance.add(content)
117+
this.ignoreInstance.add(".gitignore")
118+
} else {
119+
// Neither file exists - use default patterns for common directories
120+
this.gitIgnoreContent = undefined
121+
this.currentIgnoreSource = "default"
122+
this.addDefaultIgnorePatterns()
123+
}
76124
} catch (error) {
77-
// Should never happen: reading file failed even though it exists
78-
console.error("Unexpected error loading .rooignore:", error)
125+
console.error("Unexpected error loading ignore patterns:", error)
126+
// Fallback to default patterns on error
127+
this.currentIgnoreSource = "default"
128+
this.addDefaultIgnorePatterns()
79129
}
80130
}
81131

132+
/**
133+
* Add default ignore patterns for common directories that should typically be excluded
134+
*/
135+
private addDefaultIgnorePatterns(): void {
136+
const defaultPatterns = [
137+
"node_modules/",
138+
"vendor/",
139+
".git/",
140+
".svn/",
141+
".hg/",
142+
"dist/",
143+
"build/",
144+
"out/",
145+
"target/",
146+
"*.log",
147+
".DS_Store",
148+
"Thumbs.db",
149+
]
150+
151+
this.ignoreInstance.add(defaultPatterns)
152+
}
153+
154+
/**
155+
* Load custom patterns from .rooignore if it exists
156+
* @deprecated Use loadIgnorePatterns() instead
157+
*/
158+
private async loadRooIgnore(): Promise<void> {
159+
await this.loadIgnorePatterns()
160+
}
161+
82162
/**
83163
* Check if a file should be accessible to the LLM
84164
* @param filePath - Path to check (relative to cwd)
85165
* @returns true if file is accessible, false if ignored
86166
*/
87167
validateAccess(filePath: string): boolean {
88-
// Always allow access if .rooignore does not exist
89-
if (!this.rooIgnoreContent) {
90-
return true
91-
}
92168
try {
93169
// Normalize path to be relative to cwd and use forward slashes
94170
const absolutePath = path.resolve(this.cwd, filePath)
95171
const relativePath = path.relative(this.cwd, absolutePath).toPosix()
96172

97-
// Ignore expects paths to be path.relative()'d
173+
// Use the unified ignore instance which now handles .rooignore, .gitignore, or default patterns
98174
return !this.ignoreInstance.ignores(relativePath)
99175
} catch (error) {
100176
// console.error(`Error validating access for ${filePath}:`, error)
@@ -109,10 +185,7 @@ export class RooIgnoreController {
109185
* @returns path of file that is being accessed if it is being accessed, undefined if command is allowed
110186
*/
111187
validateCommand(command: string): string | undefined {
112-
// Always allow if no .rooignore exists
113-
if (!this.rooIgnoreContent) {
114-
return undefined
115-
}
188+
// Use unified ignore patterns (rooignore, gitignore, or defaults)
116189

117190
// Split command into parts and get the base command
118191
const parts = command.trim().split(/\s+/)
@@ -188,14 +261,30 @@ export class RooIgnoreController {
188261
}
189262

190263
/**
191-
* Get formatted instructions about the .rooignore file for the LLM
192-
* @returns Formatted instructions or undefined if .rooignore doesn't exist
264+
* Get the current ignore source being used
265+
* @returns The source of ignore patterns currently in use
266+
*/
267+
getIgnoreSource(): "rooignore" | "gitignore" | "default" | "none" {
268+
return this.currentIgnoreSource
269+
}
270+
271+
/**
272+
* Get formatted instructions about the ignore patterns for the LLM
273+
* @returns Formatted instructions or undefined if no patterns are active
193274
*/
194275
getInstructions(): string | undefined {
195-
if (!this.rooIgnoreContent) {
196-
return undefined
197-
}
276+
switch (this.currentIgnoreSource) {
277+
case "rooignore":
278+
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`
279+
280+
case "gitignore":
281+
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`
198282

199-
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`
283+
case "default":
284+
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`
285+
286+
default:
287+
return undefined
288+
}
200289
}
201290
}

src/services/code-index/manager.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -236,34 +236,17 @@ export class CodeIndexManager {
236236
this._cacheManager!,
237237
)
238238

239-
const ignoreInstance = ignore()
240239
const workspacePath = getWorkspacePath()
241240

242241
if (!workspacePath) {
243242
this._stateManager.setSystemState("Standby", "")
244243
return
245244
}
246245

247-
const ignorePath = path.join(workspacePath, ".gitignore")
248-
try {
249-
const content = await fs.readFile(ignorePath, "utf8")
250-
ignoreInstance.add(content)
251-
ignoreInstance.add(".gitignore")
252-
} catch (error) {
253-
// Should never happen: reading file failed even though it exists
254-
console.error("Unexpected error loading .gitignore:", error)
255-
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
256-
error: error instanceof Error ? error.message : String(error),
257-
stack: error instanceof Error ? error.stack : undefined,
258-
location: "_recreateServices",
259-
})
260-
}
261-
262-
// (Re)Create shared service instances
246+
// (Re)Create shared service instances - ignore patterns are now handled by RooIgnoreController
263247
const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices(
264248
this.context,
265249
this._cacheManager!,
266-
ignoreInstance,
267250
)
268251

269252
// Validate embedder configuration before proceeding

src/services/code-index/processors/__tests__/scanner.spec.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,7 @@ describe("DirectoryScanner", () => {
104104
ignores: vi.fn().mockReturnValue(false),
105105
}
106106

107-
scanner = new DirectoryScanner(
108-
mockEmbedder,
109-
mockVectorStore,
110-
mockCodeParser,
111-
mockCacheManager,
112-
mockIgnoreInstance,
113-
)
107+
scanner = new DirectoryScanner(mockEmbedder, mockVectorStore, mockCodeParser, mockCacheManager)
114108

115109
// Mock default implementations - create proper Stats object
116110
mockStats = {

src/services/code-index/processors/file-watcher.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { sanitizeErrorMessage } from "../shared/validation-helpers"
3131
* Implementation of the file watcher interface
3232
*/
3333
export class FileWatcher implements IFileWatcher {
34-
private ignoreInstance?: Ignore
3534
private fileWatcher?: vscode.FileSystemWatcher
3635
private ignoreController: RooIgnoreController
3736
private accumulatedEvents: Map<string, { uri: vscode.Uri; type: "create" | "change" | "delete" }> = new Map()
@@ -76,19 +75,18 @@ export class FileWatcher implements IFileWatcher {
7675
private readonly cacheManager: CacheManager,
7776
private embedder?: IEmbedder,
7877
private vectorStore?: IVectorStore,
79-
ignoreInstance?: Ignore,
8078
ignoreController?: RooIgnoreController,
8179
) {
8280
this.ignoreController = ignoreController || new RooIgnoreController(workspacePath)
83-
if (ignoreInstance) {
84-
this.ignoreInstance = ignoreInstance
85-
}
8681
}
8782

8883
/**
8984
* Initializes the file watcher
9085
*/
9186
async initialize(): Promise<void> {
87+
// Initialize the ignore controller
88+
await this.ignoreController.initialize()
89+
9290
// Create file watcher
9391
const filePattern = new vscode.RelativePattern(
9492
this.workspacePath,
@@ -495,12 +493,8 @@ export class FileWatcher implements IFileWatcher {
495493
}
496494
}
497495

498-
// Check if file should be ignored
499-
const relativeFilePath = generateRelativeFilePath(filePath, this.workspacePath)
500-
if (
501-
!this.ignoreController.validateAccess(filePath) ||
502-
(this.ignoreInstance && this.ignoreInstance.ignores(relativeFilePath))
503-
) {
496+
// Check if file should be ignored using unified ignore controller
497+
if (!this.ignoreController.validateAccess(filePath)) {
504498
return {
505499
path: filePath,
506500
status: "skipped" as const,

src/services/code-index/processors/scanner.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export class DirectoryScanner implements IDirectoryScanner {
3535
private readonly qdrantClient: IVectorStore,
3636
private readonly codeParser: ICodeParser,
3737
private readonly cacheManager: CacheManager,
38-
private readonly ignoreInstance: Ignore,
3938
) {}
4039

4140
/**
@@ -70,17 +69,16 @@ export class DirectoryScanner implements IDirectoryScanner {
7069
// Filter paths using .rooignore
7170
const allowedPaths = ignoreController.filterPaths(filePaths)
7271

73-
// Filter by supported extensions, ignore patterns, and excluded directories
72+
// Filter by supported extensions and excluded directories
7473
const supportedPaths = allowedPaths.filter((filePath) => {
7574
const ext = path.extname(filePath).toLowerCase()
76-
const relativeFilePath = generateRelativeFilePath(filePath, scanWorkspace)
7775

7876
// Check if file is in an ignored directory using the shared helper
7977
if (isPathInIgnoredDirectory(filePath)) {
8078
return false
8179
}
8280

83-
return scannerExtensions.includes(ext) && !this.ignoreInstance.ignores(relativeFilePath)
81+
return scannerExtensions.includes(ext)
8482
})
8583

8684
// Initialize tracking variables

0 commit comments

Comments
 (0)