Skip to content

Commit 2960abf

Browse files
Copiloteleanorjboyd
andcommitted
Implement file system watchers for project moves and deletions
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent d86d894 commit 2960abf

5 files changed

Lines changed: 188 additions & 1 deletion

File tree

src/common/workspace.apis.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ConfigurationScope,
66
Disposable,
77
FileDeleteEvent,
8+
FileRenameEvent,
89
FileSystemWatcher,
910
GlobPattern,
1011
Uri,
@@ -63,3 +64,11 @@ export function onDidDeleteFiles(
6364
): Disposable {
6465
return workspace.onDidDeleteFiles(listener, thisArgs, disposables);
6566
}
67+
68+
export function onDidRenameFiles(
69+
listener: (e: FileRenameEvent) => any,
70+
thisArgs?: any,
71+
disposables?: Disposable[],
72+
): Disposable {
73+
return workspace.onDidRenameFiles(listener, thisArgs, disposables);
74+
}

src/features/projectManager.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,35 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
170170
this._onDidChangeProjects.fire(Array.from(this._projects.values()));
171171
}
172172

173+
updateProjectUri(oldUri: Uri, newUri: Uri): void {
174+
const oldKey = oldUri.toString();
175+
const project = this._projects.get(oldKey);
176+
177+
if (!project) {
178+
return;
179+
}
180+
181+
// Remove the project with the old URI
182+
this._projects.delete(oldKey);
183+
184+
// Create a new project instance with the updated URI
185+
const updatedProject = this.create(
186+
path.basename(newUri.fsPath),
187+
newUri,
188+
{
189+
description: project.description,
190+
tooltip: project.tooltip,
191+
iconPath: (project as PythonProjectsImpl).iconPath, // Cast to implementation to access iconPath
192+
}
193+
);
194+
195+
// Add the updated project
196+
this._projects.set(newUri.toString(), updatedProject);
197+
198+
// Fire the change event to update the view
199+
this._onDidChangeProjects.fire(Array.from(this._projects.values()));
200+
}
201+
173202
getProjects(uris?: Uri[]): ReadonlyArray<PythonProject> {
174203
if (uris === undefined) {
175204
return Array.from(this._projects.values());

src/features/views/projectView.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { PythonEnvironment } from '../../api';
1313
import { ProjectViews } from '../../common/localize';
1414
import { createSimpleDebounce } from '../../common/utils/debounce';
15-
import { onDidChangeConfiguration } from '../../common/workspace.apis';
15+
import { onDidChangeConfiguration, onDidDeleteFiles, onDidRenameFiles } from '../../common/workspace.apis';
1616
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
1717
import {
1818
GlobalProjectItem,
@@ -68,6 +68,12 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
6868
this.debouncedUpdateProject.trigger();
6969
}
7070
}),
71+
onDidRenameFiles((e) => {
72+
this.handleFileRenames(e);
73+
}),
74+
onDidDeleteFiles((e) => {
75+
this.handleFileDeletions(e);
76+
}),
7177
);
7278
}
7379

@@ -224,6 +230,60 @@ export class ProjectView implements TreeDataProvider<ProjectTreeItem> {
224230
return element.parent;
225231
}
226232

233+
private handleFileRenames(e: { readonly files: ReadonlyArray<{ readonly oldUri: Uri; readonly newUri: Uri }> }): void {
234+
const projects = this.projectManager.getProjects();
235+
236+
for (const { oldUri, newUri } of e.files) {
237+
// Check if any project matches the old URI exactly or is contained within it
238+
const affectedProjects = projects.filter(project => {
239+
const projectPath = project.uri.fsPath;
240+
const oldPath = oldUri.fsPath;
241+
242+
// Check if the project path is the same as or is a child of the renamed path
243+
return projectPath === oldPath || projectPath.startsWith(oldPath + '/') || projectPath.startsWith(oldPath + '\\');
244+
});
245+
246+
for (const project of affectedProjects) {
247+
const projectPath = project.uri.fsPath;
248+
const oldPath = oldUri.fsPath;
249+
const newPath = newUri.fsPath;
250+
251+
// Calculate the new project path
252+
let newProjectPath: string;
253+
if (projectPath === oldPath) {
254+
// Project path is exactly the renamed path
255+
newProjectPath = newPath;
256+
} else {
257+
// Project path is a child of the renamed path
258+
const relativePath = projectPath.substring(oldPath.length);
259+
newProjectPath = newPath + relativePath;
260+
}
261+
262+
const newProjectUri = Uri.file(newProjectPath);
263+
this.projectManager.updateProjectUri(project.uri, newProjectUri);
264+
}
265+
}
266+
}
267+
268+
private handleFileDeletions(e: { readonly files: ReadonlyArray<Uri> }): void {
269+
const projects = this.projectManager.getProjects();
270+
271+
for (const deletedUri of e.files) {
272+
// Check if any project matches the deleted URI exactly or is contained within it
273+
const affectedProjects = projects.filter(project => {
274+
const projectPath = project.uri.fsPath;
275+
const deletedPath = deletedUri.fsPath;
276+
277+
// Check if the project path is the same as or is a child of the deleted path
278+
return projectPath === deletedPath || projectPath.startsWith(deletedPath + '/') || projectPath.startsWith(deletedPath + '\\');
279+
});
280+
281+
if (affectedProjects.length > 0) {
282+
this.projectManager.remove(affectedProjects);
283+
}
284+
}
285+
}
286+
227287
dispose() {
228288
this.disposables.forEach((d) => d.dispose());
229289
}

src/internal.api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export interface PythonProjectManager extends Disposable {
287287
): PythonProject;
288288
add(pyWorkspace: PythonProject | PythonProject[]): Promise<void>;
289289
remove(pyWorkspace: PythonProject | PythonProject[]): void;
290+
updateProjectUri(oldUri: Uri, newUri: Uri): void;
290291
getProjects(uris?: Uri[]): ReadonlyArray<PythonProject>;
291292
get(uri: Uri): PythonProject | undefined;
292293
onDidChangeProjects: Event<PythonProject[] | undefined>;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import assert from 'assert';
2+
import { Uri } from 'vscode';
3+
import { PythonProjectManagerImpl } from '../../features/projectManager';
4+
5+
suite('Project Manager Update URI tests', () => {
6+
let projectManager: PythonProjectManagerImpl;
7+
8+
setup(() => {
9+
projectManager = new PythonProjectManagerImpl();
10+
});
11+
12+
teardown(() => {
13+
projectManager.dispose();
14+
});
15+
16+
test('updateProjectUri should update existing project URI', () => {
17+
const oldUri = Uri.file('/path/to/old/project');
18+
const newUri = Uri.file('/path/to/new/project');
19+
20+
// Create a project and manually add it to the internal map to bypass the complex add method
21+
const project = projectManager.create('TestProject', oldUri, {
22+
description: 'Test project',
23+
tooltip: 'Test tooltip'
24+
});
25+
26+
// Access private _projects map to manually add the project for testing
27+
(projectManager as any)._projects.set(oldUri.toString(), project);
28+
29+
// Verify project exists with old URI
30+
const oldProject = projectManager.get(oldUri);
31+
assert.ok(oldProject, 'Project should exist with old URI');
32+
assert.equal(oldProject.uri.fsPath, oldUri.fsPath, 'Old URI should match');
33+
34+
// Update the project URI
35+
projectManager.updateProjectUri(oldUri, newUri);
36+
37+
// Verify project no longer exists with old URI
38+
const oldProjectAfterUpdate = projectManager.get(oldUri);
39+
assert.equal(oldProjectAfterUpdate, undefined, 'Project should not exist with old URI after update');
40+
41+
// Verify project exists with new URI
42+
const newProject = projectManager.get(newUri);
43+
assert.ok(newProject, 'Project should exist with new URI');
44+
assert.equal(newProject.uri.fsPath, newUri.fsPath, 'New URI should match');
45+
assert.equal(newProject.name, 'project', 'Project name should be based on new path');
46+
assert.equal(newProject.description, 'Test project', 'Description should be preserved');
47+
assert.equal(newProject.tooltip, 'Test tooltip', 'Tooltip should be preserved');
48+
});
49+
50+
test('updateProjectUri should handle non-existent project gracefully', () => {
51+
const oldUri = Uri.file('/path/to/nonexistent/project');
52+
const newUri = Uri.file('/path/to/new/project');
53+
54+
// Try to update a project that doesn't exist
55+
// This should not throw an error
56+
assert.doesNotThrow(() => {
57+
projectManager.updateProjectUri(oldUri, newUri);
58+
}, 'Should handle non-existent project gracefully');
59+
60+
// Verify no project was created
61+
const newProject = projectManager.get(newUri);
62+
assert.equal(newProject, undefined, 'No project should be created for non-existent old project');
63+
});
64+
65+
test('remove should remove multiple projects', () => {
66+
const project1Uri = Uri.file('/path/to/project1');
67+
const project2Uri = Uri.file('/path/to/project2');
68+
69+
// Create projects and manually add them to the internal map
70+
const project1 = projectManager.create('Project1', project1Uri);
71+
const project2 = projectManager.create('Project2', project2Uri);
72+
73+
// Access private _projects map to manually add projects for testing
74+
(projectManager as any)._projects.set(project1Uri.toString(), project1);
75+
(projectManager as any)._projects.set(project2Uri.toString(), project2);
76+
77+
// Verify both projects exist
78+
assert.ok(projectManager.get(project1Uri), 'Project1 should exist');
79+
assert.ok(projectManager.get(project2Uri), 'Project2 should exist');
80+
81+
// Remove both projects
82+
projectManager.remove([project1, project2]);
83+
84+
// Verify both projects are removed
85+
assert.equal(projectManager.get(project1Uri), undefined, 'Project1 should be removed');
86+
assert.equal(projectManager.get(project2Uri), undefined, 'Project2 should be removed');
87+
});
88+
});

0 commit comments

Comments
 (0)