Skip to content

Commit 0fdfcea

Browse files
hanniavaleraHannia Valera
andauthored
Migrating File Watching Functionality from Chokidar to VSCode FileSystemWatcher API (#4710)
* migrated vscode fileSystemWatcher and got rid of chokidar * fixing lockfile --------- Co-authored-by: Hannia Valera <[email protected]>
1 parent af543d6 commit 0fdfcea

9 files changed

Lines changed: 215 additions & 190 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Improvements:
1010
- Add MSVC linker error problem matching to the Problems pane. [#4675](https://github.com/microsoft/vscode-cmake-tools/pull/4675) [@bradphelan](https://github.com/bradphelan)
1111

1212
Bug Fixes:
13+
- Fix spurious preset reloads triggered by unrelated file system events such as builds, git commits, and `cmake.copyCompileCommands`. Migrated file watching from chokidar to VS Code's built-in `FileSystemWatcher` API, which also resolves high CPU usage on Apple M1. [#4703](https://github.com/microsoft/vscode-cmake-tools/issues/4703) [#2967](https://github.com/microsoft/vscode-cmake-tools/issues/2967)
1314
- Clarify that semicolons in `cmake.configureSettings` string values are escaped, and array notation should be used for CMake lists. [#4585](https://github.com/microsoft/vscode-cmake-tools/issues/4585)
1415
- Fix "CMake: Quick Start" command failing silently when no folder is open. Now shows an error message with an option to open a folder. [#4504](https://github.com/microsoft/vscode-cmake-tools/issues/4504)
1516
- Fix "CMake: Run Without Debugging" not changing the working directory when the build directory changes. [#4549](https://github.com/microsoft/vscode-cmake-tools/issues/4549)

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4025,7 +4025,6 @@
40254025
"@friedemannsommer/lcov-parser": "^5.0.0",
40264026
"@types/string.prototype.matchall": "^4.0.4",
40274027
"ajv": "^7.1.0",
4028-
"chokidar": "^3.5.1",
40294028
"handlebars": "^4.7.7",
40304029
"iconv-lite": "^0.6.2",
40314030
"js-yaml": "^4.1.1",
@@ -4047,7 +4046,6 @@
40474046
},
40484047
"resolutions": {
40494048
"ansi-regex": "^5.0.1",
4050-
"chokidar": "^3.5.1",
40514049
"glob-parent": "^6.0.2",
40524050
"hosted-git-info": "^3.0.8",
40534051
"**/minimist": "^1.2.5",

src/extension.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
'use strict';
77

8-
import * as chokidar from 'chokidar';
98
import * as path from 'path';
109
import * as vscode from 'vscode';
1110
import * as cpt from 'vscode-cpptools';
@@ -676,7 +675,7 @@ export class ExtensionManager implements vscode.Disposable {
676675
)
677676
);
678677
this.onDidChangeActiveTextEditorSub.dispose();
679-
void this.kitsWatcher.close();
678+
void this.kitsWatcher.dispose();
680679
this.projectOutlineTreeView.dispose();
681680
this.bookmarksTreeView.dispose();
682681
this.extensionActiveCommandsEmitter.dispose();
@@ -997,9 +996,16 @@ export class ExtensionManager implements vscode.Disposable {
997996
/**
998997
* Watches for changes to the kits file
999998
*/
1000-
private readonly kitsWatcher = util.chokidarOnAnyChange(
1001-
chokidar.watch(USER_KITS_FILEPATH, { ignoreInitial: true }),
1002-
_ => rollbar.takePromise(localize('rereading.kits', 'Re-reading kits'), {}, KitsController.readUserKits(this.getActiveProject())));
999+
private readonly kitsWatcher: vscode.FileSystemWatcher = (() => {
1000+
const dirUri = vscode.Uri.file(path.dirname(USER_KITS_FILEPATH));
1001+
const pattern = new vscode.RelativePattern(dirUri, path.basename(USER_KITS_FILEPATH));
1002+
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
1003+
const rereadHandler = () => rollbar.takePromise(localize('rereading.kits', 'Re-reading kits'), {}, KitsController.readUserKits(this.getActiveProject()));
1004+
watcher.onDidChange(rereadHandler);
1005+
watcher.onDidCreate(rereadHandler);
1006+
watcher.onDidDelete(rereadHandler);
1007+
return watcher;
1008+
})();
10031009

10041010
/**
10051011
* Opens a text editor with the user-local `cmake-kits.json` file.

src/kits/kitsController.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict';
22

3-
import * as chokidar from 'chokidar';
43
import * as path from 'path';
54
import * as vscode from 'vscode';
65
import * as nls from 'vscode-nls';
@@ -22,7 +21,7 @@ import * as logging from '@cmt/logging';
2221
import paths from '@cmt/paths';
2322
import { fs } from '@cmt/pr';
2423
import rollbar from '@cmt/rollbar';
25-
import { chokidarOnAnyChange, ProgressHandle, reportProgress } from '@cmt/util';
24+
import { ProgressHandle, reportProgress } from '@cmt/util';
2625
import { ConfigurationType } from 'vscode-cmake-tools';
2726

2827
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
@@ -57,7 +56,7 @@ export class KitsController {
5756
folderKits: Kit[] = [];
5857
additionalKits: Kit[] = [];
5958

60-
private constructor(readonly project: CMakeProject, private readonly _kitsWatcher: chokidar.FSWatcher) {}
59+
private constructor(readonly project: CMakeProject, private readonly _kitsWatchers: vscode.Disposable[]) {}
6160

6261
static async init(project: CMakeProject) {
6362
if (KitsController.userKits.length === 0) {
@@ -67,10 +66,20 @@ export class KitsController {
6766

6867
const expandedAdditionalKitFiles: string[] = await project.getExpandedAdditionalKitFiles();
6968
const folderKitsFiles: string[] = [KitsController._workspaceKitsPath(project.workspaceFolder)].concat(expandedAdditionalKitFiles);
70-
const kitsWatcher = chokidar.watch(folderKitsFiles, { ignoreInitial: true, followSymlinks: false });
71-
const kitsController = new KitsController(project, kitsWatcher);
72-
chokidarOnAnyChange(kitsWatcher, _ => rollbar.takePromise(localize('rereading.kits', 'Re-reading folder kits'), {},
73-
kitsController.readKits(KitsReadMode.folderKits)));
69+
70+
const kitsWatchers: vscode.Disposable[] = [];
71+
const kitsController = new KitsController(project, kitsWatchers);
72+
const rereadHandler = () => rollbar.takePromise(localize('rereading.kits', 'Re-reading folder kits'), {},
73+
kitsController.readKits(KitsReadMode.folderKits));
74+
for (const kitFile of folderKitsFiles) {
75+
const dirUri = vscode.Uri.file(path.dirname(kitFile));
76+
const pattern = new vscode.RelativePattern(dirUri, path.basename(kitFile));
77+
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
78+
watcher.onDidChange(rereadHandler);
79+
watcher.onDidCreate(rereadHandler);
80+
watcher.onDidDelete(rereadHandler);
81+
kitsWatchers.push(watcher);
82+
}
7483
project.workspaceContext.config.onChange('additionalKits', () => kitsController.readKits(KitsReadMode.folderKits));
7584

7685
await kitsController.readKits(KitsReadMode.folderKits);
@@ -81,7 +90,9 @@ export class KitsController {
8190
if (this._pickKitCancellationTokenSource) {
8291
this._pickKitCancellationTokenSource.dispose();
8392
}
84-
void this._kitsWatcher.close();
93+
for (const watcher of this._kitsWatchers) {
94+
watcher.dispose();
95+
}
8596
}
8697

8798
get availableKits() {

src/kits/variant.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as ajv from 'ajv';
2-
import * as chokidar from 'chokidar';
32
import * as yaml from 'js-yaml';
43
import * as json5 from 'json5';
54
import * as path from 'path';
@@ -192,7 +191,7 @@ export class VariantManager implements vscode.Disposable {
192191
/**
193192
* Watches for changes to the variants file on the filesystem
194193
*/
195-
private readonly _variantFileWatcher = chokidar.watch([], { ignoreInitial: true, followSymlinks: false });
194+
private readonly _variantFileWatchers: vscode.Disposable[] = [];
196195
private customVariantsFileExists: boolean = false;
197196

198197
/**
@@ -201,7 +200,9 @@ export class VariantManager implements vscode.Disposable {
201200
private initialized: boolean = false;
202201

203202
dispose() {
204-
void this._variantFileWatcher.close();
203+
for (const watcher of this._variantFileWatchers) {
204+
watcher.dispose();
205+
}
205206
this._activeVariantChanged.dispose();
206207
}
207208

@@ -215,15 +216,19 @@ export class VariantManager implements vscode.Disposable {
215216
return; // Nothing we can do. We have no directory open
216217
}
217218
// Ref: https://code.visualstudio.com/api/references/vscode-api#Uri
218-
for (const filename of ['cmake-variants.yaml',
219-
'cmake-variants.json',
220-
'.vscode/cmake-variants.yaml',
221-
'.vscode/cmake-variants.json']) {
222-
this._variantFileWatcher.add(path.join(workspaceFolder.uri.fsPath, filename));
223-
}
224-
util.chokidarOnAnyChange(
225-
this._variantFileWatcher,
226-
filePath => rollbar.invokeAsync(localize('reloading.variants.file', 'Reloading variants file {0}', filePath), () => this._reloadVariantsFile(filePath)));
219+
const variantGlob = new vscode.RelativePattern(
220+
workspaceFolder,
221+
'{cmake-variants.yaml,cmake-variants.json,.vscode/cmake-variants.yaml,.vscode/cmake-variants.json}'
222+
);
223+
const variantWatcher = vscode.workspace.createFileSystemWatcher(variantGlob);
224+
const reloadHandler = (uri: vscode.Uri) => rollbar.invokeAsync(
225+
localize('reloading.variants.file', 'Reloading variants file {0}', uri.fsPath),
226+
() => this._reloadVariantsFile(uri.fsPath)
227+
);
228+
variantWatcher.onDidChange(reloadHandler);
229+
variantWatcher.onDidCreate(reloadHandler);
230+
variantWatcher.onDidDelete(reloadHandler);
231+
this._variantFileWatchers.push(variantWatcher);
227232

228233
config.onChange('defaultVariants', () => {
229234
rollbar.invokeAsync(localize('reloading.variants.from.settings', 'Reloading variants from settings'), () => this._reloadVariantsFile());

src/presets/presetsController.ts

Lines changed: 30 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as chokidar from 'chokidar';
21
import * as path from 'path';
32
import * as vscode from 'vscode';
43
import * as nls from 'vscode-nls';
@@ -1605,14 +1604,8 @@ export class PresetsController implements vscode.Disposable {
16051604
void this.onCreatePresetsFile();
16061605
};
16071606

1608-
const events: Map<string, () => void> = new Map<string, () => void>([
1609-
["change", presetChangeHandler],
1610-
["unlink", presetChangeHandler],
1611-
["add", presetCreatedHandler]
1612-
]);
1613-
16141607
this._presetsWatchers?.dispose();
1615-
this._presetsWatchers = new FileWatcher(this._referencedFiles, events, { ignoreInitial: true, followSymlinks: false });
1608+
this._presetsWatchers = new FileWatcher(this._referencedFiles, presetChangeHandler, presetCreatedHandler);
16161609
};
16171610

16181611
dispose() {
@@ -1625,72 +1618,47 @@ export class PresetsController implements vscode.Disposable {
16251618
}
16261619

16271620
/**
1628-
* FileWatcher is a wrapper around chokidar's FSWatcher that allows for watching multiple paths.
1629-
* Chokidar's support for watching multiple paths is currently broken, if it is fixed in the future, this class can be removed.
1621+
* FileWatcher watches an array of specific file paths using VS Code's built-in
1622+
* vscode.workspace.createFileSystemWatcher API. This replaces the previous chokidar-based
1623+
* implementation to eliminate spurious preset reloads triggered by unrelated file system
1624+
* events (builds, git commits, copyCompileCommands). See issues #4703 and #2967.
16301625
*/
16311626
class FileWatcher implements vscode.Disposable {
1632-
private watchers: Map<string, chokidar.FSWatcher>;
1633-
// Debounce the change handler to avoid multiple changes being triggered by a single file change. Two change events are coming in rapid succession without this.
1627+
private watchers: vscode.Disposable[] = [];
1628+
// Debounce the change handler to avoid multiple changes being triggered by a single file change.
1629+
// Two change events are coming in rapid succession without this.
16341630
private canRunChangeHandler = true;
1635-
// Grace period flag to ignore events during watcher startup. When followSymlinks is false and
1636-
// watched files are symlinks, chokidar may emit spurious events during initialization that
1637-
// bypass ignoreInitial. This prevents infinite loops when reapplyPresets() recreates the watcher.
1638-
// See issue #4668.
1639-
private isInStartupGracePeriod = true;
1640-
1641-
public constructor(paths: string | string[], eventHandlers: Map<string, () => void>, options?: chokidar.WatchOptions) {
1642-
this.watchers = new Map<string, chokidar.FSWatcher>();
1643-
1644-
// Allow a short grace period for the watcher to stabilize before processing events.
1645-
// This handles the case where symlinks cause spurious events during watcher setup.
1646-
// See issue #4668.
1647-
setTimeout(() => (this.isInStartupGracePeriod = false), 100);
1648-
1649-
// Wrap all event handlers to respect the startup grace period
1650-
const wrappedHandlers = new Map<string, () => void>();
1651-
for (const [event, handler] of eventHandlers) {
1652-
if (event === 'change') {
1653-
// Change events get additional debouncing to avoid multiple changes
1654-
// being triggered by a single file change
1655-
const debouncedOnChange = () => {
1656-
if (this.isInStartupGracePeriod) {
1657-
return; // Ignore events during startup grace period
1658-
}
1659-
if (this.canRunChangeHandler) {
1660-
handler();
1661-
this.canRunChangeHandler = false;
1662-
setTimeout(() => (this.canRunChangeHandler = true), 500);
1663-
}
1664-
};
1665-
wrappedHandlers.set(event, debouncedOnChange);
1666-
} else {
1667-
// Other events just respect the grace period
1668-
const wrappedHandler = () => {
1669-
if (this.isInStartupGracePeriod) {
1670-
return; // Ignore events during startup grace period
1671-
}
1672-
handler();
1673-
};
1674-
wrappedHandlers.set(event, wrappedHandler);
1631+
1632+
public constructor(filePaths: string[], changeHandler: () => void, createHandler: () => void) {
1633+
const debouncedOnChange = () => {
1634+
if (this.canRunChangeHandler) {
1635+
changeHandler();
1636+
this.canRunChangeHandler = false;
1637+
setTimeout(() => (this.canRunChangeHandler = true), 500);
16751638
}
1676-
}
1639+
};
16771640

1678-
for (const path of Array.isArray(paths) ? paths : [paths]) {
1641+
for (const filePath of filePaths) {
16791642
try {
1680-
const watcher = chokidar.watch(path, { ...options });
1681-
for (const [event, handler] of wrappedHandlers) {
1682-
watcher.on(event, handler);
1683-
}
1684-
this.watchers.set(path, watcher);
1643+
const dirUri = vscode.Uri.file(path.dirname(filePath));
1644+
const pattern = new vscode.RelativePattern(dirUri, path.basename(filePath));
1645+
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
1646+
1647+
watcher.onDidChange(debouncedOnChange);
1648+
watcher.onDidCreate(createHandler);
1649+
watcher.onDidDelete(debouncedOnChange);
1650+
1651+
this.watchers.push(watcher);
16851652
} catch (error) {
1686-
log.error(localize('failed.to.watch', 'Watcher could not be created for {0}: {1}', path, util.errorToString(error)));
1653+
log.error(localize('failed.to.watch', 'Watcher could not be created for {0}: {1}', filePath, util.errorToString(error)));
16871654
}
16881655
}
16891656
}
16901657

16911658
public dispose() {
1692-
for (const watcher of this.watchers.values()) {
1693-
watcher.close().then(() => {}, () => {});
1659+
for (const watcher of this.watchers) {
1660+
watcher.dispose();
16941661
}
1662+
this.watchers = [];
16951663
}
16961664
}

src/util.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as child_process from 'child_process';
2-
import * as chokidar from 'chokidar';
32
import * as fs from 'fs';
43
import * as path from 'path';
54
import * as vscode from 'vscode';
@@ -1013,18 +1012,6 @@ export function reportProgress(message: string, progress?: ProgressHandle) {
10131012
}
10141013
}
10151014

1016-
/**
1017-
* Sets up a chokidar watcher to listen for any file changes (add, change, unlink).
1018-
* @param watcher The chokidar FSWatcher instance.
1019-
* @param listener The listener function to call on file changes.
1020-
* @returns The chokidar FSWatcher instance with the listener attached.
1021-
*/
1022-
export function chokidarOnAnyChange(watcher: chokidar.FSWatcher, listener: (path: string, stats?: fs.Stats | undefined) => void) {
1023-
return watcher.on('add', listener)
1024-
.on('change', listener)
1025-
.on('unlink', listener);
1026-
}
1027-
10281015
/**
10291016
* Checks if the given value is a string.
10301017
* @param x The value to check.

0 commit comments

Comments
 (0)