Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/integrations/workspace/WorkspaceTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ class WorkspaceTracker {
// Listen for file renaming
this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)))

// Also set up a periodic refresh
const refreshInterval = setInterval(async () => {
if (cwd) {
const [files, _] = await listFiles(cwd, true, 1_000);
let hadNewFiles = false;
files.forEach((file) => {
const normalized = this.normalizeFilePath(file);
if (!this.filePaths.has(normalized)) {
this.filePaths.add(normalized);
hadNewFiles = true;
}
});
if (hadNewFiles) {
this.workspaceDidUpdate();
}
}
}, 1000); // Check every second

// Clean up the interval when disposed
this.disposables.push({ dispose: () => clearInterval(refreshInterval) });

/*
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,
Expand Down
167 changes: 167 additions & 0 deletions src/integrations/workspace/__tests__/WorkspaceTracker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import * as vscode from "vscode"
import { ClineProvider } from "../../../core/webview/ClineProvider"
import WorkspaceTracker from "../WorkspaceTracker"
import { listFiles } from "../../../services/glob/list-files"

// Mock VSCode APIs
// Mock VSCode workspace
jest.mock("vscode", () => {
const mockWorkspace = {
workspaceFolders: [{ uri: { fsPath: "/test/workspace" } }],
onDidCreateFiles: jest.fn().mockReturnValue({ dispose: jest.fn() }),
onDidDeleteFiles: jest.fn().mockReturnValue({ dispose: jest.fn() }),
onDidRenameFiles: jest.fn().mockReturnValue({ dispose: jest.fn() }),
createFileSystemWatcher: jest.fn().mockReturnValue({
onDidCreate: jest.fn().mockReturnValue({ dispose: jest.fn() }),
dispose: jest.fn()
}),
fs: {
stat: jest.fn().mockResolvedValue({ type: 1 }) // FileType.File = 1
}
};
return { workspace: mockWorkspace };
});

// Mock the cwd variable
jest.mock("../WorkspaceTracker", () => {
const originalModule = jest.requireActual("../WorkspaceTracker");
return {
__esModule: true,
...originalModule,
default: originalModule.default,
cwd: "/test/workspace"
};
});
jest.mock("../../../services/glob/list-files")

describe("WorkspaceTracker", () => {
let workspaceTracker: WorkspaceTracker
let mockProvider: jest.Mocked<ClineProvider>
let mockListFiles: jest.Mock
let mockDisposables: Array<{ dispose: jest.Mock }>

beforeEach(() => {
jest.useFakeTimers()

// Mock provider
mockProvider = {
postMessageToWebview: jest.fn(),
} as any

// Mock listFiles
mockListFiles = listFiles as jest.Mock
mockListFiles.mockResolvedValue([["file1.txt", "file2.txt"], false])

// Mock workspace folder
;(vscode.workspace as any).workspaceFolders = [
{ uri: { fsPath: "/test/workspace" } }
]

// Create tracker
workspaceTracker = new WorkspaceTracker(mockProvider)

// Track disposables for cleanup verification
mockDisposables = []
;(vscode.workspace.onDidCreateFiles as jest.Mock).mockImplementation(() => {
const disposable = { dispose: jest.fn() }
mockDisposables.push(disposable)
return disposable
})
})

afterEach(() => {
workspaceTracker?.dispose()
jest.clearAllTimers()
jest.useRealTimers()
})

beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
workspaceTracker?.dispose();
jest.clearAllTimers();
jest.useRealTimers();
});

it("should periodically check for new files", async () => {
// Mock setInterval to execute callback immediately
const realSetInterval = global.setInterval;
const mockSetInterval = jest.fn((callback) => {
console.log('setInterval called');
callback(); // Execute immediately
return 123; // Return a dummy interval ID
}) as unknown as typeof global.setInterval;
global.setInterval = mockSetInterval;

try {
// Initial file list
mockListFiles.mockResolvedValueOnce([["/test/workspace/file1.txt"], false]);
const initPromise = workspaceTracker.initializeFilePaths();
await Promise.resolve(); // Let the first promise resolve
await initPromise;

// Verify initial state
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "workspaceUpdated",
filePaths: expect.arrayContaining(["file1.txt"]) // Relative to workspace root
});

// Clear the mock to track new calls
mockProvider.postMessageToWebview.mockClear();

// Mock the next periodic check with absolute paths
mockListFiles.mockResolvedValueOnce([[
"/test/workspace/file1.txt",
"/test/workspace/newfile.txt"
], false]);

// Create new tracker to trigger setInterval with our mock
workspaceTracker = new WorkspaceTracker(mockProvider);

// Let all promises resolve
await Promise.resolve();
await Promise.resolve();

// Log the current state
console.log('mockProvider.postMessageToWebview calls:', mockProvider.postMessageToWebview.mock.calls);

// Verify the new file was detected (paths should be relative to workspace root)
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "workspaceUpdated",
filePaths: expect.arrayContaining(["file1.txt", "newfile.txt"])
});
} finally {
global.setInterval = realSetInterval;
}
});

it("should clean up refresh interval on dispose", () => {
workspaceTracker.dispose()

// Verify all disposables were cleaned up
mockDisposables.forEach(disposable => {
expect(disposable.dispose).toHaveBeenCalled()
})
})

it("should not update webview when no new files are found", async () => {
// Initial file list
mockListFiles.mockResolvedValueOnce([["file1.txt"], false])
await workspaceTracker.initializeFilePaths()

// Clear the mock to track new calls
mockProvider.postMessageToWebview.mockClear()

// Mock the same file list (no changes)
mockListFiles.mockResolvedValueOnce([["file1.txt"], false])

// Advance timers to trigger refresh
jest.advanceTimersByTime(1000)
await Promise.resolve() // Let promises resolve

// Verify no update was sent to webview
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
})
})
1 change: 1 addition & 0 deletions webview-ui/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registry=https://registry.npmjs.org/
Loading