diff --git a/docs_espressif/en/additionalfeatures/multiple-projects.rst b/docs_espressif/en/additionalfeatures/multiple-projects.rst index 8dead8011..698a1607a 100644 --- a/docs_espressif/en/additionalfeatures/multiple-projects.rst +++ b/docs_espressif/en/additionalfeatures/multiple-projects.rst @@ -102,25 +102,56 @@ Use Multiple Build Configurations in the Same Workspace Folder Use the ESP-IDF CMake `Multiple Build Configurations Example `_ to follow this tutorial. -Use the ``ESP-IDF: Open Project Configuration`` command to create two configuration profiles: ``prod1`` and ``prod2``. Set ``sdkconfig.prod_common;sdkconfig.prod1`` and ``sdkconfig.prod_common;sdkconfig.prod2`` in the sdkconfig defaults field as shown below: - -.. image:: ../../../media/tutorials/project_conf/enterConfigName.png - -.. image:: ../../../media/tutorials/project_conf/prod1.png - -.. image:: ../../../media/tutorials/project_conf/prod2.png - -After creating each profile and setting the configuration, click the ``Save`` button. Use the ``ESP-IDF: Select Project Configuration`` command to choose the configuration to override extension configuration settings. +.. note:: -.. image:: ../../../media/tutorials/project_conf/selectConfig.png + The ESP-IDF ``multi_config`` example already ships with a ready-to-use ``CMakePresets.json`` and works out of the box. When you select a project configuration in this extension, the extension will automatically attach vendor settings under ``espressif/vscode-esp-idf`` for the chosen preset (for example, OpenOCD configuration and ``IDF_TARGET``) based on your currently selected board configuration and target in the extension. -Once a configuration profile is selected, it will appear in the status bar as shown before. +Define your configurations in ``CMakePresets.json`` (and optionally ``CMakeUserPresets.json``). Then use ``ESP-IDF: Select Project Configuration`` to choose among the discovered presets. The extension will apply the selected preset when building/flashing/monitoring. -.. image:: ../../../media/tutorials/project_conf/configInStatusBar.png +Typical entries in ``CMakePresets.json``: -Now, use the ``ESP-IDF: Build your Project`` command to build the project for ``prod1`` and ``prod2``. You will see binaries generated for each profile in the specified path. Use the ``ESP-IDF: Select Project Configuration`` command to switch between configurations. +.. code-block:: JSON -Use the ``ESP-IDF: Open Project Configuration`` command to modify, add, or delete the configuration profiles. To stop using these profiles, delete all configuration profiles. + { + "version": 3, + "configurePresets": [ + { + "name": "default", + "binaryDir": "build/default", + "displayName": "Default (development)", + "description": "Development configuration", + "cacheVariables": { + "SDKCONFIG": "./build/default/sdkconfig" + } + }, + { + "name": "prod1", + "binaryDir": "build/prod1", + "displayName": "Product 1", + "description": "Production configuration for product 1", + "cacheVariables": { + "SDKCONFIG_DEFAULTS": "sdkconfig.defaults.prod_common;sdkconfig.defaults.prod1", + "SDKCONFIG": "./build/prod1/sdkconfig" + } + }, + { + "name": "prod2", + "binaryDir": "build/prod2", + "displayName": "Product 2", + "description": "Production configuration for product 2", + "cacheVariables": { + "SDKCONFIG_DEFAULTS": "sdkconfig.defaults.prod_common;sdkconfig.defaults.prod2", + "SDKCONFIG": "./build/prod2/sdkconfig" + } + } + ] + } + +Selecting presets: + +1. Open Command Palette and run ``ESP-IDF: Select Project Configuration``. +2. Pick ``default``, ``prod1`` or ``prod2``. +3. Run ``ESP-IDF: Build Your Project`` to build with the selected preset. Switch presets any time via the same selection command. Multiple ESP-IDF Versions ------------------------- 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/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index d13034945..8d5859df2 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -257,5 +257,21 @@ "Clear Build Error Hints": "Borrar Pistas de Error de Compilación", "Clear OpenOCD Error Hints": "Borrar Pistas de Error de OpenOCD", "Open Reference": "Abrir Referencia", - "Launch Debug": "Iniciar depuración" + "Launch Debug": "Iniciar depuración", + "Cancel": "Cancelar", + "OpenOCD Exit with non-zero error code {0}": "OpenOCD salió con un código de error distinto de cero {0}", + "Loaded {0} project configuration(s) from {1}: {2}. No configuration selected.": "Se cargaron {0} configuraciones de proyecto desde {1}: {2}. Ninguna configuración seleccionada.", + "Loaded {0} project configuration(s) from {1}: {2}": "Se cargaron {0} configuraciones de proyecto desde {1}: {2}", + "Failed to parse project configuration files": "Error al analizar los archivos de configuración del proyecto", + "New configurations added: {0}": "Nuevas configuraciones agregadas: {0}", + "Configurations removed: {0}": "Configuraciones eliminadas: {0}", + "Project configuration file has been deleted.": "Se ha eliminado el archivo de configuración del proyecto.", + "Project configuration file created with {0} configuration(s). Select one to use.": "Archivo de configuración del proyecto creado con {0} configuración(es). Seleccione una para usar.", + "Open editor": "Abrir editor", + "Configuration {0}": "Configuración {0}", + "No Configuration Selected": "Ninguna configuración seleccionada", + "No project configuration selected. Click to select one": "No se seleccionó ninguna configuración de proyecto. Haga clic para seleccionar una", + "Legacy Configs ({0})": "Configuraciones heredadas ({0})", + "Found legacy project configurations: {0}. Click to migrate to the new CMakePresets.json format.": "Se encontraron configuraciones de proyecto heredadas: {0}. Haga clic para migrar al nuevo formato CMakePresets.json.", + "Project Configuration changes have been saved": "Se han guardado los cambios de configuración del proyecto" } diff --git a/l10n/bundle.l10n.pt.json b/l10n/bundle.l10n.pt.json index 2cf68d5b6..b66ba1afd 100644 --- a/l10n/bundle.l10n.pt.json +++ b/l10n/bundle.l10n.pt.json @@ -258,5 +258,21 @@ "Clear Build Error Hints": "Limpar Dicas de Erro de Compilação", "Clear OpenOCD Error Hints": "Limpar Dicas de Erro do OpenOCD", "Open Reference": "Abrir Referência", - "Launch Debug": "Iniciar depuração" + "Launch Debug": "Iniciar depuração", + "Cancel": "Cancelar", + "OpenOCD Exit with non-zero error code {0}": "OpenOCD saiu com código de erro diferente de zero {0}", + "Loaded {0} project configuration(s) from {1}: {2}. No configuration selected.": "{0} configuração(ões) de projeto carregada(s) de {1}: {2}. Nenhuma configuração selecionada.", + "Loaded {0} project configuration(s) from {1}: {2}": "{0} configuração(ões) de projeto carregada(s) de {1}: {2}", + "Failed to parse project configuration files": "Falha ao analisar os arquivos de configuração do projeto", + "New configurations added: {0}": "Novas configurações adicionadas: {0}", + "Configurations removed: {0}": "Configurações removidas: {0}", + "Project configuration file has been deleted.": "O arquivo de configuração do projeto foi excluído.", + "Project configuration file created with {0} configuration(s). Select one to use.": "Arquivo de configuração do projeto criado com {0} configuração(ões). Selecione uma para usar.", + "Open editor": "Abrir editor", + "Configuration {0}": "Configuração {0}", + "No Configuration Selected": "Nenhuma configuração selecionada", + "No project configuration selected. Click to select one": "Nenhuma configuração de projeto selecionada. Clique para selecionar uma", + "Legacy Configs ({0})": "Configurações legadas ({0})", + "Found legacy project configurations: {0}. Click to migrate to the new CMakePresets.json format.": "Foram encontradas configurações de projeto legadas: {0}. Clique para migrar para o novo formato CMakePresets.json.", + "Project Configuration changes have been saved": "As alterações na configuração do projeto foram salvas" } diff --git a/l10n/bundle.l10n.ru.json b/l10n/bundle.l10n.ru.json index ddebd793f..da3f88ca8 100644 --- a/l10n/bundle.l10n.ru.json +++ b/l10n/bundle.l10n.ru.json @@ -258,5 +258,21 @@ "Clear Build Error Hints": "Очистить подсказки по ошибкам сборки", "Clear OpenOCD Error Hints": "Очистить подсказки по ошибкам OpenOCD", "Open Reference": "Открыть ссылку", - "Launch Debug": "Запуск отладки" + "Launch Debug": "Запуск отладки", + "Cancel": "Отмена", + "OpenOCD Exit with non-zero error code {0}": "OpenOCD завершился с ненулевым кодом ошибки {0}", + "Loaded {0} project configuration(s) from {1}: {2}. No configuration selected.": "Загружено {0} конфигураций проекта из {1}: {2}. Конфигурация не выбрана.", + "Loaded {0} project configuration(s) from {1}: {2}": "Загружено {0} конфигураций проекта из {1}: {2}", + "Failed to parse project configuration files": "Не удалось проанализировать файлы конфигурации проекта", + "New configurations added: {0}": "Добавлены новые конфигурации: {0}", + "Configurations removed: {0}": "Конфигурации удалены: {0}", + "Project configuration file has been deleted.": "Файл конфигурации проекта был удален.", + "Project configuration file created with {0} configuration(s). Select one to use.": "Создан файл конфигурации проекта с {0} конфигурациями. Выберите одну для использования.", + "Open editor": "Открыть редактор", + "Configuration {0}": "Конфигурация {0}", + "No Configuration Selected": "Конфигурация не выбрана", + "No project configuration selected. Click to select one": "Конфигурация проекта не выбрана. Нажмите, чтобы выбрать", + "Legacy Configs ({0})": "Унаследованные конфигурации ({0})", + "Found legacy project configurations: {0}. Click to migrate to the new CMakePresets.json format.": "Найдены устаревшие конфигурации проекта: {0}. Нажмите, чтобы выполнить миграцию в новый формат CMakePresets.json.", + "Project Configuration changes have been saved": "Изменения конфигурации проекта сохранены" } diff --git a/l10n/bundle.l10n.zh-CN.json b/l10n/bundle.l10n.zh-CN.json index a8ff62797..2994e4afd 100644 --- a/l10n/bundle.l10n.zh-CN.json +++ b/l10n/bundle.l10n.zh-CN.json @@ -258,5 +258,21 @@ "Clear Build Error Hints": "清除构建错误提示", "Clear OpenOCD Error Hints": "清除 OpenOCD 错误提示", "Open Reference": "打开参考", - "Launch Debug": "启动调试" + "Launch Debug": "启动调试", + "Cancel": "取消", + "OpenOCD Exit with non-zero error code {0}": "OpenOCD 以非零错误代码退出:{0}", + "Loaded {0} project configuration(s) from {1}: {2}. No configuration selected.": "已从 {1} 加载 {0} 个项目配置:{2}。未选择配置。", + "Loaded {0} project configuration(s) from {1}: {2}": "已从 {1} 加载 {0} 个项目配置:{2}", + "Failed to parse project configuration files": "解析项目配置文件失败", + "New configurations added: {0}": "已添加新配置:{0}", + "Configurations removed: {0}": "已移除配置:{0}", + "Project configuration file has been deleted.": "项目配置文件已删除。", + "Project configuration file created with {0} configuration(s). Select one to use.": "已创建包含 {0} 个配置的项目配置文件。请选择一个使用。", + "Open editor": "打开编辑器", + "Configuration {0}": "配置 {0}", + "No Configuration Selected": "未选择配置", + "No project configuration selected. Click to select one": "未选择项目配置。单击选择一个", + "Legacy Configs ({0})": "旧版配置({0})", + "Found legacy project configurations: {0}. Click to migrate to the new CMakePresets.json format.": "发现旧版项目配置:{0}。单击迁移到新的 CMakePresets.json 格式。", + "Project Configuration changes have been saved": "项目配置更改已保存" } diff --git a/package.json b/package.json index ca3bbaf04..2b74cb37a 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", @@ -1295,6 +1304,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/build/buildTask.ts b/src/build/buildTask.ts index fb590effb..d76b013ca 100644 --- a/src/build/buildTask.ts +++ b/src/build/buildTask.ts @@ -119,7 +119,7 @@ export class BuildTask { let defaultCompilerArgs; if (espIdfVersion === "x.x") { Logger.warn( - "Could not determine ESP-IDF version. Using default compiler arguments for the latest known version." + "Could not determine ESP-IDF version. Using default compiler arguments for the latest known version." ); defaultCompilerArgs = [ "-G", diff --git a/src/config.ts b/src/config.ts index a787e4d88..d62a4187e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,8 +30,8 @@ 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 const USER_CONFIGURATION_FILENAME = "CMakeUserPresets.json"; } export enum BuildType { diff --git a/src/espIdf/openOcd/boardConfiguration.ts b/src/espIdf/openOcd/boardConfiguration.ts index f00eaff33..383e60868 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; @@ -186,7 +187,7 @@ export async function selectOpenOcdConfigFiles( } else if (selectedBoard && selectedBoard.target) { if (selectedBoard.label.indexOf("Custom board") !== -1) { const inputBoard = await window.showInputBox({ - placeHolder: "Enter comma-separated configuration files", + placeHolder: l10n.t("Enter comma-separated configuration files"), value: selectedBoard.target.configFiles.join(","), }); if (inputBoard) { @@ -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/openOcd/openOcdManager.ts b/src/espIdf/openOcd/openOcdManager.ts index f7bb271e3..61fd68d54 100644 --- a/src/espIdf/openOcd/openOcdManager.ts +++ b/src/espIdf/openOcd/openOcdManager.ts @@ -60,16 +60,16 @@ export class OpenOCDManager extends EventEmitter { } public async version(): Promise { - const modifiedEnv = await appendIdfAndToolsToPath(this.workspace); - if (!isBinInPath("openocd", modifiedEnv)) { - return ""; - } - 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); + const modifiedEnv = await appendIdfAndToolsToPath(this.workspace); + if (!isBinInPath("openocd", modifiedEnv)) { + return ""; + } + 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"; } @@ -159,12 +159,12 @@ export class OpenOCDManager extends EventEmitter { const modifiedEnv = await appendIdfAndToolsToPath(this.workspace); if (!isBinInPath("openocd", modifiedEnv)) { throw new Error( - "Invalid OpenOCD bin path or access is denied for the user" + "Invalid OpenOCD bin path or access is denied for the user" ); } if (typeof modifiedEnv.OPENOCD_SCRIPTS === "undefined") { throw new Error( - "OPENOCD_SCRIPTS environment variable is missing. Please set it in idf.customExtraVars or in your system environment variables." + "OPENOCD_SCRIPTS environment variable is missing. Please set it in idf.customExtraVars or in your system environment variables." ); } @@ -187,7 +187,7 @@ export class OpenOCDManager extends EventEmitter { openOcdConfigFilesList.length < 1 ) { throw new Error( - "Invalid OpenOCD Config files. Check idf.openOcdConfigs configuration value." + "Invalid OpenOCD Config files. Check idf.openOcdConfigs configuration value." ); } diff --git a/src/espIdf/setTarget/index.ts b/src/espIdf/setTarget/index.ts index b5e7fdd8c..d27738eb1 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; @@ -118,7 +122,7 @@ export async function setIdfTarget( } } else { Logger.info( - "Devkit detection script not available. A default list of targets will be displayed instead." + "Devkit detection script not available. A default list of targets will be displayed instead." ); } } catch (e) { @@ -129,7 +133,9 @@ export async function setIdfTarget( } } else { Logger.info( - "Connected ESP-IDF devkit detection is skipped while debugging. You can still select a target manually." + l10n.t( + "Connected ESP-IDF devkit detection is skipped while debugging. You can still select a target manually." + ) ); } let quickPickItems: ISetTargetQuickPickItems[] = []; @@ -165,6 +171,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 +192,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 +218,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 efb99e5cb..448cd65ad 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); @@ -3946,6 +3952,7 @@ export async function activate(context: vscode.ExtensionContext) { } }); }); + } function checkAndNotifyMissingCompileCommands() { diff --git a/src/project-conf/ProjectConfigurationManager.ts b/src/project-conf/ProjectConfigurationManager.ts index 917b3369c..e22215e96 100644 --- a/src/project-conf/ProjectConfigurationManager.ts +++ b/src/project-conf/ProjectConfigurationManager.ts @@ -20,8 +20,15 @@ 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"; +import * as idfConf from "../idfConfiguration"; export function clearSelectedProjectConfiguration(): void { if (ESP.ProjectConfiguration.store) { @@ -38,9 +45,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; @@ -56,61 +65,54 @@ 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 ); - this.initialize(); this.registerEventHandlers(); + // Initialize asynchronously + this.initialize(); } - private initialize(): void { - 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 - ); - } + private async initialize(): Promise { + 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( @@ -132,25 +134,46 @@ export class ProjectConfigurationManager { this.setNoConfigurationSelectedStatus(); } else if (this.configVersions.length > 0) { // No current selection but configurations exist - window.showInformationMessage( - `Loaded ${ - this.configVersions.length - } project configuration(s): ${this.configVersions.join(", ")}` - ); - this.setNoConfigurationSelectedStatus(); + const fileInfo = []; + if (cmakePresetsExists) fileInfo.push("CMakePresets.json"); + if (cmakeUserPresetsExists) fileInfo.push("CMakeUserPresets.json"); + + // 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( + l10n.t( + "Loaded {0} project configuration(s) from {1}: {2}. No configuration selected.", + this.configVersions.length, + fileInfo.join(" and "), + this.configVersions.join(", ") + ) + ); + this.setNoConfigurationSelectedStatus(); + } else { + // Show the current behavior when auto-selection is disabled + window.showInformationMessage( + l10n.t( + "Loaded {0} project configuration(s) from {1}: {2}", + this.configVersions.length, + 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}` - ); Logger.errorNotify( - `Failed to parse project configuration file: ${this.configFilePath}`, + l10n.t("Failed to parse project configuration files"), error, "ProjectConfigurationManager initialize" ); @@ -160,32 +183,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 CMakePresets.json file deletion + const cmakePresetsDeleteDisposable = this.cmakePresetsWatcher.onDidDelete( + async () => await this.handleConfigFileDelete() + ); + + // 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 file deletion - const deleteDisposable = this.configWatcher.onDidDelete( + // Handle CMakeUserPresets.json file deletion + const cmakeUserPresetsDeleteDisposable = this.cmakeUserPresetsWatcher.onDidDelete( async () => await this.handleConfigFileDelete() ); - // Handle file creation - const createDisposable = this.configWatcher.onDidCreate( + // 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( @@ -199,13 +245,13 @@ export class ProjectConfigurationManager { if (addedVersions.length > 0) { window.showInformationMessage( - `New versions added: ${addedVersions.join(", ")}` + l10n.t("New configurations added: {0}", addedVersions.join(", ")) ); } if (removedVersions.length > 0) { window.showInformationMessage( - `Versions removed: ${removedVersions.join(", ")}` + l10n.t("Configurations removed: {0}", removedVersions.join(", ")) ); } @@ -236,7 +282,11 @@ export class ProjectConfigurationManager { this.setNoConfigurationSelectedStatus(); } } catch (error) { - window.showErrorMessage(`Error parsing config file: ${error.message}`); + Logger.errorNotify( + `Error parsing configuration files: ${error.message}`, + error, + "ProjectConfigurationManager handleConfigFileChange" + ); this.setNoConfigurationSelectedStatus(); } } @@ -262,17 +312,20 @@ export class ProjectConfigurationManager { this.statusBarItems["projectConf"].dispose(); this.statusBarItems["projectConf"] = undefined; } - + // Optionally notify the user - window.showInformationMessage( - "Project configuration file has been deleted." - ); + Logger.infoNotify(l10n.t("Project configuration file has been deleted.")); } 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) { @@ -298,7 +351,10 @@ export class ProjectConfigurationManager { // Notify the user about available configurations window.showInformationMessage( - `Project configuration file created with ${this.configVersions.length} configuration(s). Select one to use.` + l10n.t( + "Project configuration file created with {0} configuration(s). Select one to use.", + this.configVersions.length + ) ); } } else { @@ -306,8 +362,10 @@ export class ProjectConfigurationManager { this.setNoConfigurationSelectedStatus(); } } catch (error) { - window.showErrorMessage( - `Error parsing newly created config file: ${error.message}` + Logger.errorNotify( + `Error parsing newly created configuration file: ${error.message}`, + error, + "ProjectConfigurationManager handleConfigFileCreate" ); this.setNoConfigurationSelectedStatus(); } @@ -317,9 +375,9 @@ export class ProjectConfigurationManager { * Sets the status bar to indicate no configuration is selected */ private setNoConfigurationSelectedStatus(): void { - const statusBarItemName = "No Configuration Selected"; + const statusBarItemName = l10n.t("No Configuration Selected"); const statusBarItemTooltip = - "No project configuration selected. Click to select one"; + l10n.t("No project configuration selected. Click to select one"); const commandToUse = "espIdf.projectConf"; if (this.statusBarItems["projectConf"]) { @@ -350,7 +408,12 @@ 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,12 +455,25 @@ 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 openEditorLabel = l10n.t("Open editor"); const emptyOption = await window.showInformationMessage( - l10n.t("No project configuration found"), - "Open editor" + l10n.t("No CMakePresets configure presets found"), + openEditorLabel ); - if (emptyOption === "Open editor") { + if (emptyOption === openEditorLabel) { commands.executeCommand("espIdf.projectConfigurationEditor"); } return; @@ -407,7 +483,7 @@ export class ProjectConfigurationManager { let quickPickItems = Object.keys(projectConfigurations).map((k) => { return { description: k, - label: `Configuration ${k}`, + label: l10n.t("Configuration {0}", k), target: k, }; }); @@ -424,16 +500,230 @@ export class ProjectConfigurationManager { await this.updateConfiguration(option.target); } catch (error) { - window.showErrorMessage( - `Error selecting configuration: ${error.message}` + Logger.errorNotify( + `Error selecting configuration: ${error.message}`, + error, + "ProjectConfigurationManager selectProjectConfiguration" + ); + } + } + + /** + * 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 = l10n.t( + "Legacy Configs ({0})", + legacyConfigNames.length + ); + const statusBarItemTooltip = l10n.t( + "Found legacy project configurations: {0}. Click to migrate to the new CMakePresets.json format.", + legacyConfigNames.join(", ") + ); + 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( + l10n.t("Failed to handle legacy migration: {0}", error.message), + error, + "ProjectConfigurationManager handleLegacyMigrationDialog" + ); + } + } + + /** + * 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( + l10n.t("Failed to migrate project configuration: {0}", error.message), + error, + "ProjectConfigurationManager performMigration" + ); + } + } + + /** + * 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( + l10n.t("Failed to migrate project configuration: {0}", error.message), + error, + "ProjectConfigurationManager performDirectMigration" + ); + } + } + + /** + * 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 + * 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 22100759f..a6ee28165 100644 --- a/src/project-conf/index.ts +++ b/src/project-conf/index.ts @@ -17,12 +17,23 @@ */ 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"; +import { resolveVariables, readParameter } from "../idfConfiguration"; + +const ESP_IDF_VENDOR_KEY = "espressif/vscode-esp-idf"; +const CMAKE_PRESET_VERSION = 3; +const CMAKE_PRESET_SCHEMA_VERSION = 1; export class ProjectConfigStore { private static self: ProjectConfigStore; @@ -48,15 +59,105 @@ export class ProjectConfigStore { } } +/** + * Updates the IDF target (e.g., esp32, esp32s3) for the currently selected project configuration. + * The IDF_TARGET is stored in the cacheVariables section of the ConfigurePreset. + * @param idfTarget The target chip name (e.g., "esp32", "esp32s3", "esp32c3") + * @param workspaceFolder The workspace folder Uri where the configuration files are located + */ 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 = { [ESP_IDF_VENDOR_KEY]: { settings: [] } }; + } + if (!config.vendor[ESP_IDF_VENDOR_KEY]) { + config.vendor[ESP_IDF_VENDOR_KEY] = { settings: [] }; + } + + // Remove existing openOCD setting + config.vendor[ESP_IDF_VENDOR_KEY].settings = + config.vendor[ESP_IDF_VENDOR_KEY].settings.filter( + (setting) => setting.type !== "openOCD" + ); + + // Add new openOCD setting + const openOcdDebugLevel = readParameter( + "idf.openOcdDebugLevel", + this.workspace + ) as string; + config.vendor[ESP_IDF_VENDOR_KEY].settings.push({ + type: "openOCD", + value: { + debugLevel: openOcdDebugLevel || 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; } @@ -67,25 +168,273 @@ 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 project configuration files. Please check your CMakePresets configurePresets section.` ); Logger.errorNotify( err.message, err, - "updateCurrentProfileIdfTarget project-conf" + "updateCurrentProjectConfiguration project-conf" ); return; } - projectConfJson[selectedConfig].idfTarget = idfTarget; - ESP.ProjectConfiguration.store.set( + // Apply the update function to the configuration + const updatedConfig = updateFunction(projectConfJson[selectedConfig]); + + // Save to the correct file based on where the configuration originated + await saveProjectConfigurationToCorrectFile( + workspaceFolder, selectedConfig, - projectConfJson[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); + } +} + +/** + * 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 preset (CMakePresets.json vs CMakeUserPresets.json). + * User presets take precedence over project presets when both contain the same configuration name. + * This is important for determining where to save modifications to a configuration. + * + * @param workspaceFolder The workspace folder Uri where the configuration files are located + * @param configName The name of the configuration preset to locate + * @returns 'user' if found in CMakeUserPresets.json, 'project' if in CMakePresets.json, 'unknown' if not found + */ +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 ); - await saveProjectConfFile(workspaceFolder, projectConfJson); + + // 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 preset to the CMakeUserPresets.json file. + * If the file doesn't exist, it will be created. If a preset with the same name already exists, + * it will be updated. CMakeUserPresets.json is gitignored and used for user-specific overrides. + * + * @param workspaceFolder The workspace folder Uri where CMakeUserPresets.json is located + * @param configName The name of the configuration preset to save + * @param configPreset The ConfigurePreset object to save + */ +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: CMAKE_PRESET_VERSION, + configurePresets: [] + }; + } + } else { + userPresets = { + version: CMAKE_PRESET_VERSION, + configurePresets: [] + }; + } + + // Ensure configurePresets array exists + if (!userPresets.configurePresets) { + userPresets.configurePresets = []; + } + + // Update or add the configuration + const existingIndex = userPresets.configurePresets.findIndex( + (preset: ConfigurePreset) => preset.name === configName + ); + + if (existingIndex >= 0) { + userPresets.configurePresets[existingIndex] = configPreset; + } else { + userPresets.configurePresets.push(configPreset); + } + + await writeJson(cmakeUserPresetsFilePath.fsPath, userPresets, { + spaces: 2, + }); } +/** + * Saves a configuration preset to the CMakePresets.json file. + * If the file doesn't exist, it will be created. If a preset with the same name already exists, + * it will be updated. CMakePresets.json is typically committed to version control. + * + * @param workspaceFolder The workspace folder Uri where CMakePresets.json is located + * @param configName The name of the configuration preset to save + * @param configPreset The ConfigurePreset object to save + */ +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: CMAKE_PRESET_VERSION, + configurePresets: [] + }; + } + } else { + projectPresets = { + version: CMAKE_PRESET_VERSION, + 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, + }); +} + +/** + * Saves project configuration elements to CMakePresets.json file. + * This function writes multiple ConfigurePreset objects to the project's CMakePresets.json file. + * @param workspaceFolder The workspace folder Uri where the configuration file will be saved + * @param projectConfElements An object mapping configuration names to their ConfigurePreset objects + */ 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: CMAKE_PRESET_VERSION, + cmakeMinimumRequired: { major: 3, minor: 23, patch: 0 }, + configurePresets, + }; + + await writeJson(projectConfFilePath.fsPath, cmakePresets, { + spaces: 2, + }); +} + +/** + * Legacy compatibility function to save project configuration elements to CMakePresets.json. + * This function converts legacy ProjectConfElement objects to ConfigurePreset format and saves them. + * Used during migration from the old esp_idf_project_configuration.json format. + * @param workspaceFolder The workspace folder Uri where the configuration file will be saved + * @param projectConfElements An object mapping configuration names to their legacy ProjectConfElement objects + * @deprecated Use saveProjectConfFile instead for new code + */ +export async function saveProjectConfFileLegacy( workspaceFolder: Uri, projectConfElements: { [key: string]: ProjectConfElement } ) { @@ -93,11 +442,35 @@ 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: CMAKE_PRESET_VERSION, + cmakeMinimumRequired: { major: 3, minor: 23, patch: 0 }, + configurePresets, + }; + + await writeJson(projectConfFilePath.fsPath, cmakePresets, { spaces: 2, }); } +/** + * Maps legacy parameter names to their corresponding values in a ProjectConfElement (legacy format). + * This function is used for backward compatibility when processing legacy configuration files + * that use variable substitution with parameter names like ${config:idf.buildPath}. + * + * @param param The legacy parameter name (e.g., "idf.buildPath", "idf.cmakeCompilerArgs") + * @param currentProjectConf The ProjectConfElement to extract the value from + * @returns The parameter value, or empty string if not found or undefined + * @deprecated Use getConfigurePresetParameterValue for new code working with ConfigurePresets + */ function parameterToSameProjectConfigMap( param: string, currentProjectConf: ProjectConfElement @@ -175,10 +548,19 @@ function parameterToSameProjectConfigMap( } /** - * Substitutes variables like ${workspaceFolder} and ${env:VARNAME} in a string. - * @param text The input string potentially containing variables. - * @param workspaceFolder The workspace folder Uri to resolve ${workspaceFolder}. - * @returns The string with variables substituted, or undefined if input was undefined/null. + * Substitutes variables in a string for legacy ProjectConfElement format. + * Supports variable patterns like: + * - ${workspaceFolder} or ${workspaceRoot}: Replaced with workspace folder path + * - ${env:VARNAME}: Replaced with environment variable value + * - ${config:PARAM}: Replaced with configuration parameter value + * + * This is the legacy version; for ConfigurePreset use substituteVariablesInConfigurePreset. + * + * @param text The input string potentially containing variables + * @param workspaceFolder The workspace folder Uri to resolve ${workspaceFolder} + * @param config The legacy configuration object for resolving ${config:*} variables + * @returns The string with variables substituted, or undefined if input was undefined/null + * @deprecated Use substituteVariablesInConfigurePreset for new code */ function substituteVariablesInString( text: string | undefined, @@ -207,7 +589,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; @@ -311,200 +696,1131 @@ function resolveConfigPaths( // --- Main Function --- /** - * Reads the project configuration 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 ProjectConfElement. + * @returns An object mapping configuration names to their processed ConfigurePreset. */ export async function getProjectConfigurationElements( workspaceFolder: Uri, resolvePaths: boolean = false -): Promise<{ [key: string]: ProjectConfElement }> { - const projectConfFilePath = Uri.joinPath( +): Promise<{ [key: string]: ConfigurePreset }> { + 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) { - // File not existing is normal, return empty object + // 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" + ); } - } 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" + ); + } + } + + // 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.warnNotify( + `Invalid ${fileName} format detected. Please ensure the file follows the CMakePresets specification.` ); - window.showErrorMessage( - `Error reading or parsing project configuration file (${projectConfFilePath.fsPath}): ${error.message}` + } + + return rawPresets; +} + +/** + * Resolves inheritance for a preset by merging it with its parent presets. + * CMakePresets supports inheritance where a preset can inherit from one or more parent presets + * using the "inherits" field. This function recursively resolves the inheritance chain and + * merges all parent properties, with child properties overriding parent properties. + * + * @param preset The preset to resolve inheritance for (may have an "inherits" field) + * @param allPresets All available presets from both CMakePresets.json and CMakeUserPresets.json + * @returns The preset with inheritance fully resolved and the "inherits" field removed + */ +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. + * Used during inheritance resolution to combine parent and child preset properties. + * For object properties (cacheVariables, environment, vendor), child properties are merged + * into parent properties rather than replacing them entirely. + * + * @param parent The parent preset (provides default values) + * @param child The child preset (takes precedence when properties conflict) + * @returns A new merged preset with child properties overriding parent properties + */ +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 = { + [ESP_IDF_VENDOR_KEY]: { + settings: [ + ...(parent.vendor?.[ESP_IDF_VENDOR_KEY]?.settings || []), + ...(child.vendor?.[ESP_IDF_VENDOR_KEY]?.settings || []), + ], + }, + }; + } + + return merged; +} + +/** + * Checks for the legacy project configuration file (esp_idf_project_configuration.json) + * and prompts the user to migrate it to the new CMakePresets.json format if found. + * This ensures a smooth transition from the old configuration format to the new one. + * + * @param workspaceFolder The workspace folder Uri where the legacy file might be located + */ +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); + } +} + +/** + * 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 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) { + // 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` + ); +} + +/** + * Processes legacy project configuration format (esp_idf_project_configuration.json). + * Converts the old configuration structure to the ProjectConfElement format, handling + * variable substitution and path resolution. This function is used during migration + * from the legacy format to CMakePresets. + * + * @param rawConfig The raw legacy configuration object + * @param workspaceFolder The workspace folder Uri for variable substitution and path resolution + * @param resolvePaths If true, resolves relative paths to absolute paths + * @returns A ProjectConfElement with all legacy settings converted and processed + */ +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; - 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) => + // --- 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 --- + 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 a ConfigurePreset. + * This is a complex function that handles the transformation of raw ConfigurePreset data + * by substituting variables (like ${workspaceFolder}, ${env:VAR}) and optionally resolving + * relative paths to absolute paths. + * + * The function processes: + * - binaryDir: The build output directory path + * - cacheVariables: CMake cache variables (like IDF_TARGET, SDKCONFIG) + * - environment: Environment variables for the build + * - vendor: ESP-IDF specific vendor settings (OpenOCD configs, tasks, etc.) + * + * @param preset The raw ConfigurePreset to process + * @param workspaceFolder The workspace folder Uri used for resolving ${workspaceFolder} and relative paths + * @param resolvePaths If true, converts relative paths to absolute paths; if false, returns paths as-is for display + * @returns A new ConfigurePreset with all variables substituted and paths optionally resolved + */ +async function processConfigurePresetVariables( + preset: ConfigurePreset, + workspaceFolder: Uri, + resolvePaths: boolean +): Promise { + const processedPreset: ConfigurePreset = { + ...preset, + binaryDir: preset.binaryDir + ? await processConfigurePresetPath( + preset.binaryDir, + workspaceFolder, + preset, resolvePaths - ? substituteVariablesInString(arg, workspaceFolder, rawConfig) - : arg ) - .filter(isDefined); - const ninjaArgs = buildConfig?.ninjaArgs - ?.map((arg: string) => + : undefined, + cacheVariables: preset.cacheVariables + ? await processConfigurePresetCacheVariables( + preset.cacheVariables, + workspaceFolder, + preset, 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); - } - } + : 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 path strings in ConfigurePreset by substituting variables and optionally resolving to absolute paths. + * Handles paths like "build" or "${workspaceFolder}/build" and converts them based on the resolvePaths flag. + * @param pathValue The raw path string from the preset (may contain variables) + * @param workspaceFolder The workspace folder Uri used for variable substitution and path resolution + * @param preset The ConfigurePreset containing environment and other variables for substitution + * @param resolvePaths If true, converts relative paths to absolute paths; if false, only substitutes variables + * @returns The processed path string with variables substituted and optionally resolved to absolute path + */ +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 CMake cache variables in ConfigurePreset by substituting variables and resolving paths. + * Cache variables include important CMake settings like IDF_TARGET, SDKCONFIG, and SDKCONFIG_DEFAULTS. + * String values undergo variable substitution, and path-related variables (SDKCONFIG, *PATH) are + * optionally resolved to absolute paths when resolvePaths is true. + * @param cacheVariables The cache variables object from the preset + * @param workspaceFolder The workspace folder Uri for variable substitution and path resolution + * @param preset The ConfigurePreset containing environment variables for substitution + * @param resolvePaths If true, resolves relative paths to absolute for path-related variables + * @returns A new cache variables object with all substitutions and resolutions applied + */ +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; + } + } - // --- 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, - }, - }; - }) + return processedCacheVariables; +} + +/** + * Processes environment variables in ConfigurePreset by substituting variable references. + * Environment variables can reference other variables using ${env:VAR}, ${workspaceFolder}, etc. + * These variables are available to the build process and can be used in build scripts. + * @param environment The environment variables object from the preset + * @param workspaceFolder The workspace folder Uri for variable substitution + * @param preset The ConfigurePreset containing other variables for substitution + * @param resolvePaths Currently unused for environment variables but kept for consistency + * @returns A new environment object with all variable references substituted + */ +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 ESP-IDF vendor-specific settings in ConfigurePreset. + * Vendor settings contain extension-specific configuration like: + * - OpenOCD configuration (debug level, configs, args) + * - Build tasks (preBuild, postBuild, preFlash, postFlash) + * - Compiler arguments and ninja arguments + * - Flash and monitor baud rates + * + * Each setting is processed to substitute variables in string values, arrays, and nested objects. + * @param vendor The vendor settings object from the preset (under "espressif/vscode-esp-idf" key) + * @param workspaceFolder The workspace folder Uri for variable substitution + * @param preset The ConfigurePreset containing other variables for substitution + * @param resolvePaths If true, paths in vendor settings will be resolved to absolute paths + * @returns A new vendor settings object with all variables substituted + */ +async function processConfigurePresetVendor( + vendor: ESPIDFVendorSettings, + workspaceFolder: Uri, + preset: ConfigurePreset, + resolvePaths: boolean +): Promise { + const processedVendor: ESPIDFVendorSettings = { + [ESP_IDF_VENDOR_KEY]: { + settings: [], + schemaVersion: 1 + }, + }; + + const espIdfSettings = vendor[ESP_IDF_VENDOR_KEY]?.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[ESP_IDF_VENDOR_KEY].settings.push(processedSetting); + } + + return processedVendor; +} + +/** + * Processes object values within vendor settings (e.g., OpenOCD configuration objects). + * Recursively processes string and array values within objects to substitute variables. + * For example, processes objects like { debugLevel: 2, configs: ["${env:BOARD_CONFIG}"], args: [] } + * @param obj The object to process (from a vendor setting value) + * @param workspaceFolder The workspace folder Uri for variable substitution + * @param preset The ConfigurePreset containing other variables for substitution + * @param resolvePaths Currently unused but kept for future path resolution in objects + * @returns A new object with all string and array values processed for variable substitution + */ +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). + * This function provides backward compatibility for code that still uses the legacy ProjectConfElement + * format. It delegates to processLegacyProjectConfig for the actual processing. + * + * @param element The ProjectConfElement to process + * @param workspaceFolder The workspace folder Uri for variable substitution and path resolution + * @param resolvePaths If true, resolves relative paths to absolute paths + * @returns A new ProjectConfElement with all variables substituted and paths resolved + * @deprecated Use processConfigurePresetVariables for new code + */ +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; +} + +/** + * Retrieves parameter values from a ConfigurePreset for variable substitution. + * This function maps legacy parameter names (like "idf.buildPath") to their corresponding + * values in the ConfigurePreset structure. Used to support ${config:idf.buildPath} style + * variable references in configuration strings. + * + * Supported parameters include: + * - idf.cmakeCompilerArgs, idf.ninjaArgs + * - idf.buildPath, idf.sdkconfigDefaults, idf.sdkconfigFilePath + * - idf.flashBaudRate, idf.monitorBaudRate + * - idf.openOcdDebugLevel, idf.openOcdConfigs, idf.openOcdLaunchArgs + * - idf.preBuildTask, idf.postBuildTask, idf.preFlashTask, idf.postFlashTask + * + * @param param The parameter name to look up (e.g., "idf.buildPath") + * @param preset The ConfigurePreset to extract the value from + * @returns The parameter value (string, array, or empty string if not found) + */ +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 retrieve ESP-IDF specific setting values from a ConfigurePreset's vendor section. + * ESP-IDF settings are stored in the vendor section under the "espressif/vscode-esp-idf" key. + * Each setting has a type (e.g., "openOCD", "tasks", "compileArgs") and a value. + * + * @param preset The ConfigurePreset containing vendor settings + * @param settingType The type of setting to retrieve (e.g., "openOCD", "tasks", "flashBaudRate") + * @returns The setting value if found, or undefined if the setting doesn't exist + */ +function getESPIDFSettingValue( + preset: ConfigurePreset, + settingType: string +): any { + const espIdfSettings = + preset.vendor?.[ESP_IDF_VENDOR_KEY]?.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 ); +} - return projectConfElements; +/** + * Converts ProjectConfElement to ConfigurePreset for store compatibility + */ +export function projectConfElementToConfigurePreset( + name: string, + element: ProjectConfElement +): ConfigurePreset { + return convertProjectConfElementToConfigurePreset(name, element); } /** * Type guard to filter out undefined values from arrays. + * Useful with array.filter() to remove undefined elements while maintaining type safety. + * @param value The value to check + * @returns True if the value is defined (not undefined), false otherwise */ function isDefined(value: T | undefined): value is T { return value !== undefined; } + +/** + * Converts a CMakePresets ConfigurePreset to the legacy ProjectConfElement format. + * This conversion is necessary for backward compatibility with code that expects the legacy format. + * Extracts ESP-IDF specific settings from the vendor section and maps them to the legacy structure. + * + * @param preset The ConfigurePreset to convert + * @param workspaceFolder The workspace folder Uri for path resolution + * @param resolvePaths If true, resolves relative paths to absolute paths + * @returns A ProjectConfElement representing the same configuration in the legacy format + */ +function convertConfigurePresetToProjectConfElement( + preset: ConfigurePreset, + workspaceFolder: Uri, + resolvePaths: boolean = false +): ProjectConfElement { + // Extract ESP-IDF specific settings from vendor section + const espIdfSettings = + preset.vendor?.[ESP_IDF_VENDOR_KEY]?.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 legacy ProjectConfElement to CMakePresets ConfigurePreset format. + * This conversion is used during migration and when saving configurations from legacy code. + * Maps the legacy structure to the CMakePresets format, storing ESP-IDF specific settings + * in the vendor section under "espressif/vscode-esp-idf". + * + * @param name The name for the configuration preset + * @param element The ProjectConfElement to convert + * @returns A ConfigurePreset representing the same configuration in the CMakePresets 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: { + [ESP_IDF_VENDOR_KEY]: { + settings, + schemaVersion: CMAKE_PRESET_SCHEMA_VERSION + }, + }, + }; +} diff --git a/src/project-conf/projectConfPanel.ts b/src/project-conf/projectConfPanel.ts index 41fdbb1ea..26e40c82a 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,9 +192,9 @@ 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" + l10n.t("Project Configuration changes have been saved") ); } diff --git a/src/project-conf/projectConfiguration.ts b/src/project-conf/projectConfiguration.ts index ed33d8eac..a76a373df 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,55 @@ 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[]; + schemaVersion?: number + }; +} + +export interface ConfigurePreset { + name: string; + inherits?: string | 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 +} 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; } 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" + } + } }