diff --git a/CHANGELOG.md b/CHANGELOG.md index fa059eb03..51cdfa62c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Improvements: - Make it easier for a new developer of CMake Tools to run tests. [#4620](https://github.com/microsoft/vscode-cmake-tools/pull/4620) [@cwalther](https://github.com/cwalther) Bug Fixes: +- Fix cache edits from "Edit Cache (UI)" being overwritten in presets mode when the edited variable is defined by the active configure preset. The extension now writes an override into CMakeUserPresets.json and reconfigures with that user preset so the change persists. - Fix stale C/C++ custom-configuration entries persisting after reconfigure/preset switches, which could cause Go to Definition/IntelliSense to surface symbols from inactive sources in the same folder. [#4472](https://github.com/microsoft/vscode-cmake-tools/issues/4472) - Fix IntelliSense not updating when switching the active project in multi-project workspaces with multiple `cmake.sourceDirectory` entries. [#4390](https://github.com/microsoft/vscode-cmake-tools/issues/4390) - Fix `tasks.json` schema validation rejecting valid CMake task commands `package` and `workflow`. [#4167](https://github.com/microsoft/vscode-cmake-tools/issues/4167) diff --git a/src/cmakeProject.ts b/src/cmakeProject.ts index b3fbd62eb..bab49c339 100644 --- a/src/cmakeProject.ts +++ b/src/cmakeProject.ts @@ -39,7 +39,7 @@ import rollbar from '@cmt/rollbar'; import * as telemetry from '@cmt/telemetry'; import { VariantManager } from '@cmt/kits/variant'; import * as nls from 'vscode-nls'; -import { ConfigurationWebview } from '@cmt/ui/cacheView'; +import { ConfigurationWebview, IOption } from '@cmt/ui/cacheView'; import { enableFullFeatureSet, extensionManager, updateFullFeatureSet, setContextAndStore } from '@cmt/extension'; import { CMakeCommunicationMode, ConfigurationReader, OptionConfig, UseCMakePresets, checkConfigureOverridesPresent } from '@cmt/config'; import * as preset from '@cmt/presets/preset'; @@ -2461,6 +2461,192 @@ export class CMakeProject { .then(doc => void vscode.window.showTextDocument(doc)); } + private getPresetCacheVariableType(option: IOption, presetCacheVariable: preset.CacheVarType | undefined): string { + if (option.type === 'Bool') { + return 'BOOL'; + } + + if (presetCacheVariable && typeof presetCacheVariable === 'object' && 'type' in presetCacheVariable && util.isString((presetCacheVariable as any).type) && (presetCacheVariable as any).type.toUpperCase() !== 'BOOL') { + return (presetCacheVariable as any).type; + } + + return 'STRING'; + } + + private getPresetCacheVariableValue(option: IOption): string { + return util.isBoolean(option.value) ? (option.value ? 'TRUE' : 'FALSE') : String(option.value); + } + + private getPresetCacheVariableRawValue(cacheVariable: preset.CacheVarType | undefined): string | undefined { + if (util.isString(cacheVariable)) { + return cacheVariable; + } + + if (cacheVariable === true) { + return 'TRUE'; + } + + if (cacheVariable && typeof cacheVariable === 'object' && 'value' in cacheVariable) { + const value = cacheVariable.value; + if (util.isBoolean(value)) { + return value ? 'TRUE' : 'FALSE'; + } + if (util.isString(value)) { + return value; + } + } + + return undefined; + } + + private findPresetDefinitionByName(name: string): preset.ConfigurePreset | undefined { + const userPreset = preset.userConfigurePresets(this.folderPath, true).find(configurePreset => configurePreset.name === name); + if (userPreset) { + return userPreset; + } + return preset.configurePresets(this.folderPath, true).find(configurePreset => configurePreset.name === name); + } + + private resolvePresetVariableOrigin( + presetName: string, + variableName: string, + visited: Set = new Set() + ): { presetName: string; isUserPreset: boolean; presetValue?: string } | undefined { + if (visited.has(presetName)) { + return undefined; + } + visited.add(presetName); + + const configurePreset = this.findPresetDefinitionByName(presetName); + if (!configurePreset) { + return undefined; + } + + const cacheVariable = configurePreset.cacheVariables?.[variableName]; + if (cacheVariable !== undefined) { + const isUserPreset = configurePreset.isUserPreset ?? configurePreset.__file?.__path?.endsWith('CMakeUserPresets.json') ?? false; + return { + presetName: configurePreset.name, + isUserPreset, + presetValue: this.getPresetCacheVariableRawValue(cacheVariable) + }; + } + + if (configurePreset.inherits) { + const parents = util.isString(configurePreset.inherits) ? [configurePreset.inherits] : configurePreset.inherits; + if (parents) { + for (const parent of parents) { + const origin = this.resolvePresetVariableOrigin(parent, variableName, visited); + if (origin) { + return origin; + } + } + } + } + + return undefined; + } + + private async getCacheOptionPresetMetadata(options: IOption[]): Promise> { + const metadata = new Map(); + if (!this.useCMakePresets || !this.configurePreset) { + return metadata; + } + + const activePresetName = this.configurePreset.name; + for (const option of options) { + const origin = this.resolvePresetVariableOrigin(activePresetName, option.key); + if (!origin) { + continue; + } + + const sourceKind = origin.isUserPreset + ? localize('preset.source.user', 'User preset') + : localize('preset.source.project', 'Project preset'); + + let differsFromPresetValue: boolean | undefined; + if (origin.presetValue !== undefined) { + if (option.type === 'Bool') { + differsFromPresetValue = util.isTruthy(String(option.value)) !== util.isTruthy(origin.presetValue); + } else { + differsFromPresetValue = String(option.value) !== origin.presetValue; + } + } + + metadata.set(option.key, { + presetSource: `${sourceKind}: ${origin.presetName}`, + differsFromPresetValue + }); + } + + return metadata; + } + + private async updatePresetBackedCacheVariableOverrides(dirtyOptions: IOption[]): Promise { + if (!this.useCMakePresets || !this.configurePreset || dirtyOptions.length === 0) { + return; + } + + const presetCacheVariables = this.configurePreset.cacheVariables; + if (!presetCacheVariables) { + return; + } + + const presetBackedOptions = dirtyOptions.filter(option => Object.prototype.hasOwnProperty.call(presetCacheVariables, option.key)); + if (presetBackedOptions.length === 0) { + return; + } + + const userPresets = preset.getOriginalUserPresetsFile(this.folderPath) + ?? { version: preset.getOriginalPresetsFile(this.folderPath)?.version ?? 8 }; + userPresets.configurePresets = userPresets.configurePresets ?? []; + + const currentPreset = this.configurePreset; + let targetPreset = currentPreset.isUserPreset + ? userPresets.configurePresets.find(configurePreset => configurePreset.name === currentPreset.name) + : undefined; + + if (!targetPreset) { + const defaultOverrideName = `${currentPreset.name}__cacheEditorOverride`; + let overrideName = defaultOverrideName; + let suffix = 1; + const allPresetNames = new Set(preset.allConfigurePresets(this.folderPath, true).map(configurePreset => configurePreset.name)); + + while (allPresetNames.has(overrideName) && !userPresets.configurePresets.find(configurePreset => configurePreset.name === overrideName)) { + overrideName = `${defaultOverrideName}_${suffix++}`; + } + + targetPreset = userPresets.configurePresets.find(configurePreset => configurePreset.name === overrideName); + if (!targetPreset) { + targetPreset = { + name: overrideName, + hidden: true, + inherits: currentPreset.name, + cacheVariables: {} + }; + userPresets.configurePresets.push(targetPreset); + } + } + + targetPreset.cacheVariables = targetPreset.cacheVariables ?? {}; + + for (const option of presetBackedOptions) { + const presetCacheVariable = presetCacheVariables[option.key]; + targetPreset.cacheVariables[option.key] = { + type: this.getPresetCacheVariableType(option, presetCacheVariable), + value: this.getPresetCacheVariableValue(option) + }; + } + + this.presetsController.suppressWatcherReapply = true; + await this.presetsController.updatePresetsFile(userPresets, true, false); + await this.presetsController.reapplyPresets(); + + if (targetPreset.name !== currentPreset.name) { + await this.presetsController.setConfigurePreset(targetPreset.name); + } + } + /** * Implementation of `cmake.EditCacheUI` */ @@ -2472,9 +2658,10 @@ export class CMakeProject { return 1; } - this.cacheEditorWebview = new ConfigurationWebview(drv.cachePath, () => { - void this.configureInternal(ConfigureTrigger.commandEditCacheUI, [], ConfigureType.Cache); - }); + this.cacheEditorWebview = new ConfigurationWebview(drv.cachePath, async dirtyOptions => { + await this.updatePresetBackedCacheVariableOverrides(dirtyOptions); + await this.configureInternal(ConfigureTrigger.commandEditCacheUI, [], ConfigureType.Cache); + }, async options => this.getCacheOptionPresetMetadata(options)); await this.cacheEditorWebview.initPanel(); this.cacheEditorWebview.panel.onDidDispose(() => { diff --git a/src/presets/presetsController.ts b/src/presets/presetsController.ts index 249e7f5b8..c517b5a1c 100644 --- a/src/presets/presetsController.ts +++ b/src/presets/presetsController.ts @@ -1723,7 +1723,7 @@ export class PresetsController implements vscode.Disposable { return { insertSpaces, tabSize }; } - async updatePresetsFile(presetsFile: preset.PresetsFile, isUserPresets = false): Promise { + async updatePresetsFile(presetsFile: preset.PresetsFile, isUserPresets = false, showInEditor = true): Promise { const presetsFilePath = isUserPresets ? this.userPresetsPath : this.presetsPath; const indent = this.getIndentationSettings(); try { @@ -1733,6 +1733,10 @@ export class PresetsController implements vscode.Disposable { return; } + if (!showInEditor) { + return undefined; + } + return vscode.window.showTextDocument(vscode.Uri.file(presetsFilePath)); } diff --git a/src/ui/cacheView.ts b/src/ui/cacheView.ts index 92567f65d..b5ccb19c0 100644 --- a/src/ui/cacheView.ts +++ b/src/ui/cacheView.ts @@ -12,12 +12,14 @@ nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFo const localize: nls.LocalizeFunc = nls.loadMessageBundle(); export interface IOption { - key: string; // same as CMake cache variable key names - type: string; // "Bool" for boolean and "String" for anything else for now + key: string; // same as CMake cache variable key names + type: string; // "Bool" for boolean and "String" for anything else for now helpString: string; choices: string[]; - value: string; // value from the cache file or changed in the UI + value: string | boolean; // value from the cache file or changed in the UI dirty: boolean; // if the variable was edited in the UI + presetSource?: string; // where this variable is defined in presets + differsFromPresetValue?: boolean; // true if cache value differs from preset value } /** @@ -50,7 +52,11 @@ export class ConfigurationWebview { private options: IOption[] = []; - constructor(protected cachePath: string, protected save: () => void) { + constructor( + protected cachePath: string, + protected save: (dirtyOptions: IOption[]) => Promise, + protected getPresetOptionMetadata?: (options: IOption[]) => Promise> + ) { this.panel = vscode.window.createWebviewPanel( 'cmakeConfiguration', // Identifies the type of the webview. Used internally this.cmakeCacheEditorText, // Title of the panel displayed to the user @@ -66,11 +72,17 @@ export class ConfigurationWebview { async persistCacheEntries() { if (this.isDirty) { telemetry.logEvent("editCMakeCache", { command: "saveCMakeCacheUI" }); - await this.saveCmakeCache(this.options); + const dirtyOptions = this.options.filter(option => option.dirty); + await this.saveCmakeCache(dirtyOptions); void vscode.window.showInformationMessage(localize('cmake.cache.saved', 'CMake options have been saved.')); - // start configure - this.save(); + // Start configure after persisting the edited values. + await this.save(dirtyOptions); this.isDirty = false; + // Force a post-configure refresh so the editor reflects cache changes immediately. + this.options = await this.getConfigurationOptions(); + if (this.panel.visible) { + await this.renderWebview(this.panel, false); + } } } @@ -207,7 +219,11 @@ export class ConfigurationWebview { async saveCmakeCache(options: IOption[]) { const cmakeCache = await CMakeCache.fromPath(this.cachePath); - await cmakeCache.saveAll(options); + const serializedOptions = options.map(option => ({ + key: option.key, + value: util.isBoolean(option.value) ? (option.value ? 'TRUE' : 'FALSE') : option.value + })); + await cmakeCache.saveAll(serializedOptions); } /** @@ -225,6 +241,18 @@ export class ConfigurationWebview { options.push({ key: entry.key, helpString: entry.helpString, choices: entry.choices, type: (entry.type === CacheEntryType.Bool) ? "Bool" : "String", value: entry.value, dirty: false }); } } + + if (this.getPresetOptionMetadata) { + const metadataByKey = await this.getPresetOptionMetadata(options); + for (const option of options) { + const metadata = metadataByKey.get(option.key); + if (metadata) { + option.presetSource = metadata.presetSource; + option.differsFromPresetValue = metadata.differsFromPresetValue; + } + } + } + return options; } @@ -254,6 +282,10 @@ export class ConfigurationWebview { const saveButtonText = localize("save", "Save"); const keyColumnText = localize("key", "Key"); const valueColumnText = localize("value", "Value"); + const sourceColumnText = localize("source", "Source"); + const notSetInPresetsText = localize("not.set.in.presets", "Cmake Cache"); + const differsFromPresetText = localize("differs.from.preset", "Changed in cache"); + const matchesPresetText = localize("matches.preset", "Matches preset"); let html = ` @@ -484,6 +516,7 @@ export class ConfigurationWebview { ${keyColumnText} ${valueColumnText} + ${sourceColumnText} ${key} @@ -506,9 +539,10 @@ export class ConfigurationWebview { const id = escapeAttribute(option.key); let editControls = ''; + const stringValue = util.isBoolean(option.value) ? (option.value ? 'TRUE' : 'FALSE') : String(option.value); if (option.type === "Bool") { - editControls = ` + editControls = `