Skip to content

Commit 17b5e00

Browse files
Add FilePathDiscovery
1 parent 790c33c commit 17b5e00

3 files changed

Lines changed: 656 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { Discovery } from "../discovery";
2+
import {
3+
EventEmitter,
4+
RelativePattern,
5+
Uri,
6+
WorkspaceFoldersChangeEvent,
7+
workspace,
8+
} from "vscode";
9+
import { MultiFileSystemWatcher } from "./multi-file-system-watcher";
10+
import { AppEventEmitter } from "../events";
11+
import { extLogger } from "..";
12+
import { FilePathSet } from "../file-path-set";
13+
import { exists, lstat } from "fs-extra";
14+
import { containsPath } from "../../pure/files";
15+
import { getOnDiskWorkspaceFoldersObjects } from "./workspace-folders";
16+
17+
interface PathData {
18+
path: string;
19+
}
20+
21+
/**
22+
* Discovers all files matching a given filter contained in the workspace.
23+
*
24+
* Scans the whole workspace on startup, and then watches for changes to files
25+
* to do the minimum work to keep up with changes.
26+
*
27+
* Can configure which changes it watches for, which files are considered
28+
* relevant, and what extra data to compute for each file.
29+
*/
30+
export abstract class FilePathDiscovery<T extends PathData> extends Discovery {
31+
/** The set of known paths we are tracking */
32+
protected paths: T[] = [];
33+
protected readonly onDidChangePathsEmitter: AppEventEmitter<void>;
34+
35+
private readonly changedFilePaths = new FilePathSet();
36+
private readonly watcher: MultiFileSystemWatcher = this.push(
37+
new MultiFileSystemWatcher(),
38+
);
39+
40+
/**
41+
* @param name Name of the discovery operation, for logging purposes.
42+
* @param fileWatchPattern Passed to `vscode.RelativePattern` to determine the files to watch for changes to.
43+
*/
44+
constructor(name: string, private readonly fileWatchPattern: string) {
45+
super(name, extLogger);
46+
47+
this.onDidChangePathsEmitter = this.push(new EventEmitter<void>());
48+
this.push(
49+
workspace.onDidChangeWorkspaceFolders(
50+
this.workspaceFoldersChanged.bind(this),
51+
),
52+
);
53+
this.push(this.watcher.onDidChange(this.fileChanged.bind(this)));
54+
}
55+
56+
/**
57+
* Compute any extra data to be stored regarding the given path.
58+
*/
59+
protected abstract getDataForPath(path: string): Promise<T>;
60+
61+
/**
62+
* Is the given path relevant to this discovery operation?
63+
*/
64+
protected abstract pathIsRelevant(path: string): boolean;
65+
66+
/**
67+
* Should the given new data overwrite the existing data we have stored?
68+
*/
69+
protected abstract shouldOverwriteExistingData(
70+
newData: T,
71+
existingData: T,
72+
): boolean;
73+
74+
/**
75+
* Do the initial scan of the entire workspace and set up watchers for future changes.
76+
*/
77+
public async initialRefresh() {
78+
getOnDiskWorkspaceFoldersObjects().forEach((workspaceFolder) => {
79+
this.changedFilePaths.addPath(workspaceFolder.uri.fsPath);
80+
});
81+
82+
this.updateWatchers();
83+
return this.refresh();
84+
}
85+
86+
private workspaceFoldersChanged(event: WorkspaceFoldersChangeEvent) {
87+
event.added.forEach((workspaceFolder) => {
88+
this.changedFilePaths.addPath(workspaceFolder.uri.fsPath);
89+
});
90+
event.removed.forEach((workspaceFolder) => {
91+
this.changedFilePaths.addPath(workspaceFolder.uri.fsPath);
92+
});
93+
94+
this.updateWatchers();
95+
void this.refresh();
96+
}
97+
98+
private updateWatchers() {
99+
this.watcher.clear();
100+
for (const workspaceFolder of getOnDiskWorkspaceFoldersObjects()) {
101+
// Watch for changes to individual files
102+
this.watcher.addWatch(
103+
new RelativePattern(workspaceFolder, this.fileWatchPattern),
104+
);
105+
// need to explicitly watch for changes to directories themselves.
106+
this.watcher.addWatch(new RelativePattern(workspaceFolder, "**/"));
107+
}
108+
}
109+
110+
private fileChanged(uri: Uri) {
111+
this.changedFilePaths.addPath(uri.fsPath);
112+
void this.refresh();
113+
}
114+
115+
protected async discover() {
116+
let pathsUpdated = false;
117+
let path: string | undefined;
118+
while ((path = this.changedFilePaths.popPath()) !== undefined) {
119+
if (await this.handledChangedPath(path)) {
120+
pathsUpdated = true;
121+
}
122+
}
123+
124+
if (pathsUpdated) {
125+
this.onDidChangePathsEmitter.fire();
126+
}
127+
}
128+
129+
private async handledChangedPath(path: string): Promise<boolean> {
130+
if (!(await exists(path)) || !this.pathIsInWorkspace(path)) {
131+
return this.handledRemovedPath(path);
132+
}
133+
if ((await lstat(path)).isDirectory()) {
134+
return await this.handleChangedDirectory(path);
135+
}
136+
return this.handleChangedFile(path);
137+
}
138+
139+
private pathIsInWorkspace(path: string): boolean {
140+
return getOnDiskWorkspaceFoldersObjects().some((workspaceFolder) =>
141+
containsPath(workspaceFolder.uri.fsPath, path),
142+
);
143+
}
144+
145+
private handledRemovedPath(path: string): boolean {
146+
const oldLength = this.paths.length;
147+
this.paths = this.paths.filter((q) => !containsPath(path, q.path));
148+
return this.paths.length !== oldLength;
149+
}
150+
151+
private async handleChangedDirectory(path: string): Promise<boolean> {
152+
const newPaths = await workspace.findFiles(
153+
new RelativePattern(path, this.fileWatchPattern),
154+
);
155+
156+
let pathsUpdated = false;
157+
for (const path of newPaths) {
158+
if (await this.addOrUpdatePath(path.fsPath)) {
159+
pathsUpdated = true;
160+
}
161+
}
162+
return pathsUpdated;
163+
}
164+
165+
private async handleChangedFile(path: string): Promise<boolean> {
166+
if (this.pathIsRelevant(path)) {
167+
return await this.addOrUpdatePath(path);
168+
} else {
169+
return false;
170+
}
171+
}
172+
173+
private async addOrUpdatePath(path: string): Promise<boolean> {
174+
const data = await this.getDataForPath(path);
175+
const existingDataIndex = this.paths.findIndex((x) => x.path === path);
176+
if (existingDataIndex !== -1) {
177+
if (
178+
this.shouldOverwriteExistingData(data, this.paths[existingDataIndex])
179+
) {
180+
this.paths.splice(existingDataIndex, 1, data);
181+
return true;
182+
} else {
183+
return false;
184+
}
185+
} else {
186+
this.paths.push(data);
187+
return true;
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)