Skip to content

Commit 524ba93

Browse files
committed
feat: Add rooignore implementation with ReclineIgnoreController, tests, and integration
1 parent ffec4c6 commit 524ba93

File tree

5 files changed

+175
-1
lines changed

5 files changed

+175
-1
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as fs from "fs/promises"
2+
import { ReclineIgnoreController } from "./ReclineIgnoreController"
3+
import { fileExistsAtPath } from "../../utils/fs"
4+
5+
jest.mock("../../utils/fs")
6+
jest.mock("fs/promises")
7+
8+
const mockedFileExistsAtPath = fileExistsAtPath as jest.Mock
9+
const mockedReadFile = fs.readFile as jest.Mock
10+
11+
describe("ReclineIgnoreController", () => {
12+
const cwd = "/workspace"
13+
14+
beforeEach(() => {
15+
jest.resetAllMocks()
16+
})
17+
18+
test("should load .rooignore and filter files appropriately", async () => {
19+
// Simulate that .rooignore exists and contains test patterns.
20+
mockedFileExistsAtPath.mockResolvedValue(true)
21+
const ignoreContent = "test-ignored.txt\n*.ignored\n"
22+
mockedReadFile.mockResolvedValue(ignoreContent)
23+
24+
const controller = new ReclineIgnoreController(cwd)
25+
await controller.initialize()
26+
27+
// Files matching the ignore patterns should be blocked.
28+
expect(controller.validateAccess("test-ignored.txt")).toBe(false)
29+
expect(controller.validateAccess("foo.ignored")).toBe(false)
30+
31+
// Files not matching ignore patterns should be allowed.
32+
expect(controller.validateAccess("not-ignored.txt")).toBe(true)
33+
})
34+
35+
test("should filter paths correctly", async () => {
36+
mockedFileExistsAtPath.mockResolvedValue(true)
37+
const ignoreContent = "ignored.txt\n"
38+
mockedReadFile.mockResolvedValue(ignoreContent)
39+
40+
const controller = new ReclineIgnoreController(cwd)
41+
await controller.initialize()
42+
43+
const files = ["ignored.txt", "keep.txt", "another.txt"]
44+
const filtered = controller.filterPaths(files)
45+
expect(filtered).toEqual(["keep.txt", "another.txt"])
46+
})
47+
48+
test("should allow file access if .rooignore does not exist", async () => {
49+
// Simulate that .rooignore does not exist.
50+
mockedFileExistsAtPath.mockResolvedValue(false)
51+
52+
const controller = new ReclineIgnoreController(cwd)
53+
await controller.initialize()
54+
55+
// Without any ignore rules, all files should be allowed.
56+
expect(controller.validateAccess("test-ignored.txt")).toBe(true)
57+
expect(controller.validateAccess("anyfile.ignored")).toBe(true)
58+
})
59+
})
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as vscode from "vscode"
2+
import * as path from "path"
3+
import * as fs from "fs/promises"
4+
import ignore, { Ignore } from "ignore"
5+
import { fileExistsAtPath } from "../../utils/fs"
6+
7+
export class ReclineIgnoreController {
8+
private ignoreInstance: Ignore
9+
private disposables: vscode.Disposable[] = []
10+
private cwd: string
11+
12+
constructor(cwd: string) {
13+
this.cwd = cwd
14+
this.ignoreInstance = ignore()
15+
this.setupFileWatcher()
16+
}
17+
18+
public async initialize(): Promise<void> {
19+
await this.loadIgnoreFile()
20+
}
21+
22+
private setupFileWatcher(): void {
23+
const ignorePattern = new vscode.RelativePattern(this.cwd, ".rooignore")
24+
const watcher = vscode.workspace.createFileSystemWatcher(ignorePattern)
25+
this.disposables.push(
26+
watcher.onDidChange(() => this.loadIgnoreFile()),
27+
watcher.onDidCreate(() => this.loadIgnoreFile()),
28+
watcher.onDidDelete(() => this.loadIgnoreFile()),
29+
watcher,
30+
)
31+
}
32+
33+
private async loadIgnoreFile(): Promise<void> {
34+
// Reset the ignore instance
35+
this.ignoreInstance = ignore()
36+
const ignorePath = path.join(this.cwd, ".rooignore")
37+
if (await fileExistsAtPath(ignorePath)) {
38+
try {
39+
const content = await fs.readFile(ignorePath, "utf8")
40+
this.ignoreInstance.add(content)
41+
// Always ignore the .clineignore file itself.
42+
this.ignoreInstance.add(".rooignore")
43+
} catch (error) {
44+
console.error("Error loading .rooignore:", error)
45+
vscode.window.showWarningMessage("Error loading .rooignore file.")
46+
}
47+
}
48+
}
49+
50+
public validateAccess(filePath: string): boolean {
51+
try {
52+
const absolutePath = path.resolve(this.cwd, filePath)
53+
const relativePath = path.relative(this.cwd, absolutePath).split(path.sep).join(path.posix.sep)
54+
return !this.ignoreInstance.ignores(relativePath)
55+
} catch (error) {
56+
// On error, allow access to avoid accidentally blocking files.
57+
return true
58+
}
59+
}
60+
61+
public filterPaths(paths: string[]): string[] {
62+
return paths.filter((file) => this.validateAccess(file))
63+
}
64+
65+
public dispose(): void {
66+
this.disposables.forEach((d) => d.dispose())
67+
this.disposables = []
68+
}
69+
}

src/extension.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode"
22

33
import { ClineProvider } from "./core/webview/ClineProvider"
4+
import { ReclineIgnoreController } from "./core/ignore/ReclineIgnoreController"
45
import { createClineAPI } from "./exports"
56
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
67
import { CodeActionProvider } from "./core/CodeActionProvider"
@@ -18,14 +19,20 @@ import { McpServerManager } from "./services/mcp/McpServerManager"
1819

1920
let outputChannel: vscode.OutputChannel
2021
let extensionContext: vscode.ExtensionContext
22+
let ignoreController: ReclineIgnoreController | undefined
23+
24+
// Export the ignore controller instance
25+
export function getIgnoreController(): ReclineIgnoreController | undefined {
26+
return ignoreController
27+
}
2128

2229
// This method is called when your extension is activated.
2330
// Your extension is activated the very first time the command is executed.
2431
export function activate(context: vscode.ExtensionContext) {
2532
extensionContext = context
2633
outputChannel = vscode.window.createOutputChannel("Roo-Code")
2734
context.subscriptions.push(outputChannel)
28-
outputChannel.appendLine("Roo-Code extension activated")
35+
outputChannel.appendLine("Roo-Code extension activated with .rooignore functionality")
2936

3037
// Get default commands from configuration.
3138
const defaultCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
@@ -35,6 +42,20 @@ export function activate(context: vscode.ExtensionContext) {
3542
context.globalState.update("allowedCommands", defaultCommands)
3643
}
3744

45+
// Initialize the ignore controller
46+
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
47+
if (cwd) {
48+
ignoreController = new ReclineIgnoreController(cwd)
49+
ignoreController
50+
.initialize()
51+
.then(() => {
52+
console.log("Loaded .rooignore file")
53+
})
54+
.catch((error) => {
55+
console.error("Failed to load .rooignore file:", error)
56+
})
57+
context.subscriptions.push(ignoreController)
58+
}
3859
const sidebarProvider = new ClineProvider(context, outputChannel)
3960

4061
context.subscriptions.push(

src/integrations/misc/extract-text.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,28 @@ import pdf from "pdf-parse/lib/pdf-parse"
44
import mammoth from "mammoth"
55
import fs from "fs/promises"
66
import { isBinaryFile } from "isbinaryfile"
7+
import * as vscode from "vscode"
8+
import { getIgnoreController } from "../../extension"
79

810
export async function extractTextFromFile(filePath: string): Promise<string> {
911
try {
1012
await fs.access(filePath)
1113
} catch (error) {
1214
throw new Error(`File not found: ${filePath}`)
1315
}
16+
17+
// Check if file is ignored by .rooignore
18+
const ignoreController = getIgnoreController()
19+
if (ignoreController) {
20+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
21+
if (workspaceRoot) {
22+
const relativePath = path.relative(workspaceRoot, filePath)
23+
if (!ignoreController.validateAccess(relativePath)) {
24+
throw new Error(`File access blocked by .rooignore: ${filePath}`)
25+
}
26+
}
27+
}
28+
1429
const fileExtension = path.extname(filePath).toLowerCase()
1530
switch (fileExtension) {
1631
case ".pdf":

src/integrations/misc/open-file.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from "path"
22
import * as os from "os"
33
import * as vscode from "vscode"
44
import { arePathsEqual } from "../../utils/path"
5+
import { getIgnoreController } from "../../extension"
56

67
export async function openImage(dataUri: string) {
78
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
@@ -38,6 +39,15 @@ export async function openFile(filePath: string, options: OpenFileOptions = {})
3839

3940
const uri = vscode.Uri.file(fullPath)
4041

42+
// Check if file is ignored
43+
const ignoreController = getIgnoreController()
44+
if (ignoreController) {
45+
const relativePath = path.relative(workspaceRoot, fullPath)
46+
if (!ignoreController.validateAccess(relativePath)) {
47+
throw new Error("File access blocked by .rooignore")
48+
}
49+
}
50+
4151
// Check if file exists
4252
try {
4353
await vscode.workspace.fs.stat(uri)

0 commit comments

Comments
 (0)