Skip to content

Commit 948ce78

Browse files
Copilotsnehara99
andauthored
Make preset reloading less aggressive: gate on configureOnEdit and dirty preset files (#4793)
* Initial plan * Make preset reloading less aggressive: only reapply when configureOnEdit is true and preset files had unsaved changes Co-authored-by: snehara99 <[email protected]> * Restore .npmrc and yarn.lock accidentally modified during build Co-authored-by: snehara99 <[email protected]> * Add clarifying comments for _referencedFiles population in presetsController Co-authored-by: snehara99 <[email protected]> * Add changelog entry for issue #4792 Co-authored-by: snehara99 <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: snehara99 <[email protected]>
1 parent 3c9a3b4 commit 948ce78

3 files changed

Lines changed: 96 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Bug Fixes:
3131
- Fix bookmarked targets running a full build instead of building only the specific target when triggered from the Bookmarks view. [#4771](https://github.com/microsoft/vscode-cmake-tools/issues/4771)
3232
- Fix `cmake.compileFile` command truncating long compile commands at ~1024 characters on macOS. The command is now sent to the terminal in chunks to avoid VS Code terminal buffer limitations. [#4470](https://github.com/microsoft/vscode-cmake-tools/issues/4470)
3333
- Fix configure/build sometimes using stale preset values when unsaved changes to included preset files are auto-saved before configure. The extension now explicitly refreshes presets from disk after saving, instead of relying solely on the asynchronous file watcher. [#4502](https://github.com/microsoft/vscode-cmake-tools/issues/4502)
34+
- Fix overly aggressive preset reloading on every build/debug/launch. Preset reapplication now only runs when `cmake.configureOnEdit` is enabled and preset files had unsaved changes, instead of unconditionally re-reading all preset files. [#4792](https://github.com/microsoft/vscode-cmake-tools/issues/4792)
3435
- Reduce overly verbose logging when CMake configure or build fails. The Output panel no longer floods with duplicated output, and the channel is only revealed on error rather than unconditionally. [#4749](https://github.com/microsoft/vscode-cmake-tools/issues/4749)
3536
- Fix Test Results panel not hyperlinking file paths for GoogleTest failures. The default `cmake.ctest.failurePatterns` now includes a pattern matching GoogleTest's `file:line: Failure` output format. [#4589](https://github.com/microsoft/vscode-cmake-tools/issues/4589)
3637
- Fix wrong path created for artifact when parsing code model using CMake File API. [#3015](https://github.com/microsoft/vscode-cmake-tools/issues/3015)

src/cmakeProject.ts

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,7 +1843,34 @@ export class CMakeProject {
18431843
* Save all open files. "maybe" because the user may have disabled auto-saving
18441844
* with `config.saveBeforeBuild`.
18451845
*/
1846-
async maybeAutoSaveAll(showCommandOnly?: boolean): Promise<boolean> {
1846+
/**
1847+
* Save all open files. "maybe" because the user may have disabled auto-saving
1848+
* with `config.saveBeforeBuild`.
1849+
*
1850+
* When preset files have unsaved changes, this method also:
1851+
* 1. Suppresses the file-watcher to avoid redundant reapplyPresets() calls
1852+
* 2. Saves all open editors
1853+
* 3. Conditionally reapplies presets from disk so the caller sees fresh state
1854+
* 4. Resumes the file-watcher
1855+
*
1856+
* @param showCommandOnly When true, skip the "saving files" log message
1857+
* @param requireConfigureOnEdit When true (default), dirty presets are only
1858+
* reapplied if cmake.configureOnEdit is also enabled. Pass false for
1859+
* explicit-configure paths where the user expects presets to be refreshed
1860+
* regardless of that setting (see #4792).
1861+
*/
1862+
async maybeAutoSaveAll(showCommandOnly?: boolean, requireConfigureOnEdit: boolean = true): Promise<boolean> {
1863+
// Snapshot dirty preset state *before* saving so we can report it back.
1864+
// After saveAll() the dirty flags will be cleared.
1865+
const hadDirtyPresets = this.useCMakePresets && this.haveUnsavedPresetFileChanges();
1866+
1867+
// Suppress watcher-triggered reapply during save — we will explicitly
1868+
// await reapplyPresets() below when needed, so the file-watcher's
1869+
// fire-and-forget call would be redundant (see #4792).
1870+
if (hadDirtyPresets) {
1871+
this.presetsController.suppressWatcherReapply = true;
1872+
}
1873+
18471874
// Save open files before we configure/build
18481875
if (this.workspaceContext.config.saveBeforeBuild) {
18491876
if (!showCommandOnly) {
@@ -1881,28 +1908,58 @@ export class CMakeProject {
18811908
if (chosen?.title === yesAndDoNotShowAgain) {
18821909
await cmakeConfiguration.update(showSaveFailedNotificationString, false, vscode.ConfigurationTarget.Global);
18831910
}
1884-
return chosen !== undefined && (chosen.title === yesButtonTitle || chosen.title === yesAndDoNotShowAgain);
1911+
const saved = chosen !== undefined && (chosen.title === yesButtonTitle || chosen.title === yesAndDoNotShowAgain);
1912+
if (!saved) {
1913+
this.presetsController.suppressWatcherReapply = false;
1914+
return false;
1915+
}
18851916
}
18861917
}
1918+
1919+
// After saving, explicitly refresh presets from disk so that any
1920+
// just-saved changes are picked up before configure/build runs.
1921+
// Without this, the async file-watcher may not have completed yet
1922+
// (see #4502). The configureOnEdit gate is skipped for explicit-
1923+
// configure paths (requireConfigureOnEdit=false) — see #4792.
1924+
if (hadDirtyPresets && (!requireConfigureOnEdit || this.workspaceContext.config.configureOnEdit)) {
1925+
await this.presetsController.reapplyPresets();
1926+
}
1927+
// Resume normal file-watcher behavior now that the explicit reapply
1928+
// (if any) has completed. This is a no-op when hadDirtyPresets was false.
1929+
this.presetsController.suppressWatcherReapply = false;
1930+
18871931
return true;
18881932
}
18891933

1934+
/**
1935+
* Returns true when at least one of the preset files tracked by the
1936+
* presets controller (including files pulled in via "include") is currently
1937+
* dirty (has unsaved changes) in VS Code.
1938+
*/
1939+
private haveUnsavedPresetFileChanges(): boolean {
1940+
const dirtyPaths = new Set(
1941+
vscode.workspace.textDocuments
1942+
.filter(doc => doc.isDirty)
1943+
.map(doc => util.platformNormalizePath(doc.uri.fsPath))
1944+
);
1945+
return this.presetsController.referencedFiles
1946+
.some(f => dirtyPaths.has(util.platformNormalizePath(f)));
1947+
}
1948+
18901949
/**
18911950
* Wraps pre/post configure logic around an actual configure function
18921951
* @param cb The actual configure callback. Called to do the configure
18931952
*/
18941953
private async doConfigure(type: ConfigureType, progress: ProgressHandle, cb: (consumer: CMakeOutputConsumer) => Promise<ConfigureResult>): Promise<ConfigureResult> {
18951954
progress.report({ message: localize('saving.open.files', 'Saving open files') });
1896-
if (!await this.maybeAutoSaveAll(type === ConfigureType.ShowCommandOnly)) {
1955+
// configureOnEdit is intentionally NOT required here. doConfigure()
1956+
// is the explicit configure path — when the user manually triggers
1957+
// configure, we should always pick up just-saved preset changes
1958+
// regardless of configureOnEdit (which only controls *automatic*
1959+
// reconfigure on edit). See #4792.
1960+
if (!await this.maybeAutoSaveAll(type === ConfigureType.ShowCommandOnly, false)) {
18971961
return { exitCode: -1, resultType: ConfigureResultType.Other };
18981962
}
1899-
// After saving files, explicitly refresh presets from disk so that any
1900-
// just-saved changes to preset files (including included files) are picked
1901-
// up before configure runs. Without this, the asynchronous file-watcher
1902-
// may not have re-expanded the presets yet (see #4502).
1903-
if (this.useCMakePresets) {
1904-
await this.presetsController.reapplyPresets();
1905-
}
19061963
if (!this.useCMakePresets) {
19071964
if (!this.activeKit) {
19081965
await vscode.window.showErrorMessage(localize('cannot.configure.no.kit', 'Cannot configure: No kit is active for this CMake project'));
@@ -1989,18 +2046,10 @@ export class CMakeProject {
19892046
if (!drv) {
19902047
return null;
19912048
}
1992-
// First, save open files
2049+
// First, save open files and conditionally reapply dirty presets
19932050
if (!await this.maybeAutoSaveAll()) {
19942051
return { exitCode: -1 };
19952052
}
1996-
// After saving, explicitly refresh presets from disk so that the
1997-
// needsReconfigure check below sees up-to-date preset state.
1998-
// Without this, the async file-watcher's fire-and-forget reapplyPresets()
1999-
// may not have completed yet, causing needsReconfigure() to return false
2000-
// even though preset files just changed on disk (see #4502).
2001-
if (this.useCMakePresets) {
2002-
await this.presetsController.reapplyPresets();
2003-
}
20042053
if (await this.needsReconfigure()) {
20052054
return this.configureInternal(ConfigureTrigger.compilation, [], ConfigureType.Normal, undefined, cancellationToken);
20062055
} else {
@@ -2976,9 +3025,6 @@ export class CMakeProject {
29763025
if (!await this.maybeAutoSaveAll()) {
29773026
return null;
29783027
}
2979-
if (this.useCMakePresets) {
2980-
await this.presetsController.reapplyPresets();
2981-
}
29823028

29833029
// Ensure that we've configured the project already. If we haven't, `getOrSelectLaunchTarget` won't see any
29843030
// executable targets and may show an uneccessary prompt to the user

src/presets/presetsController.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ export class PresetsController implements vscode.Disposable {
2828
private _presetsWatchers: FileWatcher | undefined;
2929
private _sourceDirChangedSub: vscode.Disposable | undefined;
3030
private _isChangingPresets = false;
31+
// Populated by reapplyPresets() with paths of all preset files (CMakePresets.json,
32+
// CMakeUserPresets.json, and any files pulled in via "include").
3133
private _referencedFiles: string[] = [];
3234
private _presetsParser!: PresetsParser; // Using definite assigment (!) because we initialize it in the init method
3335
private _reapplyInProgress: Promise<void> = Promise.resolve();
36+
private _suppressWatcherReapply: boolean = false;
3437

3538
private readonly _presetsChangedEmitter = new vscode.EventEmitter<preset.PresetsFile | undefined>();
3639
private readonly _userPresetsChangedEmitter = new vscode.EventEmitter<preset.PresetsFile | undefined>();
@@ -133,6 +136,21 @@ export class PresetsController implements vscode.Disposable {
133136
return this._presetsParser.userPresetsPath;
134137
}
135138

139+
get referencedFiles(): readonly string[] {
140+
return this._referencedFiles;
141+
}
142+
143+
/**
144+
* When true, the file-watcher's change handler will not call reapplyPresets().
145+
* Set this before saveAll() when the caller will explicitly await reapplyPresets()
146+
* afterward, to avoid redundant re-reads triggered by the OS file-change
147+
* notification for the same save (see #4792).
148+
* Cleared automatically at the end of reapplyPresets().
149+
*/
150+
set suppressWatcherReapply(value: boolean) {
151+
this._suppressWatcherReapply = value;
152+
}
153+
136154
get workspaceFolder() {
137155
return this.project.workspaceFolder;
138156
}
@@ -191,7 +209,8 @@ export class PresetsController implements vscode.Disposable {
191209
this.project.workspaceContext.config.allowUnsupportedPresetsVersions
192210
);
193211

194-
// reset all expanded presets storage.
212+
// Collect the paths of all referenced preset files (main files + includes).
213+
// resetPresetsFiles() populates the referencedFiles map as it parses each file.
195214
this._referencedFiles = Array.from(referencedFiles.keys());
196215

197216
this.project.minCMakeVersion = preset.minCMakeVersion(this.folderPath);
@@ -202,6 +221,10 @@ export class PresetsController implements vscode.Disposable {
202221
// Don't need to set build/test presets here since they are reapplied in setConfigurePreset
203222

204223
await this.watchPresetsChange();
224+
225+
// Clear after completing so that late watcher events from a
226+
// prior saveAll() remain suppressed for the entire reapply.
227+
this._suppressWatcherReapply = false;
205228
};
206229
this._reapplyInProgress = this._reapplyInProgress.then(doReapply, doReapply);
207230
return this._reapplyInProgress;
@@ -1715,7 +1738,9 @@ export class PresetsController implements vscode.Disposable {
17151738
private async watchPresetsChange() {
17161739

17171740
const presetChangeHandler = () => {
1718-
void this.reapplyPresets();
1741+
if (!this._suppressWatcherReapply) {
1742+
void this.reapplyPresets();
1743+
}
17191744
};
17201745
const presetCreatedHandler = () => {
17211746
void this.onCreatePresetsFile();

0 commit comments

Comments
 (0)