diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe9d96e7a..b02efaaef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.23 Features: +- Add "Pin Cache Variable" functionality to the Project Status panel, allowing users to pin frequently changed CMake cache variables for quick inline editing without opening the full Cache Editor UI. [#3463](https://github.com/microsoft/vscode-cmake-tools/issues/3463) - Add `cmake.shell` setting to route CMake/CTest/CPack subprocess invocations through a custom shell (e.g., Git Bash, MSYS2), enabling embedded toolchains that require POSIX path translation on Windows. [#1750](https://github.com/microsoft/vscode-cmake-tools/issues/1750) - triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate) - Add command to clear build diagnostics from the Problems pane. [#4691](https://github.com/microsoft/vscode-cmake-tools/pull/4691) diff --git a/package.json b/package.json index 374d08b8f9..0691b9ebe1 100644 --- a/package.json +++ b/package.json @@ -882,6 +882,27 @@ "category": "CMake", "icon": "$(refresh)" }, + { + "command": "cmake.projectStatus.pinCacheVariable", + "when": "cmake:enableFullFeatureSet", + "title": "%cmake-tools.command.cmake.projectStatus.pinCacheVariable.title%", + "category": "CMake", + "icon": "$(pin)" + }, + { + "command": "cmake.projectStatus.editPinnedCacheVariable", + "when": "cmake:enableFullFeatureSet", + "title": "%cmake-tools.command.cmake.projectStatus.editPinnedCacheVariable.title%", + "category": "CMake", + "icon": "$(edit)" + }, + { + "command": "cmake.projectStatus.unpinCacheVariable", + "when": "cmake:enableFullFeatureSet", + "title": "%cmake-tools.command.cmake.projectStatus.unpinCacheVariable.title%", + "category": "CMake", + "icon": "$(close)" + }, { "command": "cmake.pinnedCommands.add", "when": "cmake:enableFullFeatureSet", @@ -1588,6 +1609,18 @@ "command": "cmake.projectStatus.update", "when": "never" }, + { + "command": "cmake.projectStatus.pinCacheVariable", + "when": "never" + }, + { + "command": "cmake.projectStatus.editPinnedCacheVariable", + "when": "never" + }, + { + "command": "cmake.projectStatus.unpinCacheVariable", + "when": "never" + }, { "command": "cmake.pinnedCommands.add", "when": "never" @@ -1735,6 +1768,11 @@ "when": "view == cmake.projectStatus && cmake:enableFullFeatureSet", "group": "navigation@4" }, + { + "command": "cmake.projectStatus.pinCacheVariable", + "when": "view == cmake.projectStatus && cmake:enableFullFeatureSet", + "group": "navigation@5" + }, { "command": "cmake.outline.configureAll", "when": "view == cmake.outline && !cmake:isBuilding", @@ -1934,6 +1972,21 @@ "when": "view == cmake.projectStatus && cmake:enableFullFeatureSet && viewItem == 'activeProject'", "group": "inline" }, + { + "command": "cmake.projectStatus.editPinnedCacheVariable", + "when": "view == cmake.projectStatus && cmake:enableFullFeatureSet && viewItem == 'pinnedCacheVariable'", + "group": "inline@1" + }, + { + "command": "cmake.projectStatus.unpinCacheVariable", + "when": "view == cmake.projectStatus && cmake:enableFullFeatureSet && viewItem == 'pinnedCacheVariable'", + "group": "inline@2" + }, + { + "command": "cmake.projectStatus.pinCacheVariable", + "when": "view == cmake.projectStatus && cmake:enableFullFeatureSet && viewItem == 'pinnedCacheVariables'", + "group": "inline" + }, { "command": "cmake.outline.search", "when": "view == cmake.outline && viewItem == 'filter'", diff --git a/package.nls.json b/package.nls.json index 0959fe5455..6a5206601a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -418,6 +418,9 @@ "cmake-tools.configuration.cmake.launchBehavior.newTerminal.markdownDescriptions": "A new terminal instance is created and the target is launched in it. Existing terminals are not automatically cleaned up.", "cmake-tools.configuration.cmake.loadCompileCommands.description": "Controls whether the extension reads compile_commands.json to enable single file compilation.", "cmake-tools.command.cmake.projectStatus.update.title": "Refresh the project status", + "cmake-tools.command.cmake.projectStatus.pinCacheVariable.title": "Pin a Cache Variable", + "cmake-tools.command.cmake.projectStatus.editPinnedCacheVariable.title": "Edit Pinned Cache Variable", + "cmake-tools.command.cmake.projectStatus.unpinCacheVariable.title": "Unpin Cache Variable", "cmake-tools.command.cmake.pinnedCommands.add.title": "Add a CMake command to pin", "cmake-tools.command.cmake.pinnedCommands.remove.title": "Unpin Command", "cmake-tools.command.cmake.pinnedCommands.run.title": "Run Command", diff --git a/src/extension.ts b/src/extension.ts index d36f7df37f..dac362528e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -361,7 +361,7 @@ export class ExtensionManager implements vscode.Disposable { /** * The project status view controller */ - projectStatus = new ProjectStatus(); + projectStatus = new ProjectStatus(this.extensionContext); // NOTE: (from sidebar) The project controller manages all the projects in the workspace public readonly projectController = new ProjectController(this.extensionContext, this.projectStatus, this.workspaceConfig); diff --git a/src/ui/projectStatus.ts b/src/ui/projectStatus.ts index 9460687e13..2881b67e36 100644 --- a/src/ui/projectStatus.ts +++ b/src/ui/projectStatus.ts @@ -4,6 +4,7 @@ import CMakeProject from '@cmt/cmakeProject'; import * as preset from '@cmt/presets/preset'; import { runCommand } from '@cmt/util'; import { OptionConfig, checkBuildOverridesPresent, checkConfigureOverridesPresent, checkTestOverridesPresent, checkPackageOverridesPresent } from '@cmt/config'; +import { CMakeCache, CacheEntryType } from '@cmt/cache'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); @@ -16,12 +17,14 @@ const noWorkflowPresetSelected = localize('no.workflow.preset.selected', '[No Wo export let treeDataProvider: TreeDataProvider; +const pinnedCacheVariablesKey = 'cmake.pinnedCacheVariables'; + export class ProjectStatus { protected disposables: vscode.Disposable[] = []; - constructor() { - treeDataProvider = new TreeDataProvider(); + constructor(extensionContext?: vscode.ExtensionContext) { + treeDataProvider = new TreeDataProvider(extensionContext); this.disposables.push(...[ // Commands for projectStatus items vscode.commands.registerCommand('cmake.projectStatus.stop', async (_node: Node) => { @@ -119,6 +122,15 @@ export class ProjectStatus { }), vscode.commands.registerCommand('cmake.projectStatus.update', async () => { await this.refresh(); + }), + vscode.commands.registerCommand('cmake.projectStatus.pinCacheVariable', async () => { + await treeDataProvider.pinCacheVariable(); + }), + vscode.commands.registerCommand('cmake.projectStatus.editPinnedCacheVariable', async (node: PinnedCacheVariableNode) => { + await treeDataProvider.editPinnedCacheVariable(node); + }), + vscode.commands.registerCommand('cmake.projectStatus.unpinCacheVariable', async (node: PinnedCacheVariableNode) => { + await treeDataProvider.unpinCacheVariable(node); }) ]); } @@ -186,12 +198,14 @@ class TreeDataProvider implements vscode.TreeDataProvider, vscode.Disposab private testNode: TestNode | undefined; private packageNode: PackageNode | undefined; private workflowNode: WorkflowNode | undefined; + private extensionContext?: vscode.ExtensionContext; get onDidChangeTreeData(): vscode.Event { return this._onDidChangeTreeData.event; } - constructor() { + constructor(extensionContext?: vscode.ExtensionContext) { + this.extensionContext = extensionContext; this.treeView = vscode.window.createTreeView('cmake.projectStatus', { treeDataProvider: this }); } @@ -289,6 +303,13 @@ class TreeDataProvider implements vscode.TreeDataProvider, vscode.Disposab } nodes.push(configNode); } + // Add pinned cache variables between configure and build + const pinnedNames = this.getPinnedCacheVariableNames(); + if (pinnedNames.length > 0) { + const pinnedNode = new PinnedCacheVariablesNode(pinnedNames); + await pinnedNode.initialize(); + nodes.push(pinnedNode); + } if (!this.isBuildButtonHidden) { const buildNode = new BuildNode(); this.buildNode = buildNode; @@ -434,6 +455,92 @@ class TreeDataProvider implements vscode.TreeDataProvider, vscode.Disposab } } + // Pinned cache variable management + getPinnedCacheVariableNames(): string[] { + if (!this.extensionContext) { + return []; + } + return this.extensionContext.workspaceState.get(pinnedCacheVariablesKey, []); + } + + private async savePinnedCacheVariableNames(names: string[]): Promise { + if (!this.extensionContext) { + return; + } + await this.extensionContext.workspaceState.update(pinnedCacheVariablesKey, names); + } + + async pinCacheVariable(): Promise { + if (!this.activeCMakeProject) { + return; + } + const cachePathStr = await this.activeCMakeProject.cachePath; + if (!cachePathStr) { + void vscode.window.showErrorMessage(localize('no.cache.available', 'No CMake cache available. Please configure the project first.')); + return; + } + const cache = await CMakeCache.fromPath(cachePathStr); + const entries = cache.allEntries.filter(e => e.type !== CacheEntryType.Internal && e.type !== CacheEntryType.Static); + const alreadyPinned = new Set(this.getPinnedCacheVariableNames()); + + const items = entries + .filter(e => !alreadyPinned.has(e.key)) + .map(e => ({ + label: e.key, + description: String(e.value), + detail: e.helpString + })); + + if (items.length === 0) { + void vscode.window.showInformationMessage(localize('no.variables.to.pin', 'No cache variables available to pin.')); + return; + } + + const chosen = await vscode.window.showQuickPick(items, { + placeHolder: localize('select.variable.to.pin', 'Select a cache variable to pin') + }); + + if (chosen) { + const names = this.getPinnedCacheVariableNames(); + names.push(chosen.label); + await this.savePinnedCacheVariableNames(names); + await this.refresh(); + } + } + + async editPinnedCacheVariable(node: PinnedCacheVariableNode): Promise { + if (!this.activeCMakeProject || !node.variableName) { + return; + } + + const currentValue = node.variableValue ?? ''; + const newValue = await vscode.window.showInputBox({ + prompt: localize('enter.new.value', 'Enter new value for {0}', node.variableName), + value: currentValue + }); + + if (newValue !== undefined && newValue !== currentValue) { + // Update cmake.configureSettings with the new value + const configurationScope = this.activeCMakeProject.workspaceFolder; + const config = vscode.workspace.getConfiguration('cmake', configurationScope); + const currentSettings = config.get<{ [key: string]: any }>('configureSettings') ?? {}; + currentSettings[node.variableName] = newValue; + await config.update('configureSettings', currentSettings, vscode.ConfigurationTarget.WorkspaceFolder); + + // configureOnEdit is handled automatically by the config onChange subscription + await this.refresh(); + } + } + + async unpinCacheVariable(node: PinnedCacheVariableNode): Promise { + if (!node.variableName) { + return; + } + const names = this.getPinnedCacheVariableNames().filter(n => n !== node.variableName); + await this.savePinnedCacheVariableNames(names); + await this.refresh(); + } + } class Node extends vscode.TreeItem { @@ -1142,3 +1249,66 @@ class Variant extends Node { this.label = treeDataProvider.cmakeProject.activeVariantName || "Debug"; } } + +class PinnedCacheVariablesNode extends Node { + + private pinnedNames: string[]; + private childNodes: PinnedCacheVariableNode[] = []; + + constructor(pinnedNames: string[]) { + super(); + this.pinnedNames = pinnedNames; + } + + async initialize(): Promise { + this.label = localize('pinned.cache.variables', 'Pinned Cache Variables'); + this.tooltip = this.label; + this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; + this.contextValue = 'pinnedCacheVariables'; + await this.initializeChildren(); + } + + private async initializeChildren(): Promise { + this.childNodes = []; + + let cache: CMakeCache | undefined; + if (treeDataProvider.cmakeProject) { + const cachePathStr = await treeDataProvider.cmakeProject.cachePath; + if (cachePathStr) { + cache = await CMakeCache.fromPath(cachePathStr); + } + } + + for (const name of this.pinnedNames) { + const node = new PinnedCacheVariableNode(name); + const entry = cache?.get(name); + node.variableValue = entry ? String(entry.value) : undefined; + await node.initialize(); + this.childNodes.push(node); + } + } + + getChildren(): Node[] { + return this.childNodes; + } +} + +export class PinnedCacheVariableNode extends Node { + + public variableName: string; + public variableValue: string | undefined; + + constructor(variableName: string) { + super(); + this.variableName = variableName; + } + + async initialize(): Promise { + const displayValue = this.variableValue ?? localize('not.configured', '[Not configured]'); + this.label = this.variableName; + this.description = displayValue; + this.tooltip = `${this.variableName}: ${displayValue}`; + this.collapsibleState = vscode.TreeItemCollapsibleState.None; + this.contextValue = 'pinnedCacheVariable'; + } +}