Skip to content

Commit c6b90b4

Browse files
authored
Merge pull request #228 from RooVetGit/more_efficient_filetracker
More efficient workspace tracker
2 parents 5bf9a74 + db9827e commit c6b90b4

File tree

3 files changed

+85
-14
lines changed

3 files changed

+85
-14
lines changed

.changeset/early-icons-roll.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
More efficient workspace tracker

src/integrations/workspace/WorkspaceTracker.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { listFiles } from "../../services/glob/list-files"
44
import { ClineProvider } from "../../core/webview/ClineProvider"
55

66
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
7+
const MAX_INITIAL_FILES = 1_000
78

89
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
910
class WorkspaceTracker {
1011
private providerRef: WeakRef<ClineProvider>
1112
private disposables: vscode.Disposable[] = []
1213
private filePaths: Set<string> = new Set()
14+
private updateTimer: NodeJS.Timeout | null = null
1315

1416
constructor(provider: ClineProvider) {
1517
this.providerRef = new WeakRef(provider)
@@ -21,8 +23,8 @@ class WorkspaceTracker {
2123
if (!cwd) {
2224
return
2325
}
24-
const [files, _] = await listFiles(cwd, true, 1_000)
25-
files.forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
26+
const [files, _] = await listFiles(cwd, true, MAX_INITIAL_FILES)
27+
files.slice(0, MAX_INITIAL_FILES).forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
2628
this.workspaceDidUpdate()
2729
}
2830

@@ -49,16 +51,23 @@ class WorkspaceTracker {
4951
}
5052

5153
private workspaceDidUpdate() {
52-
if (!cwd) {
53-
return
54+
if (this.updateTimer) {
55+
clearTimeout(this.updateTimer)
5456
}
55-
this.providerRef.deref()?.postMessageToWebview({
56-
type: "workspaceUpdated",
57-
filePaths: Array.from(this.filePaths).map((file) => {
58-
const relativePath = path.relative(cwd, file).toPosix()
59-
return file.endsWith("/") ? relativePath + "/" : relativePath
57+
58+
this.updateTimer = setTimeout(() => {
59+
if (!cwd) {
60+
return
61+
}
62+
this.providerRef.deref()?.postMessageToWebview({
63+
type: "workspaceUpdated",
64+
filePaths: Array.from(this.filePaths).map((file) => {
65+
const relativePath = path.relative(cwd, file).toPosix()
66+
return file.endsWith("/") ? relativePath + "/" : relativePath
67+
})
6068
})
61-
})
69+
this.updateTimer = null
70+
}, 300) // Debounce for 300ms
6271
}
6372

6473
private normalizeFilePath(filePath: string): string {
@@ -67,6 +76,11 @@ class WorkspaceTracker {
6776
}
6877

6978
private async addFilePath(filePath: string): Promise<string> {
79+
// Allow for some buffer to account for files being created/deleted during a task
80+
if (this.filePaths.size >= MAX_INITIAL_FILES * 2) {
81+
return filePath
82+
}
83+
7084
const normalizedPath = this.normalizeFilePath(filePath)
7185
try {
7286
const stat = await vscode.workspace.fs.stat(vscode.Uri.file(normalizedPath))
@@ -87,6 +101,10 @@ class WorkspaceTracker {
87101
}
88102

89103
public dispose() {
104+
if (this.updateTimer) {
105+
clearTimeout(this.updateTimer)
106+
this.updateTimer = null
107+
}
90108
this.disposables.forEach((d) => d.dispose())
91109
}
92110
}

src/integrations/workspace/__tests__/WorkspaceTracker.test.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ describe("WorkspaceTracker", () => {
3838

3939
beforeEach(() => {
4040
jest.clearAllMocks()
41+
jest.useFakeTimers()
4142

4243
// Create provider mock
43-
mockProvider = { postMessageToWebview: jest.fn() } as any
44+
mockProvider = {
45+
postMessageToWebview: jest.fn().mockResolvedValue(undefined)
46+
} as unknown as ClineProvider & { postMessageToWebview: jest.Mock }
4447

4548
// Create tracker instance
4649
workspaceTracker = new WorkspaceTracker(mockProvider)
@@ -51,17 +54,20 @@ describe("WorkspaceTracker", () => {
5154
;(listFiles as jest.Mock).mockResolvedValue(mockFiles)
5255

5356
await workspaceTracker.initializeFilePaths()
57+
jest.runAllTimers()
5458

5559
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
5660
type: "workspaceUpdated",
57-
filePaths: ["file1.ts", "file2.ts"]
61+
filePaths: expect.arrayContaining(["file1.ts", "file2.ts"])
5862
})
63+
expect((mockProvider.postMessageToWebview as jest.Mock).mock.calls[0][0].filePaths).toHaveLength(2)
5964
})
6065

6166
it("should handle file creation events", async () => {
6267
// Get the creation callback and call it
6368
const [[callback]] = mockOnDidCreate.mock.calls
6469
await callback({ fsPath: "/test/workspace/newfile.ts" })
70+
jest.runAllTimers()
6571

6672
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
6773
type: "workspaceUpdated",
@@ -73,10 +79,12 @@ describe("WorkspaceTracker", () => {
7379
// First add a file
7480
const [[createCallback]] = mockOnDidCreate.mock.calls
7581
await createCallback({ fsPath: "/test/workspace/file.ts" })
82+
jest.runAllTimers()
7683

7784
// Then delete it
7885
const [[deleteCallback]] = mockOnDidDelete.mock.calls
7986
await deleteCallback({ fsPath: "/test/workspace/file.ts" })
87+
jest.runAllTimers()
8088

8189
// The last call should have empty filePaths
8290
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
@@ -91,15 +99,55 @@ describe("WorkspaceTracker", () => {
9199

92100
const [[callback]] = mockOnDidCreate.mock.calls
93101
await callback({ fsPath: "/test/workspace/newdir" })
102+
jest.runAllTimers()
94103

95104
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
96105
type: "workspaceUpdated",
97-
filePaths: ["newdir"]
106+
filePaths: expect.arrayContaining(["newdir"])
98107
})
108+
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
109+
expect(lastCall[0].filePaths).toHaveLength(1)
99110
})
100111

101-
it("should clean up watchers on dispose", () => {
112+
it("should respect file limits", async () => {
113+
// Create array of unique file paths for initial load
114+
const files = Array.from({ length: 1001 }, (_, i) => `/test/workspace/file${i}.ts`)
115+
;(listFiles as jest.Mock).mockResolvedValue([files, false])
116+
117+
await workspaceTracker.initializeFilePaths()
118+
jest.runAllTimers()
119+
120+
// Should only have 1000 files initially
121+
const expectedFiles = Array.from({ length: 1000 }, (_, i) => `file${i}.ts`).sort()
122+
const calls = (mockProvider.postMessageToWebview as jest.Mock).mock.calls
123+
124+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
125+
type: "workspaceUpdated",
126+
filePaths: expect.arrayContaining(expectedFiles)
127+
})
128+
expect(calls[0][0].filePaths).toHaveLength(1000)
129+
130+
// Should allow adding up to 2000 total files
131+
const [[callback]] = mockOnDidCreate.mock.calls
132+
for (let i = 0; i < 1000; i++) {
133+
await callback({ fsPath: `/test/workspace/extra${i}.ts` })
134+
}
135+
jest.runAllTimers()
136+
137+
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
138+
expect(lastCall[0].filePaths).toHaveLength(2000)
139+
140+
// Adding one more file beyond 2000 should not increase the count
141+
await callback({ fsPath: "/test/workspace/toomany.ts" })
142+
jest.runAllTimers()
143+
144+
const finalCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
145+
expect(finalCall[0].filePaths).toHaveLength(2000)
146+
})
147+
148+
it("should clean up watchers and timers on dispose", () => {
102149
workspaceTracker.dispose()
103150
expect(mockDispose).toHaveBeenCalled()
151+
jest.runAllTimers() // Ensure any pending timers are cleared
104152
})
105153
})

0 commit comments

Comments
 (0)