mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
More efficient workspace tracker
This commit is contained in:
5
.changeset/early-icons-roll.md
Normal file
5
.changeset/early-icons-roll.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
More efficient workspace tracker
|
||||||
@@ -4,12 +4,14 @@ import { listFiles } from "../../services/glob/list-files"
|
|||||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||||
|
|
||||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||||
|
const MAX_INITIAL_FILES = 1_000
|
||||||
|
|
||||||
// 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
|
// 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
|
||||||
class WorkspaceTracker {
|
class WorkspaceTracker {
|
||||||
private providerRef: WeakRef<ClineProvider>
|
private providerRef: WeakRef<ClineProvider>
|
||||||
private disposables: vscode.Disposable[] = []
|
private disposables: vscode.Disposable[] = []
|
||||||
private filePaths: Set<string> = new Set()
|
private filePaths: Set<string> = new Set()
|
||||||
|
private updateTimer: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
constructor(provider: ClineProvider) {
|
constructor(provider: ClineProvider) {
|
||||||
this.providerRef = new WeakRef(provider)
|
this.providerRef = new WeakRef(provider)
|
||||||
@@ -21,8 +23,8 @@ class WorkspaceTracker {
|
|||||||
if (!cwd) {
|
if (!cwd) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const [files, _] = await listFiles(cwd, true, 1_000)
|
const [files, _] = await listFiles(cwd, true, MAX_INITIAL_FILES)
|
||||||
files.forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
|
files.slice(0, MAX_INITIAL_FILES).forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
|
||||||
this.workspaceDidUpdate()
|
this.workspaceDidUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,16 +51,23 @@ class WorkspaceTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private workspaceDidUpdate() {
|
private workspaceDidUpdate() {
|
||||||
if (!cwd) {
|
if (this.updateTimer) {
|
||||||
return
|
clearTimeout(this.updateTimer)
|
||||||
}
|
}
|
||||||
this.providerRef.deref()?.postMessageToWebview({
|
|
||||||
type: "workspaceUpdated",
|
this.updateTimer = setTimeout(() => {
|
||||||
filePaths: Array.from(this.filePaths).map((file) => {
|
if (!cwd) {
|
||||||
const relativePath = path.relative(cwd, file).toPosix()
|
return
|
||||||
return file.endsWith("/") ? relativePath + "/" : relativePath
|
}
|
||||||
|
this.providerRef.deref()?.postMessageToWebview({
|
||||||
|
type: "workspaceUpdated",
|
||||||
|
filePaths: Array.from(this.filePaths).map((file) => {
|
||||||
|
const relativePath = path.relative(cwd, file).toPosix()
|
||||||
|
return file.endsWith("/") ? relativePath + "/" : relativePath
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
this.updateTimer = null
|
||||||
|
}, 300) // Debounce for 300ms
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeFilePath(filePath: string): string {
|
private normalizeFilePath(filePath: string): string {
|
||||||
@@ -67,6 +76,11 @@ class WorkspaceTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async addFilePath(filePath: string): Promise<string> {
|
private async addFilePath(filePath: string): Promise<string> {
|
||||||
|
// Allow for some buffer to account for files being created/deleted during a task
|
||||||
|
if (this.filePaths.size >= MAX_INITIAL_FILES * 2) {
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedPath = this.normalizeFilePath(filePath)
|
const normalizedPath = this.normalizeFilePath(filePath)
|
||||||
try {
|
try {
|
||||||
const stat = await vscode.workspace.fs.stat(vscode.Uri.file(normalizedPath))
|
const stat = await vscode.workspace.fs.stat(vscode.Uri.file(normalizedPath))
|
||||||
@@ -87,6 +101,10 @@ class WorkspaceTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
|
if (this.updateTimer) {
|
||||||
|
clearTimeout(this.updateTimer)
|
||||||
|
this.updateTimer = null
|
||||||
|
}
|
||||||
this.disposables.forEach((d) => d.dispose())
|
this.disposables.forEach((d) => d.dispose())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,12 @@ describe("WorkspaceTracker", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
jest.useFakeTimers()
|
||||||
|
|
||||||
// Create provider mock
|
// Create provider mock
|
||||||
mockProvider = { postMessageToWebview: jest.fn() } as any
|
mockProvider = {
|
||||||
|
postMessageToWebview: jest.fn().mockResolvedValue(undefined)
|
||||||
|
} as unknown as ClineProvider & { postMessageToWebview: jest.Mock }
|
||||||
|
|
||||||
// Create tracker instance
|
// Create tracker instance
|
||||||
workspaceTracker = new WorkspaceTracker(mockProvider)
|
workspaceTracker = new WorkspaceTracker(mockProvider)
|
||||||
@@ -51,17 +54,20 @@ describe("WorkspaceTracker", () => {
|
|||||||
;(listFiles as jest.Mock).mockResolvedValue(mockFiles)
|
;(listFiles as jest.Mock).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
await workspaceTracker.initializeFilePaths()
|
await workspaceTracker.initializeFilePaths()
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
filePaths: ["file1.ts", "file2.ts"]
|
filePaths: expect.arrayContaining(["file1.ts", "file2.ts"])
|
||||||
})
|
})
|
||||||
|
expect((mockProvider.postMessageToWebview as jest.Mock).mock.calls[0][0].filePaths).toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle file creation events", async () => {
|
it("should handle file creation events", async () => {
|
||||||
// Get the creation callback and call it
|
// Get the creation callback and call it
|
||||||
const [[callback]] = mockOnDidCreate.mock.calls
|
const [[callback]] = mockOnDidCreate.mock.calls
|
||||||
await callback({ fsPath: "/test/workspace/newfile.ts" })
|
await callback({ fsPath: "/test/workspace/newfile.ts" })
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
@@ -73,10 +79,12 @@ describe("WorkspaceTracker", () => {
|
|||||||
// First add a file
|
// First add a file
|
||||||
const [[createCallback]] = mockOnDidCreate.mock.calls
|
const [[createCallback]] = mockOnDidCreate.mock.calls
|
||||||
await createCallback({ fsPath: "/test/workspace/file.ts" })
|
await createCallback({ fsPath: "/test/workspace/file.ts" })
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
// Then delete it
|
// Then delete it
|
||||||
const [[deleteCallback]] = mockOnDidDelete.mock.calls
|
const [[deleteCallback]] = mockOnDidDelete.mock.calls
|
||||||
await deleteCallback({ fsPath: "/test/workspace/file.ts" })
|
await deleteCallback({ fsPath: "/test/workspace/file.ts" })
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
// The last call should have empty filePaths
|
// The last call should have empty filePaths
|
||||||
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
|
||||||
@@ -91,15 +99,55 @@ describe("WorkspaceTracker", () => {
|
|||||||
|
|
||||||
const [[callback]] = mockOnDidCreate.mock.calls
|
const [[callback]] = mockOnDidCreate.mock.calls
|
||||||
await callback({ fsPath: "/test/workspace/newdir" })
|
await callback({ fsPath: "/test/workspace/newdir" })
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||||
type: "workspaceUpdated",
|
type: "workspaceUpdated",
|
||||||
filePaths: ["newdir"]
|
filePaths: expect.arrayContaining(["newdir"])
|
||||||
})
|
})
|
||||||
|
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||||
|
expect(lastCall[0].filePaths).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should clean up watchers on dispose", () => {
|
it("should respect file limits", async () => {
|
||||||
|
// Create array of unique file paths for initial load
|
||||||
|
const files = Array.from({ length: 1001 }, (_, i) => `/test/workspace/file${i}.ts`)
|
||||||
|
;(listFiles as jest.Mock).mockResolvedValue([files, false])
|
||||||
|
|
||||||
|
await workspaceTracker.initializeFilePaths()
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
|
// Should only have 1000 files initially
|
||||||
|
const expectedFiles = Array.from({ length: 1000 }, (_, i) => `file${i}.ts`).sort()
|
||||||
|
const calls = (mockProvider.postMessageToWebview as jest.Mock).mock.calls
|
||||||
|
|
||||||
|
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||||
|
type: "workspaceUpdated",
|
||||||
|
filePaths: expect.arrayContaining(expectedFiles)
|
||||||
|
})
|
||||||
|
expect(calls[0][0].filePaths).toHaveLength(1000)
|
||||||
|
|
||||||
|
// Should allow adding up to 2000 total files
|
||||||
|
const [[callback]] = mockOnDidCreate.mock.calls
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
await callback({ fsPath: `/test/workspace/extra${i}.ts` })
|
||||||
|
}
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
|
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||||
|
expect(lastCall[0].filePaths).toHaveLength(2000)
|
||||||
|
|
||||||
|
// Adding one more file beyond 2000 should not increase the count
|
||||||
|
await callback({ fsPath: "/test/workspace/toomany.ts" })
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
|
const finalCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
|
||||||
|
expect(finalCall[0].filePaths).toHaveLength(2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should clean up watchers and timers on dispose", () => {
|
||||||
workspaceTracker.dispose()
|
workspaceTracker.dispose()
|
||||||
expect(mockDispose).toHaveBeenCalled()
|
expect(mockDispose).toHaveBeenCalled()
|
||||||
|
jest.runAllTimers() // Ensure any pending timers are cleared
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user