From 752b0cf3521eca599590c93dc7de08628e76a75e Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 23 Dec 2024 21:04:45 -0800 Subject: [PATCH 1/4] Use createFileSystemWatcher to more reliably watch for file system changes --- .../workspace/WorkspaceTracker.ts | 68 +++++++------------ 1 file changed, 23 insertions(+), 45 deletions(-) diff --git a/src/integrations/workspace/WorkspaceTracker.ts b/src/integrations/workspace/WorkspaceTracker.ts index 10dfac8f9e1..69fc860d379 100644 --- a/src/integrations/workspace/WorkspaceTracker.ts +++ b/src/integrations/workspace/WorkspaceTracker.ts @@ -27,58 +27,36 @@ class WorkspaceTracker { } private registerListeners() { - // Listen for file creation - // .bind(this) ensures the callback refers to class instance when using this, not necessary when using arrow function - this.disposables.push(vscode.workspace.onDidCreateFiles(this.onFilesCreated.bind(this))) - - // Listen for file deletion - this.disposables.push(vscode.workspace.onDidDeleteFiles(this.onFilesDeleted.bind(this))) + // Create a file system watcher for all files + const watcher = vscode.workspace.createFileSystemWatcher('**') - // Listen for file renaming - this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this))) - - /* - An event that is emitted when a workspace folder is added or removed. - **Note:** this event will not fire if the first workspace folder is added, removed or changed, - because in that case the currently executing extensions (including the one that listens to this - event) will be terminated and restarted so that the (deprecated) `rootPath` property is updated - to point to the first workspace folder. - */ - // In other words, we don't have to worry about the root workspace folder ([0]) changing since the extension will be restarted and our cwd will be updated to reflect the new workspace folder. (We don't care about non root workspace folders, since cline will only be working within the root folder cwd) - // this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged.bind(this))) - } - - private async onFilesCreated(event: vscode.FileCreateEvent) { - await Promise.all( - event.files.map(async (file) => { - await this.addFilePath(file.fsPath) - }), + // Listen for file creation + this.disposables.push( + watcher.onDidCreate(async (uri) => { + await this.addFilePath(uri.fsPath) + this.workspaceDidUpdate() + }) ) - this.workspaceDidUpdate() - } - private async onFilesDeleted(event: vscode.FileDeleteEvent) { - let updated = false - await Promise.all( - event.files.map(async (file) => { - if (await this.removeFilePath(file.fsPath)) { - updated = true + // Listen for file deletion + this.disposables.push( + watcher.onDidDelete(async (uri) => { + if (await this.removeFilePath(uri.fsPath)) { + this.workspaceDidUpdate() } - }), + }) ) - if (updated) { - this.workspaceDidUpdate() - } - } - private async onFilesRenamed(event: vscode.FileRenameEvent) { - await Promise.all( - event.files.map(async (file) => { - await this.removeFilePath(file.oldUri.fsPath) - await this.addFilePath(file.newUri.fsPath) - }), + // Listen for file changes (which could include renames) + this.disposables.push( + watcher.onDidChange(async (uri) => { + await this.addFilePath(uri.fsPath) + this.workspaceDidUpdate() + }) ) - this.workspaceDidUpdate() + + // Add the watcher itself to disposables + this.disposables.push(watcher) } private workspaceDidUpdate() { From b3e2e61f47703c75f4351dda99c35dfdec9925ad Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 23 Dec 2024 21:11:27 -0800 Subject: [PATCH 2/4] Add changeset --- .changeset/gentle-masks-notice.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gentle-masks-notice.md diff --git a/.changeset/gentle-masks-notice.md b/.changeset/gentle-masks-notice.md new file mode 100644 index 00000000000..f58f092e16c --- /dev/null +++ b/.changeset/gentle-masks-notice.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Use createFileSystemWatcher to more reliably update list of files to @-mention From 51b9418e31f86a421f3dfe5a566437016156df0d Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 23 Dec 2024 21:32:02 -0800 Subject: [PATCH 3/4] Tests --- src/core/__tests__/Cline.test.ts | 17 +-- .../__tests__/WorkspaceTracker.test.ts | 116 ++++++++++++++++++ 2 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/integrations/workspace/__tests__/WorkspaceTracker.test.ts diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index ed755bd3997..99f411eaec1 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -121,13 +121,16 @@ jest.mock('vscode', () => { name: 'mock-workspace', index: 0 }], - onDidCreateFiles: jest.fn(() => mockDisposable), - onDidDeleteFiles: jest.fn(() => mockDisposable), - onDidRenameFiles: jest.fn(() => mockDisposable), - onDidSaveTextDocument: jest.fn(() => mockDisposable), - onDidChangeTextDocument: jest.fn(() => mockDisposable), - onDidOpenTextDocument: jest.fn(() => mockDisposable), - onDidCloseTextDocument: jest.fn(() => mockDisposable) + createFileSystemWatcher: jest.fn(() => ({ + onDidCreate: jest.fn(() => mockDisposable), + onDidDelete: jest.fn(() => mockDisposable), + onDidChange: jest.fn(() => mockDisposable), + dispose: jest.fn() + })), + fs: { + stat: jest.fn().mockResolvedValue({ type: 1 }) // FileType.File = 1 + }, + onDidSaveTextDocument: jest.fn(() => mockDisposable) }, env: { uriScheme: 'vscode', diff --git a/src/integrations/workspace/__tests__/WorkspaceTracker.test.ts b/src/integrations/workspace/__tests__/WorkspaceTracker.test.ts new file mode 100644 index 00000000000..13988519af6 --- /dev/null +++ b/src/integrations/workspace/__tests__/WorkspaceTracker.test.ts @@ -0,0 +1,116 @@ +import * as vscode from "vscode" +import WorkspaceTracker from "../WorkspaceTracker" +import { ClineProvider } from "../../../core/webview/ClineProvider" +import { listFiles } from "../../../services/glob/list-files" + +// Mock modules +const mockOnDidCreate = jest.fn() +const mockOnDidDelete = jest.fn() +const mockOnDidChange = jest.fn() +const mockDispose = jest.fn() + +const mockWatcher = { + onDidCreate: mockOnDidCreate.mockReturnValue({ dispose: mockDispose }), + onDidDelete: mockOnDidDelete.mockReturnValue({ dispose: mockDispose }), + onDidChange: mockOnDidChange.mockReturnValue({ dispose: mockDispose }), + dispose: mockDispose +} + +jest.mock("vscode", () => ({ + workspace: { + workspaceFolders: [{ + uri: { fsPath: "/test/workspace" }, + name: "test", + index: 0 + }], + createFileSystemWatcher: jest.fn(() => mockWatcher), + fs: { + stat: jest.fn().mockResolvedValue({ type: 1 }) // FileType.File = 1 + } + }, + FileType: { File: 1, Directory: 2 } +})) + +jest.mock("../../../services/glob/list-files") + +describe("WorkspaceTracker", () => { + let workspaceTracker: WorkspaceTracker + let mockProvider: ClineProvider + + beforeEach(() => { + jest.clearAllMocks() + + // Create provider mock + mockProvider = { postMessageToWebview: jest.fn() } as any + + // Create tracker instance + workspaceTracker = new WorkspaceTracker(mockProvider) + }) + + it("should initialize with workspace files", async () => { + const mockFiles = [["/test/workspace/file1.ts", "/test/workspace/file2.ts"], false] + ;(listFiles as jest.Mock).mockResolvedValue(mockFiles) + + await workspaceTracker.initializeFilePaths() + + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "workspaceUpdated", + filePaths: ["file1.ts", "file2.ts"] + }) + }) + + it("should handle file creation events", async () => { + // Get the creation callback and call it + const [[callback]] = mockOnDidCreate.mock.calls + await callback({ fsPath: "/test/workspace/newfile.ts" }) + + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "workspaceUpdated", + filePaths: ["newfile.ts"] + }) + }) + + it("should handle file deletion events", async () => { + // First add a file + const [[createCallback]] = mockOnDidCreate.mock.calls + await createCallback({ fsPath: "/test/workspace/file.ts" }) + + // Then delete it + const [[deleteCallback]] = mockOnDidDelete.mock.calls + await deleteCallback({ fsPath: "/test/workspace/file.ts" }) + + // The last call should have empty filePaths + expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({ + type: "workspaceUpdated", + filePaths: [] + }) + }) + + it("should handle file change events", async () => { + const [[callback]] = mockOnDidChange.mock.calls + await callback({ fsPath: "/test/workspace/changed.ts" }) + + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "workspaceUpdated", + filePaths: ["changed.ts"] + }) + }) + + it("should handle directory paths correctly", async () => { + // Mock stat to return directory type + ;(vscode.workspace.fs.stat as jest.Mock).mockResolvedValueOnce({ type: 2 }) // FileType.Directory = 2 + + const [[callback]] = mockOnDidCreate.mock.calls + await callback({ fsPath: "/test/workspace/newdir" }) + + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "workspaceUpdated", + filePaths: ["newdir"] + }) + }) + + it("should clean up watchers on dispose", () => { + workspaceTracker.dispose() + expect(mockDispose).toHaveBeenCalled() + }) +}) \ No newline at end of file From 5d3c4652eea1e4da42782d1c946d6c357958dd77 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 24 Dec 2024 14:08:11 -0800 Subject: [PATCH 4/4] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5d3f696121c..f38a644f317 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f - Quick prompt copying from history - OpenRouter compression support - Includes current time in the system prompt +- Uses a file system watcher to more reliably watch for file system changes - Language selection for Cline's communication (English, Japanese, Spanish, French, German, and more) - Support for Meta 3, 3.1, and 3.2 models via AWS Bedrock - Runs alongside the original Cline