From 07609ef9c6cc6933aa39968b6a1f77d259adcbb3 Mon Sep 17 00:00:00 2001
From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Date: Sun, 1 Nov 2026 18:35:25 +0800
Subject: [PATCH 1/3] chore: v0.1.33
---
Cargo.lock | 70 +++++++++++++++++++--------------------
Cargo.toml | 2 +-
package.json | 2 +-
src-tauri/tauri.conf.json | 2 +-
4 files changed, 38 insertions(+), 38 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 02bb0950e..c3ad78746 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4038,7 +4038,7 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ide-agent"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dunce",
"encoding_rs",
@@ -4061,7 +4061,7 @@ dependencies = [
[[package]]
name = "ide-analysis"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ast-grep-core",
"ide-core",
@@ -4075,7 +4075,7 @@ dependencies = [
[[package]]
name = "ide-build"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dirs 6.0.0",
"dunce",
@@ -4090,7 +4090,7 @@ dependencies = [
[[package]]
name = "ide-clipboard"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
@@ -4099,7 +4099,7 @@ dependencies = [
[[package]]
name = "ide-core"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dunce",
"encoding_rs",
@@ -4115,7 +4115,7 @@ dependencies = [
[[package]]
name = "ide-coverage"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"ide-run",
@@ -4129,7 +4129,7 @@ dependencies = [
[[package]]
name = "ide-dap"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"ide-jsonrpc",
@@ -4147,7 +4147,7 @@ dependencies = [
[[package]]
name = "ide-db"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"bb8",
"dirs 6.0.0",
@@ -4169,7 +4169,7 @@ dependencies = [
[[package]]
name = "ide-docker"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"libc",
@@ -4183,7 +4183,7 @@ dependencies = [
[[package]]
name = "ide-fonts"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"objc2-core-foundation",
"objc2-core-text",
@@ -4195,7 +4195,7 @@ dependencies = [
[[package]]
name = "ide-github"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"ide-vcs",
@@ -4209,7 +4209,7 @@ dependencies = [
[[package]]
name = "ide-host"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dirs 6.0.0",
"dunce",
@@ -4254,7 +4254,7 @@ dependencies = [
[[package]]
name = "ide-http"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"parking_lot",
@@ -4272,7 +4272,7 @@ dependencies = [
[[package]]
name = "ide-index"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"blake3",
"criterion",
@@ -4294,7 +4294,7 @@ dependencies = [
[[package]]
name = "ide-jsonrpc"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"encoding_rs",
"futures",
@@ -4310,7 +4310,7 @@ dependencies = [
[[package]]
name = "ide-language"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"blake3",
"criterion",
@@ -4365,7 +4365,7 @@ dependencies = [
[[package]]
name = "ide-lsp"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dunce",
"futures",
@@ -4390,7 +4390,7 @@ dependencies = [
[[package]]
name = "ide-notebook"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"futures",
"ide-core",
@@ -4413,7 +4413,7 @@ dependencies = [
[[package]]
name = "ide-plugin"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"ide-language",
@@ -4431,7 +4431,7 @@ dependencies = [
[[package]]
name = "ide-profiler"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"ide-run",
@@ -4444,7 +4444,7 @@ dependencies = [
[[package]]
name = "ide-remote"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"globset",
"ide-core",
@@ -4461,7 +4461,7 @@ dependencies = [
[[package]]
name = "ide-run"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dunce",
"encoding_rs",
@@ -4484,7 +4484,7 @@ dependencies = [
[[package]]
name = "ide-sdk"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dunce",
"ide-core",
@@ -4503,7 +4503,7 @@ dependencies = [
[[package]]
name = "ide-search"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"criterion",
"globset",
@@ -4523,7 +4523,7 @@ dependencies = [
[[package]]
name = "ide-spell"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"parking_lot",
@@ -4536,7 +4536,7 @@ dependencies = [
[[package]]
name = "ide-ssh"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dirs 6.0.0",
"flate2",
@@ -4555,7 +4555,7 @@ dependencies = [
[[package]]
name = "ide-syntax"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"ide-language",
@@ -4570,7 +4570,7 @@ dependencies = [
[[package]]
name = "ide-terminal"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"libc",
@@ -4583,7 +4583,7 @@ dependencies = [
[[package]]
name = "ide-text"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"criterion",
"ide-core",
@@ -4595,7 +4595,7 @@ dependencies = [
[[package]]
name = "ide-tools"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"flate2",
"hex",
@@ -4616,7 +4616,7 @@ dependencies = [
[[package]]
name = "ide-vcs"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"dunce",
"encoding_rs",
@@ -4635,7 +4635,7 @@ dependencies = [
[[package]]
name = "ide-vfs"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"chardetng",
"dunce",
@@ -4657,7 +4657,7 @@ dependencies = [
[[package]]
name = "ide-web"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"ide-core",
"open",
@@ -4673,7 +4673,7 @@ dependencies = [
[[package]]
name = "ide-workspace"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"blake3",
"dunce",
@@ -6144,7 +6144,7 @@ dependencies = [
[[package]]
name = "openidea"
-version = "0.1.32"
+version = "0.1.33"
dependencies = [
"blake3",
"block2 0.6.2",
diff --git a/Cargo.toml b/Cargo.toml
index f8b7ed866..9da1d526f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@ resolver = "3"
members = ["crates/*", "src-tauri"]
[workspace.package]
-version = "0.1.32"
+version = "0.1.33"
edition = "2024"
rust-version = "1.96"
license = "Apache-2.0"
diff --git a/package.json b/package.json
index 9cf94c037..9fb5ec265 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "openidea",
"private": true,
"description": "An open-source IDE inspired by IntelliJ IDEA",
- "version": "0.1.32",
+ "version": "0.1.33",
"license": "Apache-2.0",
"type": "module",
"scripts": {
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 9cbf6ee47..9a1c84fbf 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenIDEA",
- "version": "0.1.32",
+ "version": "0.1.33",
"identifier": "ai.l0ng.openidea",
"build": {
"beforeDevCommand": "pnpm dev",
From 49f9c57bccbfd300f2bb6e4136e02ee806b93089 Mon Sep 17 00:00:00 2001
From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Date: Sun, 1 Nov 2026 18:46:40 +0800
Subject: [PATCH 2/3] docs: remove the README build section
---
README.md | 20 --------------------
1 file changed, 20 deletions(-)
diff --git a/README.md b/README.md
index 774889602..59b4f7c02 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,6 @@
·
Benchmark
·
- Build
- ·
Discord
@@ -47,24 +45,6 @@ IDEA analyses Python more deeply, so "ready" doesn't mean the same work on
both sides. Both are measured from the outside, the same way: see
[`perf/compare`](perf/compare) to run it on your machine and projects.
-## Build
-
-```sh
-pnpm install
-pnpm tauri dev
-```
-
-Rust 1.96, Node 22, pnpm and the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/).
-
-Native notebook execution tests are opt-in and need two distinct Python environments
-with `ipykernel` installed:
-
-```sh
-OPENIDEA_TEST_PYTHON=/first/env/bin/python \
-OPENIDEA_TEST_PYTHON_SECOND=/second/env/bin/python \
-cargo test -p ide-notebook --test kernel_runtime -- --ignored --test-threads=1
-```
-
## License
[Apache 2.0](LICENSE). Bundled third-party assets are listed in [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).
From 210f9a78ac32b99af07a634ae5d9c340667def51 Mon Sep 17 00:00:00 2001
From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Date: Sun, 1 Nov 2026 19:02:00 +0800
Subject: [PATCH 3/3] feat(run): project CMake profiles (#679)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CMake run configurations used fixed arguments (-B build, Debug) and only recognised existing build directories.
Projects can now define CMake profiles under Settings › Build, Execution, Deployment › CMake: build type, generator, CMake arguments, build directory (default cmake-build-), build arguments and environment. Each profile gets its own Configure, Build, CTest and target run configurations, named after the profile when there are several. Configure runs first only while the build directory has no CMakeCache.txt, and a generator change clears the stale cache before configuring. Projects without profiles keep the previous detection. Arguments are stored as lists and edited with the same quoting rules as program arguments. Toolchain selection is not part of this change.
Refs #655
---
crates/ide-core/src/settings.rs | 56 ++++++
crates/ide-host/src/ops/run.rs | 9 +-
crates/ide-run/src/detect.rs | 10 +-
crates/ide-run/src/detect/cmake.rs | 177 +++++++++++++++++-
crates/ide-run/src/lib.rs | 9 +
crates/ide-run/tests/detect.rs | 69 +++++++
src/bindings.ts | 26 +++
src/features/run/index.ts | 9 +-
src/features/settings/__tests__/cmake.test.ts | 46 +++++
.../settings/__tests__/cmakeEditor.test.tsx | 72 +++++++
.../settings/__tests__/dialog.test.tsx | 23 +++
src/features/settings/model/schema.ts | 4 +
src/features/settings/pages/CMake.module.css | 4 +
src/features/settings/pages/cmake.tsx | 151 +++++++++++++++
src/features/settings/pages/execution.tsx | 20 ++
src/platform/cmakeSettings.ts | 38 ++++
src/platform/locales/text/zh.json | 14 +-
src/platform/locales/textCatalog.test.ts | 1 +
18 files changed, 726 insertions(+), 12 deletions(-)
create mode 100644 src/features/settings/__tests__/cmake.test.ts
create mode 100644 src/features/settings/__tests__/cmakeEditor.test.tsx
create mode 100644 src/features/settings/pages/CMake.module.css
create mode 100644 src/features/settings/pages/cmake.tsx
create mode 100644 src/platform/cmakeSettings.ts
diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs
index 7f360b1f1..7c2c17a37 100644
--- a/crates/ide-core/src/settings.rs
+++ b/crates/ide-core/src/settings.rs
@@ -58,6 +58,43 @@ pub struct Settings {
pub deployment: DeploymentSettings,
/// Build, Execution, Deployment › Run Targets.
pub run_targets: Vec,
+ pub cmake: CMakeSettings,
+}
+
+/// Absent profiles retain automatic discovery for existing projects.
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
+#[serde(default, rename_all = "camelCase")]
+pub struct CMakeSettings {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[specta(optional)]
+ pub profiles: Option>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
+#[serde(default, rename_all = "camelCase")]
+pub struct CMakeProfile {
+ pub name: String,
+ pub build_type: String,
+ pub generator: String,
+ pub cmake_args: Vec,
+ /// Empty uses cmake-build-.
+ pub build_dir: String,
+ pub build_args: Vec,
+ pub env: BTreeMap,
+}
+
+impl Default for CMakeProfile {
+ fn default() -> Self {
+ Self {
+ name: "Debug".into(),
+ build_type: "Debug".into(),
+ generator: String::new(),
+ cmake_args: Vec::new(),
+ build_dir: String::new(),
+ build_args: Vec::new(),
+ env: BTreeMap::new(),
+ }
+ }
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
@@ -154,6 +191,7 @@ impl Default for Settings {
ssh: SshSettings::default(),
deployment: DeploymentSettings::default(),
run_targets: Vec::new(),
+ cmake: CMakeSettings::default(),
}
}
}
@@ -765,6 +803,24 @@ impl Settings {
mod tests {
use super::*;
+ #[test]
+ fn cmake_profile_argument_arrays_round_trip() {
+ let mut settings = Settings::default();
+ assert!(settings.cmake.profiles.is_none());
+ assert!(CMakeProfile::default().cmake_args.is_empty());
+ assert!(CMakeProfile::default().build_args.is_empty());
+ let profile = CMakeProfile {
+ cmake_args: ["-DMESSAGE=two words", "-DQUOTE=\"hello\"", ""].map(String::from).into(),
+ build_args: ["-j", "8", "--", "LABEL=it's a build"].map(String::from).into(),
+ ..Default::default()
+ };
+ settings.cmake.profiles = Some(vec![profile.clone()]);
+ let value = serde_json::to_value(&settings).unwrap();
+ assert_eq!(value["cmake"]["profiles"][0]["cmakeArgs"], serde_json::json!(profile.cmake_args));
+ assert_eq!(value["cmake"]["profiles"][0]["buildArgs"], serde_json::json!(profile.build_args));
+ assert_eq!(serde_json::from_value::(value).unwrap(), settings);
+ }
+
#[test]
fn ui_font_fallback_defaults_empty_and_round_trips() {
let old: Settings = serde_json::from_str(r#"{"appearance":{"useCustomFont":true}}"#).unwrap();
diff --git a/crates/ide-host/src/ops/run.rs b/crates/ide-host/src/ops/run.rs
index 21f21e0a4..8798e52ac 100644
--- a/crates/ide-host/src/ops/run.rs
+++ b/crates/ide-host/src/ops/run.rs
@@ -50,10 +50,11 @@ fn detected_and_saved(
suppressed: &[String],
) -> Vec {
let excluded_poms = crate::project::unlinked_maven_poms(root, settings);
- let mut all: Vec = ide_run::detect_excluding(root, &excluded_poms)
- .into_iter()
- .filter(|d| !suppressed.contains(&d.id) && !saved.iter().any(|u| u.id == d.id))
- .collect();
+ let mut all: Vec =
+ ide_run::detect_with_profiles(root, &excluded_poms, settings.cmake.profiles.as_deref())
+ .into_iter()
+ .filter(|d| !suppressed.contains(&d.id) && !saved.iter().any(|u| u.id == d.id))
+ .collect();
all.extend(saved.iter().cloned());
for config in all.iter_mut().filter(|c| c.kind != "gradle") {
ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(config), root, settings);
diff --git a/crates/ide-run/src/detect.rs b/crates/ide-run/src/detect.rs
index 9f6e2c5fe..13a1f5d5c 100644
--- a/crates/ide-run/src/detect.rs
+++ b/crates/ide-run/src/detect.rs
@@ -16,6 +16,14 @@ pub(crate) fn detect(root: &Path) -> Vec {
}
pub(crate) fn detect_excluding(root: &Path, excluded_poms: &[PathBuf]) -> Vec {
+ detect_with_profiles(root, excluded_poms, None)
+}
+
+pub(crate) fn detect_with_profiles(
+ root: &Path,
+ excluded_poms: &[PathBuf],
+ profiles: Option<&[ide_core::settings::CMakeProfile]>,
+) -> Vec {
let mut out = Vec::new();
detect_node(root, &mut out);
detect_deno(root, &mut out);
@@ -24,7 +32,7 @@ pub(crate) fn detect_excluding(root: &Path, excluded_poms: &[PathBuf]) -> Vec) {
+pub(super) fn detect(root: &Path, profiles: Option<&[CMakeProfile]>, out: &mut Vec) {
if !root.join("CMakeLists.txt").is_file() {
return;
}
+ if let Some(profiles) = profiles {
+ for (index, profile) in profiles.iter().enumerate() {
+ detect_profile(root, profile, index, profiles.len() > 1, out);
+ }
+ return;
+ }
let mut builds = Vec::new();
if let Ok(entries) = fs::read_dir(root) {
builds.extend(entries.flatten().filter_map(|e| {
@@ -79,11 +86,69 @@ pub(super) fn detect(root: &Path, out: &mut Vec) {
);
before(&mut test, &build_id);
out.push(test);
- targets(root, &build, &dir, &prefix, out);
+ targets(root, &build, &dir, &prefix, None, out);
}
}
-fn targets(root: &Path, build: &Path, dir: &str, prefix: &str, out: &mut Vec) {
+fn detect_profile(root: &Path, profile: &CMakeProfile, index: usize, multiple: bool, out: &mut Vec) {
+ let dir = if profile.build_dir.trim().is_empty() {
+ format!("cmake-build-{}", profile.build_type.to_lowercase())
+ } else {
+ profile.build_dir.clone()
+ };
+ let build = root.join(&dir);
+ let prefix = format!("cmake:profile:{index}");
+ let configure_id = format!("{prefix}:configure");
+ let build_id = format!("{prefix}:build");
+ let start = out.len();
+ let mut configure =
+ config(&configure_id, format!("CMake Configure ({dir})"), "cmake", "cmake", &["-S", ".", "-B", &dir]);
+ if !profile.generator.trim().is_empty() {
+ configure.args.extend(["-G".into(), profile.generator.clone()]);
+ }
+ configure.args.push(format!("-DCMAKE_BUILD_TYPE={}", profile.build_type));
+ configure.args.push("-DCMAKE_EXPORT_COMPILE_COMMANDS=ON".into());
+ configure.args.extend_from_slice(&profile.cmake_args);
+ configure.extras.params.insert("buildDir".into(), dir.clone());
+ out.push(configure);
+ let mut all = config(
+ &build_id,
+ format!("CMake Build ({dir})"),
+ "cmake",
+ "cmake",
+ &["--build", &dir, "--config", &profile.build_type],
+ );
+ all.args.extend_from_slice(&profile.build_args);
+ if !build.join("CMakeCache.txt").is_file() {
+ before(&mut all, &configure_id);
+ }
+ out.push(all);
+ let mut test = config(
+ format!("{prefix}:test"),
+ format!("CTest ({dir})"),
+ "cmake",
+ "ctest",
+ &["--test-dir", &dir, "-C", &profile.build_type, "--output-on-failure"],
+ );
+ before(&mut test, &build_id);
+ out.push(test);
+ targets(root, &build, &dir, &prefix, Some(profile), out);
+ for config in &mut out[start..] {
+ config.env = profile.env.clone();
+ if multiple {
+ config.name = format!("{} [{}]", config.name, profile.name);
+ }
+ }
+}
+
+fn targets(
+ root: &Path,
+ build: &Path,
+ dir: &str,
+ prefix: &str,
+ profile: Option<&CMakeProfile>,
+ out: &mut Vec,
+) {
let reply = build.join(".cmake/api/v1/reply");
let Some(index_path) = fs::read_dir(&reply).ok().and_then(|entries| {
entries
@@ -113,6 +178,9 @@ fn targets(root: &Path, build: &Path, dir: &str, prefix: &str, out: &mut Vec crate::IdeResult<()> {
&& config.args.first().is_some_and(|a| a == "-S")
&& let Some(dir) = config.extras.params.get("buildDir")
{
- let query = cwd.join(dir).join(".cmake/api/v1/query/client-openidea");
+ let build = cwd.join(dir);
+ let generator = config.args.windows(2).find(|a| a[0] == "-G").map(|a| a[1].as_str());
+ if let Some(generator) = generator
+ && let Ok(cache) = fs::read_to_string(build.join("CMakeCache.txt"))
+ && let Some(cached) = cache.lines().find_map(|l| l.strip_prefix("CMAKE_GENERATOR:INTERNAL="))
+ && cached != generator
+ {
+ fs::remove_file(build.join("CMakeCache.txt"))?;
+ let files = build.join("CMakeFiles");
+ if files.is_dir() {
+ fs::remove_dir_all(files)?;
+ }
+ }
+ let query = build.join(".cmake/api/v1/query/client-openidea");
fs::create_dir_all(&query)?;
fs::write(query.join("codemodel-v2"), "")?;
}
Ok(())
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn profiles_select_file_api_configuration_and_apply_target_options() {
+ let root = tempfile::tempdir().unwrap();
+ fs::write(root.path().join("CMakeLists.txt"), "project(app)").unwrap();
+ let reply = root.path().join("output/.cmake/api/v1/reply");
+ fs::create_dir_all(&reply).unwrap();
+ let write = |name: &str, value: Value| fs::write(reply.join(name), value.to_string()).unwrap();
+ write(
+ "index-1.json",
+ serde_json::json!({"objects":[{"kind":"codemodel","version":{"major":2},"jsonFile":"model.json"}]}),
+ );
+ write(
+ "model.json",
+ serde_json::json!({"paths":{"source":root.path()},"configurations":[
+ {"name":"Debug","targets":[{"jsonFile":"debug.json"}]},
+ {"name":"Release","targets":[{"jsonFile":"release.json"}]}
+ ]}),
+ );
+ for name in ["debug", "release"] {
+ write(
+ &format!("{name}.json"),
+ serde_json::json!({"name":"app","type":"EXECUTABLE","artifacts":[{"path":format!("{name}/app")}]}),
+ );
+ }
+ let profile = CMakeProfile {
+ name: "Production".into(),
+ build_type: "Release".into(),
+ build_dir: "output".into(),
+ build_args: ["-j", "8", "--", "verbose=1"].map(String::from).into(),
+ env: [("FOO".into(), "bar".into())].into(),
+ ..Default::default()
+ };
+ let mut configs = Vec::new();
+ detect(root.path(), Some(std::slice::from_ref(&profile)), &mut configs);
+ let run = configs.iter().find(|c| c.kind == "native").unwrap();
+ assert!(run.command.ends_with("release/app"));
+ assert_eq!(run.env, profile.env);
+ assert_eq!(configs.iter().filter(|c| c.kind == "native").count(), 1);
+ let build = configs.iter().find(|c| c.id == run.extras.before_launch[0].target).unwrap();
+ assert_eq!(
+ build.args,
+ ["--build", "output", "--target", "app", "--config", "Release", "-j", "8", "--", "verbose=1"]
+ );
+ assert_eq!(build.env, profile.env);
+ assert_eq!(build.extras.before_launch[0].target, "cmake:profile:0:configure");
+ fs::write(root.path().join("output/CMakeCache.txt"), "CMAKE_GENERATOR:INTERNAL=Ninja\n").unwrap();
+ let mut configured = Vec::new();
+ detect(root.path(), Some(std::slice::from_ref(&profile)), &mut configured);
+ for id in ["cmake:profile:0:build", build.id.as_str()] {
+ let build = configured.iter().find(|c| c.id == id).unwrap();
+ assert!(build.extras.before_launch.is_empty(), "{id} should use CMake's automatic regeneration");
+ }
+ }
+
+ #[test]
+ fn generator_change_clears_only_cmake_state_at_launch() {
+ let root = tempfile::tempdir().unwrap();
+ let build = root.path().join("cmake-build-debug");
+ fs::create_dir_all(build.join("CMakeFiles")).unwrap();
+ fs::write(build.join("CMakeCache.txt"), "CMAKE_GENERATOR:INTERNAL=Unix Makefiles\n").unwrap();
+ fs::write(build.join("keep.txt"), "artifact").unwrap();
+ fs::write(root.path().join("CMakeLists.txt"), "project(app)").unwrap();
+ let mut configs = Vec::new();
+ detect(root.path(), Some(&[CMakeProfile { generator: "Ninja".into(), ..Default::default() }]), &mut configs);
+ assert!(build.join("CMakeCache.txt").exists());
+ prepare(&configs[0], root.path()).unwrap();
+ assert!(!build.join("CMakeCache.txt").exists());
+ assert!(!build.join("CMakeFiles").exists());
+ assert!(build.join("keep.txt").exists());
+ assert!(build.join(".cmake/api/v1/query/client-openidea/codemodel-v2").exists());
+ }
+}
diff --git a/crates/ide-run/src/lib.rs b/crates/ide-run/src/lib.rs
index db94a4586..95bf00bd3 100644
--- a/crates/ide-run/src/lib.rs
+++ b/crates/ide-run/src/lib.rs
@@ -201,6 +201,15 @@ pub fn detect_excluding(root: &Path, excluded_poms: &[PathBuf]) -> Vec,
+) -> Vec {
+ detect::detect_with_profiles(root, excluded_poms, profiles)
+}
+
/// Reads `/.openidea/run.toml`; a missing file yields no configs.
pub fn load_user_configs(root: &Path) -> IdeResult> {
config::load(root)
diff --git a/crates/ide-run/tests/detect.rs b/crates/ide-run/tests/detect.rs
index 601f9af7f..578019334 100644
--- a/crates/ide-run/tests/detect.rs
+++ b/crates/ide-run/tests/detect.rs
@@ -652,3 +652,72 @@ fn go_default_build_output_is_ignored_by_version_control() {
let ignore = fs::read_to_string(dir.path().join(".openidea/.gitignore")).unwrap();
assert!(ignore.lines().any(|l| l == "/bin/"), "{ignore}");
}
+
+#[test]
+fn cmake_profiles_control_commands_names_and_environment() {
+ use ide_core::settings::CMakeProfile;
+ let dir = project(&[("CMakeLists.txt", "project(app)")]);
+ for generator in ["", "Ninja", "Unix Makefiles", "Ninja Multi-Config", "Visual Studio 17 2022"] {
+ let profile = CMakeProfile {
+ name: "Production".into(),
+ build_type: "Release".into(),
+ generator: generator.into(),
+ cmake_args: ["-DFEATURE=ON", "-DMESSAGE=two words", "-DQUOTE=\"hello world\"", ""].map(String::from).into(),
+ build_dir: "output dir".into(),
+ build_args: ["-j", "8", "--", "VERBOSE=1"].map(String::from).into(),
+ env: [("MY_ENV".into(), "yes".into())].into(),
+ };
+ let configs = ide_run::detect_with_profiles(dir.path(), &[], Some(std::slice::from_ref(&profile)));
+ let configure = find(&configs, "cmake:profile:0:configure");
+ let mut expected = vec!["-S", ".", "-B", "output dir"];
+ if !generator.is_empty() {
+ expected.extend(["-G", generator]);
+ }
+ expected.extend([
+ "-DCMAKE_BUILD_TYPE=Release",
+ "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
+ "-DFEATURE=ON",
+ "-DMESSAGE=two words",
+ "-DQUOTE=\"hello world\"",
+ "",
+ ]);
+ assert_eq!(configure.args, expected);
+ assert_eq!(configure.name, "CMake Configure (output dir)");
+ let build = find(&configs, "cmake:profile:0:build");
+ assert_eq!(build.args, ["--build", "output dir", "--config", "Release", "-j", "8", "--", "VERBOSE=1"]);
+ assert_eq!(build.extras.before_launch[0].target, configure.id);
+ assert!(configs.iter().all(|c| c.env == profile.env));
+ let configs = ide_run::detect_with_profiles(dir.path(), &[], Some(&[CMakeProfile::default(), profile]));
+ assert!(find(&configs, "cmake:profile:0:build").name.ends_with("[Debug]"));
+ assert!(find(&configs, "cmake:profile:1:build").name.ends_with("[Production]"));
+ assert_eq!(find(&configs, "cmake:profile:0:build").args[1], "cmake-build-debug");
+ }
+ assert_eq!(ide_run::detect_with_profiles(dir.path(), &[], None), detect(dir.path()));
+ assert!(ide_run::detect_with_profiles(dir.path(), &[], Some(&[])).is_empty());
+}
+
+#[test]
+fn cmake_profile_build_configures_only_without_a_cache() {
+ use ide_core::settings::CMakeProfile;
+ let root = project(&[("CMakeLists.txt", "project(app)")]);
+ for dir in ["", "custom output"] {
+ let profile = CMakeProfile { build_dir: dir.into(), generator: "Ninja".into(), ..Default::default() };
+ let build_dir = root.path().join(if dir.is_empty() { "cmake-build-debug" } else { dir });
+ for cached in [false, true] {
+ if cached {
+ fs::create_dir_all(&build_dir).unwrap();
+ // Generator changes are handled by an explicit Configure, not every Build.
+ fs::write(build_dir.join("CMakeCache.txt"), "CMAKE_GENERATOR:INTERNAL=Unix Makefiles\n").unwrap();
+ }
+ let configs = ide_run::detect_with_profiles(root.path(), &[], Some(std::slice::from_ref(&profile)));
+ let build = find(&configs, "cmake:profile:0:build");
+ if cached {
+ assert!(build.extras.before_launch.is_empty());
+ } else {
+ assert_eq!(build.extras.before_launch.len(), 1);
+ assert_eq!(build.extras.before_launch[0].target, "cmake:profile:0:configure");
+ }
+ assert_eq!(find(&configs, "cmake:profile:0:test").extras.before_launch[0].target, build.id);
+ }
+ }
+}
diff --git a/src/bindings.ts b/src/bindings.ts
index b65240187..645f8668a 100644
--- a/src/bindings.ts
+++ b/src/bindings.ts
@@ -1639,6 +1639,30 @@ export type ByteRange = {
total: number,
};
+export type CMakeProfile = {
+ name?: string,
+ buildType?: string,
+ generator?: string,
+ cmakeArgs?: string[],
+ /** Empty uses cmake-build-. */
+ buildDir?: string,
+ buildArgs?: string[],
+ env?: { [key in string]: string },
+};
+
+/** Absent profiles retain automatic discovery for existing projects. */
+export type CMakeSettings = CMakeSettings_Serialize | CMakeSettings_Deserialize;
+
+/** Absent profiles retain automatic discovery for existing projects. */
+export type CMakeSettings_Deserialize = {
+ profiles?: CMakeProfile[] | null,
+};
+
+/** Absent profiles retain automatic discovery for existing projects. */
+export type CMakeSettings_Serialize = {
+ profiles?: CMakeProfile[] | null,
+};
+
export type CallHierarchy = {
items: HierarchyItem[],
incomplete: ReferencesIncomplete | null,
@@ -5635,6 +5659,7 @@ export type Settings_Deserialize = {
deployment?: DeploymentSettings,
/** Build, Execution, Deployment › Run Targets. */
runTargets?: RunTarget[],
+ cmake?: CMakeSettings_Deserialize,
};
export type Settings_Serialize = {
@@ -5690,6 +5715,7 @@ export type Settings_Serialize = {
deployment: DeploymentSettings,
/** Build, Execution, Deployment › Run Targets. */
runTargets: RunTarget[],
+ cmake: CMakeSettings_Serialize,
};
export type Severity = "error" | "warning" | "info" | "hint";
diff --git a/src/features/run/index.ts b/src/features/run/index.ts
index 5cac309c2..a07662b1d 100644
--- a/src/features/run/index.ts
+++ b/src/features/run/index.ts
@@ -538,7 +538,14 @@ export function activate(): () => void {
}),
// Detected configs carry the selected toolchains (interpreter, JDK, Maven home and options).
onIdeEvent("settingsChanged", (e) => {
- if (e.key === "languages" || e.key.startsWith("languages.")) scheduleReload();
+ if (
+ !e.key ||
+ e.key === "languages" ||
+ e.key.startsWith("languages.") ||
+ e.key === "cmake" ||
+ e.key.startsWith("cmake.")
+ )
+ scheduleReload();
}),
);
diff --git a/src/features/settings/__tests__/cmake.test.ts b/src/features/settings/__tests__/cmake.test.ts
new file mode 100644
index 000000000..bf5da3a79
--- /dev/null
+++ b/src/features/settings/__tests__/cmake.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, it } from "vitest";
+import { validateSettings } from "../model/schema";
+import { DEFAULT_CMAKE_PROFILE } from "@/platform/cmakeSettings";
+
+describe("CMake settings", () => {
+ it("preserves absent and explicitly empty profiles", () => {
+ expect(validateSettings({}).cmake).toEqual({});
+ expect(validateSettings({ cmake: { profiles: [] } }).cmake.profiles).toEqual([]);
+ });
+ it("preserves argument arrays and drops malformed argument values", () => {
+ const cmakeArgs = ["-DMESSAGE=two words", '-DQUOTE="hello"', ""];
+ const buildArgs = ["-j", "8", "--", "LABEL=it's a build"];
+ const profiles = validateSettings({ cmake: { profiles: [{ cmakeArgs, buildArgs }] } }).cmake.profiles;
+ expect(profiles?.[0]).toMatchObject({ cmakeArgs, buildArgs });
+ expect(
+ validateSettings({ cmake: { profiles: [{ cmakeArgs: ["-DFOO=bar", 42, null] }] } }).cmake.profiles?.[0]
+ ?.cmakeArgs,
+ ).toEqual(["-DFOO=bar"]);
+ });
+ it("retains custom types and generators while normalizing malformed fields", () => {
+ expect(
+ validateSettings({
+ cmake: {
+ profiles: [
+ {
+ name: "Custom",
+ buildType: "Coverage",
+ generator: "Custom generator",
+ buildArgs: 5,
+ env: { KEEP: "yes", DROP: 5 },
+ },
+ null,
+ ],
+ },
+ }).cmake.profiles,
+ ).toEqual([
+ {
+ ...DEFAULT_CMAKE_PROFILE,
+ name: "Custom",
+ buildType: "Coverage",
+ generator: "Custom generator",
+ env: { KEEP: "yes" },
+ },
+ ]);
+ });
+});
diff --git a/src/features/settings/__tests__/cmakeEditor.test.tsx b/src/features/settings/__tests__/cmakeEditor.test.tsx
new file mode 100644
index 000000000..2d11a991b
--- /dev/null
+++ b/src/features/settings/__tests__/cmakeEditor.test.tsx
@@ -0,0 +1,72 @@
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, expect, it } from "vitest";
+import { DEFAULT_CMAKE_PROFILE } from "@/platform/cmakeSettings";
+import { parseArgs } from "@/platform/commandLine";
+import { PageContext } from "../components/context";
+import { validateSettings } from "../model/schema";
+import { useSettingsDialog } from "../model/staging";
+import { CMakeProfiles } from "../pages/cmake";
+
+const savedDialog = useSettingsDialog.getState();
+afterEach(() => {
+ cleanup();
+ useSettingsDialog.setState(savedDialog, true);
+});
+
+it.each([
+ ["CMake arguments", "cmakeArgs"],
+ ["Build arguments", "buildArgs"],
+] as const)("round-trips spaces, quotes and empty values in %s", async (label, key) => {
+ const args = ["two words", 'a "quote"', "it's", "", String.raw`C:\some path\file`];
+ useSettingsDialog.setState({
+ base: validateSettings({ cmake: { profiles: [{ ...DEFAULT_CMAKE_PROFILE, [key]: args }] } }),
+ staged: {},
+ });
+ const mount = () =>
+ render(
+
+
+ ,
+ );
+ const view = mount();
+ const input = screen.getByLabelText(label);
+ expect(parseArgs(input.value)).toEqual(args);
+ await userEvent.clear(input);
+ const text = String.raw`-DNAME="two words" "-DQUOTE=it's" ""`;
+ await userEvent.type(input, text);
+ expect(input).toHaveValue(text);
+ fireEvent.blur(input);
+ const expected = ["-DNAME=two words", "-DQUOTE=it's", ""];
+ expect(parseArgs(input.value)).toEqual(expected);
+ expect(useSettingsDialog.getState().staged["project|cmake.profiles"]?.value).toEqual([
+ { ...DEFAULT_CMAKE_PROFILE, [key]: expected },
+ ]);
+ view.unmount();
+ mount();
+ expect(parseArgs(screen.getByLabelText(label).value)).toEqual(expected);
+});
+
+it("displays each profile's arguments when switching profiles", () => {
+ useSettingsDialog.setState({
+ base: validateSettings({
+ cmake: {
+ profiles: [
+ { ...DEFAULT_CMAKE_PROFILE, name: "Debug" },
+ { ...DEFAULT_CMAKE_PROFILE, name: "Release", cmakeArgs: ["-DMESSAGE=release build"] },
+ ],
+ },
+ }),
+ staged: {},
+ });
+ render(
+
+
+ ,
+ );
+ fireEvent.change(screen.getByLabelText("CMake arguments"), { target: { value: '"unfinished ' } });
+ fireEvent.click(screen.getByRole("option", { name: "Release" }));
+ expect(parseArgs(screen.getByLabelText("CMake arguments").value)).toEqual([
+ "-DMESSAGE=release build",
+ ]);
+});
diff --git a/src/features/settings/__tests__/dialog.test.tsx b/src/features/settings/__tests__/dialog.test.tsx
index dbdb5f024..a72f47525 100644
--- a/src/features/settings/__tests__/dialog.test.tsx
+++ b/src/features/settings/__tests__/dialog.test.tsx
@@ -84,6 +84,29 @@ describe("Settings dialog", () => {
expect(settingsSet).toHaveBeenLastCalledWith("global", "editor.encodings.nativeToAscii", "false");
});
+ it("edits CMake profiles in project settings and persists them on Apply", async () => {
+ useProject.setState({ project: { root: "/project" } as never });
+ const dialog = await openDialog("build.cmake");
+ expect(within(dialog).getByRole("option", { name: "Debug" })).toBeInTheDocument();
+ fireEvent.click(
+ within(within(dialog).getByRole("toolbar", { name: "CMake profile actions" })).getByRole("button", {
+ name: "Add",
+ }),
+ );
+ fireEvent.change(within(dialog).getByLabelText("Name"), { target: { value: "Release" } });
+ fireEvent.change(within(dialog).getByLabelText("Build type"), { target: { value: "Release" } });
+ fireEvent.change(within(dialog).getByLabelText("Generator"), { target: { value: "Ninja" } });
+ fireEvent.change(within(dialog).getByLabelText("Build arguments"), { target: { value: "-j 8" } });
+ expect(settingsSet).not.toHaveBeenCalled();
+ await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" })));
+ const write = settingsSet.mock.calls.find((c) => c[1] === "cmake.profiles")!;
+ expect(write[0]).toBe("project");
+ expect(JSON.parse(write[2])).toEqual([
+ expect.objectContaining({ name: "Debug", buildType: "Debug" }),
+ expect.objectContaining({ name: "Release", buildType: "Release", generator: "Ninja", buildArgs: ["-j", "8"] }),
+ ]);
+ });
+
it("finds Gradle JVM and applies project Gradle settings", async () => {
useProject.setState({ project: { root: "/project" } as never });
const dialog = await openDialog("build.gradle");
diff --git a/src/features/settings/model/schema.ts b/src/features/settings/model/schema.ts
index 8bf6e185e..d92540d3a 100644
--- a/src/features/settings/model/schema.ts
+++ b/src/features/settings/model/schema.ts
@@ -4,6 +4,7 @@
* so `validateSettings` never throws — anything unknown or malformed falls
* back to the default (mirrors `ide-core::settings` defaults).
*/
+import { validateCMake } from "@/platform/cmakeSettings";
import { DEFAULT_IGNORED_NAMES } from "@/platform/ignoredNames";
import { validateFileTypeOverrides, validateFileTypes, type FileTypeAssociations } from "@/platform/fileTypes";
import type { LanguageServerSettings, Settings } from "@/platform/ipc";
@@ -82,6 +83,7 @@ export interface ValidSettings extends WorkflowSettings {
/** System Settings › HTTP Proxy (global). */
proxy: ProxySettingsValid;
languages: LanguagesValid;
+ cmake: ReturnType;
projectStructure: ProjectStructureValid;
inspections: InspectionSettingsValid;
/** Editor › Natural Languages › Spelling. */
@@ -150,6 +152,7 @@ export const DEFAULT_SETTINGS: ValidSettings = {
notifications: DEFAULT_NOTIFICATIONS,
proxy: DEFAULT_PROXY,
languages: DEFAULT_LANGUAGES,
+ cmake: {},
projectStructure: DEFAULT_PROJECT_STRUCTURE,
inspections: DEFAULT_INSPECTIONS,
spelling: { ideWords: [], projectWords: [], customDictionaries: [] },
@@ -254,6 +257,7 @@ export function validateSettings(raw: Settings | unknown): ValidSettings {
notifications: validateNotifications(root.notifications),
proxy: validateProxy(root.proxy),
languages: validateLanguages(root.languages),
+ cmake: validateCMake(root.cmake),
projectStructure: validateProjectStructure(root.projectStructure),
inspections: validateInspections(root.inspections),
spelling: validateSpelling(root.spelling),
diff --git a/src/features/settings/pages/CMake.module.css b/src/features/settings/pages/CMake.module.css
new file mode 100644
index 000000000..1196d4e8f
--- /dev/null
+++ b/src/features/settings/pages/CMake.module.css
@@ -0,0 +1,4 @@
+.profile {
+ width: 100%;
+ text-align: left;
+}
diff --git a/src/features/settings/pages/cmake.tsx b/src/features/settings/pages/cmake.tsx
new file mode 100644
index 000000000..6bdf008b0
--- /dev/null
+++ b/src/features/settings/pages/cmake.tsx
@@ -0,0 +1,151 @@
+import { useId, useState } from "react";
+import { DEFAULT_CMAKE_PROFILE, type CMakeProfileValid } from "@/platform/cmakeSettings";
+import { formatArgs, parseArgs } from "@/platform/commandLine";
+import { tx } from "@/platform/i18n";
+import { IconButton, Input, T } from "@/ui";
+import { Row, Section } from "../components/Form";
+import { useSetting } from "../components/context";
+import { TableEditor } from "./workflowControls";
+import styles from "./Workflow.module.css";
+import own from "./CMake.module.css";
+
+function ArgumentsInput({ id, value, onChange }: { id: string; value: string[]; onChange: (args: string[]) => void }) {
+ const [text, setText] = useState();
+ return (
+ {
+ setText(e.target.value);
+ onChange(parseArgs(e.target.value));
+ }}
+ onBlur={() => setText(undefined)}
+ />
+ );
+}
+
+export function CMakeProfiles() {
+ const [saved, setProfiles] = useSetting("cmake.profiles");
+ const profiles = saved ?? [DEFAULT_CMAKE_PROFILE];
+ const [selected, setSelected] = useState(0);
+ const index = Math.min(selected, Math.max(0, profiles.length - 1));
+ const current = profiles[index];
+ const id = useId();
+ const update = (patch: Partial) =>
+ setProfiles(profiles.map((p, i) => (i === index ? { ...p, ...patch } : p)));
+ const fields = [
+ ["name", "Name"],
+ ["buildType", "Build type"],
+ ["generator", "Generator"],
+ ["cmakeArgs", "CMake arguments"],
+ ["buildDir", "Build directory"],
+ ["buildArgs", "Build arguments"],
+ ] as const;
+ return (
+
+
+
+
+ {
+ setProfiles([
+ ...profiles,
+ { ...DEFAULT_CMAKE_PROFILE, name: tx`CMake profile ${profiles.length + 1}`, env: {} },
+ ]);
+ setSelected(profiles.length);
+ }}
+ />
+ {
+ setProfiles(profiles.filter((_, i) => i !== index));
+ setSelected(Math.max(0, index - 1));
+ }}
+ />
+
+
+ {profiles.map((p, i) => (
+
+ ))}
+ {profiles.length === 0 && No CMake profiles. Click + to add one.}
+
+
+
+ {current && (
+
+ {fields.map(([key, label]) => (
+
+ {key === "cmakeArgs" || key === "buildArgs" ? (
+ update({ [key]: args })}
+ />
+ ) : (
+ update({ [key]: e.target.value })}
+ />
+ )}
+
+ ))}
+
+
+
+ ({ name, value }))}
+ columns={[
+ { key: "name", title: "Name", placeholder: "NAME" },
+ { key: "value", title: "Value", placeholder: "value" },
+ ]}
+ onChange={(rows) => update({ env: Object.fromEntries(rows.map((r) => [r.name ?? "", r.value ?? ""])) })}
+ empty="No variables"
+ aria-label="Environment variables"
+ />
+
+
+
+ Quote arguments containing spaces. Build arguments go to cmake --build; use -- before native build tool
+ options.
+
+
+
+ )}
+
+ );
+}
diff --git a/src/features/settings/pages/execution.tsx b/src/features/settings/pages/execution.tsx
index 77ce59a02..3498655c9 100644
--- a/src/features/settings/pages/execution.tsx
+++ b/src/features/settings/pages/execution.tsx
@@ -3,6 +3,7 @@
* Stepping), Python Debugger and Run/Debug. Consumed by the run and debugger
* features (and `ide-run` for encoding / termination timeout).
*/
+import { CMakeProfiles } from "./cmake";
import { WORKFLOW_LIMITS } from "@/platform/workflowSettings";
import type { SelectOption } from "@/ui";
import { GlobListEditor, NumberInput, Row } from "../components/Form";
@@ -90,6 +91,25 @@ function TooltipDelayRow() {
}
export const EXECUTION_PAGES: PageSpec[] = [
+ {
+ id: "build.cmake",
+ title: "CMake",
+ group: BUILD_GROUP,
+ order: 357,
+ projectScope: true,
+ defaultScope: "project",
+ keywords: [
+ "profile",
+ "generator",
+ "Ninja",
+ "Build type",
+ "CMake arguments",
+ "Build directory",
+ "Build arguments",
+ "Environment variables",
+ ],
+ component: CMakeProfiles,
+ },
{
id: "build.gradle",
projectScope: true,
diff --git a/src/platform/cmakeSettings.ts b/src/platform/cmakeSettings.ts
new file mode 100644
index 000000000..6c4cd5edf
--- /dev/null
+++ b/src/platform/cmakeSettings.ts
@@ -0,0 +1,38 @@
+import type { CMakeProfile } from "@/bindings";
+
+export type CMakeProfileValid = Required;
+
+export const DEFAULT_CMAKE_PROFILE: CMakeProfileValid = {
+ name: "Debug",
+ buildType: "Debug",
+ generator: "",
+ cmakeArgs: [],
+ buildDir: "",
+ buildArgs: [],
+ env: {},
+};
+
+export function validateCMake(raw: unknown): { profiles?: CMakeProfileValid[] } {
+ const profiles = (raw as { profiles?: unknown } | undefined)?.profiles;
+ if (!Array.isArray(profiles)) return {};
+ const string = (value: unknown, fallback = "") => (typeof value === "string" ? value : fallback);
+ const args = (value: unknown): string[] =>
+ Array.isArray(value) ? value.filter((arg): arg is string => typeof arg === "string") : [];
+ return {
+ profiles: profiles
+ .filter((p) => p && typeof p === "object")
+ .map((p) => ({
+ name: string(p.name, "Debug"),
+ buildType: string(p.buildType, "Debug"),
+ generator: string(p.generator),
+ cmakeArgs: args(p.cmakeArgs),
+ buildDir: string(p.buildDir),
+ buildArgs: args(p.buildArgs),
+ env: Object.fromEntries(
+ Object.entries(p.env && typeof p.env === "object" ? p.env : {}).filter(
+ (entry): entry is [string, string] => typeof entry[1] === "string",
+ ),
+ ),
+ })),
+ };
+}
diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json
index 5c81a726f..839f1066e 100644
--- a/src/platform/locales/text/zh.json
+++ b/src/platform/locales/text/zh.json
@@ -5454,5 +5454,17 @@
"Cannot read Java formatter XML {path}: {error}": "无法读取 Java 格式化 XML {path}:{error}",
"Java formatter configuration unavailable": "Java 格式化配置不可用",
"Choose an Eclipse formatter XML file.": "请选择 Eclipse 格式化 XML 文件。",
- "Not a formatter XML file: {path}": "不是格式化 XML 文件:{path}"
+ "Not a formatter XML file: {path}": "不是格式化 XML 文件:{path}",
+ "CMake profiles": "CMake 配置",
+ "CMake profile actions": "CMake 配置操作",
+ "No CMake profiles. Click + to add one.": "没有 CMake 配置。点击 + 添加。",
+ "Profile settings": "配置设置",
+ "Build type": "构建类型",
+ "Generator": "生成器",
+ "CMake arguments": "CMake 参数",
+ "Build directory": "构建目录",
+ "Build arguments": "构建参数",
+ "CMake default": "CMake 默认",
+ "Quote arguments containing spaces. Build arguments go to cmake --build; use -- before native build tool options.": "包含空格的参数请加引号。构建参数传给 cmake --build;原生构建工具参数前请加 --。",
+ "CMake profile {0}": "CMake 配置 {0}"
}
diff --git a/src/platform/locales/textCatalog.test.ts b/src/platform/locales/textCatalog.test.ts
index 7d74d60e9..3a418ebf5 100644
--- a/src/platform/locales/textCatalog.test.ts
+++ b/src/platform/locales/textCatalog.test.ts
@@ -80,6 +80,7 @@ const BUILT_AT_RUNTIME = new Set([]);
/** Text that stays as written in every locale: names, code, formats, shortcuts. */
const UNTRANSLATED = new Set([
// Product names
+ "CMake",
"Prettier",
"IntelliJ IDEA",
// Go environment variables and placeholder values