|
| 1 | +import path from "path" |
| 2 | +import { fileExistsAtPath } from "../../utils/fs" |
| 3 | +import fs from "fs/promises" |
| 4 | +import ignore, { Ignore } from "ignore" |
| 5 | +import * as vscode from "vscode" |
| 6 | + |
| 7 | +/** |
| 8 | + * Controls code indexer file inclusion by providing override patterns for gitignored files. |
| 9 | + * Uses the 'ignore' library to support standard .gitignore syntax in .rooindex files. |
| 10 | + * |
| 11 | + * The .rooindex file allows developers to specify patterns for files that should be |
| 12 | + * indexed even if they are gitignored. This is useful for: |
| 13 | + * - Generated code (TypeScript definitions, API clients) |
| 14 | + * - Meta-repository patterns with nested repositories |
| 15 | + * - Monorepos with selective version control |
| 16 | + * - Projects with generated documentation or configuration |
| 17 | + */ |
| 18 | +export class RooIndexController { |
| 19 | + private cwd: string |
| 20 | + private includeInstance: Ignore |
| 21 | + private disposables: vscode.Disposable[] = [] |
| 22 | + rooIndexContent: string | undefined |
| 23 | + |
| 24 | + constructor(cwd: string) { |
| 25 | + this.cwd = cwd |
| 26 | + this.includeInstance = ignore() |
| 27 | + this.rooIndexContent = undefined |
| 28 | + // Set up file watcher for .rooindex |
| 29 | + this.setupFileWatcher() |
| 30 | + } |
| 31 | + |
| 32 | + /** |
| 33 | + * Initialize the controller by loading custom patterns |
| 34 | + * Must be called after construction and before using the controller |
| 35 | + */ |
| 36 | + async initialize(): Promise<void> { |
| 37 | + await this.loadRooIndex() |
| 38 | + } |
| 39 | + |
| 40 | + /** |
| 41 | + * Set up the file watcher for .rooindex changes |
| 42 | + */ |
| 43 | + private setupFileWatcher(): void { |
| 44 | + const rooindexPattern = new vscode.RelativePattern(this.cwd, ".rooindex") |
| 45 | + const fileWatcher = vscode.workspace.createFileSystemWatcher(rooindexPattern) |
| 46 | + |
| 47 | + // Watch for changes and updates |
| 48 | + this.disposables.push( |
| 49 | + fileWatcher.onDidChange(() => { |
| 50 | + this.loadRooIndex() |
| 51 | + }), |
| 52 | + fileWatcher.onDidCreate(() => { |
| 53 | + this.loadRooIndex() |
| 54 | + }), |
| 55 | + fileWatcher.onDidDelete(() => { |
| 56 | + this.loadRooIndex() |
| 57 | + }), |
| 58 | + ) |
| 59 | + |
| 60 | + // Add fileWatcher itself to disposables |
| 61 | + this.disposables.push(fileWatcher) |
| 62 | + } |
| 63 | + |
| 64 | + /** |
| 65 | + * Load custom patterns from .rooindex if it exists |
| 66 | + */ |
| 67 | + private async loadRooIndex(): Promise<void> { |
| 68 | + try { |
| 69 | + // Reset include instance to prevent duplicate patterns |
| 70 | + this.includeInstance = ignore() |
| 71 | + const indexPath = path.join(this.cwd, ".rooindex") |
| 72 | + if (await fileExistsAtPath(indexPath)) { |
| 73 | + const content = await fs.readFile(indexPath, "utf8") |
| 74 | + this.rooIndexContent = content |
| 75 | + // Add patterns to the include instance |
| 76 | + // Note: We're using ignore library in reverse - patterns match what to INCLUDE |
| 77 | + this.includeInstance.add(content) |
| 78 | + } else { |
| 79 | + this.rooIndexContent = undefined |
| 80 | + } |
| 81 | + } catch (error) { |
| 82 | + // Should never happen: reading file failed even though it exists |
| 83 | + console.error("Unexpected error loading .rooindex:", error) |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * Check if a file should be included for indexing based on .rooindex patterns |
| 89 | + * @param filePath - Path to check (relative to cwd or absolute) |
| 90 | + * @returns true if file matches an inclusion pattern, false otherwise |
| 91 | + */ |
| 92 | + shouldInclude(filePath: string): boolean { |
| 93 | + // If .rooindex does not exist, no overrides |
| 94 | + if (!this.rooIndexContent) { |
| 95 | + return false |
| 96 | + } |
| 97 | + try { |
| 98 | + // Convert to relative path for pattern matching |
| 99 | + let relativePath: string |
| 100 | + if (path.isAbsolute(filePath)) { |
| 101 | + relativePath = path.relative(this.cwd, filePath) |
| 102 | + } else { |
| 103 | + relativePath = filePath |
| 104 | + } |
| 105 | + |
| 106 | + // Normalize path separators for cross-platform compatibility |
| 107 | + relativePath = relativePath.replace(/\\/g, "/") |
| 108 | + |
| 109 | + // Check if the path matches any include pattern |
| 110 | + // We're using the ignore library to match patterns, but we want inclusion behavior |
| 111 | + // The library returns true if a path should be ignored, but we're using it for inclusion |
| 112 | + // So if ignores() returns true, it means the path matches our inclusion pattern |
| 113 | + return this.includeInstance.ignores(relativePath) |
| 114 | + } catch (error) { |
| 115 | + // On error, don't include the file |
| 116 | + return false |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * Filter an array of paths to include only those that match .rooindex patterns |
| 122 | + * @param paths - Array of paths to filter |
| 123 | + * @returns Array of paths that match inclusion patterns |
| 124 | + */ |
| 125 | + filterForInclusion(paths: string[]): string[] { |
| 126 | + if (!this.rooIndexContent) { |
| 127 | + return [] |
| 128 | + } |
| 129 | + |
| 130 | + try { |
| 131 | + return paths.filter((p) => this.shouldInclude(p)) |
| 132 | + } catch (error) { |
| 133 | + console.error("Error filtering paths for inclusion:", error) |
| 134 | + return [] |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Check if a file that would normally be gitignored should be included for indexing |
| 140 | + * @param filePath - Path to check |
| 141 | + * @param isGitignored - Whether the file is gitignored |
| 142 | + * @returns true if the file should be included despite being gitignored |
| 143 | + */ |
| 144 | + shouldOverrideGitignore(filePath: string, isGitignored: boolean): boolean { |
| 145 | + // If not gitignored, no need to override |
| 146 | + if (!isGitignored) { |
| 147 | + return false |
| 148 | + } |
| 149 | + |
| 150 | + // Check if .rooindex says to include this gitignored file |
| 151 | + return this.shouldInclude(filePath) |
| 152 | + } |
| 153 | + |
| 154 | + /** |
| 155 | + * Clean up resources when the controller is no longer needed |
| 156 | + */ |
| 157 | + dispose(): void { |
| 158 | + this.disposables.forEach((d) => d.dispose()) |
| 159 | + this.disposables = [] |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * Get formatted instructions about the .rooindex file |
| 164 | + * @returns Formatted instructions or undefined if .rooindex doesn't exist |
| 165 | + */ |
| 166 | + getInstructions(): string | undefined { |
| 167 | + if (!this.rooIndexContent) { |
| 168 | + return undefined |
| 169 | + } |
| 170 | + |
| 171 | + return `# .rooindex\n\n(The following patterns from .rooindex specify files that should be indexed even if they are gitignored. This allows the code indexer to access generated code, nested repositories, and other files excluded from version control but valuable for AI context.)\n\n${this.rooIndexContent}` |
| 172 | + } |
| 173 | +} |
0 commit comments