From d13738646f56b8c916c57415369381bd38e17a12 Mon Sep 17 00:00:00 2001 From: Radu Date: Tue, 9 Sep 2025 12:31:37 +0300 Subject: [PATCH 1/9] refactor: migrate from old format to CMakePresets --- src/config.ts | 3 +- .../ProjectConfigurationManager.ts | 244 ++++- src/project-conf/index.ts | 881 +++++++++++++++--- src/project-conf/projectConfPanel.ts | 13 +- src/project-conf/projectConfiguration.ts | 45 + 5 files changed, 1005 insertions(+), 181 deletions(-) diff --git a/src/config.ts b/src/config.ts index a787e4d88..f0db7df60 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,8 +30,7 @@ export namespace ESP { export namespace ProjectConfiguration { export let store: ProjectConfigStore; export const SELECTED_CONFIG = "SELECTED_PROJECT_CONFIG"; - export const PROJECT_CONFIGURATION_FILENAME = - "esp_idf_project_configuration.json"; + export const PROJECT_CONFIGURATION_FILENAME = "CMakePresets.json"; } export enum BuildType { diff --git a/src/project-conf/ProjectConfigurationManager.ts b/src/project-conf/ProjectConfigurationManager.ts index 917b3369c..71d69388b 100644 --- a/src/project-conf/ProjectConfigurationManager.ts +++ b/src/project-conf/ProjectConfigurationManager.ts @@ -20,7 +20,8 @@ import { CommandKeys, createCommandDictionary } from "../cmdTreeView/cmdStore"; import { createStatusBarItem } from "../statusBar"; import { getIdfTargetFromSdkconfig } from "../workspaceConfig"; import { Logger } from "../logger/logger"; -import { getProjectConfigurationElements } from "./index"; +import { getProjectConfigurationElements, configurePresetToProjectConfElement, promptLegacyMigration, migrateLegacyConfiguration } from "./index"; +import { pathExists } from "fs-extra"; import { configureClangSettings } from "../clang"; export function clearSelectedProjectConfiguration(): void { @@ -68,32 +69,15 @@ export class ProjectConfigurationManager { false ); - this.initialize(); this.registerEventHandlers(); + // Initialize asynchronously + this.initialize(); } - private initialize(): void { + private async initialize(): Promise { if (!fileExists(this.configFilePath)) { - // File doesn't exist - this is normal for projects without multiple configurations - this.configVersions = []; - - // If configuration status bar item exists, remove it - if (this.statusBarItems["projectConf"]) { - this.statusBarItems["projectConf"].dispose(); - this.statusBarItems["projectConf"] = undefined; - } - - // Clear any potentially stale configuration - const currentSelectedConfig = ESP.ProjectConfiguration.store.get( - ESP.ProjectConfiguration.SELECTED_CONFIG - ); - if (currentSelectedConfig) { - ESP.ProjectConfiguration.store.clear(currentSelectedConfig); - ESP.ProjectConfiguration.store.clear( - ESP.ProjectConfiguration.SELECTED_CONFIG - ); - } - + // CMakePresets.json doesn't exist - check for legacy file + await this.checkForLegacyFile(); return; } @@ -350,7 +334,10 @@ export class ProjectConfigurationManager { this.workspaceUri, true // Resolve paths for building ); - ESP.ProjectConfiguration.store.set(configName, resolvedConfig[configName]); + + // Convert ConfigurePreset to ProjectConfElement for store compatibility + const legacyElement = configurePresetToProjectConfElement(resolvedConfig[configName]); + ESP.ProjectConfiguration.store.set(configName, legacyElement); // Update UI if (this.statusBarItems["projectConf"]) { @@ -392,8 +379,17 @@ export class ProjectConfigurationManager { !projectConfigurations || Object.keys(projectConfigurations).length === 0 ) { + // Check if we have legacy configurations to migrate + const legacyFilePath = Uri.joinPath(this.workspaceUri, "esp_idf_project_configuration.json"); + + if (await pathExists(legacyFilePath.fsPath)) { + // Show migration dialog + await this.handleLegacyMigrationDialog(legacyFilePath); + return; + } + const emptyOption = await window.showInformationMessage( - l10n.t("No project configuration found"), + l10n.t("No CMakePresets configure presets found"), "Open editor" ); @@ -430,6 +426,204 @@ export class ProjectConfigurationManager { } } + /** + * Checks for legacy esp_idf_project_configuration.json file and shows appropriate status + */ + private async checkForLegacyFile(): Promise { + const legacyFilePath = Uri.joinPath(this.workspaceUri, "esp_idf_project_configuration.json").fsPath; + + if (fileExists(legacyFilePath)) { + // Legacy file exists - show status bar with migration option + this.configVersions = []; + + try { + const legacyContent = readFileSync(legacyFilePath); + if (legacyContent && legacyContent.trim() !== "") { + const legacyData = JSON.parse(legacyContent); + const legacyConfigNames = Object.keys(legacyData); + + if (legacyConfigNames.length > 0) { + // Show status bar indicating legacy configurations are available + this.setLegacyConfigurationStatus(legacyConfigNames); + + // Show migration notification + this.showLegacyMigrationNotification(legacyConfigNames); + return; + } + } + } catch (error) { + Logger.warn(`Failed to parse legacy configuration file: ${error.message}`); + } + } + + // No configuration files found - clear everything + this.clearConfigurationState(); + } + + /** + * Sets status bar to indicate legacy configurations are available + */ + private setLegacyConfigurationStatus(legacyConfigNames: string[]): void { + const statusBarItemName = `Legacy Configs (${legacyConfigNames.length})`; + const statusBarItemTooltip = `Found legacy project configurations: ${legacyConfigNames.join(", ")}. Click to migrate to CMakePresets.json format.`; + const commandToUse = "espIdf.projectConf"; + + if (this.statusBarItems["projectConf"]) { + this.statusBarItems["projectConf"].dispose(); + } + + this.statusBarItems["projectConf"] = createStatusBarItem( + `$(${ + this.commandDictionary[CommandKeys.SelectProjectConfiguration].iconId + }) ${statusBarItemName}`, + statusBarItemTooltip, + commandToUse, + 99, + this.commandDictionary[CommandKeys.SelectProjectConfiguration] + .checkboxState + ); + } + + /** + * Shows notification about legacy configurations + */ + private async showLegacyMigrationNotification(legacyConfigNames: string[]): Promise { + const message = l10n.t( + "Found {0} legacy project configuration(s): {1}. Would you like to migrate them to the new CMakePresets.json format? Your original file will remain unchanged.", + legacyConfigNames.length, + legacyConfigNames.join(", ") + ); + + const migrateOption = l10n.t("Migrate Now"); + const laterOption = l10n.t("Later"); + + const choice = await window.showInformationMessage( + message, + migrateOption, + laterOption + ); + + if (choice === migrateOption) { + // Directly perform migration without additional popup + const legacyFilePath = Uri.joinPath(this.workspaceUri, "esp_idf_project_configuration.json"); + await this.performDirectMigration(legacyFilePath); + } + } + + /** + * Handles the legacy migration dialog when user clicks on project configuration + */ + private async handleLegacyMigrationDialog(legacyFilePath: Uri): Promise { + try { + const legacyContent = readFileSync(legacyFilePath.fsPath); + const legacyData = JSON.parse(legacyContent); + const legacyConfigNames = Object.keys(legacyData); + + const message = l10n.t( + "Found {0} legacy project configuration(s): {1}. Would you like to migrate them to the new CMakePresets.json format?", + legacyConfigNames.length, + legacyConfigNames.join(", ") + ); + + const migrateOption = l10n.t("Migrate Now"); + const cancelOption = l10n.t("Cancel"); + + const choice = await window.showInformationMessage( + message, + { modal: true }, + migrateOption, + cancelOption + ); + + if (choice === migrateOption) { + await this.performMigration(legacyFilePath); + } + } catch (error) { + Logger.errorNotify( + "Failed to handle legacy migration", + error, + "handleLegacyMigrationDialog" + ); + window.showErrorMessage( + l10n.t("Failed to process legacy configuration file: {0}", error.message) + ); + } + } + + /** + * Performs the actual migration and updates UI (with confirmation dialog) + */ + private async performMigration(legacyFilePath: Uri): Promise { + try { + await promptLegacyMigration(this.workspaceUri, legacyFilePath); + + // After migration, reinitialize to show the new configurations + await this.initialize(); + + window.showInformationMessage( + l10n.t("Project configurations successfully migrated to CMakePresets.json format!") + ); + } catch (error) { + Logger.errorNotify( + "Failed to perform migration", + error, + "performMigration" + ); + window.showErrorMessage( + l10n.t("Failed to migrate project configuration: {0}", error.message) + ); + } + } + + /** + * Performs direct migration without additional confirmation (for notification) + */ + private async performDirectMigration(legacyFilePath: Uri): Promise { + try { + await migrateLegacyConfiguration(this.workspaceUri, legacyFilePath); + + // After migration, reinitialize to show the new configurations + await this.initialize(); + + window.showInformationMessage( + l10n.t("Project configurations successfully migrated to CMakePresets.json format!") + ); + } catch (error) { + Logger.errorNotify( + "Failed to perform direct migration", + error, + "performDirectMigration" + ); + window.showErrorMessage( + l10n.t("Failed to migrate project configuration: {0}", error.message) + ); + } + } + + /** + * Clears all configuration state + */ + private clearConfigurationState(): void { + this.configVersions = []; + + // If configuration status bar item exists, remove it + if (this.statusBarItems["projectConf"]) { + this.statusBarItems["projectConf"].dispose(); + this.statusBarItems["projectConf"] = undefined; + } + + // Clear any potentially stale configuration + const currentSelectedConfig = ESP.ProjectConfiguration.store.get( + ESP.ProjectConfiguration.SELECTED_CONFIG + ); + if (currentSelectedConfig) { + ESP.ProjectConfiguration.store.clear(currentSelectedConfig); + ESP.ProjectConfiguration.store.clear( + ESP.ProjectConfiguration.SELECTED_CONFIG + ); + } + } + /** * Dispose of the file system watcher */ diff --git a/src/project-conf/index.ts b/src/project-conf/index.ts index 22100759f..70ef4208e 100644 --- a/src/project-conf/index.ts +++ b/src/project-conf/index.ts @@ -17,10 +17,10 @@ */ import * as path from "path"; -import { ExtensionContext, Uri, window } from "vscode"; +import { ExtensionContext, Uri, window, l10n } from "vscode"; import { ESP } from "../config"; import { pathExists, readJson, writeJson } from "fs-extra"; -import { ProjectConfElement } from "./projectConfiguration"; +import { ProjectConfElement, CMakePresets, ConfigurePreset, BuildPreset, ESPIDFSettings, ESPIDFVendorSettings } from "./projectConfiguration"; import { Logger } from "../logger/logger"; import { resolveVariables } from "../idfConfiguration"; @@ -67,7 +67,7 @@ export async function updateCurrentProfileIdfTarget( if (!projectConfJson[selectedConfig]) { const err = new Error( - `Configuration "${selectedConfig}" not found in ${ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME}.` + `Configuration preset "${selectedConfig}" not found in ${ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME}. Please check your CMakePresets configurePresets section.` ); Logger.errorNotify( err.message, @@ -76,7 +76,12 @@ export async function updateCurrentProfileIdfTarget( ); return; } - projectConfJson[selectedConfig].idfTarget = idfTarget; + + // Update IDF_TARGET in cacheVariables for ConfigurePreset + if (!projectConfJson[selectedConfig].cacheVariables) { + projectConfJson[selectedConfig].cacheVariables = {}; + } + projectConfJson[selectedConfig].cacheVariables.IDF_TARGET = idfTarget; ESP.ProjectConfiguration.store.set( selectedConfig, @@ -86,6 +91,30 @@ export async function updateCurrentProfileIdfTarget( } export async function saveProjectConfFile( + workspaceFolder: Uri, + projectConfElements: { [key: string]: ConfigurePreset } +) { + const projectConfFilePath = Uri.joinPath( + workspaceFolder, + ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME + ); + + // Use ConfigurePreset objects directly + const configurePresets: ConfigurePreset[] = Object.values(projectConfElements); + + const cmakePresets: CMakePresets = { + version: 1, + cmakeMinimumRequired: { major: 3, minor: 23, patch: 0 }, + configurePresets, + }; + + await writeJson(projectConfFilePath.fsPath, cmakePresets, { + spaces: 2, + }); +} + +// Legacy compatibility function +export async function saveProjectConfFileLegacy( workspaceFolder: Uri, projectConfElements: { [key: string]: ProjectConfElement } ) { @@ -93,7 +122,19 @@ export async function saveProjectConfFile( workspaceFolder, ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME ); - await writeJson(projectConfFilePath.fsPath, projectConfElements, { + + // Convert to CMakePresets format + const configurePresets: ConfigurePreset[] = Object.keys(projectConfElements).map(name => + convertProjectConfElementToConfigurePreset(name, projectConfElements[name]) + ); + + const cmakePresets: CMakePresets = { + version: 1, + cmakeMinimumRequired: { major: 3, minor: 23, patch: 0 }, + configurePresets, + }; + + await writeJson(projectConfFilePath.fsPath, cmakePresets, { spaces: 2, }); } @@ -311,16 +352,16 @@ function resolveConfigPaths( // --- Main Function --- /** - * Reads the project configuration JSON file, performs variable substitution + * Reads the CMakePresets.json file, performs variable substitution * on relevant fields, resolves paths, and returns the structured configuration. * @param workspaceFolder The Uri of the current workspace folder. * @param resolvePaths Whether to resolve paths to absolute paths (true for building, false for display) - * @returns An object mapping configuration names to their processed ProjectConfElement. + * @returns An object mapping configuration names to their processed ConfigurePreset. */ export async function getProjectConfigurationElements( workspaceFolder: Uri, resolvePaths: boolean = false -): Promise<{ [key: string]: ProjectConfElement }> { +): Promise<{ [key: string]: ConfigurePreset }> { const projectConfFilePath = Uri.joinPath( workspaceFolder, ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME @@ -328,7 +369,8 @@ export async function getProjectConfigurationElements( const doesPathExists = await pathExists(projectConfFilePath.fsPath); if (!doesPathExists) { - // File not existing is normal, return empty object + // Check if legacy file exists and prompt for migration + await checkAndPromptLegacyMigration(workspaceFolder); return {}; } @@ -345,161 +387,599 @@ export async function getProjectConfigurationElements( "getProjectConfigurationElements" ); window.showErrorMessage( - `Error reading or parsing project configuration file (${projectConfFilePath.fsPath}): ${error.message}` + `Error reading or parsing CMakePresets.json file (${projectConfFilePath.fsPath}): ${error.message}` ); return {}; // Return empty if JSON is invalid or unreadable } - const projectConfElements: { [key: string]: ProjectConfElement } = {}; + const projectConfElements: { [key: string]: ConfigurePreset } = {}; + + // Only support CMakePresets format + if (projectConfJson.version !== undefined && projectConfJson.configurePresets) { + // CMakePresets format + const cmakePresets = projectConfJson as CMakePresets; + + if (!cmakePresets.configurePresets || cmakePresets.configurePresets.length === 0) { + return {}; + } + + // Process each configure preset + for (const preset of cmakePresets.configurePresets) { + try { + // Apply variable substitution and path resolution directly to ConfigurePreset + const processedPreset = await processConfigurePresetVariables( + preset, + workspaceFolder, + resolvePaths + ); + + projectConfElements[preset.name] = processedPreset; + } catch (error) { + Logger.warn( + `Failed to process configure preset "${preset.name}": ${error.message}`, + error + ); + } + } + } else { + // This might be a legacy file that wasn't migrated + Logger.warn( + `Invalid CMakePresets.json format detected. Expected 'version' and 'configurePresets' fields.`, + new Error("Invalid CMakePresets format") + ); + window.showErrorMessage( + `Invalid CMakePresets.json format. Please ensure the file follows the CMakePresets specification.` + ); + return {}; + } + + return projectConfElements; +} + +/** + * Checks for legacy project configuration file and prompts user for migration + */ +async function checkAndPromptLegacyMigration(workspaceFolder: Uri): Promise { + const legacyFilePath = Uri.joinPath(workspaceFolder, "esp_idf_project_configuration.json"); + + if (await pathExists(legacyFilePath.fsPath)) { + await promptLegacyMigration(workspaceFolder, legacyFilePath); + } +} + +/** + * Prompts user to migrate legacy configuration file + */ +export async function promptLegacyMigration(workspaceFolder: Uri, legacyFilePath: Uri): Promise { + const message = l10n.t( + "A legacy project configuration file (esp_idf_project_configuration.json) was found. " + + "Would you like to migrate it to the new CMakePresets.json format? " + + "Your original file will remain unchanged." + ); + + const migrateOption = l10n.t("Migrate"); + const cancelOption = l10n.t("Cancel"); + + const choice = await window.showInformationMessage( + message, + { modal: true }, + migrateOption, + cancelOption + ); + + if (choice === migrateOption) { + await migrateLegacyConfiguration(workspaceFolder, legacyFilePath); + } +} - // Process each configuration defined in the JSON - await Promise.all( - Object.keys(projectConfJson).map(async (confName) => { - const rawConfig = projectConfJson[confName]; - if (typeof rawConfig !== "object" || rawConfig === null) { +/** + * Migrates legacy configuration to CMakePresets format + */ +export async function migrateLegacyConfiguration(workspaceFolder: Uri, legacyFilePath: Uri): Promise { + // Read legacy configuration + const legacyConfig = await readJson(legacyFilePath.fsPath); + + // Convert to new format + const projectConfElements: { [key: string]: ProjectConfElement } = {}; + + // Process legacy configurations + for (const [confName, rawConfig] of Object.entries(legacyConfig)) { + if (typeof rawConfig === "object" && rawConfig !== null) { + try { + const processedElement = await processLegacyProjectConfig( + rawConfig, + workspaceFolder, + false // Don't resolve paths for migration + ); + projectConfElements[confName] = processedElement; + } catch (error) { Logger.warn( - `Configuration entry "${confName}" is not a valid object. Skipping.`, - new Error("Invalid config entry") + `Failed to migrate configuration "${confName}": ${error.message}`, + error ); - return; // Skip invalid entries } + } + } + + // Save in new format using legacy compatibility function + await saveProjectConfFileLegacy(workspaceFolder, projectConfElements); + + Logger.info(`Successfully migrated ${Object.keys(projectConfElements).length} configurations to CMakePresets.json`); +} - const buildConfig = rawConfig.build; - const openOCDConfig = rawConfig.openOCD; - const tasksConfig = rawConfig.tasks; - const envConfig = rawConfig.env; - - // --- Process Build Configuration --- - const buildDirectoryPath = resolvePaths - ? resolveConfigPaths( - workspaceFolder, - rawConfig, - buildConfig?.buildDirectoryPath, - resolvePaths - ) - : buildConfig?.buildDirectoryPath; - const sdkconfigDefaults = resolvePaths - ? resolveConfigPaths( - workspaceFolder, - rawConfig, - buildConfig?.sdkconfigDefaults, - resolvePaths - ) - : buildConfig?.sdkconfigDefaults; - const sdkconfigFilePath = resolvePaths - ? resolveConfigPaths( - workspaceFolder, - rawConfig, - buildConfig?.sdkconfigFilePath, - resolvePaths - ) - : buildConfig?.sdkconfigFilePath; - const compileArgs = buildConfig?.compileArgs - ?.map((arg: string) => - resolvePaths - ? substituteVariablesInString(arg, workspaceFolder, rawConfig) - : arg - ) - .filter(isDefined); - const ninjaArgs = buildConfig?.ninjaArgs - ?.map((arg: string) => - resolvePaths - ? substituteVariablesInString(arg, workspaceFolder, rawConfig) - : arg - ) - .filter(isDefined); - - // --- Process Environment Variables --- - let processedEnv: { [key: string]: string } | undefined; - if (typeof envConfig === "object" && envConfig !== null) { - processedEnv = {}; - for (const key in envConfig) { - if (Object.prototype.hasOwnProperty.call(envConfig, key)) { - const rawValue = envConfig[key]; - if (typeof rawValue === "string") { - processedEnv[key] = resolvePaths - ? substituteVariablesInString( - rawValue, - workspaceFolder, - rawConfig - ) ?? "" - : rawValue; - } else { - processedEnv[key] = String(rawValue); - } - } + + +/** + * Processes legacy project configuration format + */ +async function processLegacyProjectConfig( + rawConfig: any, + workspaceFolder: Uri, + resolvePaths: boolean +): Promise { + const buildConfig = rawConfig.build; + const openOCDConfig = rawConfig.openOCD; + const tasksConfig = rawConfig.tasks; + const envConfig = rawConfig.env; + + // --- Process Build Configuration --- + const buildDirectoryPath = resolvePaths + ? resolveConfigPaths( + workspaceFolder, + rawConfig, + buildConfig?.buildDirectoryPath, + resolvePaths + ) + : buildConfig?.buildDirectoryPath; + const sdkconfigDefaults = resolvePaths + ? resolveConfigPaths( + workspaceFolder, + rawConfig, + buildConfig?.sdkconfigDefaults, + resolvePaths + ) + : buildConfig?.sdkconfigDefaults; + const sdkconfigFilePath = resolvePaths + ? resolveConfigPaths( + workspaceFolder, + rawConfig, + buildConfig?.sdkconfigFilePath, + resolvePaths + ) + : buildConfig?.sdkconfigFilePath; + const compileArgs = buildConfig?.compileArgs + ?.map((arg: string) => + resolvePaths + ? substituteVariablesInString(arg, workspaceFolder, rawConfig) + : arg + ) + .filter(isDefined); + const ninjaArgs = buildConfig?.ninjaArgs + ?.map((arg: string) => + resolvePaths + ? substituteVariablesInString(arg, workspaceFolder, rawConfig) + : arg + ) + .filter(isDefined); + + // --- Process Environment Variables --- + let processedEnv: { [key: string]: string } | undefined; + if (typeof envConfig === "object" && envConfig !== null) { + processedEnv = {}; + for (const key in envConfig) { + if (Object.prototype.hasOwnProperty.call(envConfig, key)) { + const rawValue = envConfig[key]; + if (typeof rawValue === "string") { + processedEnv[key] = resolvePaths + ? substituteVariablesInString( + rawValue, + workspaceFolder, + rawConfig + ) ?? "" + : rawValue; + } else { + processedEnv[key] = String(rawValue); } } + } + } - // --- Process OpenOCD Configuration --- - const openOCDConfigs = openOCDConfig?.configs; - const openOCDArgs = openOCDConfig?.args - ?.map((arg: string) => - resolvePaths - ? substituteVariablesInString(arg, workspaceFolder, rawConfig) - : arg - ) - .filter(isDefined); - - // --- Process Tasks --- - const preBuild = resolvePaths - ? substituteVariablesInString( - tasksConfig?.preBuild, - workspaceFolder, - rawConfig - ) - : tasksConfig?.preBuild; - const preFlash = resolvePaths - ? substituteVariablesInString( - tasksConfig?.preFlash, - workspaceFolder, - rawConfig - ) - : tasksConfig?.preFlash; - const postBuild = resolvePaths - ? substituteVariablesInString( - tasksConfig?.postBuild, - workspaceFolder, - rawConfig - ) - : tasksConfig?.postBuild; - const postFlash = resolvePaths - ? substituteVariablesInString( - tasksConfig?.postFlash, - workspaceFolder, - rawConfig - ) - : tasksConfig?.postFlash; - - // --- Assemble the Processed Configuration --- - projectConfElements[confName] = { - build: { - compileArgs: compileArgs ?? [], - ninjaArgs: ninjaArgs ?? [], - buildDirectoryPath: buildDirectoryPath, - sdkconfigDefaults: sdkconfigDefaults ?? [], - sdkconfigFilePath: sdkconfigFilePath, - }, - env: processedEnv ?? {}, - idfTarget: rawConfig.idfTarget, - flashBaudRate: rawConfig.flashBaudRate, - monitorBaudRate: rawConfig.monitorBaudRate, - openOCD: { - debugLevel: openOCDConfig?.debugLevel, - configs: openOCDConfigs ?? [], - args: openOCDArgs ?? [], - }, - tasks: { - preBuild: preBuild, - preFlash: preFlash, - postBuild: postBuild, - postFlash: postFlash, - }, - }; - }) - ); + // --- Process OpenOCD Configuration --- + const openOCDConfigs = openOCDConfig?.configs; + const openOCDArgs = openOCDConfig?.args + ?.map((arg: string) => + resolvePaths + ? substituteVariablesInString(arg, workspaceFolder, rawConfig) + : arg + ) + .filter(isDefined); - return projectConfElements; + // --- Process Tasks --- + const preBuild = resolvePaths + ? substituteVariablesInString( + tasksConfig?.preBuild, + workspaceFolder, + rawConfig + ) + : tasksConfig?.preBuild; + const preFlash = resolvePaths + ? substituteVariablesInString( + tasksConfig?.preFlash, + workspaceFolder, + rawConfig + ) + : tasksConfig?.preFlash; + const postBuild = resolvePaths + ? substituteVariablesInString( + tasksConfig?.postBuild, + workspaceFolder, + rawConfig + ) + : tasksConfig?.postBuild; + const postFlash = resolvePaths + ? substituteVariablesInString( + tasksConfig?.postFlash, + workspaceFolder, + rawConfig + ) + : tasksConfig?.postFlash; + + // --- Assemble the Processed Configuration --- + return { + build: { + compileArgs: compileArgs ?? [], + ninjaArgs: ninjaArgs ?? [], + buildDirectoryPath: buildDirectoryPath, + sdkconfigDefaults: sdkconfigDefaults ?? [], + sdkconfigFilePath: sdkconfigFilePath, + }, + env: processedEnv ?? {}, + idfTarget: rawConfig.idfTarget, + flashBaudRate: rawConfig.flashBaudRate, + monitorBaudRate: rawConfig.monitorBaudRate, + openOCD: { + debugLevel: openOCDConfig?.debugLevel, + configs: openOCDConfigs ?? [], + args: openOCDArgs ?? [], + }, + tasks: { + preBuild: preBuild, + preFlash: preFlash, + postBuild: postBuild, + postFlash: postFlash, + }, + }; +} + +/** + * Processes variable substitution and path resolution for ConfigurePreset + */ +async function processConfigurePresetVariables( + preset: ConfigurePreset, + workspaceFolder: Uri, + resolvePaths: boolean +): Promise { + const processedPreset: ConfigurePreset = { + ...preset, + binaryDir: preset.binaryDir ? await processConfigurePresetPath(preset.binaryDir, workspaceFolder, preset, resolvePaths) : undefined, + cacheVariables: preset.cacheVariables ? await processConfigurePresetCacheVariables(preset.cacheVariables, workspaceFolder, preset, resolvePaths) : undefined, + environment: preset.environment ? await processConfigurePresetEnvironment(preset.environment, workspaceFolder, preset, resolvePaths) : undefined, + vendor: preset.vendor ? await processConfigurePresetVendor(preset.vendor, workspaceFolder, preset, resolvePaths) : undefined, + }; + + return processedPreset; +} + +/** + * Processes paths in ConfigurePreset + */ +async function processConfigurePresetPath( + pathValue: string, + workspaceFolder: Uri, + preset: ConfigurePreset, + resolvePaths: boolean +): Promise { + // Apply variable substitution + let processedPath = substituteVariablesInConfigurePreset(pathValue, workspaceFolder, preset); + + if (resolvePaths && processedPath) { + // Resolve relative paths to absolute paths + if (!path.isAbsolute(processedPath)) { + processedPath = path.join(workspaceFolder.fsPath, processedPath); + } + } + + return processedPath || pathValue; +} + +/** + * Processes cache variables in ConfigurePreset + */ +async function processConfigurePresetCacheVariables( + cacheVariables: { [key: string]: any }, + workspaceFolder: Uri, + preset: ConfigurePreset, + resolvePaths: boolean +): Promise<{ [key: string]: any }> { + const processedCacheVariables: { [key: string]: any } = {}; + + for (const [key, value] of Object.entries(cacheVariables)) { + if (typeof value === "string") { + processedCacheVariables[key] = substituteVariablesInConfigurePreset(value, workspaceFolder, preset); + + // Special handling for path-related cache variables + if (resolvePaths && (key === "SDKCONFIG" || key.includes("PATH"))) { + const processedValue = processedCacheVariables[key]; + if (processedValue && !path.isAbsolute(processedValue)) { + processedCacheVariables[key] = path.join(workspaceFolder.fsPath, processedValue); + } + } + } else { + processedCacheVariables[key] = value; + } + } + + return processedCacheVariables; +} + +/** + * Processes environment variables in ConfigurePreset + */ +async function processConfigurePresetEnvironment( + environment: { [key: string]: string }, + workspaceFolder: Uri, + preset: ConfigurePreset, + resolvePaths: boolean +): Promise<{ [key: string]: string }> { + const processedEnvironment: { [key: string]: string } = {}; + + for (const [key, value] of Object.entries(environment)) { + processedEnvironment[key] = substituteVariablesInConfigurePreset(value, workspaceFolder, preset) || value; + } + + return processedEnvironment; +} + +/** + * Processes vendor-specific settings in ConfigurePreset + */ +async function processConfigurePresetVendor( + vendor: ESPIDFVendorSettings, + workspaceFolder: Uri, + preset: ConfigurePreset, + resolvePaths: boolean +): Promise { + const processedVendor: ESPIDFVendorSettings = { + "espressif/vscode-esp-idf": { + settings: [] + } + }; + + const espIdfSettings = vendor["espressif/vscode-esp-idf"]?.settings || []; + + for (const setting of espIdfSettings) { + const processedSetting: ESPIDFSettings = { ...setting }; + + // Process string values in settings + if (typeof setting.value === "string") { + processedSetting.value = substituteVariablesInConfigurePreset(setting.value, workspaceFolder, preset) || setting.value; + } else if (Array.isArray(setting.value)) { + // Process arrays of strings + processedSetting.value = setting.value.map(item => + typeof item === "string" + ? substituteVariablesInConfigurePreset(item, workspaceFolder, preset) || item + : item + ); + } else if (typeof setting.value === "object" && setting.value !== null) { + // Process objects (like openOCD settings) + processedSetting.value = await processConfigurePresetSettingObject(setting.value, workspaceFolder, preset, resolvePaths); + } + + processedVendor["espressif/vscode-esp-idf"].settings.push(processedSetting); + } + + return processedVendor; +} + +/** + * Processes object values in vendor settings + */ +async function processConfigurePresetSettingObject( + obj: any, + workspaceFolder: Uri, + preset: ConfigurePreset, + resolvePaths: boolean +): Promise { + const processedObj: any = {}; + + for (const [key, value] of Object.entries(obj)) { + if (typeof value === "string") { + processedObj[key] = substituteVariablesInConfigurePreset(value, workspaceFolder, preset) || value; + } else if (Array.isArray(value)) { + processedObj[key] = value.map(item => + typeof item === "string" + ? substituteVariablesInConfigurePreset(item, workspaceFolder, preset) || item + : item + ); + } else { + processedObj[key] = value; + } + } + + return processedObj; +} + +/** + * Processes variable substitution and path resolution for ProjectConfElement (legacy compatibility) + */ +async function processProjectConfElementVariables( + element: ProjectConfElement, + workspaceFolder: Uri, + resolvePaths: boolean +): Promise { + // Create a temporary raw config object for variable substitution + const rawConfig = { + build: element.build, + env: element.env, + idfTarget: element.idfTarget, + flashBaudRate: element.flashBaudRate, + monitorBaudRate: element.monitorBaudRate, + openOCD: element.openOCD, + tasks: element.tasks, + }; + + return processLegacyProjectConfig(rawConfig, workspaceFolder, resolvePaths); +} + +/** + * Substitutes variables like ${workspaceFolder} and ${env:VARNAME} in a string for ConfigurePreset. + * @param text The input string potentially containing variables. + * @param workspaceFolder The workspace folder Uri to resolve ${workspaceFolder}. + * @param preset The ConfigurePreset to resolve ${config:VARNAME} variables. + * @returns The string with variables substituted, or undefined if input was undefined/null. + */ +function substituteVariablesInConfigurePreset( + text: string | undefined, + workspaceFolder: Uri, + preset: ConfigurePreset +): string | undefined { + if (text === undefined || text === null) { + return undefined; + } + + let result = text; + + const regexp = /\$\{(.*?)\}/g; // Find ${anything} + result = result.replace(regexp, (match: string, name: string) => { + if (match.indexOf("config:") > 0) { + const configVar = name.substring( + name.indexOf("config:") + "config:".length + ); + + const delimiterIndex = configVar.indexOf(","); + let configVarName = configVar; + let prefix = ""; + + // Check if a delimiter (e.g., ",") is present + if (delimiterIndex > -1) { + configVarName = configVar.substring(0, delimiterIndex); + prefix = configVar.substring(delimiterIndex + 1).trim(); + } + + const configVarValue = getConfigurePresetParameterValue(configVarName, preset); + + if (!configVarValue) { + return match; + } + + if (prefix && Array.isArray(configVarValue)) { + return configVarValue.map((value) => `${prefix}${value}`).join(" "); + } + + if (prefix && typeof configVarValue === "string") { + return `${prefix} ${configVarValue}`; + } + + return configVarValue; + } + if (match.indexOf("env:") > 0) { + const envVarName = name.substring(name.indexOf("env:") + "env:".length); + if (preset.environment && preset.environment[envVarName]) { + return preset.environment[envVarName]; + } + if (process.env[envVarName]) { + return process.env[envVarName]; + } + return match; + } + if (match.indexOf("workspaceRoot") > 0) { + return workspaceFolder.fsPath; + } + if (match.indexOf("workspaceFolder") > 0) { + return workspaceFolder.fsPath; + } + return match; + }); + + // Substitute ${config:VARNAME} + result = resolveVariables(result, workspaceFolder); + + return result; +} + +/** + * Gets parameter value from ConfigurePreset for variable substitution + */ +function getConfigurePresetParameterValue(param: string, preset: ConfigurePreset): any { + switch (param) { + case "idf.cmakeCompilerArgs": + return getESPIDFSettingValue(preset, "compileArgs") || ""; + case "idf.ninjaArgs": + return getESPIDFSettingValue(preset, "ninjaArgs") || ""; + case "idf.buildPath": + return preset.binaryDir || ""; + case "idf.sdkconfigDefaults": + const sdkconfigDefaults = preset.cacheVariables?.SDKCONFIG_DEFAULTS; + return sdkconfigDefaults ? sdkconfigDefaults.split(";") : ""; + case "idf.flashBaudRate": + return getESPIDFSettingValue(preset, "flashBaudRate") || ""; + case "idf.monitorBaudRate": + return getESPIDFSettingValue(preset, "monitorBaudRate") || ""; + case "idf.openOcdDebugLevel": + const openOCDSettings = getESPIDFSettingValue(preset, "openOCD"); + return openOCDSettings?.debugLevel && openOCDSettings.debugLevel > -1 + ? openOCDSettings.debugLevel.toString() + : ""; + case "idf.openOcdConfigs": + const openOCDConfigs = getESPIDFSettingValue(preset, "openOCD"); + return openOCDConfigs?.configs && openOCDConfigs.configs.length + ? openOCDConfigs.configs + : ""; + case "idf.openOcdLaunchArgs": + const openOCDArgs = getESPIDFSettingValue(preset, "openOCD"); + return openOCDArgs?.args && openOCDArgs.args.length + ? openOCDArgs.args + : ""; + case "idf.preBuildTask": + const preBuildTask = getESPIDFSettingValue(preset, "tasks"); + return preBuildTask?.preBuild || ""; + case "idf.postBuildTask": + const postBuildTask = getESPIDFSettingValue(preset, "tasks"); + return postBuildTask?.postBuild || ""; + case "idf.preFlashTask": + const preFlashTask = getESPIDFSettingValue(preset, "tasks"); + return preFlashTask?.preFlash || ""; + case "idf.postFlashTask": + const postFlashTask = getESPIDFSettingValue(preset, "tasks"); + return postFlashTask?.postFlash || ""; + case "idf.sdkconfigFilePath": + return preset.cacheVariables?.SDKCONFIG || ""; + default: + return ""; + } +} + +/** + * Helper function to get ESP-IDF setting value from ConfigurePreset + */ +function getESPIDFSettingValue(preset: ConfigurePreset, settingType: string): any { + const espIdfSettings = preset.vendor?.["espressif/vscode-esp-idf"]?.settings || []; + const setting = espIdfSettings.find(s => s.type === settingType); + return setting ? setting.value : undefined; +} + +/** + * Converts ConfigurePreset to ProjectConfElement for store compatibility + */ +export function configurePresetToProjectConfElement(preset: ConfigurePreset): ProjectConfElement { + return convertConfigurePresetToProjectConfElement(preset, Uri.file(""), false); +} + +/** + * Converts ProjectConfElement to ConfigurePreset for store compatibility + */ +export function projectConfElementToConfigurePreset(name: string, element: ProjectConfElement): ConfigurePreset { + return convertProjectConfElementToConfigurePreset(name, element); } /** @@ -508,3 +988,102 @@ export async function getProjectConfigurationElements( function isDefined(value: T | undefined): value is T { return value !== undefined; } + +/** + * Converts a CMakePresets ConfigurePreset to the legacy ProjectConfElement format + */ +function convertConfigurePresetToProjectConfElement( + preset: ConfigurePreset, + workspaceFolder: Uri, + resolvePaths: boolean = false +): ProjectConfElement { + // Extract ESP-IDF specific settings from vendor section + const espIdfSettings = preset.vendor?.["espressif/vscode-esp-idf"]?.settings || []; + + // Helper function to find setting by type + const findSetting = (type: string): any => { + const setting = espIdfSettings.find(s => s.type === type); + return setting ? setting.value : undefined; + }; + + // Extract values with defaults + const compileArgs = findSetting("compileArgs") || []; + const ninjaArgs = findSetting("ninjaArgs") || []; + const flashBaudRate = findSetting("flashBaudRate") || ""; + const monitorBaudRate = findSetting("monitorBaudRate") || ""; + const openOCDSettings = findSetting("openOCD") || { debugLevel: -1, configs: [], args: [] }; + const taskSettings = findSetting("tasks") || { preBuild: "", preFlash: "", postBuild: "", postFlash: "" }; + + // Process paths based on resolvePaths flag + const binaryDir = preset.binaryDir || ""; + const buildDirectoryPath = resolvePaths && binaryDir + ? (path.isAbsolute(binaryDir) ? binaryDir : path.join(workspaceFolder.fsPath, binaryDir)) + : binaryDir; + + // Process SDKCONFIG_DEFAULTS - convert semicolon-separated string to array + const sdkconfigDefaultsStr = preset.cacheVariables?.SDKCONFIG_DEFAULTS || ""; + const sdkconfigDefaults = sdkconfigDefaultsStr ? sdkconfigDefaultsStr.split(";") : []; + + return { + build: { + compileArgs: Array.isArray(compileArgs) ? compileArgs : [], + ninjaArgs: Array.isArray(ninjaArgs) ? ninjaArgs : [], + buildDirectoryPath, + sdkconfigDefaults, + sdkconfigFilePath: preset.cacheVariables?.SDKCONFIG || "", + }, + env: preset.environment || {}, + idfTarget: preset.cacheVariables?.IDF_TARGET || "", + flashBaudRate, + monitorBaudRate, + openOCD: { + debugLevel: openOCDSettings.debugLevel || -1, + configs: Array.isArray(openOCDSettings.configs) ? openOCDSettings.configs : [], + args: Array.isArray(openOCDSettings.args) ? openOCDSettings.args : [], + }, + tasks: { + preBuild: taskSettings.preBuild || "", + preFlash: taskSettings.preFlash || "", + postBuild: taskSettings.postBuild || "", + postFlash: taskSettings.postFlash || "", + }, + }; +} + +/** + * Converts a ProjectConfElement to CMakePresets ConfigurePreset format + */ +function convertProjectConfElementToConfigurePreset( + name: string, + element: ProjectConfElement +): ConfigurePreset { + // Convert SDKCONFIG_DEFAULTS array to semicolon-separated string + const sdkconfigDefaults = element.build.sdkconfigDefaults.length > 0 + ? element.build.sdkconfigDefaults.join(";") + : undefined; + + const settings: ESPIDFSettings[] = [ + { type: "compileArgs", value: element.build.compileArgs }, + { type: "ninjaArgs", value: element.build.ninjaArgs }, + { type: "flashBaudRate", value: element.flashBaudRate }, + { type: "monitorBaudRate", value: element.monitorBaudRate }, + { type: "openOCD", value: element.openOCD }, + { type: "tasks", value: element.tasks }, + ]; + + return { + name, + binaryDir: element.build.buildDirectoryPath || undefined, + cacheVariables: { + ...(element.idfTarget && { IDF_TARGET: element.idfTarget }), + ...(sdkconfigDefaults && { SDKCONFIG_DEFAULTS: sdkconfigDefaults }), + ...(element.build.sdkconfigFilePath && { SDKCONFIG: element.build.sdkconfigFilePath }), + }, + environment: Object.keys(element.env).length > 0 ? element.env : undefined, + vendor: { + "espressif/vscode-esp-idf": { + settings, + }, + }, + }; +} diff --git a/src/project-conf/projectConfPanel.ts b/src/project-conf/projectConfPanel.ts index 41fdbb1ea..46ac1cd9b 100644 --- a/src/project-conf/projectConfPanel.ts +++ b/src/project-conf/projectConfPanel.ts @@ -28,7 +28,7 @@ import { } from "vscode"; import { join } from "path"; import { ESP } from "../config"; -import { getProjectConfigurationElements, saveProjectConfFile } from "."; +import { getProjectConfigurationElements, saveProjectConfFileLegacy, configurePresetToProjectConfElement, projectConfElementToConfigurePreset } from "."; import { IdfTarget } from "../espIdf/setTarget/getTargets"; export class projectConfigurationPanel { @@ -96,10 +96,17 @@ export class projectConfigurationPanel { this.panel.webview.html = this.createSetupHtml(scriptPath); this.panel.webview.onDidReceiveMessage(async (message) => { - let projectConfObj = await getProjectConfigurationElements( + let projectConfPresets = await getProjectConfigurationElements( this.workspaceFolder, false // Don't resolve paths for display ); + + // Convert ConfigurePresets to legacy format for webview compatibility + let projectConfObj: { [key: string]: ProjectConfElement } = {}; + for (const [name, preset] of Object.entries(projectConfPresets)) { + projectConfObj[name] = configurePresetToProjectConfElement(preset); + } + switch (message.command) { case "command": break; @@ -185,7 +192,7 @@ export class projectConfigurationPanel { }) { const projectConfKeys = Object.keys(projectConfDict); this.clearSelectedProject(projectConfKeys); - await saveProjectConfFile(this.workspaceFolder, projectConfDict); + await saveProjectConfFileLegacy(this.workspaceFolder, projectConfDict); window.showInformationMessage( "Project Configuration changes has been saved" ); diff --git a/src/project-conf/projectConfiguration.ts b/src/project-conf/projectConfiguration.ts index ed33d8eac..cf541e845 100644 --- a/src/project-conf/projectConfiguration.ts +++ b/src/project-conf/projectConfiguration.ts @@ -16,6 +16,7 @@ * limitations under the License. */ +// Legacy interface for backward compatibility export interface ProjectConfElement { build: { compileArgs: string[]; @@ -40,3 +41,47 @@ export interface ProjectConfElement { postFlash: string; }; } + +// New CMakePresets interfaces +export interface CMakeVersion { + major: number; + minor: number; + patch: number; +} + +export interface ESPIDFSettings { + type: "compileArgs" | "ninjaArgs" | "flashBaudRate" | "monitorBaudRate" | "openOCD" | "tasks"; + value: any; +} + +export interface ESPIDFVendorSettings { + "espressif/vscode-esp-idf": { + settings: ESPIDFSettings[]; + }; +} + +export interface ConfigurePreset { + name: string; + binaryDir?: string; + cacheVariables?: { + IDF_TARGET?: string; + SDKCONFIG_DEFAULTS?: string; + SDKCONFIG?: string; + [key: string]: any; + }; + environment?: { [key: string]: string }; + vendor?: ESPIDFVendorSettings; +} + +export interface BuildPreset { + name: string; + configurePreset: string; +} + +export interface CMakePresets { + $schema?: string; + version: number; + cmakeMinimumRequired?: CMakeVersion; + configurePresets?: ConfigurePreset[]; + buildPresets?: BuildPreset[]; // Optional - not used by ESP-IDF extension +} From 05b318c2f5b26b4aca9208f342ab9db8123303ac Mon Sep 17 00:00:00 2001 From: Radu Date: Tue, 9 Sep 2025 14:59:42 +0300 Subject: [PATCH 2/9] feature: add idf.py --preset --- package.json | 6 +++ src/build/buildTask.ts | 102 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/package.json b/package.json index ca3bbaf04..8ac99459f 100644 --- a/package.json +++ b/package.json @@ -753,6 +753,12 @@ "description": "%param.buildPath%", "scope": "resource" }, + "idf.useCMakePresets": { + "type": "boolean", + "default": false, + "description": "Use idf.py --preset build instead of direct CMake calls. Requires ESP-IDF with CMakePresets support.", + "scope": "resource" + }, "idf.buildPathWin": { "type": "string", "default": "${workspaceFolder}\\build", diff --git a/src/build/buildTask.ts b/src/build/buildTask.ts index fb590effb..360d1df3a 100644 --- a/src/build/buildTask.ts +++ b/src/build/buildTask.ts @@ -78,6 +78,22 @@ export class BuildTask { throw new Error("ALREADY_BUILDING"); } this.building(true); + + // Check if CMakePresets build mode is enabled + const useCMakePresets = idfConf.readParameter( + "idf.useCMakePresets", + this.currentWorkspace + ) as boolean; + + if (useCMakePresets) { + return await this.buildWithPresets(buildType); + } + + // Continue with traditional CMake build + return await this.buildWithCMake(buildType); + } + + private async buildWithCMake(buildType?: ESP.BuildType) { await ensureDir(this.buildDirPath); const modifiedEnv = await appendIdfAndToolsToPath(this.currentWorkspace); const processOptions = { @@ -290,4 +306,90 @@ export class BuildTask { buildPresentationOptions ); } + + private async buildWithPresets(buildType?: ESP.BuildType) { + await ensureDir(this.buildDirPath); + const modifiedEnv = await appendIdfAndToolsToPath(this.currentWorkspace); + + // Get the selected project configuration preset name + const selectedConfig = ESP.ProjectConfiguration.store.get( + ESP.ProjectConfiguration.SELECTED_CONFIG + ); + + if (!selectedConfig) { + throw new Error( + "No project configuration selected. Please select a CMakePresets configuration first." + ); + } + + const currentWorkspaceFolder = vscode.workspace.workspaceFolders.find( + (w) => w.uri === this.currentWorkspace + ); + + const notificationMode = idfConf.readParameter( + "idf.notificationMode", + this.currentWorkspace + ) as string; + const showTaskOutput = + notificationMode === idfConf.NotificationMode.All || + notificationMode === idfConf.NotificationMode.Output + ? vscode.TaskRevealKind.Always + : vscode.TaskRevealKind.Silent; + + // Build idf.py command with preset + const pythonBinPath = await getVirtualEnvPythonPath(this.currentWorkspace); + const idfPy = join(this.idfPathDir, "tools", "idf.py"); + + let args = [idfPy, "--preset", selectedConfig]; + + // Add build type specific arguments if needed + if (buildType) { + switch (buildType) { + case ESP.BuildType.Bootloader: + args.push("bootloader"); + break; + case ESP.BuildType.PartitionTable: + args.push("partition-table"); + break; + default: + args.push("build"); + break; + } + } else { + args.push("build"); + } + + Logger.info(`Building with CMakePresets using: ${pythonBinPath} ${args.join(" ")}`); + + const processOptions = { + cwd: this.currentWorkspace.fsPath, + env: modifiedEnv, + }; + + const buildExecution = new vscode.ProcessExecution( + pythonBinPath, + args, + processOptions + ); + + const buildPresentationOptions = { + reveal: showTaskOutput, + showReuseMessage: false, + clear: false, + panel: vscode.TaskPanelKind.Shared, + } as vscode.TaskPresentationOptions; + + TaskManager.addTask( + { + type: "esp-idf", + command: `ESP-IDF Build (CMakePresets: ${selectedConfig})`, + taskId: "idf-build-presets-task", + }, + currentWorkspaceFolder || vscode.TaskScope.Workspace, + `ESP-IDF Build (CMakePresets: ${selectedConfig})`, + buildExecution, + ["espIdf"], + buildPresentationOptions + ); + } } From 76a0ba54e728a4bec8865a747c19a59c0da6dad5 Mon Sep 17 00:00:00 2001 From: Radu Date: Wed, 17 Sep 2025 16:37:28 +0300 Subject: [PATCH 3/9] feat: add JSON validation for local schema --- .../esp-idf-cmakepresets-schema-v1.json | 122 ++++++++++++++++++ package.json | 9 ++ 2 files changed, 131 insertions(+) create mode 100644 internal/com.espressif.idf.uploads/cmakepresets/esp-idf-cmakepresets-schema-v1.json diff --git a/internal/com.espressif.idf.uploads/cmakepresets/esp-idf-cmakepresets-schema-v1.json b/internal/com.espressif.idf.uploads/cmakepresets/esp-idf-cmakepresets-schema-v1.json new file mode 100644 index 000000000..951d9a354 --- /dev/null +++ b/internal/com.espressif.idf.uploads/cmakepresets/esp-idf-cmakepresets-schema-v1.json @@ -0,0 +1,122 @@ +{ + "$id": "https://dl.espressif.com/schemas/esp-idf-cmakepresets-schema-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "ESP-IDF CMakePresets Extension v1.0", + "description": "Extends the official CMakePresets schema (v3) with strict ESP-IDF vendor fields; requires CMake >= 3.21.", + "allOf": [ + { + "$ref": "https://raw.githubusercontent.com/Kitware/CMake/master/Help/manual/presets/schema.json" + }, + { + "type": "object", + "properties": { + "version": { + "const": 3, + "description": "CMake Presets format version. Must be 3." + }, + "cmakeMinimumRequired": { + "type": "object", + "properties": { + "major": { "type": "integer", "const": 3 }, + "minor": { "type": "integer", "minimum": 21 }, + "patch": { "type": "integer", "minimum": 0 } + }, + "required": ["major", "minor"] + }, + "configurePresets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "vendor": { + "type": "object", + "properties": { + "espressif/vscode-esp-idf": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "integer", + "enum": [1], + "description": "ESP-IDF vendor schema version." + }, + "settings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "compileArgs", + "ninjaArgs", + "flashBaudRate", + "monitorBaudRate", + "openOCD", + "tasks" + ] + }, + "value": {} + }, + "required": ["type", "value"], + "additionalProperties": false, + "allOf": [ + { + "if": { "properties": { "type": { "enum": ["compileArgs", "ninjaArgs"] } } }, + "then": { "properties": { "value": { "type": "array", "items": { "type": "string" } } } } + }, + { + "if": { "properties": { "type": { "enum": ["flashBaudRate", "monitorBaudRate"] } } }, + "then": { "properties": { "value": { "type": "string" } } } + }, + { + "if": { "properties": { "type": { "const": "openOCD" } } }, + "then": { + "properties": { + "value": { + "type": "object", + "properties": { + "debugLevel": { "type": "integer" }, + "configs": { "type": "array", "items": { "type": "string" } }, + "args": { "type": "array", "items": { "type": "string" } } + }, + "required": ["debugLevel", "configs", "args"], + "additionalProperties": false + } + } + } + }, + { + "if": { "properties": { "type": { "const": "tasks" } } }, + "then": { + "properties": { + "value": { + "type": "object", + "properties": { + "preBuild": { "type": "string" }, + "preFlash": { "type": "string" }, + "postBuild": { "type": "string" }, + "postFlash": { "type": "string" } + }, + "additionalProperties": false + } + } + } + } + ] + } + } + }, + "required": ["schemaVersion", "settings"], + "additionalProperties": false + } + }, + "additionalProperties": true + } + } + } + } + }, + "required": ["version", "cmakeMinimumRequired", "configurePresets"] + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 8ac99459f..beece7e5e 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,15 @@ "main": "./dist/extension", "l10n": "./l10n", "contributes": { + "jsonValidation": [ + { + "fileMatch": [ + "CMakePresets.json", + "CMakeUserPresets.json" + ], + "url": "./internal/com.espressif.idf.uploads/cmakepresets/esp-idf-cmakepresets-schema-v1.json" + } + ], "walkthroughs": [ { "id": "espIdf.walkthrough.basic-usage", From 4885b426490a3f2b5f4a5fdcee3f883b3c53d0d2 Mon Sep 17 00:00:00 2001 From: Radu Date: Wed, 17 Sep 2025 16:40:23 +0300 Subject: [PATCH 4/9] feat: add CMakeUserPresets.json support --- src/config.ts | 1 + .../ProjectConfigurationManager.ts | 226 ++++--- src/project-conf/index.ts | 568 ++++++++++++++---- src/project-conf/projectConfiguration.ts | 9 +- src/statusBar/index.ts | 16 +- 5 files changed, 635 insertions(+), 185 deletions(-) diff --git a/src/config.ts b/src/config.ts index f0db7df60..d62a4187e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,6 +31,7 @@ export namespace ESP { export let store: ProjectConfigStore; export const SELECTED_CONFIG = "SELECTED_PROJECT_CONFIG"; export const PROJECT_CONFIGURATION_FILENAME = "CMakePresets.json"; + export const USER_CONFIGURATION_FILENAME = "CMakeUserPresets.json"; } export enum BuildType { diff --git a/src/project-conf/ProjectConfigurationManager.ts b/src/project-conf/ProjectConfigurationManager.ts index 71d69388b..312fbaa94 100644 --- a/src/project-conf/ProjectConfigurationManager.ts +++ b/src/project-conf/ProjectConfigurationManager.ts @@ -20,7 +20,12 @@ import { CommandKeys, createCommandDictionary } from "../cmdTreeView/cmdStore"; import { createStatusBarItem } from "../statusBar"; import { getIdfTargetFromSdkconfig } from "../workspaceConfig"; import { Logger } from "../logger/logger"; -import { getProjectConfigurationElements, configurePresetToProjectConfElement, promptLegacyMigration, migrateLegacyConfiguration } from "./index"; +import { + getProjectConfigurationElements, + configurePresetToProjectConfElement, + promptLegacyMigration, + migrateLegacyConfiguration, +} from "./index"; import { pathExists } from "fs-extra"; import { configureClangSettings } from "../clang"; @@ -39,9 +44,11 @@ export function clearSelectedProjectConfiguration(): void { } export class ProjectConfigurationManager { - private readonly configFilePath: string; + private readonly cmakePresetsFilePath: string; + private readonly cmakeUserPresetsFilePath: string; private configVersions: string[] = []; - private configWatcher: FileSystemWatcher; + private cmakePresetsWatcher: FileSystemWatcher; + private cmakeUserPresetsWatcher: FileSystemWatcher; private statusBarItems: { [key: string]: StatusBarItem }; private workspaceUri: Uri; private context: ExtensionContext; @@ -57,13 +64,26 @@ export class ProjectConfigurationManager { this.statusBarItems = statusBarItems; this.commandDictionary = createCommandDictionary(); - this.configFilePath = Uri.joinPath( + this.cmakePresetsFilePath = Uri.joinPath( workspaceUri, ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME ).fsPath; - this.configWatcher = workspace.createFileSystemWatcher( - this.configFilePath, + this.cmakeUserPresetsFilePath = Uri.joinPath( + workspaceUri, + ESP.ProjectConfiguration.USER_CONFIGURATION_FILENAME + ).fsPath; + + // Watch both CMakePresets.json and CMakeUserPresets.json + this.cmakePresetsWatcher = workspace.createFileSystemWatcher( + this.cmakePresetsFilePath, + false, + false, + false + ); + + this.cmakeUserPresetsWatcher = workspace.createFileSystemWatcher( + this.cmakeUserPresetsFilePath, false, false, false @@ -75,26 +95,23 @@ export class ProjectConfigurationManager { } private async initialize(): Promise { - if (!fileExists(this.configFilePath)) { - // CMakePresets.json doesn't exist - check for legacy file + const cmakePresetsExists = fileExists(this.cmakePresetsFilePath); + const cmakeUserPresetsExists = fileExists(this.cmakeUserPresetsFilePath); + + if (!cmakePresetsExists && !cmakeUserPresetsExists) { + // Neither CMakePresets.json nor CMakeUserPresets.json exists - check for legacy file await this.checkForLegacyFile(); return; } try { - const configContent = readFileSync(this.configFilePath); - - // Handle edge case: File exists but is empty - if (!configContent || configContent.trim() === "") { - Logger.warn( - `Project configuration file is empty: ${this.configFilePath}` - ); - this.configVersions = []; - return; - } + // Use the updated getProjectConfigurationElements function that handles both files + const projectConfElements = await getProjectConfigurationElements( + this.workspaceUri, + false // Don't resolve paths for initialization + ); - const configData = JSON.parse(configContent); - this.configVersions = Object.keys(configData); + this.configVersions = Object.keys(projectConfElements); // Check if the currently selected configuration is valid const currentSelectedConfig = ESP.ProjectConfiguration.store.get( @@ -116,25 +133,31 @@ export class ProjectConfigurationManager { this.setNoConfigurationSelectedStatus(); } else if (this.configVersions.length > 0) { // No current selection but configurations exist + const fileInfo = []; + if (cmakePresetsExists) fileInfo.push("CMakePresets.json"); + if (cmakeUserPresetsExists) fileInfo.push("CMakeUserPresets.json"); + window.showInformationMessage( `Loaded ${ this.configVersions.length - } project configuration(s): ${this.configVersions.join(", ")}` + } project configuration(s) from ${fileInfo.join( + " and " + )}: ${this.configVersions.join(", ")}` ); this.setNoConfigurationSelectedStatus(); } else { - // Empty configuration file + // No configurations found Logger.info( - `Project configuration file loaded but contains no configurations: ${this.configFilePath}` + `Project configuration files loaded but contain no configurations` ); this.setNoConfigurationSelectedStatus(); } } catch (error) { window.showErrorMessage( - `Error reading or parsing project configuration file (${this.configFilePath}): ${error.message}` + `Error reading or parsing project configuration files: ${error.message}` ); Logger.errorNotify( - `Failed to parse project configuration file: ${this.configFilePath}`, + `Failed to parse project configuration files`, error, "ProjectConfigurationManager initialize" ); @@ -144,32 +167,55 @@ export class ProjectConfigurationManager { } private registerEventHandlers(): void { - // Handle file changes - const changeDisposable = this.configWatcher.onDidChange( + // Handle CMakePresets.json file changes + const cmakePresetsChangeDisposable = this.cmakePresetsWatcher.onDidChange( async () => await this.handleConfigFileChange() ); - // Handle file deletion - const deleteDisposable = this.configWatcher.onDidDelete( + // Handle CMakePresets.json file deletion + const cmakePresetsDeleteDisposable = this.cmakePresetsWatcher.onDidDelete( async () => await this.handleConfigFileDelete() ); - // Handle file creation - const createDisposable = this.configWatcher.onDidCreate( + // Handle CMakePresets.json file creation + const cmakePresetsCreateDisposable = this.cmakePresetsWatcher.onDidCreate( + async () => await this.handleConfigFileCreate() + ); + + // Handle CMakeUserPresets.json file changes + const cmakeUserPresetsChangeDisposable = this.cmakeUserPresetsWatcher.onDidChange( + async () => await this.handleConfigFileChange() + ); + + // Handle CMakeUserPresets.json file deletion + const cmakeUserPresetsDeleteDisposable = this.cmakeUserPresetsWatcher.onDidDelete( + async () => await this.handleConfigFileDelete() + ); + + // Handle CMakeUserPresets.json file creation + const cmakeUserPresetsCreateDisposable = this.cmakeUserPresetsWatcher.onDidCreate( async () => await this.handleConfigFileCreate() ); this.context.subscriptions.push( - changeDisposable, - deleteDisposable, - createDisposable + cmakePresetsChangeDisposable, + cmakePresetsDeleteDisposable, + cmakePresetsCreateDisposable, + cmakeUserPresetsChangeDisposable, + cmakeUserPresetsDeleteDisposable, + cmakeUserPresetsCreateDisposable ); } private async handleConfigFileChange(): Promise { try { - const configData = await readJson(this.configFilePath); - const currentVersions = Object.keys(configData); + // Use the updated getProjectConfigurationElements function that handles both files + const projectConfElements = await getProjectConfigurationElements( + this.workspaceUri, + false // Don't resolve paths for change handling + ); + + const currentVersions = Object.keys(projectConfElements); // Find added versions const addedVersions = currentVersions.filter( @@ -183,13 +229,13 @@ export class ProjectConfigurationManager { if (addedVersions.length > 0) { window.showInformationMessage( - `New versions added: ${addedVersions.join(", ")}` + `New configurations added: ${addedVersions.join(", ")}` ); } if (removedVersions.length > 0) { window.showInformationMessage( - `Versions removed: ${removedVersions.join(", ")}` + `Configurations removed: ${removedVersions.join(", ")}` ); } @@ -220,7 +266,9 @@ export class ProjectConfigurationManager { this.setNoConfigurationSelectedStatus(); } } catch (error) { - window.showErrorMessage(`Error parsing config file: ${error.message}`); + window.showErrorMessage( + `Error parsing configuration files: ${error.message}` + ); this.setNoConfigurationSelectedStatus(); } } @@ -255,8 +303,13 @@ export class ProjectConfigurationManager { private async handleConfigFileCreate(): Promise { try { - const configData = await readJson(this.configFilePath); - this.configVersions = Object.keys(configData); + // Use the updated getProjectConfigurationElements function that handles both files + const projectConfElements = await getProjectConfigurationElements( + this.workspaceUri, + false // Don't resolve paths for creation handling + ); + + this.configVersions = Object.keys(projectConfElements); // If we have versions, check if current selection is valid if (this.configVersions.length > 0) { @@ -291,7 +344,7 @@ export class ProjectConfigurationManager { } } catch (error) { window.showErrorMessage( - `Error parsing newly created config file: ${error.message}` + `Error parsing newly created configuration file: ${error.message}` ); this.setNoConfigurationSelectedStatus(); } @@ -334,9 +387,11 @@ export class ProjectConfigurationManager { this.workspaceUri, true // Resolve paths for building ); - + // Convert ConfigurePreset to ProjectConfElement for store compatibility - const legacyElement = configurePresetToProjectConfElement(resolvedConfig[configName]); + const legacyElement = configurePresetToProjectConfElement( + resolvedConfig[configName] + ); ESP.ProjectConfiguration.store.set(configName, legacyElement); // Update UI @@ -380,8 +435,11 @@ export class ProjectConfigurationManager { Object.keys(projectConfigurations).length === 0 ) { // Check if we have legacy configurations to migrate - const legacyFilePath = Uri.joinPath(this.workspaceUri, "esp_idf_project_configuration.json"); - + const legacyFilePath = Uri.joinPath( + this.workspaceUri, + "esp_idf_project_configuration.json" + ); + if (await pathExists(legacyFilePath.fsPath)) { // Show migration dialog await this.handleLegacyMigrationDialog(legacyFilePath); @@ -430,32 +488,37 @@ export class ProjectConfigurationManager { * Checks for legacy esp_idf_project_configuration.json file and shows appropriate status */ private async checkForLegacyFile(): Promise { - const legacyFilePath = Uri.joinPath(this.workspaceUri, "esp_idf_project_configuration.json").fsPath; - + const legacyFilePath = Uri.joinPath( + this.workspaceUri, + "esp_idf_project_configuration.json" + ).fsPath; + if (fileExists(legacyFilePath)) { // Legacy file exists - show status bar with migration option this.configVersions = []; - + try { const legacyContent = readFileSync(legacyFilePath); if (legacyContent && legacyContent.trim() !== "") { const legacyData = JSON.parse(legacyContent); const legacyConfigNames = Object.keys(legacyData); - + if (legacyConfigNames.length > 0) { // Show status bar indicating legacy configurations are available this.setLegacyConfigurationStatus(legacyConfigNames); - + // Show migration notification this.showLegacyMigrationNotification(legacyConfigNames); return; } } } catch (error) { - Logger.warn(`Failed to parse legacy configuration file: ${error.message}`); + Logger.warn( + `Failed to parse legacy configuration file: ${error.message}` + ); } } - + // No configuration files found - clear everything this.clearConfigurationState(); } @@ -465,7 +528,9 @@ export class ProjectConfigurationManager { */ private setLegacyConfigurationStatus(legacyConfigNames: string[]): void { const statusBarItemName = `Legacy Configs (${legacyConfigNames.length})`; - const statusBarItemTooltip = `Found legacy project configurations: ${legacyConfigNames.join(", ")}. Click to migrate to CMakePresets.json format.`; + const statusBarItemTooltip = `Found legacy project configurations: ${legacyConfigNames.join( + ", " + )}. Click to migrate to CMakePresets.json format.`; const commandToUse = "espIdf.projectConf"; if (this.statusBarItems["projectConf"]) { @@ -487,25 +552,30 @@ export class ProjectConfigurationManager { /** * Shows notification about legacy configurations */ - private async showLegacyMigrationNotification(legacyConfigNames: string[]): Promise { + private async showLegacyMigrationNotification( + legacyConfigNames: string[] + ): Promise { const message = l10n.t( "Found {0} legacy project configuration(s): {1}. Would you like to migrate them to the new CMakePresets.json format? Your original file will remain unchanged.", legacyConfigNames.length, legacyConfigNames.join(", ") ); - + const migrateOption = l10n.t("Migrate Now"); const laterOption = l10n.t("Later"); - + const choice = await window.showInformationMessage( message, migrateOption, laterOption ); - + if (choice === migrateOption) { // Directly perform migration without additional popup - const legacyFilePath = Uri.joinPath(this.workspaceUri, "esp_idf_project_configuration.json"); + const legacyFilePath = Uri.joinPath( + this.workspaceUri, + "esp_idf_project_configuration.json" + ); await this.performDirectMigration(legacyFilePath); } } @@ -513,28 +583,30 @@ export class ProjectConfigurationManager { /** * Handles the legacy migration dialog when user clicks on project configuration */ - private async handleLegacyMigrationDialog(legacyFilePath: Uri): Promise { + private async handleLegacyMigrationDialog( + legacyFilePath: Uri + ): Promise { try { const legacyContent = readFileSync(legacyFilePath.fsPath); const legacyData = JSON.parse(legacyContent); const legacyConfigNames = Object.keys(legacyData); - + const message = l10n.t( "Found {0} legacy project configuration(s): {1}. Would you like to migrate them to the new CMakePresets.json format?", legacyConfigNames.length, legacyConfigNames.join(", ") ); - + const migrateOption = l10n.t("Migrate Now"); const cancelOption = l10n.t("Cancel"); - + const choice = await window.showInformationMessage( message, { modal: true }, migrateOption, cancelOption ); - + if (choice === migrateOption) { await this.performMigration(legacyFilePath); } @@ -545,7 +617,10 @@ export class ProjectConfigurationManager { "handleLegacyMigrationDialog" ); window.showErrorMessage( - l10n.t("Failed to process legacy configuration file: {0}", error.message) + l10n.t( + "Failed to process legacy configuration file: {0}", + error.message + ) ); } } @@ -556,12 +631,14 @@ export class ProjectConfigurationManager { private async performMigration(legacyFilePath: Uri): Promise { try { await promptLegacyMigration(this.workspaceUri, legacyFilePath); - + // After migration, reinitialize to show the new configurations await this.initialize(); - + window.showInformationMessage( - l10n.t("Project configurations successfully migrated to CMakePresets.json format!") + l10n.t( + "Project configurations successfully migrated to CMakePresets.json format!" + ) ); } catch (error) { Logger.errorNotify( @@ -581,12 +658,14 @@ export class ProjectConfigurationManager { private async performDirectMigration(legacyFilePath: Uri): Promise { try { await migrateLegacyConfiguration(this.workspaceUri, legacyFilePath); - + // After migration, reinitialize to show the new configurations await this.initialize(); - + window.showInformationMessage( - l10n.t("Project configurations successfully migrated to CMakePresets.json format!") + l10n.t( + "Project configurations successfully migrated to CMakePresets.json format!" + ) ); } catch (error) { Logger.errorNotify( @@ -625,9 +704,10 @@ export class ProjectConfigurationManager { } /** - * Dispose of the file system watcher + * Dispose of the file system watchers */ public dispose(): void { - this.configWatcher.dispose(); + this.cmakePresetsWatcher.dispose(); + this.cmakeUserPresetsWatcher.dispose(); } } diff --git a/src/project-conf/index.ts b/src/project-conf/index.ts index 70ef4208e..bd01f9847 100644 --- a/src/project-conf/index.ts +++ b/src/project-conf/index.ts @@ -20,7 +20,14 @@ import * as path from "path"; import { ExtensionContext, Uri, window, l10n } from "vscode"; import { ESP } from "../config"; import { pathExists, readJson, writeJson } from "fs-extra"; -import { ProjectConfElement, CMakePresets, ConfigurePreset, BuildPreset, ESPIDFSettings, ESPIDFVendorSettings } from "./projectConfiguration"; +import { + ProjectConfElement, + CMakePresets, + ConfigurePreset, + BuildPreset, + ESPIDFSettings, + ESPIDFVendorSettings, +} from "./projectConfiguration"; import { Logger } from "../logger/logger"; import { resolveVariables } from "../idfConfiguration"; @@ -76,7 +83,7 @@ export async function updateCurrentProfileIdfTarget( ); return; } - + // Update IDF_TARGET in cacheVariables for ConfigurePreset if (!projectConfJson[selectedConfig].cacheVariables) { projectConfJson[selectedConfig].cacheVariables = {}; @@ -100,7 +107,9 @@ export async function saveProjectConfFile( ); // Use ConfigurePreset objects directly - const configurePresets: ConfigurePreset[] = Object.values(projectConfElements); + const configurePresets: ConfigurePreset[] = Object.values( + projectConfElements + ); const cmakePresets: CMakePresets = { version: 1, @@ -124,7 +133,9 @@ export async function saveProjectConfFileLegacy( ); // Convert to CMakePresets format - const configurePresets: ConfigurePreset[] = Object.keys(projectConfElements).map(name => + const configurePresets: ConfigurePreset[] = Object.keys( + projectConfElements + ).map((name) => convertProjectConfElementToConfigurePreset(name, projectConfElements[name]) ); @@ -248,7 +259,10 @@ function substituteVariablesInString( configVarName = configVar.substring(0, delimiterIndex); prefix = configVar.substring(delimiterIndex + 1).trim(); } - const configVarValue = parameterToSameProjectConfigMap(configVarName, config); + const configVarValue = parameterToSameProjectConfigMap( + configVarName, + config + ); if (!configVarValue) { return match; @@ -352,8 +366,8 @@ function resolveConfigPaths( // --- Main Function --- /** - * Reads the CMakePresets.json file, performs variable substitution - * on relevant fields, resolves paths, and returns the structured configuration. + * Reads both CMakePresets.json and CMakeUserPresets.json files, performs variable substitution + * on relevant fields, resolves paths, and returns the merged structured configuration. * @param workspaceFolder The Uri of the current workspace folder. * @param resolvePaths Whether to resolve paths to absolute paths (true for building, false for display) * @returns An object mapping configuration names to their processed ConfigurePreset. @@ -362,44 +376,274 @@ export async function getProjectConfigurationElements( workspaceFolder: Uri, resolvePaths: boolean = false ): Promise<{ [key: string]: ConfigurePreset }> { - const projectConfFilePath = Uri.joinPath( + const allRawPresets: { [key: string]: ConfigurePreset } = {}; + + // Read CMakePresets.json + const cmakePresetsFilePath = Uri.joinPath( workspaceFolder, ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME ); - const doesPathExists = await pathExists(projectConfFilePath.fsPath); - if (!doesPathExists) { - // Check if legacy file exists and prompt for migration + // Read CMakeUserPresets.json + const cmakeUserPresetsFilePath = Uri.joinPath( + workspaceFolder, + ESP.ProjectConfiguration.USER_CONFIGURATION_FILENAME + ); + + const cmakePresetsExists = await pathExists(cmakePresetsFilePath.fsPath); + const cmakeUserPresetsExists = await pathExists( + cmakeUserPresetsFilePath.fsPath + ); + + // If neither file exists, check for legacy file + if (!cmakePresetsExists && !cmakeUserPresetsExists) { await checkAndPromptLegacyMigration(workspaceFolder); return {}; } - let projectConfJson; - try { - projectConfJson = await readJson(projectConfFilePath.fsPath); - if (typeof projectConfJson !== "object" || projectConfJson === null) { - throw new Error("Configuration file content is not a valid JSON object."); + // First pass: Load all raw presets from both files without processing inheritance + if (cmakePresetsExists) { + try { + const cmakePresetsJson = await readJson(cmakePresetsFilePath.fsPath); + if (typeof cmakePresetsJson === "object" && cmakePresetsJson !== null) { + const presets = await loadRawConfigurationFile( + cmakePresetsJson, + "CMakePresets.json" + ); + Object.assign(allRawPresets, presets); + } + } catch (error) { + Logger.errorNotify( + `Failed to read or parse ${ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME}`, + error, + "getProjectConfigurationElements" + ); + window.showErrorMessage( + `Error reading or parsing CMakePresets.json file (${cmakePresetsFilePath.fsPath}): ${error.message}` + ); } - } catch (error) { - Logger.errorNotify( - `Failed to read or parse ${ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME}`, - error, - "getProjectConfigurationElements" + } + + if (cmakeUserPresetsExists) { + try { + const cmakeUserPresetsJson = await readJson( + cmakeUserPresetsFilePath.fsPath + ); + if ( + typeof cmakeUserPresetsJson === "object" && + cmakeUserPresetsJson !== null + ) { + const presets = await loadRawConfigurationFile( + cmakeUserPresetsJson, + "CMakeUserPresets.json" + ); + // User presets override project presets with the same name + Object.assign(allRawPresets, presets); + } + } catch (error) { + Logger.errorNotify( + `Failed to read or parse ${ESP.ProjectConfiguration.USER_CONFIGURATION_FILENAME}`, + error, + "getProjectConfigurationElements" + ); + window.showErrorMessage( + `Error reading or parsing CMakeUserPresets.json file (${cmakeUserPresetsFilePath.fsPath}): ${error.message}` + ); + } + } + + // Second pass: Resolve inheritance and process variables + const processedPresets: { [key: string]: ConfigurePreset } = {}; + for (const [name, preset] of Object.entries(allRawPresets)) { + try { + const resolvedPreset = await resolvePresetInheritance( + preset, + allRawPresets + ); + const processedPreset = await processConfigurePresetVariables( + resolvedPreset, + workspaceFolder, + resolvePaths + ); + processedPresets[name] = processedPreset; + } catch (error) { + Logger.warn( + `Failed to process configure preset "${name}": ${error.message}`, + error + ); + } + } + + return processedPresets; +} + +/** + * Loads raw presets from a configuration file without processing inheritance or variables + * @param configJson The parsed JSON content of the configuration file + * @param fileName The name of the file being processed (for error messages) + * @returns An object mapping configuration names to their raw ConfigurePreset + */ +async function loadRawConfigurationFile( + configJson: any, + fileName: string +): Promise<{ [key: string]: ConfigurePreset }> { + const rawPresets: { [key: string]: ConfigurePreset } = {}; + + // Only support CMakePresets format + if (configJson.version !== undefined && configJson.configurePresets) { + const cmakePresets = configJson as CMakePresets; + + if ( + !cmakePresets.configurePresets || + cmakePresets.configurePresets.length === 0 + ) { + return {}; + } + + // Load each configure preset without processing + for (const preset of cmakePresets.configurePresets) { + rawPresets[preset.name] = { ...preset }; + } + } else { + // This might be a legacy file that wasn't migrated + Logger.warn( + `Invalid ${fileName} format detected. Expected 'version' and 'configurePresets' fields.`, + new Error("Invalid CMakePresets format") ); window.showErrorMessage( - `Error reading or parsing CMakePresets.json file (${projectConfFilePath.fsPath}): ${error.message}` + `Invalid ${fileName} format. Please ensure the file follows the CMakePresets specification.` + ); + } + + return rawPresets; +} + +/** + * Resolves inheritance for a preset by merging it with its parent presets + * @param preset The preset to resolve inheritance for + * @param allPresets All available presets (for inheritance lookup) + * @returns The preset with inheritance resolved + */ +async function resolvePresetInheritance( + preset: ConfigurePreset, + allPresets: { [key: string]: ConfigurePreset } +): Promise { + // If no inheritance, return as-is + if (!preset.inherits) { + return { ...preset }; + } + + // Handle both single string and array of strings for inherits + const parentNames = Array.isArray(preset.inherits) + ? preset.inherits + : [preset.inherits]; + + // Start with an empty base preset + let resolvedPreset: ConfigurePreset = { name: preset.name }; + + // Apply each parent preset in order + for (const parentName of parentNames) { + const parentPreset = allPresets[parentName]; + if (!parentPreset) { + Logger.warn( + `Preset "${preset.name}" inherits from "${parentName}" which was not found`, + new Error("Missing parent preset") + ); + continue; + } + + // Recursively resolve parent's inheritance first + const resolvedParent = await resolvePresetInheritance( + parentPreset, + allPresets ); - return {}; // Return empty if JSON is invalid or unreadable + + // Merge parent into resolved preset + resolvedPreset = mergePresets(resolvedPreset, resolvedParent); + } + + // Finally, merge the current preset (child overrides parent) + resolvedPreset = mergePresets(resolvedPreset, preset); + + // Remove the inherits property from the final result + delete resolvedPreset.inherits; + + return resolvedPreset; +} + +/** + * Merges two presets, with the child preset overriding the parent preset + * @param parent The parent preset + * @param child The child preset (takes precedence) + * @returns The merged preset + */ +function mergePresets( + parent: ConfigurePreset, + child: ConfigurePreset +): ConfigurePreset { + const merged: ConfigurePreset = { ...parent }; + + // Merge basic properties + if (child.name !== undefined) merged.name = child.name; + if (child.binaryDir !== undefined) merged.binaryDir = child.binaryDir; + if (child.inherits !== undefined) merged.inherits = child.inherits; + + // Merge cacheVariables (child overrides parent) + if (child.cacheVariables || parent.cacheVariables) { + merged.cacheVariables = { + ...(parent.cacheVariables || {}), + ...(child.cacheVariables || {}), + }; + } + + // Merge environment (child overrides parent) + if (child.environment || parent.environment) { + merged.environment = { + ...(parent.environment || {}), + ...(child.environment || {}), + }; + } + + // Merge vendor settings (child overrides parent) + if (child.vendor || parent.vendor) { + merged.vendor = { + "espressif/vscode-esp-idf": { + settings: [ + ...(parent.vendor?.["espressif/vscode-esp-idf"]?.settings || []), + ...(child.vendor?.["espressif/vscode-esp-idf"]?.settings || []), + ], + }, + }; } + return merged; +} + +/** + * Processes a single configuration file (CMakePresets.json or CMakeUserPresets.json) + * @param configJson The parsed JSON content of the configuration file + * @param workspaceFolder The workspace folder Uri + * @param resolvePaths Whether to resolve paths to absolute paths + * @param fileName The name of the file being processed (for error messages) + * @returns An object mapping configuration names to their processed ConfigurePreset + * @deprecated Use loadRawConfigurationFile and resolvePresetInheritance instead + */ +async function processConfigurationFile( + configJson: any, + workspaceFolder: Uri, + resolvePaths: boolean, + fileName: string +): Promise<{ [key: string]: ConfigurePreset }> { const projectConfElements: { [key: string]: ConfigurePreset } = {}; // Only support CMakePresets format - if (projectConfJson.version !== undefined && projectConfJson.configurePresets) { - // CMakePresets format - const cmakePresets = projectConfJson as CMakePresets; - - if (!cmakePresets.configurePresets || cmakePresets.configurePresets.length === 0) { + if (configJson.version !== undefined && configJson.configurePresets) { + const cmakePresets = configJson as CMakePresets; + + if ( + !cmakePresets.configurePresets || + cmakePresets.configurePresets.length === 0 + ) { return {}; } @@ -412,11 +656,11 @@ export async function getProjectConfigurationElements( workspaceFolder, resolvePaths ); - + projectConfElements[preset.name] = processedPreset; } catch (error) { Logger.warn( - `Failed to process configure preset "${preset.name}": ${error.message}`, + `Failed to process configure preset "${preset.name}" from ${fileName}: ${error.message}`, error ); } @@ -424,13 +668,12 @@ export async function getProjectConfigurationElements( } else { // This might be a legacy file that wasn't migrated Logger.warn( - `Invalid CMakePresets.json format detected. Expected 'version' and 'configurePresets' fields.`, + `Invalid ${fileName} format detected. Expected 'version' and 'configurePresets' fields.`, new Error("Invalid CMakePresets format") ); window.showErrorMessage( - `Invalid CMakePresets.json format. Please ensure the file follows the CMakePresets specification.` + `Invalid ${fileName} format. Please ensure the file follows the CMakePresets specification.` ); - return {}; } return projectConfElements; @@ -439,9 +682,14 @@ export async function getProjectConfigurationElements( /** * Checks for legacy project configuration file and prompts user for migration */ -async function checkAndPromptLegacyMigration(workspaceFolder: Uri): Promise { - const legacyFilePath = Uri.joinPath(workspaceFolder, "esp_idf_project_configuration.json"); - +async function checkAndPromptLegacyMigration( + workspaceFolder: Uri +): Promise { + const legacyFilePath = Uri.joinPath( + workspaceFolder, + "esp_idf_project_configuration.json" + ); + if (await pathExists(legacyFilePath.fsPath)) { await promptLegacyMigration(workspaceFolder, legacyFilePath); } @@ -450,23 +698,26 @@ async function checkAndPromptLegacyMigration(workspaceFolder: Uri): Promise { +export async function promptLegacyMigration( + workspaceFolder: Uri, + legacyFilePath: Uri +): Promise { const message = l10n.t( "A legacy project configuration file (esp_idf_project_configuration.json) was found. " + - "Would you like to migrate it to the new CMakePresets.json format? " + - "Your original file will remain unchanged." + "Would you like to migrate it to the new CMakePresets.json format? " + + "Your original file will remain unchanged." ); - + const migrateOption = l10n.t("Migrate"); const cancelOption = l10n.t("Cancel"); - + const choice = await window.showInformationMessage( message, { modal: true }, migrateOption, cancelOption ); - + if (choice === migrateOption) { await migrateLegacyConfiguration(workspaceFolder, legacyFilePath); } @@ -475,13 +726,16 @@ export async function promptLegacyMigration(workspaceFolder: Uri, legacyFilePath /** * Migrates legacy configuration to CMakePresets format */ -export async function migrateLegacyConfiguration(workspaceFolder: Uri, legacyFilePath: Uri): Promise { +export async function migrateLegacyConfiguration( + workspaceFolder: Uri, + legacyFilePath: Uri +): Promise { // Read legacy configuration const legacyConfig = await readJson(legacyFilePath.fsPath); - + // Convert to new format const projectConfElements: { [key: string]: ProjectConfElement } = {}; - + // Process legacy configurations for (const [confName, rawConfig] of Object.entries(legacyConfig)) { if (typeof rawConfig === "object" && rawConfig !== null) { @@ -500,14 +754,16 @@ export async function migrateLegacyConfiguration(workspaceFolder: Uri, legacyFil } } } - + // Save in new format using legacy compatibility function await saveProjectConfFileLegacy(workspaceFolder, projectConfElements); - - Logger.info(`Successfully migrated ${Object.keys(projectConfElements).length} configurations to CMakePresets.json`); -} - + Logger.info( + `Successfully migrated ${ + Object.keys(projectConfElements).length + } configurations to CMakePresets.json` + ); +} /** * Processes legacy project configuration format @@ -661,10 +917,38 @@ async function processConfigurePresetVariables( ): Promise { const processedPreset: ConfigurePreset = { ...preset, - binaryDir: preset.binaryDir ? await processConfigurePresetPath(preset.binaryDir, workspaceFolder, preset, resolvePaths) : undefined, - cacheVariables: preset.cacheVariables ? await processConfigurePresetCacheVariables(preset.cacheVariables, workspaceFolder, preset, resolvePaths) : undefined, - environment: preset.environment ? await processConfigurePresetEnvironment(preset.environment, workspaceFolder, preset, resolvePaths) : undefined, - vendor: preset.vendor ? await processConfigurePresetVendor(preset.vendor, workspaceFolder, preset, resolvePaths) : undefined, + binaryDir: preset.binaryDir + ? await processConfigurePresetPath( + preset.binaryDir, + workspaceFolder, + preset, + resolvePaths + ) + : undefined, + cacheVariables: preset.cacheVariables + ? await processConfigurePresetCacheVariables( + preset.cacheVariables, + workspaceFolder, + preset, + resolvePaths + ) + : undefined, + environment: preset.environment + ? await processConfigurePresetEnvironment( + preset.environment, + workspaceFolder, + preset, + resolvePaths + ) + : undefined, + vendor: preset.vendor + ? await processConfigurePresetVendor( + preset.vendor, + workspaceFolder, + preset, + resolvePaths + ) + : undefined, }; return processedPreset; @@ -680,15 +964,19 @@ async function processConfigurePresetPath( resolvePaths: boolean ): Promise { // Apply variable substitution - let processedPath = substituteVariablesInConfigurePreset(pathValue, workspaceFolder, preset); - + let processedPath = substituteVariablesInConfigurePreset( + pathValue, + workspaceFolder, + preset + ); + if (resolvePaths && processedPath) { // Resolve relative paths to absolute paths if (!path.isAbsolute(processedPath)) { processedPath = path.join(workspaceFolder.fsPath, processedPath); } } - + return processedPath || pathValue; } @@ -702,23 +990,30 @@ async function processConfigurePresetCacheVariables( resolvePaths: boolean ): Promise<{ [key: string]: any }> { const processedCacheVariables: { [key: string]: any } = {}; - + for (const [key, value] of Object.entries(cacheVariables)) { if (typeof value === "string") { - processedCacheVariables[key] = substituteVariablesInConfigurePreset(value, workspaceFolder, preset); - + processedCacheVariables[key] = substituteVariablesInConfigurePreset( + value, + workspaceFolder, + preset + ); + // Special handling for path-related cache variables if (resolvePaths && (key === "SDKCONFIG" || key.includes("PATH"))) { const processedValue = processedCacheVariables[key]; if (processedValue && !path.isAbsolute(processedValue)) { - processedCacheVariables[key] = path.join(workspaceFolder.fsPath, processedValue); + processedCacheVariables[key] = path.join( + workspaceFolder.fsPath, + processedValue + ); } } } else { processedCacheVariables[key] = value; } } - + return processedCacheVariables; } @@ -732,11 +1027,13 @@ async function processConfigurePresetEnvironment( resolvePaths: boolean ): Promise<{ [key: string]: string }> { const processedEnvironment: { [key: string]: string } = {}; - + for (const [key, value] of Object.entries(environment)) { - processedEnvironment[key] = substituteVariablesInConfigurePreset(value, workspaceFolder, preset) || value; + processedEnvironment[key] = + substituteVariablesInConfigurePreset(value, workspaceFolder, preset) || + value; } - + return processedEnvironment; } @@ -751,33 +1048,47 @@ async function processConfigurePresetVendor( ): Promise { const processedVendor: ESPIDFVendorSettings = { "espressif/vscode-esp-idf": { - settings: [] - } + settings: [], + }, }; - + const espIdfSettings = vendor["espressif/vscode-esp-idf"]?.settings || []; - + for (const setting of espIdfSettings) { const processedSetting: ESPIDFSettings = { ...setting }; - + // Process string values in settings if (typeof setting.value === "string") { - processedSetting.value = substituteVariablesInConfigurePreset(setting.value, workspaceFolder, preset) || setting.value; + processedSetting.value = + substituteVariablesInConfigurePreset( + setting.value, + workspaceFolder, + preset + ) || setting.value; } else if (Array.isArray(setting.value)) { // Process arrays of strings - processedSetting.value = setting.value.map(item => - typeof item === "string" - ? substituteVariablesInConfigurePreset(item, workspaceFolder, preset) || item + processedSetting.value = setting.value.map((item) => + typeof item === "string" + ? substituteVariablesInConfigurePreset( + item, + workspaceFolder, + preset + ) || item : item ); } else if (typeof setting.value === "object" && setting.value !== null) { // Process objects (like openOCD settings) - processedSetting.value = await processConfigurePresetSettingObject(setting.value, workspaceFolder, preset, resolvePaths); + processedSetting.value = await processConfigurePresetSettingObject( + setting.value, + workspaceFolder, + preset, + resolvePaths + ); } - + processedVendor["espressif/vscode-esp-idf"].settings.push(processedSetting); } - + return processedVendor; } @@ -791,21 +1102,27 @@ async function processConfigurePresetSettingObject( resolvePaths: boolean ): Promise { const processedObj: any = {}; - + for (const [key, value] of Object.entries(obj)) { if (typeof value === "string") { - processedObj[key] = substituteVariablesInConfigurePreset(value, workspaceFolder, preset) || value; + processedObj[key] = + substituteVariablesInConfigurePreset(value, workspaceFolder, preset) || + value; } else if (Array.isArray(value)) { - processedObj[key] = value.map(item => - typeof item === "string" - ? substituteVariablesInConfigurePreset(item, workspaceFolder, preset) || item + processedObj[key] = value.map((item) => + typeof item === "string" + ? substituteVariablesInConfigurePreset( + item, + workspaceFolder, + preset + ) || item : item ); } else { processedObj[key] = value; } } - + return processedObj; } @@ -865,8 +1182,11 @@ function substituteVariablesInConfigurePreset( configVarName = configVar.substring(0, delimiterIndex); prefix = configVar.substring(delimiterIndex + 1).trim(); } - - const configVarValue = getConfigurePresetParameterValue(configVarName, preset); + + const configVarValue = getConfigurePresetParameterValue( + configVarName, + preset + ); if (!configVarValue) { return match; @@ -910,7 +1230,10 @@ function substituteVariablesInConfigurePreset( /** * Gets parameter value from ConfigurePreset for variable substitution */ -function getConfigurePresetParameterValue(param: string, preset: ConfigurePreset): any { +function getConfigurePresetParameterValue( + param: string, + preset: ConfigurePreset +): any { switch (param) { case "idf.cmakeCompilerArgs": return getESPIDFSettingValue(preset, "compileArgs") || ""; @@ -962,23 +1285,36 @@ function getConfigurePresetParameterValue(param: string, preset: ConfigurePreset /** * Helper function to get ESP-IDF setting value from ConfigurePreset */ -function getESPIDFSettingValue(preset: ConfigurePreset, settingType: string): any { - const espIdfSettings = preset.vendor?.["espressif/vscode-esp-idf"]?.settings || []; - const setting = espIdfSettings.find(s => s.type === settingType); +function getESPIDFSettingValue( + preset: ConfigurePreset, + settingType: string +): any { + const espIdfSettings = + preset.vendor?.["espressif/vscode-esp-idf"]?.settings || []; + const setting = espIdfSettings.find((s) => s.type === settingType); return setting ? setting.value : undefined; } /** * Converts ConfigurePreset to ProjectConfElement for store compatibility */ -export function configurePresetToProjectConfElement(preset: ConfigurePreset): ProjectConfElement { - return convertConfigurePresetToProjectConfElement(preset, Uri.file(""), false); +export function configurePresetToProjectConfElement( + preset: ConfigurePreset +): ProjectConfElement { + return convertConfigurePresetToProjectConfElement( + preset, + Uri.file(""), + false + ); } /** * Converts ProjectConfElement to ConfigurePreset for store compatibility */ -export function projectConfElementToConfigurePreset(name: string, element: ProjectConfElement): ConfigurePreset { +export function projectConfElementToConfigurePreset( + name: string, + element: ProjectConfElement +): ConfigurePreset { return convertProjectConfElementToConfigurePreset(name, element); } @@ -998,11 +1334,12 @@ function convertConfigurePresetToProjectConfElement( resolvePaths: boolean = false ): ProjectConfElement { // Extract ESP-IDF specific settings from vendor section - const espIdfSettings = preset.vendor?.["espressif/vscode-esp-idf"]?.settings || []; - + const espIdfSettings = + preset.vendor?.["espressif/vscode-esp-idf"]?.settings || []; + // Helper function to find setting by type const findSetting = (type: string): any => { - const setting = espIdfSettings.find(s => s.type === type); + const setting = espIdfSettings.find((s) => s.type === type); return setting ? setting.value : undefined; }; @@ -1011,18 +1348,32 @@ function convertConfigurePresetToProjectConfElement( const ninjaArgs = findSetting("ninjaArgs") || []; const flashBaudRate = findSetting("flashBaudRate") || ""; const monitorBaudRate = findSetting("monitorBaudRate") || ""; - const openOCDSettings = findSetting("openOCD") || { debugLevel: -1, configs: [], args: [] }; - const taskSettings = findSetting("tasks") || { preBuild: "", preFlash: "", postBuild: "", postFlash: "" }; + const openOCDSettings = findSetting("openOCD") || { + debugLevel: -1, + configs: [], + args: [], + }; + const taskSettings = findSetting("tasks") || { + preBuild: "", + preFlash: "", + postBuild: "", + postFlash: "", + }; // Process paths based on resolvePaths flag const binaryDir = preset.binaryDir || ""; - const buildDirectoryPath = resolvePaths && binaryDir - ? (path.isAbsolute(binaryDir) ? binaryDir : path.join(workspaceFolder.fsPath, binaryDir)) - : binaryDir; + const buildDirectoryPath = + resolvePaths && binaryDir + ? path.isAbsolute(binaryDir) + ? binaryDir + : path.join(workspaceFolder.fsPath, binaryDir) + : binaryDir; // Process SDKCONFIG_DEFAULTS - convert semicolon-separated string to array const sdkconfigDefaultsStr = preset.cacheVariables?.SDKCONFIG_DEFAULTS || ""; - const sdkconfigDefaults = sdkconfigDefaultsStr ? sdkconfigDefaultsStr.split(";") : []; + const sdkconfigDefaults = sdkconfigDefaultsStr + ? sdkconfigDefaultsStr.split(";") + : []; return { build: { @@ -1038,7 +1389,9 @@ function convertConfigurePresetToProjectConfElement( monitorBaudRate, openOCD: { debugLevel: openOCDSettings.debugLevel || -1, - configs: Array.isArray(openOCDSettings.configs) ? openOCDSettings.configs : [], + configs: Array.isArray(openOCDSettings.configs) + ? openOCDSettings.configs + : [], args: Array.isArray(openOCDSettings.args) ? openOCDSettings.args : [], }, tasks: { @@ -1058,9 +1411,10 @@ function convertProjectConfElementToConfigurePreset( element: ProjectConfElement ): ConfigurePreset { // Convert SDKCONFIG_DEFAULTS array to semicolon-separated string - const sdkconfigDefaults = element.build.sdkconfigDefaults.length > 0 - ? element.build.sdkconfigDefaults.join(";") - : undefined; + const sdkconfigDefaults = + element.build.sdkconfigDefaults.length > 0 + ? element.build.sdkconfigDefaults.join(";") + : undefined; const settings: ESPIDFSettings[] = [ { type: "compileArgs", value: element.build.compileArgs }, @@ -1077,7 +1431,9 @@ function convertProjectConfElementToConfigurePreset( cacheVariables: { ...(element.idfTarget && { IDF_TARGET: element.idfTarget }), ...(sdkconfigDefaults && { SDKCONFIG_DEFAULTS: sdkconfigDefaults }), - ...(element.build.sdkconfigFilePath && { SDKCONFIG: element.build.sdkconfigFilePath }), + ...(element.build.sdkconfigFilePath && { + SDKCONFIG: element.build.sdkconfigFilePath, + }), }, environment: Object.keys(element.env).length > 0 ? element.env : undefined, vendor: { diff --git a/src/project-conf/projectConfiguration.ts b/src/project-conf/projectConfiguration.ts index cf541e845..486aa737a 100644 --- a/src/project-conf/projectConfiguration.ts +++ b/src/project-conf/projectConfiguration.ts @@ -50,7 +50,13 @@ export interface CMakeVersion { } export interface ESPIDFSettings { - type: "compileArgs" | "ninjaArgs" | "flashBaudRate" | "monitorBaudRate" | "openOCD" | "tasks"; + type: + | "compileArgs" + | "ninjaArgs" + | "flashBaudRate" + | "monitorBaudRate" + | "openOCD" + | "tasks"; value: any; } @@ -62,6 +68,7 @@ export interface ESPIDFVendorSettings { export interface ConfigurePreset { name: string; + inherits?: string | string[]; binaryDir?: string; cacheVariables?: { IDF_TARGET?: string; diff --git a/src/statusBar/index.ts b/src/statusBar/index.ts index 8ed1f5669..89dad62cf 100644 --- a/src/statusBar/index.ts +++ b/src/statusBar/index.ts @@ -69,11 +69,17 @@ export async function createCmdsStatusBarItems(workspaceFolder: Uri) { let projectConf = ESP.ProjectConfiguration.store.get( ESP.ProjectConfiguration.SELECTED_CONFIG ); - let projectConfPath = path.join( + let cmakePresetsPath = path.join( workspaceFolder.fsPath, ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME ); - let projectConfExists = await pathExists(projectConfPath); + let cmakeUserPresetsPath = path.join( + workspaceFolder.fsPath, + ESP.ProjectConfiguration.USER_CONFIGURATION_FILENAME + ); + let cmakePresetsExists = await pathExists(cmakePresetsPath); + let cmakeUserPresetsExists = await pathExists(cmakeUserPresetsPath); + let anyConfigFileExists = cmakePresetsExists || cmakeUserPresetsExists; let currentIdfVersion = await getCurrentIdfSetup(workspaceFolder, false); @@ -129,8 +135,8 @@ export async function createCmdsStatusBarItems(workspaceFolder: Uri) { } } - // Only create the project configuration status bar item if the configuration file exists - if (projectConfExists) { + // Only create the project configuration status bar item if any configuration file exists + if (anyConfigFileExists) { if (!projectConf) { // No configuration selected but file exists with configurations let statusBarItemName = "No Configuration Selected"; @@ -158,7 +164,7 @@ export async function createCmdsStatusBarItems(workspaceFolder: Uri) { ); } } else if (statusBarItems["projectConf"]) { - // If the configuration file doesn't exist but the status bar item does, remove it + // If no configuration files exist but the status bar item does, remove it statusBarItems["projectConf"].dispose(); statusBarItems["projectConf"] = undefined; } From 6f94cf829c5ead47b60ae4340ca8791cd587df8e Mon Sep 17 00:00:00 2001 From: Radu Date: Thu, 25 Sep 2025 19:59:40 +0300 Subject: [PATCH 5/9] Disable CMake extension autoconfiguration - Disable prompts from CMake extension that might confuse the esp-idf users. - Disable autoconfiguration from CMake extension - Remove CMake status bar icons --- templates/.vscode/settings.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/templates/.vscode/settings.json b/templates/.vscode/settings.json index 0bb915ab8..b55d209b4 100644 --- a/templates/.vscode/settings.json +++ b/templates/.vscode/settings.json @@ -1,3 +1,21 @@ { - "C_Cpp.intelliSenseEngine": "default" + "C_Cpp.intelliSenseEngine": "default", + "cmake.configureOnOpen": false, + "cmake.configureOnEdit": false, + "cmake.automaticReconfigure": false, + "cmake.autoSelectActiveFolder": false, + "cmake.options.advanced": { + "build": { + "statusBarVisibility": "inherit", + "inheritDefault": "hidden" + }, + "launch": { + "statusBarVisibility": "inherit", + "inheritDefault": "hidden" + }, + "debug": { + "statusBarVisibility": "inherit", + "inheritDefault": "hidden" + } + } } From f027df11bfa73346b94de5c36f58a6d9b7e3a0c0 Mon Sep 17 00:00:00 2001 From: Radu Date: Thu, 25 Sep 2025 22:03:35 +0300 Subject: [PATCH 6/9] feat: Add save configuration option as default --- package.json | 6 ++++ package.nls.json | 3 +- src/extension.ts | 8 ++++- .../ProjectConfigurationManager.ts | 33 ++++++++++++++----- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index beece7e5e..e02cab655 100644 --- a/package.json +++ b/package.json @@ -1310,6 +1310,12 @@ "default": 60, "scope": "resource", "description": "%param.serialPortDetectionTimeout%" + }, + "idf.saveLastProjectConfiguration": { + "type": "boolean", + "default": true, + "scope": "resource", + "description": "%param.saveLastProjectConfiguration%" } } } diff --git a/package.nls.json b/package.nls.json index 9cb08f006..f004c3a49 100644 --- a/package.nls.json +++ b/package.nls.json @@ -212,5 +212,6 @@ "command.errorHints.clearAll.title": "Clear All Error Hints", "command.errorHints.clearBuild.title": "Clear Build Error Hints", "command.errorHints.clearOpenOCD.title": "Clear OpenOCD Error Hints", - "Launch Debug": "Launch Debug" + "Launch Debug": "Launch Debug", + "param.saveLastProjectConfiguration": "Save and restore the last selected project configuration when reopening a workspace. When enabled, the extension will restore the last used configuration if it exists, otherwise no configuration will be selected. When disabled, no configuration will be selected by default." } diff --git a/src/extension.ts b/src/extension.ts index efb99e5cb..487095bf1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -282,7 +282,13 @@ export async function activate(context: vscode.ExtensionContext) { Logger.init(context); ESP.GlobalConfiguration.store = ExtensionConfigStore.init(context); ESP.ProjectConfiguration.store = ProjectConfigStore.init(context); - clearSelectedProjectConfiguration(); + + // Only clear selected project configuration if the setting is disabled + const saveLastProjectConfiguration = idfConf.readParameter("idf.saveLastProjectConfiguration"); + if (saveLastProjectConfiguration === false) { + clearSelectedProjectConfiguration(); + } + Telemetry.init(idfConf.readParameter("idf.telemetry") || false); utils.setExtensionContext(context); ChangelogViewer.showChangeLogAndUpdateVersion(context); diff --git a/src/project-conf/ProjectConfigurationManager.ts b/src/project-conf/ProjectConfigurationManager.ts index 312fbaa94..32a57d845 100644 --- a/src/project-conf/ProjectConfigurationManager.ts +++ b/src/project-conf/ProjectConfigurationManager.ts @@ -28,6 +28,7 @@ import { } from "./index"; import { pathExists } from "fs-extra"; import { configureClangSettings } from "../clang"; +import * as idfConf from "../idfConfiguration"; export function clearSelectedProjectConfiguration(): void { if (ESP.ProjectConfiguration.store) { @@ -137,14 +138,30 @@ export class ProjectConfigurationManager { if (cmakePresetsExists) fileInfo.push("CMakePresets.json"); if (cmakeUserPresetsExists) fileInfo.push("CMakeUserPresets.json"); - window.showInformationMessage( - `Loaded ${ - this.configVersions.length - } project configuration(s) from ${fileInfo.join( - " and " - )}: ${this.configVersions.join(", ")}` - ); - this.setNoConfigurationSelectedStatus(); + // Check if we should show no configuration selected status + const saveLastProjectConfiguration = idfConf.readParameter("idf.saveLastProjectConfiguration", this.workspaceUri); + + if (saveLastProjectConfiguration !== false) { + // When setting is enabled, show no configuration selected status + window.showInformationMessage( + `Loaded ${ + this.configVersions.length + } project configuration(s) from ${fileInfo.join( + " and " + )}: ${this.configVersions.join(", ")}. No configuration selected.` + ); + this.setNoConfigurationSelectedStatus(); + } else { + // Show the current behavior when auto-selection is disabled + window.showInformationMessage( + `Loaded ${ + this.configVersions.length + } project configuration(s) from ${fileInfo.join( + " and " + )}: ${this.configVersions.join(", ")}` + ); + this.setNoConfigurationSelectedStatus(); + } } else { // No configurations found Logger.info( From 19fae5a1f6ac64d3c55efacb7e69505c527c86f2 Mon Sep 17 00:00:00 2001 From: Radu Date: Wed, 1 Oct 2025 16:38:24 +0300 Subject: [PATCH 7/9] fix: updating configurations --- src/espIdf/openOcd/boardConfiguration.ts | 5 + src/espIdf/setTarget/index.ts | 19 +- src/extension.ts | 1 + src/project-conf/index.ts | 272 ++++++++++++++++++++++- 4 files changed, 285 insertions(+), 12 deletions(-) diff --git a/src/espIdf/openOcd/boardConfiguration.ts b/src/espIdf/openOcd/boardConfiguration.ts index f00eaff33..73938a55f 100644 --- a/src/espIdf/openOcd/boardConfiguration.ts +++ b/src/espIdf/openOcd/boardConfiguration.ts @@ -23,6 +23,7 @@ import { commands, ConfigurationTarget, l10n, Uri, window } from "vscode"; import { defaultBoards } from "./defaultBoards"; import { IdfToolsManager } from "../../idfToolsManager"; import { getIdfTargetFromSdkconfig } from "../../workspaceConfig"; +import { updateCurrentProfileOpenOcdConfigs } from "../../project-conf"; export interface IdfBoard { name: string; @@ -199,6 +200,10 @@ export async function selectOpenOcdConfigFiles( ConfigurationTarget.WorkspaceFolder, workspaceFolder ); + + // Update project configuration with OpenOCD configs if a configuration is selected + await updateCurrentProfileOpenOcdConfigs(selectedBoard.target.configFiles, workspaceFolder); + Logger.infoNotify( l10n.t(`OpenOCD Board configuration files set to {boards}.`, { boards: selectedBoard.target.configFiles.join(","), diff --git a/src/espIdf/setTarget/index.ts b/src/espIdf/setTarget/index.ts index b5e7fdd8c..7296d538f 100644 --- a/src/espIdf/setTarget/index.ts +++ b/src/espIdf/setTarget/index.ts @@ -36,7 +36,11 @@ import { OutputChannel } from "../../logger/outputChannel"; import { selectOpenOcdConfigFiles } from "../openOcd/boardConfiguration"; import { getTargetsFromEspIdf, IdfTarget } from "./getTargets"; import { setTargetInIDF } from "./setTargetInIdf"; -import { updateCurrentProfileIdfTarget } from "../../project-conf"; +import { + updateCurrentProfileIdfTarget, + updateCurrentProfileOpenOcdConfigs, + updateCurrentProfileCustomExtraVars +} from "../../project-conf"; import { DevkitsCommand } from "./DevkitsCommand"; export let isSettingIDFTarget = false; @@ -165,6 +169,10 @@ export async function setIdfTarget( configurationTarget, workspaceFolder.uri ); + + // Update project configuration with OpenOCD configs if a configuration is selected + await updateCurrentProfileOpenOcdConfigs(configFiles, workspaceFolder.uri); + // Store USB location if available if (selectedTarget.boardInfo.location) { const customExtraVars = readParameter( @@ -182,6 +190,12 @@ export async function setIdfTarget( configurationTarget, workspaceFolder.uri ); + + // Update project configuration with custom extra vars if a configuration is selected + await updateCurrentProfileCustomExtraVars( + { "OPENOCD_USB_ADAPTER_LOCATION": location }, + workspaceFolder.uri + ); } } else { await selectOpenOcdConfigFiles( @@ -202,6 +216,9 @@ export async function setIdfTarget( configurationTarget, workspaceFolder.uri ); + + // Update project configuration with IDF_TARGET if a configuration is selected + // Note: IDF_TARGET goes in cacheVariables, not environment await updateCurrentProfileIdfTarget( selectedTarget.idfTarget.target, workspaceFolder.uri diff --git a/src/extension.ts b/src/extension.ts index 487095bf1..448cd65ad 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3952,6 +3952,7 @@ export async function activate(context: vscode.ExtensionContext) { } }); }); + } function checkAndNotifyMissingCompileCommands() { diff --git a/src/project-conf/index.ts b/src/project-conf/index.ts index bd01f9847..700a9c351 100644 --- a/src/project-conf/index.ts +++ b/src/project-conf/index.ts @@ -59,11 +59,91 @@ export async function updateCurrentProfileIdfTarget( idfTarget: string, workspaceFolder: Uri ) { + await updateCurrentProjectConfiguration(workspaceFolder, (config) => { + // Update IDF_TARGET in cacheVariables for ConfigurePreset + if (!config.cacheVariables) { + config.cacheVariables = {}; + } + config.cacheVariables.IDF_TARGET = idfTarget; + return config; + }); +} + +/** + * Updates OpenOCD configuration for the currently selected project configuration + */ +export async function updateCurrentProfileOpenOcdConfigs( + configs: string[], + workspaceFolder: Uri +) { + await updateCurrentProjectConfiguration(workspaceFolder, (config) => { + // Update OpenOCD configs in vendor settings + if (!config.vendor) { + config.vendor = { "espressif/vscode-esp-idf": { settings: [] } }; + } + if (!config.vendor["espressif/vscode-esp-idf"]) { + config.vendor["espressif/vscode-esp-idf"] = { settings: [] }; + } + + // Remove existing openOCD setting + config.vendor["espressif/vscode-esp-idf"].settings = + config.vendor["espressif/vscode-esp-idf"].settings.filter( + (setting) => setting.type !== "openOCD" + ); + + // Add new openOCD setting + config.vendor["espressif/vscode-esp-idf"].settings.push({ + type: "openOCD", + value: { + debugLevel: 2, + configs: configs, + args: [] + } + }); + + return config; + }); +} + +/** + * Updates custom extra variables for the currently selected project configuration + * Note: IDF_TARGET is excluded as it should be in cacheVariables, not environment + */ +export async function updateCurrentProfileCustomExtraVars( + customVars: { [key: string]: string }, + workspaceFolder: Uri +) { + await updateCurrentProjectConfiguration(workspaceFolder, (config) => { + // Update custom extra variables in environment + if (!config.environment) { + config.environment = {}; + } + + // Filter out IDF_TARGET as it should be in cacheVariables, not environment + const filteredVars = { ...customVars }; + delete filteredVars.IDF_TARGET; + + // Merge the custom variables into the environment (excluding IDF_TARGET) + Object.assign(config.environment, filteredVars); + + return config; + }); +} + + +/** + * Generic function to update any configuration setting for the currently selected project configuration + */ +export async function updateCurrentProjectConfiguration( + workspaceFolder: Uri, + updateFunction: (config: ConfigurePreset) => ConfigurePreset +): Promise { const selectedConfig = ESP.ProjectConfiguration.store.get( ESP.ProjectConfiguration.SELECTED_CONFIG ); if (!selectedConfig) { + // No configuration selected - don't update any files return; } @@ -74,27 +154,197 @@ export async function updateCurrentProfileIdfTarget( if (!projectConfJson[selectedConfig]) { const err = new Error( - `Configuration preset "${selectedConfig}" not found in ${ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME}. Please check your CMakePresets configurePresets section.` + `Configuration preset "${selectedConfig}" not found in project configuration files. Please check your CMakePresets configurePresets section.` ); Logger.errorNotify( err.message, err, - "updateCurrentProfileIdfTarget project-conf" + "updateCurrentProjectConfiguration project-conf" ); return; } - // Update IDF_TARGET in cacheVariables for ConfigurePreset - if (!projectConfJson[selectedConfig].cacheVariables) { - projectConfJson[selectedConfig].cacheVariables = {}; + // Apply the update function to the configuration + const updatedConfig = updateFunction(projectConfJson[selectedConfig]); + + // Update the store + ESP.ProjectConfiguration.store.set(selectedConfig, updatedConfig); + + // Save to the correct file based on where the configuration originated + await saveProjectConfigurationToCorrectFile(workspaceFolder, selectedConfig, updatedConfig); +} + +/** + * Saves a single configuration to the correct file based on its source + */ +export async function saveProjectConfigurationToCorrectFile( + workspaceFolder: Uri, + configName: string, + configPreset: ConfigurePreset +) { + // Determine which file the configuration should be saved to + const configSource = await determineConfigurationSource(workspaceFolder, configName); + + if (configSource === 'user') { + await saveConfigurationToUserPresets(workspaceFolder, configName, configPreset); + } else if (configSource === 'project') { + await saveConfigurationToProjectPresets(workspaceFolder, configName, configPreset); + } else { + // If source is unknown and we have a selected config, default to user presets + // This handles the case where a user modifies a configuration that doesn't exist yet + await saveConfigurationToUserPresets(workspaceFolder, configName, configPreset); + } +} + +/** + * Determines the source file for a configuration (project vs user presets) + */ +async function determineConfigurationSource( + workspaceFolder: Uri, + configName: string +): Promise<'project' | 'user' | 'unknown'> { + const cmakePresetsFilePath = Uri.joinPath( + workspaceFolder, + ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME + ); + const cmakeUserPresetsFilePath = Uri.joinPath( + workspaceFolder, + ESP.ProjectConfiguration.USER_CONFIGURATION_FILENAME + ); + + // Check if config exists in CMakeUserPresets.json first (user presets take precedence) + if (await pathExists(cmakeUserPresetsFilePath.fsPath)) { + try { + const userPresetsJson = await readJson(cmakeUserPresetsFilePath.fsPath); + if (userPresetsJson?.configurePresets?.some((preset: any) => preset.name === configName)) { + return 'user'; + } + } catch (error) { + Logger.error(`Error reading user presets file: ${error.message}`, error, "determineConfigurationSource"); + } + } + + // Check if config exists in CMakePresets.json + if (await pathExists(cmakePresetsFilePath.fsPath)) { + try { + const projectPresetsJson = await readJson(cmakePresetsFilePath.fsPath); + if (projectPresetsJson?.configurePresets?.some((preset: any) => preset.name === configName)) { + return 'project'; + } + } catch (error) { + Logger.error(`Error reading project presets file: ${error.message}`, error, "determineConfigurationSource"); + } + } + + return 'unknown'; +} + +/** + * Saves a configuration to CMakeUserPresets.json + */ +async function saveConfigurationToUserPresets( + workspaceFolder: Uri, + configName: string, + configPreset: ConfigurePreset +) { + const cmakeUserPresetsFilePath = Uri.joinPath( + workspaceFolder, + ESP.ProjectConfiguration.USER_CONFIGURATION_FILENAME + ); + + let userPresets: CMakePresets; + + // Read existing user presets or create new structure + if (await pathExists(cmakeUserPresetsFilePath.fsPath)) { + try { + userPresets = await readJson(cmakeUserPresetsFilePath.fsPath); + } catch (error) { + Logger.error(`Error reading user presets file: ${error.message}`, error, "saveConfigurationToUserPresets"); + userPresets = { + version: 3, + configurePresets: [] + }; + } + } else { + userPresets = { + version: 3, + configurePresets: [] + }; + } + + // Ensure configurePresets array exists + if (!userPresets.configurePresets) { + userPresets.configurePresets = []; } - projectConfJson[selectedConfig].cacheVariables.IDF_TARGET = idfTarget; - ESP.ProjectConfiguration.store.set( - selectedConfig, - projectConfJson[selectedConfig] + // Update or add the configuration + const existingIndex = userPresets.configurePresets.findIndex( + (preset: ConfigurePreset) => preset.name === configName ); - await saveProjectConfFile(workspaceFolder, projectConfJson); + + if (existingIndex >= 0) { + userPresets.configurePresets[existingIndex] = configPreset; + } else { + userPresets.configurePresets.push(configPreset); + } + + await writeJson(cmakeUserPresetsFilePath.fsPath, userPresets, { + spaces: 2, + }); +} + +/** + * Saves a configuration to CMakePresets.json + */ +async function saveConfigurationToProjectPresets( + workspaceFolder: Uri, + configName: string, + configPreset: ConfigurePreset +) { + const cmakePresetsFilePath = Uri.joinPath( + workspaceFolder, + ESP.ProjectConfiguration.PROJECT_CONFIGURATION_FILENAME + ); + + let projectPresets: CMakePresets; + + // Read existing project presets or create new structure + if (await pathExists(cmakePresetsFilePath.fsPath)) { + try { + projectPresets = await readJson(cmakePresetsFilePath.fsPath); + } catch (error) { + Logger.error(`Error reading project presets file: ${error.message}`, error, "saveConfigurationToProjectPresets"); + projectPresets = { + version: 3, + configurePresets: [] + }; + } + } else { + projectPresets = { + version: 3, + configurePresets: [] + }; + } + + // Ensure configurePresets array exists + if (!projectPresets.configurePresets) { + projectPresets.configurePresets = []; + } + + // Update or add the configuration + const existingIndex = projectPresets.configurePresets.findIndex( + (preset: ConfigurePreset) => preset.name === configName + ); + + if (existingIndex >= 0) { + projectPresets.configurePresets[existingIndex] = configPreset; + } else { + projectPresets.configurePresets.push(configPreset); + } + + await writeJson(cmakePresetsFilePath.fsPath, projectPresets, { + spaces: 2, + }); } export async function saveProjectConfFile( @@ -112,7 +362,7 @@ export async function saveProjectConfFile( ); const cmakePresets: CMakePresets = { - version: 1, + version: 3, cmakeMinimumRequired: { major: 3, minor: 23, patch: 0 }, configurePresets, }; From ce608911b32713c3a2c11bc156be98d2edfa8478 Mon Sep 17 00:00:00 2001 From: Radu Date: Fri, 17 Oct 2025 10:30:42 +0300 Subject: [PATCH 8/9] fix: keep selected preset buildPath consistent after config updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This keeps idfConfiguration.parameterToProjectConfigMap('idf.buildPath') returning the preset’s binaryDir (as buildDirectoryPath) instead of falling back to the default ${workspaceFolder}/build. - Fixes issue where Set Target invoked idf.py with the wrong -B dir after profile edits or when “Save last project configuration” is enabled. --- src/project-conf/index.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/project-conf/index.ts b/src/project-conf/index.ts index 700a9c351..fe13436c5 100644 --- a/src/project-conf/index.ts +++ b/src/project-conf/index.ts @@ -167,11 +167,27 @@ export async function updateCurrentProjectConfiguration( // Apply the update function to the configuration const updatedConfig = updateFunction(projectConfJson[selectedConfig]); - // Update the store - ESP.ProjectConfiguration.store.set(selectedConfig, updatedConfig); - // Save to the correct file based on where the configuration originated - await saveProjectConfigurationToCorrectFile(workspaceFolder, selectedConfig, updatedConfig); + await saveProjectConfigurationToCorrectFile( + workspaceFolder, + selectedConfig, + updatedConfig + ); + + // Keep in-memory store consistent with consumers expecting legacy ProjectConfElement + // Re-read processed presets (with resolved paths) and convert to legacy shape + try { + const resolvedConfigs = await getProjectConfigurationElements( + workspaceFolder, + true + ); + const resolvedPreset = resolvedConfigs[selectedConfig] || updatedConfig; + const legacyElement = configurePresetToProjectConfElement(resolvedPreset); + ESP.ProjectConfiguration.store.set(selectedConfig, legacyElement); + } catch (e) { + // Fallback: ensure we at least keep the updated preset in store + ESP.ProjectConfiguration.store.set(selectedConfig, updatedConfig); + } } /** From b7a6b02d3e324d483cc07c420c6fb6e48749e670 Mon Sep 17 00:00:00 2001 From: Radu Date: Fri, 17 Oct 2025 12:16:12 +0300 Subject: [PATCH 9/9] fix(openocd): coalesce/silence version checks; store OpenOCD output as text - Run openocd --version with silent output and coalesce concurrent calls to avoid duplicate banners during set-target/devkit detection/hints. - Store aggregated OpenOCD output as string (text-only) to avoid Buffer typing issues and unnecessary Buffer.concat allocations. --- src/espIdf/openOcd/openOcdManager.ts | 45 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/espIdf/openOcd/openOcdManager.ts b/src/espIdf/openOcd/openOcdManager.ts index f7bb271e3..a9570c8b7 100644 --- a/src/espIdf/openOcd/openOcdManager.ts +++ b/src/espIdf/openOcd/openOcdManager.ts @@ -49,10 +49,11 @@ export class OpenOCDManager extends EventEmitter { } private static instance: OpenOCDManager; private server: ChildProcess; - private chan: Buffer; + private chan: string; private statusBar: vscode.StatusBarItem; private workspace: vscode.Uri; private encounteredErrors: boolean = false; + private versionPromise: Promise | null = null; // coalesce concurrent lookups only private constructor() { super(); @@ -60,20 +61,32 @@ export class OpenOCDManager extends EventEmitter { } public async version(): Promise { - const modifiedEnv = await appendIdfAndToolsToPath(this.workspace); - if (!isBinInPath("openocd", modifiedEnv)) { - return ""; + // Coalesce concurrent calls; do not cache long-term to respect version changes + if (this.versionPromise) { + return this.versionPromise; } - const resp = await sspawn("openocd", ["--version"], { - cwd: this.workspace.fsPath, - env: modifiedEnv, - }); - const versionString = resp.toString(); - const match = versionString.match(/v\d+\.\d+\.\d+\-\S*/gi); - if (!match) { - return "failed+to+match+version"; + + this.versionPromise = (async () => { + const modifiedEnv = await appendIdfAndToolsToPath(this.workspace); + if (!isBinInPath("openocd", modifiedEnv)) { + return ""; + } + const resp = await sspawn("openocd", ["--version"], { + cwd: this.workspace.fsPath, + env: modifiedEnv, + silent: true, + appendMode: "append", + }); + const versionString = resp.toString(); + const match = versionString.match(/v\d+\.\d+\.\d+\-\S*/gi); + return match ? match[0].replace("-dirty", "") : "failed+to+match+version"; + })(); + + try { + return await this.versionPromise; + } finally { + this.versionPromise = null; } - return match[0].replace("-dirty", ""); } public statusBarItem(): vscode.StatusBarItem { @@ -263,7 +276,7 @@ export class OpenOCDManager extends EventEmitter { } this.stop(); }); - this.updateStatusText("❇️ OpenOCD Server (Running)"); + this.updateStatusText("❇️ OpenOCD Server (Running)"); OutputChannel.show(); } @@ -306,7 +319,7 @@ export class OpenOCDManager extends EventEmitter { if (PreCheck.isWorkspaceFolderOpen()) { this.workspace = vscode.workspace.workspaceFolders[0].uri; } - this.chan = Buffer.alloc(0); + this.chan = ""; OutputChannel.init(); if (vscode.env.uiKind !== vscode.UIKind.Web) { this.registerOpenOCDStatusBarItem(); @@ -314,6 +327,6 @@ export class OpenOCDManager extends EventEmitter { } private sendToOutputChannel(data: Buffer) { - this.chan = Buffer.concat([this.chan, data]); + this.chan = (this.chan || "") + data.toString(); } }