Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
195 changes: 191 additions & 4 deletions src/cmakeProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string> = 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<Map<string, { presetSource: string; differsFromPresetValue?: boolean }>> {
const metadata = new Map<string, { presetSource: string; differsFromPresetValue?: boolean }>();
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<void> {
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`
*/
Expand All @@ -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(() => {
Expand Down
6 changes: 5 additions & 1 deletion src/presets/presetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1723,7 +1723,7 @@ export class PresetsController implements vscode.Disposable {
return { insertSpaces, tabSize };
}

async updatePresetsFile(presetsFile: preset.PresetsFile, isUserPresets = false): Promise<vscode.TextEditor | undefined> {
async updatePresetsFile(presetsFile: preset.PresetsFile, isUserPresets = false, showInEditor = true): Promise<vscode.TextEditor | undefined> {
const presetsFilePath = isUserPresets ? this.userPresetsPath : this.presetsPath;
const indent = this.getIndentationSettings();
try {
Expand All @@ -1733,6 +1733,10 @@ export class PresetsController implements vscode.Disposable {
return;
}

if (!showInEditor) {
return undefined;
}

return vscode.window.showTextDocument(vscode.Uri.file(presetsFilePath));
}

Expand Down
60 changes: 50 additions & 10 deletions src/ui/cacheView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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<void>,
protected getPresetOptionMetadata?: (options: IOption[]) => Promise<Map<string, { presetSource: string; differsFromPresetValue?: boolean }>>
) {
this.panel = vscode.window.createWebviewPanel(
'cmakeConfiguration', // Identifies the type of the webview. Used internally
this.cmakeCacheEditorText, // Title of the panel displayed to the user
Expand All @@ -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);
}
}
}

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -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 = `
<!DOCTYPE html>
Expand Down Expand Up @@ -484,6 +516,7 @@ export class ConfigurationWebview {
<th style="width: 30px"></th>
<th style="width: 1px; white-space: nowrap;">${keyColumnText}</th>
<th>${valueColumnText}</th>
<th style="width: 1px; white-space: nowrap;">${sourceColumnText}</th>
</tr>
${key}
</table>
Expand All @@ -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 = `<input class="cmake-input-bool" id="${id}" type="checkbox" ${util.isTruthy(option.value) ? 'checked' : ''}>
editControls = `<input class="cmake-input-bool" id="${id}" type="checkbox" ${util.isTruthy(stringValue) ? 'checked' : ''}>
<label id="LABEL_${id}" for="${id}"/>`;
} else {
const hasChoices = option.choices.length > 0;
Expand All @@ -517,14 +551,20 @@ export class ConfigurationWebview {
${option.choices.map(ch => `<option value="${escapeAttribute(ch)}">`).join()}
</datalist>`;
}
editControls += `<input class="cmake-input-text" id="${id}" value="${escapeAttribute(option.value)}" style="width: 90%;"
editControls += `<input class="cmake-input-text" id="${id}" value="${escapeAttribute(stringValue)}" style="width: 90%;"
type="text" ${hasChoices ? `list="CHOICES_${id}"` : ''}>`;
}

let sourceText = option.presetSource || notSetInPresetsText;
if (option.presetSource && option.differsFromPresetValue !== undefined) {
sourceText = `${sourceText} | ${option.differsFromPresetValue ? differsFromPresetText : matchesPresetText}`;
}

return `<tr class="content-tr">
<td></td>
<td title="${escapeAttribute(option.helpString)}">${escapeHtml(option.key)}</td>
<td>${editControls}</td>
<td>${escapeHtml(sourceText)}</td>
</tr>`;
});

Expand Down