diff --git a/crates/ide-build/src/lib.rs b/crates/ide-build/src/lib.rs index eb613c5cf..3c436dec1 100644 --- a/crates/ide-build/src/lib.rs +++ b/crates/ide-build/src/lib.rs @@ -76,7 +76,7 @@ pub fn load_linked( gradle_settings: &ide_core::settings::GradleSettings, ) -> BuildModel { let maven = maven::has_projects(root, settings).then(|| { - let profiles = sync::maven_profiles(cache_dir); + let profiles = settings.profiles(&sync::maven_profiles(cache_dir)); let repo = maven_repo.or_else(maven::default_local_repo); let mut m = maven::load_with_linked(root, repo, &profiles, &settings.linked_projects); maven::UnlinkedProjects::from_model(&m, settings).filter(&mut m); @@ -222,6 +222,30 @@ mod tests { assert!(gradle_external_roots(&root, &temp.path().join("cache"), &settings).is_empty()); } + #[test] + fn settings_profiles_and_checkbox_choices_control_modules_before_sync() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + std::fs::create_dir(root.join("extra")).unwrap(); + std::fs::write(root.join("extra/pom.xml"), "extra").unwrap(); + std::fs::write( + root.join("pom.xml"), + r#"rootpom + extraextra"#, + ) + .unwrap(); + let cache = root.join("cache"); + let settings = ide_core::settings::MavenSettings { active_profiles: "extra".into(), ..Default::default() }; + let modules = |settings: &ide_core::settings::MavenSettings| { + load_linked(root, &cache, None, settings, &Default::default()).maven.unwrap().projects[0].modules.clone() + }; + assert!(modules(&Default::default()).is_empty()); + assert_eq!(modules(&settings), ["extra/pom.xml"]); + assert_eq!(maven::load_linked(root, &settings).projects[0].modules, ["extra/pom.xml"]); + save_maven_profiles(&cache, &["!extra".into()]).unwrap(); + assert!(modules(&settings).is_empty()); + } + #[test] fn detects_build_systems_and_build_files() { let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/ide-build/src/maven.rs b/crates/ide-build/src/maven.rs index c6ccbff6f..0268adb63 100644 --- a/crates/ide-build/src/maven.rs +++ b/crates/ide-build/src/maven.rs @@ -1063,7 +1063,7 @@ pub fn load(root: &Path) -> MavenModel { /// [`load`] with the POMs `settings` link (Add Maven Projects). pub fn load_linked(root: &Path, settings: &ide_core::settings::MavenSettings) -> MavenModel { - load_with_linked(root, default_local_repo(), &[], &settings.linked_projects) + load_with_linked(root, default_local_repo(), &settings.profiles(&[]), &settings.linked_projects) } pub(crate) fn load_with_repo(root: &Path, local_repo: Option) -> MavenModel { diff --git a/crates/ide-build/src/sync.rs b/crates/ide-build/src/sync.rs index 0a262c624..13336817d 100644 --- a/crates/ide-build/src/sync.rs +++ b/crates/ide-build/src/sync.rs @@ -304,7 +304,7 @@ pub fn sync_maven( let mut model = maven::load_with_linked( root, maven::default_local_repo(), - &options.profiles, + &options.maven_settings.profiles(&options.profiles), &options.maven_settings.linked_projects, ); maven::UnlinkedProjects::from_model(&model, &options.maven_settings).filter(&mut model); diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs index 9cbcb7ba2..11548028d 100644 --- a/crates/ide-core/src/settings.rs +++ b/crates/ide-core/src/settings.rs @@ -926,6 +926,8 @@ pub struct MavenSettings { pub home: String, /// User `settings.xml` (`-s`); empty = `~/.m2/settings.xml`. pub user_settings: String, + /// Toolchains file (`-t`); empty = `~/.m2/toolchains.xml`. + pub toolchains: String, /// Local repository (`-Dmaven.repo.local`); empty = from `settings.xml`. pub local_repository: String, /// Run Maven through the Maven Daemon (`mvnd`) for faster builds. mvnd embeds @@ -937,9 +939,25 @@ pub struct MavenSettings { /// Thread count of Maven builds (`-T`): a number of threads (`4`) or a core /// multiplier (`2C`, `2.5C`); empty = Maven's default (one thread per module). pub threads: String, + /// Active Maven profiles (`-P`), comma-separated; empty leaves profiles to the POM/settings. + pub active_profiles: String, } impl MavenSettings { + /// Settings profiles followed by the Maven tool window's choices. + pub fn profiles(&self, choices: &[String]) -> Vec { + let mut profiles = Vec::new(); + for profile in self.active_profiles.split(',').chain(choices.iter().map(String::as_str)).map(str::trim) { + if !profile.is_empty() && !profiles.iter().any(|p| p == profile) { + profiles.push(profile.to_owned()); + } + } + let excluded: Vec = + profiles.iter().filter_map(|p| p.strip_prefix(['!', '-']).map(str::to_owned)).collect(); + profiles.retain(|p| !excluded.contains(p)); + profiles + } + /// The `-T` value when it is one Maven accepts: a thread count (`4`) or a /// core multiplier with an optional decimal fraction (`1C`, `2.5C`). /// Anything else is dropped: a bad `-T` would make every Maven run fail. @@ -1399,6 +1417,24 @@ mod maven_linkage_tests { assert_eq!(threads("2..5C").as_deref(), None); } + #[test] + fn maven_profiles_combine_settings_and_tool_window_choices() { + let settings = MavenSettings { active_profiles: " a, b, ,a ".into(), ..Default::default() }; + assert_eq!(settings.profiles(&["!b".into(), "c".into(), "a".into()]), ["a", "!b", "c"]); + let settings = MavenSettings { active_profiles: "!a".into(), ..Default::default() }; + assert_eq!(settings.profiles(&["a".into()]), ["!a"]); + } + + #[test] + fn maven_profiles_and_toolchains_round_trip_and_default_to_empty() { + let old: MavenSettings = serde_json::from_str(r#"{"home":"/opt/maven"}"#).unwrap(); + assert!(old.toolchains.is_empty() && old.active_profiles.is_empty()); + let settings = + MavenSettings { toolchains: "config files/toolchains.xml".into(), active_profiles: "a,b,!c".into(), ..old }; + let encoded = serde_json::to_string(&settings).unwrap(); + assert_eq!(serde_json::from_str::(&encoded).unwrap(), settings); + } + #[test] fn thread_count_round_trips_and_defaults_to_empty() { // Old settings files predate the thread count; `serde(default)` keeps them loadable. diff --git a/crates/ide-sdk/src/apply.rs b/crates/ide-sdk/src/apply.rs index 075e61bd9..ebc12bb16 100644 --- a/crates/ide-sdk/src/apply.rs +++ b/crates/ide-sdk/src/apply.rs @@ -408,33 +408,34 @@ pub fn gradle_env( } /// Maven options from settings, placed before the goals: `-s `, -/// `-Dmaven.repo.local=` and `-T ` (the thread count). +/// `-t `, `-Dmaven.repo.local=`, `-T ` and `-P `. pub fn maven_options(root: &Path, s: &Settings) -> Vec { let m = &s.languages.java.maven; let mut out = Vec::new(); if let Some(file) = setting_path(root, &m.user_settings) { out.extend(["-s".to_owned(), file.to_string_lossy().into_owned()]); } + if let Some(file) = setting_path(root, &m.toolchains) { + out.extend(["-t".to_owned(), file.to_string_lossy().into_owned()]); + } if let Some(dir) = setting_path(root, &m.local_repository) { out.push(format!("-Dmaven.repo.local={}", dir.to_string_lossy())); } if let Some(threads) = m.thread_count() { out.extend(["-T".to_owned(), threads.to_owned()]); } + if let Some(profiles) = non_empty(&m.active_profiles) { + out.extend(["-P".to_owned(), profiles.to_owned()]); + } out } fn maven_settings_prefixes(args: &[String]) -> Vec { let mut prefixes = vec![0]; let mut i = 0; - while let Some(arg) = args.get(i) { - if (arg == "-s" || arg == "-T") && args.get(i + 1).is_some() { - i += 2; - } else if arg.starts_with("-Dmaven.repo.local=") { - i += 1; - } else { - break; - } + while i < args.len() { + let Some(n) = maven_option_len(&args[i..]) else { break }; + i += n; prefixes.push(i); } prefixes @@ -1110,43 +1111,62 @@ fn is_python_command(cmd: &str) -> bool { const PY_RUNNERS: [&str; 3] = ["uv", "poetry", "pdm"]; -/// [`maven_options`] as the groups they are passed in (`-s `, `-Dmaven.repo.local=…`, -/// `-T `). +fn maven_option_flag(arg: &str) -> Option<&'static str> { + for (short, long) in + [("-s", "--settings"), ("-t", "--toolchains"), ("-T", "--threads"), ("-P", "--activate-profiles")] + { + if arg.starts_with(short) || arg == long || arg.starts_with(&format!("{long}=")) { + return Some(short); + } + } + arg.starts_with("-Dmaven.repo.local=").then_some("-Dmaven.repo.local") +} + +fn maven_option_len(args: &[String]) -> Option { + let arg = args.first()?; + maven_option_flag(arg)?; + let separate = matches!( + arg.as_str(), + "-s" | "--settings" | "-t" | "--toolchains" | "-T" | "--threads" | "-P" | "--activate-profiles" + ); + Some(if separate { 2.min(args.len()) } else { 1 }) +} + +/// [`maven_options`] grouped with each option's value. fn maven_option_groups(options: &[String]) -> Vec<&[String]> { let mut groups = Vec::new(); let mut i = 0; while i < options.len() { - let n = if options[i] == "-s" || options[i] == "-T" { 2.min(options.len() - i) } else { 1 }; + let n = maven_option_len(&options[i..]).unwrap_or(1); groups.push(&options[i..i + n]); i += n; } groups } -/// Whether `args` set what `group` sets: a user settings file, the local -/// repository, or the thread count. +/// Whether the configuration sets the same option, including attached and long forms. fn sets_maven_option(args: &[String], group: &[String]) -> bool { - match group.first().map(String::as_str) { - Some("-s") => args.iter().any(|a| a == "-s" || a == "--settings" || a.starts_with("--settings=")), - // `-T 4`, `--threads 4`, `-T4` and `--threads=4` all set the thread count. - Some("-T") => args.iter().any(|a| { - a == "-T" || a == "--threads" || a.starts_with("--threads=") || (a.starts_with("-T") && a.len() > 2) - }), - Some(flag) => { - let key = flag.split_once('=').map_or(flag, |(k, _)| k); - args.iter().any(|a| a.split_once('=').map_or(a.as_str(), |(k, _)| k) == key) - || args - .windows(2) - .any(|a| a[0] == "-D" && format!("-D{}", a[1].split('=').next().unwrap_or_default()) == key) - } - None => true, - } + let Some(flag) = group.first().and_then(|a| maven_option_flag(a)) else { return true }; + args.iter().any(|a| maven_option_flag(a) == Some(flag)) + || (flag == "-Dmaven.repo.local" + && args.windows(2).any(|a| a[0] == "-D" && a[1].starts_with("maven.repo.local="))) } -/// Settings › Maven's options ahead of the goals, each unless the configuration sets it itself. -fn add_maven_options(args: &mut Vec, options: &[String]) { - let missing: Vec = - maven_option_groups(options).into_iter().filter(|g| !sets_maven_option(args, g)).flatten().cloned().collect(); +/// Settings options precede the goals. Tool-window checkbox profiles are additive; +/// profiles explicitly typed in the configuration replace the Settings profiles. +fn add_maven_options(args: &mut Vec, options: &[String], additive_profiles: bool) { + let missing: Vec = maven_option_groups(options) + .into_iter() + .filter(|g| { + if additive_profiles && g[0] == "-P" { + !args.windows(g.len()).any(|w| w == *g) + } else { + !sets_maven_option(args, g) + } + }) + .flatten() + .cloned() + .collect(); args.splice(0..0, missing); } @@ -1315,7 +1335,11 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings { c.command = mvn.to_string_lossy().into_owned(); } - add_maven_options(&mut c.args, &mvn_options); + let additive_profiles = generated_maven_goal(c) + && c.id.split_once(".xml:").is_some_and(|(_, goals)| { + !goals.split_whitespace().any(|a| maven_option_flag(a) == Some("-P")) + }); + add_maven_options(&mut c.args, &mvn_options, additive_profiles); } _ => {} } diff --git a/crates/ide-sdk/src/maven_profiles.rs b/crates/ide-sdk/src/maven_profiles.rs index b5eeed576..d9210925d 100644 --- a/crates/ide-sdk/src/maven_profiles.rs +++ b/crates/ide-sdk/src/maven_profiles.rs @@ -12,15 +12,15 @@ use std::path::{Path, PathBuf}; const PREFIX: &str = "jdtls-maven-settings-"; /// Points jdtls at a user `settings.xml` activating `profiles` (`-P` entries of the -/// last Maven sync), written to `dir`. Through `settings`, so a running server +/// last Maven sync), combined with Settings profiles and written to `dir`. Through `settings`, so a running server /// re-imports its Maven projects instead of restarting. `!id` entries can't be /// expressed in `settings.xml` and are left to the POMs' own activation. pub fn configure_jdtls(cfg: &mut LanguageServerConfig, root: &Path, s: &Settings, profiles: &[String], dir: &Path) { if cfg.name != "jdtls" { return; } - let active: Vec<&str> = - profiles.iter().map(|p| p.trim()).filter(|p| !p.is_empty() && !p.starts_with('!')).collect(); + let profiles = s.languages.java.maven.profiles(profiles); + let active: Vec<&str> = profiles.iter().map(String::as_str).filter(|p| !p.starts_with(['!', '-'])).collect(); let Some(file) = (!active.is_empty()).then(|| write_settings(root, s, &active, dir)).flatten() else { remove_generated(dir, None); return; @@ -192,6 +192,30 @@ mod tests { assert!(with_active_profiles("", &["a&b"]).unwrap().contains("a&b")); } + #[test] + fn jdtls_combines_settings_profiles_and_checkbox_choices() { + let tmp = tempfile::tempdir().unwrap(); + let mut s = Settings::default(); + s.languages.java.maven.user_settings = "config files/settings.xml".into(); + s.languages.java.maven.active_profiles = " a, c, !d, a ".into(); + std::fs::create_dir_all(tmp.path().join("config files")).unwrap(); + std::fs::write(tmp.path().join("config files/settings.xml"), "").unwrap(); + let mut cfg = jdtls(); + configure_jdtls(&mut cfg, tmp.path(), &s, &["b".into(), "!c".into(), "d".into()], &tmp.path().join("cache")); + let text = std::fs::read_to_string(user_settings(&cfg).unwrap()).unwrap(); + assert!(text.contains("")); + assert_eq!(text.matches("a").count(), 1); + assert!(text.contains("b")); + assert!(!text.contains("c")); + assert!(!text.contains("d")); + + let mut cfg = jdtls(); + configure_jdtls(&mut cfg, tmp.path(), &s, &[], &tmp.path().join("cache")); + let text = std::fs::read_to_string(user_settings(&cfg).unwrap()).unwrap(); + assert!(text.contains("a")); + assert!(text.contains("c")); + } + #[test] fn jdtls_imports_with_the_synced_profiles() { let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index b29027e43..eb988bc37 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -343,20 +343,27 @@ fn project_jdk_and_maven_settings() { s.languages.java.jdk = "jdk17/bin/java".into(); s.languages.java.maven.home = maven.to_string_lossy().into_owned(); s.languages.java.maven.user_settings = "/m2/settings.xml".into(); + s.languages.java.maven.toolchains = "/m2/toolchains.xml".into(); s.languages.java.maven.local_repository = "/m2/repo".into(); assert_eq!(project_jdk(tmp.path(), &s).map(|j| j.major), Some(17)); assert_eq!(maven_command(tmp.path(), &s), Some(maven.join("bin/mvn"))); - assert_eq!(maven_options(tmp.path(), &s), ["-s", "/m2/settings.xml", "-Dmaven.repo.local=/m2/repo"]); + assert_eq!( + maven_options(tmp.path(), &s), + ["-s", "/m2/settings.xml", "-t", "/m2/toolchains.xml", "-Dmaven.repo.local=/m2/repo"] + ); let mut configs = vec![run("maven", "./mvnw", &["package"]), run("gradle", "./gradlew", &["build"])]; apply_to_run_configs(&mut configs, tmp.path(), &s); let home = project_jdk(tmp.path(), &s).unwrap().home.to_string_lossy().into_owned(); assert_eq!(configs[0].command, maven.join("bin/mvn").to_string_lossy()); - assert_eq!(configs[0].args, ["-s", "/m2/settings.xml", "-Dmaven.repo.local=/m2/repo", "package"]); + assert_eq!( + configs[0].args, + ["-s", "/m2/settings.xml", "-t", "/m2/toolchains.xml", "-Dmaven.repo.local=/m2/repo", "package"] + ); assert_eq!(configs[0].env["JAVA_HOME"], home); assert_eq!(configs[1].env["JAVA_HOME"], home); apply_to_run_configs(&mut configs, tmp.path(), &s); - assert_eq!(configs[0].args.len(), 4, "applied once"); + assert_eq!(configs[0].args.len(), 6, "applied once"); s.languages.java.jdk = "missing".into(); assert_eq!(project_jdk(tmp.path(), &s), None); @@ -448,6 +455,122 @@ fn maven_daemon_uses_the_runner_jre_version() { } } +#[test] +fn maven_options_carry_active_profiles_into_the_command_line() { + let tmp = tempfile::tempdir().unwrap(); + let mut s = Settings::default(); + s.languages.java.maven.active_profiles = "dev, integration".into(); + assert_eq!(maven_options(tmp.path(), &s), ["-P", "dev, integration"]); + s.languages.java.maven.active_profiles = " ".into(); + assert!(maven_options(tmp.path(), &s).is_empty()); + + // A run configuration's own -P wins; repeated application does not duplicate the settings group. + s.languages.java.maven.active_profiles = "dev".into(); + let mut configs = vec![run("maven", "./mvnw", &["package"])]; + apply_to_run_configs(&mut configs, tmp.path(), &s); + assert_eq!(configs[0].args, ["-P", "dev", "package"]); + apply_to_run_configs(&mut configs, tmp.path(), &s); + assert_eq!(configs[0].args, ["-P", "dev", "package"]); + let mut configured = vec![run("maven", "./mvnw", &["-P", "prod", "package"])]; + apply_to_run_configs(&mut configured, tmp.path(), &s); + assert_eq!(configured[0].args, ["-P", "prod", "package"]); +} + +#[test] +fn maven_profile_and_toolchain_overrides_accept_every_option_form() { + let tmp = tempfile::tempdir().unwrap(); + let mut s = Settings::default(); + s.languages.java.maven.active_profiles = "a".into(); + s.languages.java.maven.toolchains = "config files/toolchains.xml".into(); + let toolchains = tmp.path().join("config files/toolchains.xml").to_string_lossy().into_owned(); + assert_eq!(maven_options(tmp.path(), &s), ["-t", &toolchains, "-P", "a"]); + for own in [ + vec!["-P", "b,!c", "-t", "own file.xml"], + vec!["-Pb,!c", "-town file.xml"], + vec!["--activate-profiles", "b,!c", "--toolchains", "own file.xml"], + vec!["--activate-profiles=b,!c", "--toolchains=own file.xml"], + ] { + let mut args = own.clone(); + args.push("test"); + let mut config = run("maven", "mvn", &args); + config.detected = false; + for _ in 0..3 { + apply_to_run_configs(std::slice::from_mut(&mut config), tmp.path(), &s); + assert_eq!(config.args, args, "configuration options win: {own:?}"); + } + } +} + +#[test] +fn maven_goal_profiles_combine_settings_and_checkboxes_but_keep_explicit_overrides() { + use crate::apply::prepare_maven_run; + + let tmp = tempfile::tempdir().unwrap(); + let mut s = Settings::default(); + s.languages.java.maven.active_profiles = "a".into(); + s.languages.java.maven.toolchains = "config files/toolchains.xml".into(); + let mut config = run("maven", "mvn", &["test", "-Pb,!c"]); + config.id = "maven:goal:pom.xml:test".into(); + config.detected = false; + let mut expected = maven_options(tmp.path(), &s); + expected.extend(["test".into(), "-Pb,!c".into()]); + for _ in 0..3 { + prepare_maven_run(&mut config, tmp.path(), &s); + assert_eq!(config.args, expected); + apply_to_run_configs(std::slice::from_mut(&mut config), tmp.path(), &s); + assert_eq!(config.args, expected); + } + s.languages.java.maven.active_profiles = "new".into(); + prepare_maven_run(&mut config, tmp.path(), &s); + assert_eq!(&config.args[2..], ["-P", "new", "test", "-Pb,!c"]); + + for own in + [vec!["-P", "prod"], vec!["-Pprod"], vec!["--activate-profiles", "prod"], vec!["--activate-profiles=prod"]] + { + let mut args = vec!["test"]; + args.extend(own); + let mut config = run("maven", "mvn", &args); + config.id = format!("maven:goal:pom.xml:{}", args.join(" ")); + config.detected = false; + for _ in 0..3 { + prepare_maven_run(&mut config, tmp.path(), &s); + assert_eq!(&config.args[2..], args); + } + } +} + +#[test] +fn maven_launch_refreshes_attached_and_long_settings_prefixes() { + use crate::apply::prepare_maven_run; + + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("pom.xml"), "").unwrap(); + let mut s = Settings::default(); + s.languages.java.maven.active_profiles = "new".into(); + s.languages.java.maven.toolchains = "new toolchains.xml".into(); + for prefix in [ + vec!["-told file.xml", "-Pa"], + vec!["--toolchains", "old file.xml", "--activate-profiles", "a"], + vec!["--toolchains=old file.xml", "--activate-profiles=a"], + ] { + for id in + ["maven:goal:pom.xml:test", "maven:test", "context:maven:test:ExampleTest", "workspace:abc:maven:test"] + { + let mut args = prefix.clone(); + args.push("test"); + let mut config = run("maven", "mvn", &args); + config.id = id.into(); + config.detected = !id.starts_with("maven:goal:"); + let mut expected = maven_options(tmp.path(), &s); + expected.push("test".into()); + for _ in 0..3 { + prepare_maven_run(&mut config, tmp.path(), &s); + assert_eq!(config.args, expected, "{id}: {prefix:?}"); + } + } + } +} + #[test] fn maven_options_carry_the_thread_count_into_the_command_line() { let tmp = tempfile::tempdir().unwrap(); diff --git a/src/bindings.ts b/src/bindings.ts index a5f57e642..be0c013cc 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -5610,6 +5610,8 @@ export type MavenSettings = { home?: string, /** User `settings.xml` (`-s`); empty = `~/.m2/settings.xml`. */ userSettings?: string, + /** Toolchains file (`-t`); empty = `~/.m2/toolchains.xml`. */ + toolchains?: string, /** Local repository (`-Dmaven.repo.local`); empty = from `settings.xml`. */ localRepository?: string, /** @@ -5627,6 +5629,8 @@ export type MavenSettings = { * multiplier (`2C`, `2.5C`); empty = Maven's default (one thread per module). */ threads?: string, + /** Active Maven profiles (`-P`), comma-separated; empty leaves profiles to the POM/settings. */ + activeProfiles?: string, }; /** Tools › MCP Server: agents use the IDE's built-in tools. */ diff --git a/src/features/build-tools/model.test.ts b/src/features/build-tools/model.test.ts index ec0fa2a5e..7b9f39b76 100644 --- a/src/features/build-tools/model.test.ts +++ b/src/features/build-tools/model.test.ts @@ -184,6 +184,19 @@ describe("Maven tree", () => { describe("Maven runs", () => { const core = maven.projects[1]!; + it.each([["-P", "prod"], ["-Pprod"], ["--activate-profiles", "prod"], ["--activate-profiles=prod"]])( + "keeps explicit Maven profiles %j", + (...profiles) => { + const goals = ["test", ...profiles]; + const config = mavenGoalConfig(core, goals, maven, { + offline: false, + skipTests: false, + profiles: { ci: true, dev: false }, + }); + expect(config.args).toEqual(goals); + }, + ); + it("passes goals, offline, skip tests and changed profiles", () => { expect(profilesArg(maven.profiles, {})).toBeNull(); expect(profilesArg(maven.profiles, { ci: true, dev: false })).toBe("!dev,ci"); diff --git a/src/features/build-tools/model.ts b/src/features/build-tools/model.ts index 398dc6539..a72b32fd4 100644 --- a/src/features/build-tools/model.ts +++ b/src/features/build-tools/model.ts @@ -455,7 +455,11 @@ export function mavenArgs( if (options.offline && !has("-o") && !has("--offline")) args.push("-o"); if (options.skipTests && !has("-DskipTests")) args.push("-DskipTests"); const profiles = profilesArg(model.profiles, options.profiles); - if (profiles && !args.some((a) => a.startsWith("-P"))) args.push(`-P${profiles}`); + if ( + profiles && + !args.some((a) => a.startsWith("-P") || a === "--activate-profiles" || a.startsWith("--activate-profiles=")) + ) + args.push(`-P${profiles}`); return args; } diff --git a/src/features/settings/__tests__/dialog.test.tsx b/src/features/settings/__tests__/dialog.test.tsx index 97a425f38..47092e220 100644 --- a/src/features/settings/__tests__/dialog.test.tsx +++ b/src/features/settings/__tests__/dialog.test.tsx @@ -134,6 +134,36 @@ describe("Settings dialog", () => { ); }); + it("persists Maven toolchains and profiles in project settings", async () => { + useProject.setState({ project: { root: "/project" } as never }); + const dialog = await openDialog("build.maven"); + fireEvent.change(within(dialog).getByLabelText("Toolchains file"), { + target: { value: "config files/toolchains.xml" }, + }); + fireEvent.change(within(dialog).getByLabelText("Active profiles"), { target: { value: "a, b,!c" } }); + await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" }))); + expect(settingsSet.mock.calls).toEqual( + expect.arrayContaining([ + ["project", "languages.java.maven.toolchains", '"config files/toolchains.xml"'], + ["project", "languages.java.maven.activeProfiles", '"a, b,!c"'], + ]), + ); + }); + + it("finds the Maven active profiles setting", async () => { + useProject.setState({ project: { root: "/project" } as never }); + const dialog = await openDialog("build.maven"); + fireEvent.change(within(dialog).getByLabelText("Search settings"), { target: { value: "Active profiles" } }); + expect(within(dialog).getByLabelText("Active profiles")).toBeInTheDocument(); + }); + + it("finds the Maven toolchains setting", async () => { + useProject.setState({ project: { root: "/project" } as never }); + const dialog = await openDialog("build.maven"); + fireEvent.change(within(dialog).getByLabelText("Search settings"), { target: { value: "Toolchains" } }); + expect(within(dialog).getByLabelText("Toolchains file")).toBeInTheDocument(); + }); + it("searches pages and highlights matching option labels", async () => { const dialog = await openDialog("appearance"); fireEvent.change(within(dialog).getByLabelText("Search settings"), { target: { value: "soft wrap" } }); diff --git a/src/features/settings/__tests__/languages.test.ts b/src/features/settings/__tests__/languages.test.ts index 27f9c7e5e..76e209a40 100644 --- a/src/features/settings/__tests__/languages.test.ts +++ b/src/features/settings/__tests__/languages.test.ts @@ -48,12 +48,14 @@ describe("languages settings", () => { runnerJre: "/opt/jdk-21", home: "/opt/maven", userSettings: "", + toolchains: "", localRepository: "", unlinkedProjects: [], linkedProjects: [], daemon: false, mvndHome: "/opt/mvnd", threads: "2C", + activeProfiles: "", }); expect(validateSettings({}).languages).toEqual(DEFAULT_LANGUAGES); expect(validateLanguages({ java: { maven: { threads: 4 } } }).java.maven.threads).toBe(""); diff --git a/src/features/settings/languages/LanguagePages.tsx b/src/features/settings/languages/LanguagePages.tsx index c70691b0d..44d9cff15 100644 --- a/src/features/settings/languages/LanguagePages.tsx +++ b/src/features/settings/languages/LanguagePages.tsx @@ -494,6 +494,7 @@ export function GradlePage() { export function MavenPage() { const [daemon, setDaemon] = useSetting("languages.java.maven.daemon"); const [threads, setThreads] = useSetting("languages.java.maven.threads"); + const [activeProfiles, setActiveProfiles] = useSetting("languages.java.maven.activeProfiles"); const id = useId(); return (
@@ -527,6 +528,14 @@ export function MavenPage() { file hint="Passed as -s (mirrors, servers, proxies)." /> + + + setActiveProfiles(e.target.value)} + aria-label="Active profiles" + /> +