Skip to content

Commit c10b417

Browse files
authored
Add generic file watcher api (#1802)
- Required by #1803 - Required by #1663 ## Checklist - [ ] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [ ] I have not broken the cheatsheet
1 parent 1d4fe7a commit c10b417

File tree

5 files changed

+78
-3
lines changed

5 files changed

+78
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Disposable } from "./ide.types";
2+
3+
export type PathChangeListener = () => void;
4+
5+
export interface FileSystem {
6+
/**
7+
* Recursively watch a directory for changes.
8+
* @param path The path of the directory to watch
9+
* @param onDidChange A function to call on changes
10+
* @returns A disposable to cancel the watcher
11+
*/
12+
watchDir(path: string, onDidChange: PathChangeListener): Disposable;
13+
}

packages/common/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export * from "./ide/types/Events";
2626
export * from "./ide/types/QuickPickOptions";
2727
export * from "./ide/types/events.types";
2828
export * from "./ide/types/Paths";
29+
export * from "./ide/types/FileSystem.types";
2930
export * from "./types/RangeExpansionBehavior";
3031
export * from "./types/InputBoxOptions";
3132
export * from "./types/Position";

packages/cursorless-engine/src/cursorlessEngine.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Command, CommandServerApi, Hats, IDE } from "@cursorless/common";
1+
import {
2+
Command,
3+
CommandServerApi,
4+
FileSystem,
5+
Hats,
6+
IDE,
7+
} from "@cursorless/common";
28
import { StoredTargetMap, TestCaseRecorder, TreeSitter } from ".";
39
import { CursorlessEngine } from "./api/CursorlessEngineApi";
410
import { ScopeProvider } from "./api/ScopeProvider";
@@ -22,6 +28,7 @@ export function createCursorlessEngine(
2228
ide: IDE,
2329
hats: Hats,
2430
commandServerApi: CommandServerApi | null,
31+
fileSystem: FileSystem,
2532
): CursorlessEngine {
2633
injectIde(ide);
2734

packages/cursorless-vscode/src/extension.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
ScopeVisualizerCommandApi,
3838
VisualizationType,
3939
} from "./ScopeVisualizerCommandApi";
40+
import { VscodeFileSystem } from "./ide/vscode/VscodeFileSystem";
4041

4142
/**
4243
* Extension entrypoint called by VSCode on Cursorless startup.
@@ -51,7 +52,7 @@ export async function activate(
5152
): Promise<CursorlessApi> {
5253
const parseTreeApi = await getParseTreeApi();
5354

54-
const { vscodeIDE, hats } = await createVscodeIde(context);
55+
const { vscodeIDE, hats, fileSystem } = await createVscodeIde(context);
5556

5657
const normalizedIde =
5758
vscodeIDE.runMode === "production"
@@ -83,6 +84,7 @@ export async function activate(
8384
normalizedIde ?? vscodeIDE,
8485
hats,
8586
commandServerApi,
87+
fileSystem,
8688
);
8789

8890
const statusBarItem = StatusBarItem.create("cursorless.showQuickPick");
@@ -129,7 +131,7 @@ async function createVscodeIde(context: vscode.ExtensionContext) {
129131
);
130132
await hats.init();
131133

132-
return { vscodeIDE, hats };
134+
return { vscodeIDE, hats, fileSystem: new VscodeFileSystem() };
133135
}
134136

135137
function createTreeSitter(parseTreeApi: ParseTreeApi): TreeSitter {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
Disposable,
3+
FileSystem,
4+
PathChangeListener,
5+
walkFiles,
6+
} from "@cursorless/common";
7+
import { stat } from "fs/promises";
8+
import { max } from "lodash";
9+
10+
export class VscodeFileSystem implements FileSystem {
11+
watchDir(path: string, onDidChange: PathChangeListener): Disposable {
12+
// Just poll for now; we can take advantage of VSCode's sophisticated
13+
// watcher later. Note that we would need to do a version check, as VSCode
14+
// file watcher is only available in more recent versions of VSCode.
15+
return new PollingFileSystemWatcher(path, onDidChange);
16+
}
17+
}
18+
19+
const CHECK_INTERVAL_MS = 1000;
20+
21+
class PollingFileSystemWatcher implements Disposable {
22+
private maxMtimeMs: number = -1;
23+
private timer: NodeJS.Timer;
24+
25+
constructor(
26+
private readonly path: string,
27+
private readonly onDidChange: PathChangeListener,
28+
) {
29+
this.checkForChanges = this.checkForChanges.bind(this);
30+
this.timer = setInterval(this.checkForChanges, CHECK_INTERVAL_MS);
31+
}
32+
33+
private async checkForChanges() {
34+
const paths = await walkFiles(this.path);
35+
36+
const maxMtime =
37+
max(
38+
(await Promise.all(paths.map((file) => stat(file)))).map(
39+
(stat) => stat.mtimeMs,
40+
),
41+
) ?? 0;
42+
43+
if (maxMtime > this.maxMtimeMs) {
44+
this.maxMtimeMs = maxMtime;
45+
this.onDidChange();
46+
}
47+
}
48+
49+
dispose() {
50+
clearInterval(this.timer);
51+
}
52+
}

0 commit comments

Comments
 (0)