From 7fa1d08b94bd89ba1717afd353e14640ad94de8a Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:27:08 +0800 Subject: [PATCH] feat(java): Add Project JDK and Maven home / settings.xml / local repository settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JDK was always picked automatically (JAVA_HOME, PATH, the usual install dirs, by the module's Java version) and Maven was the project's mvnw or mvn on PATH, with no way to override either. languages.java.jdk (Languages & Frameworks › Java › Project JDK) now sets the default jdtls runtime, the JDK of Run/Debug and JAVA_HOME for Maven and Gradle; languages.java.maven (Build Tools › Maven) sets the Maven home, -s and -Dmaven.repo.local for sync, builds, tool-window goals and detected run configurations. JDKs and Maven installations are detected for the pickers. Closes #13 --- crates/ide-build/src/compile/mod.rs | 1 + crates/ide-build/src/maven.rs | 3 + crates/ide-build/src/sync.rs | 8 +- crates/ide-core/src/settings.rs | 17 ++++ crates/ide-sdk/src/apply.rs | 55 ++++++++++- crates/ide-sdk/src/discover.rs | 99 +++++++++++++++++++ crates/ide-sdk/src/java.rs | 86 +++++++++++++--- crates/ide-sdk/src/lib.rs | 4 + crates/ide-sdk/src/tests.rs | 51 +++++++++- src-tauri/src/commands/build.rs | 44 ++++++--- src-tauri/src/commands/java.rs | 12 ++- src-tauri/src/state.rs | 4 +- src/bindings.ts | 26 ++++- src/features/build-tools/actions.test.tsx | 1 + src/features/build-tools/index.ts | 4 + src/features/build-tools/model.test.ts | 6 ++ src/features/build-tools/model.ts | 2 +- .../settings/__tests__/languages.test.ts | 4 +- .../settings/languages/LanguagePages.tsx | 62 +++++++++++- src/features/settings/pages/execution.tsx | 12 ++- src/features/settings/pages/languages.tsx | 12 ++- src/platform/languageSettings.ts | 19 +++- 22 files changed, 484 insertions(+), 48 deletions(-) diff --git a/crates/ide-build/src/compile/mod.rs b/crates/ide-build/src/compile/mod.rs index 20f12d62..68049c9c 100644 --- a/crates/ide-build/src/compile/mod.rs +++ b/crates/ide-build/src/compile/mod.rs @@ -225,6 +225,7 @@ fn maven_steps(root: &Path, model: &BuildModel, kind: BuildKind, module: Option< } let step = |p: &maven::MavenProject, extra: &[String]| { let mut s = BuildStep::new(&p.command, &["-B", "-Dstyle.color=never"], &subdir(root, &p.dir), Format::Maven); + s.args.extend(p.options.iter().cloned()); s.args.extend(extra.iter().cloned()); s.args.extend(goals.iter().map(|g| (*g).to_owned())); s.env = p.env.clone(); diff --git a/crates/ide-build/src/maven.rs b/crates/ide-build/src/maven.rs index c4d2aac1..d40e7440 100644 --- a/crates/ide-build/src/maven.rs +++ b/crates/ide-build/src/maven.rs @@ -92,6 +92,8 @@ pub struct MavenProject { pub java_version: Option, /// Maven launcher: the nearest `mvnw` up to the project root (absolute), else `mvn`. pub command: String, + /// Options before the goals (`-s`, `-Dmaven.repo.local=`); filled in by the shell from settings. + pub options: Vec, /// Environment for runs (`JAVA_HOME` of the matching JDK); filled in by the shell. pub env: BTreeMap, } @@ -924,6 +926,7 @@ pub(crate) fn load_with_repo(root: &Path, local_repo: Option) -> MavenM profiles: pom.profiles.iter().map(|p| p.id.clone()).collect(), java_version: jv, command: wrapper(root, &dir), + options: Vec::new(), env: BTreeMap::new(), }); for m in modules.into_iter().rev() { diff --git a/crates/ide-build/src/sync.rs b/crates/ide-build/src/sync.rs index fe984efc..2470c425 100644 --- a/crates/ide-build/src/sync.rs +++ b/crates/ide-build/src/sync.rs @@ -29,6 +29,10 @@ pub struct SyncOptions { pub profiles: Vec, /// Extra environment (`JAVA_HOME`). pub env: BTreeMap, + /// Maven launcher replacing the project's `mvnw` / `mvn` (Settings › Maven home). + pub maven_command: Option, + /// Maven options before the goals (`-s`, `-Dmaven.repo.local=`). + pub maven_options: Vec, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -184,6 +188,7 @@ pub fn sync_maven( let effective = cache_dir.join(format!("effective-pom-{n}.xml")); let _ = std::fs::remove_file(&effective); let mut args: Vec = vec!["-B".into(), "-fae".into(), "-Dstyle.color=never".into()]; + args.extend(options.maven_options.iter().cloned()); if options.offline { args.push("-o".into()); } @@ -194,7 +199,8 @@ pub fn sync_maven( args.push(format!("-Doutput={}", effective.to_string_lossy())); args.push("dependency:tree".into()); progress(SyncProgress { message: format!("Reading {}", top.display_name), fraction: None }); - let out = run(&top.command, &args, &root.join(&top.dir), &options.env, cancel, |line| { + let command = options.maven_command.as_deref().unwrap_or(&top.command); + let out = run(command, &args, &root.join(&top.dir), &options.env, cancel, |line| { if let Some((name, i, total)) = maven_building(line) { let fraction = (i.saturating_sub(1) as f32 / total.max(1) as f32).min(1.0); progress(SyncProgress { message: format!("Building model of {name}"), fraction: Some(fraction) }); diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs index 38dece8b..84669f25 100644 --- a/crates/ide-core/src/settings.rs +++ b/crates/ide-core/src/settings.rs @@ -401,9 +401,26 @@ pub struct GoSettings { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)] #[serde(default, rename_all = "camelCase")] pub struct JavaSettings { + /// Project JDK home; empty = per module, the installed JDK matching the + /// Java version it targets. + pub jdk: String, /// Extra JVM options for the Java language server (jdtls), space-separated /// (VS Code's `java.jdt.ls.vmargs`). pub jdtls_vm_args: String, + /// Build Tools › Maven. + pub maven: MavenSettings, +} + +/// Build Tools › Maven. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)] +#[serde(default, rename_all = "camelCase")] +pub struct MavenSettings { + /// Maven home (or its `mvn` executable); empty = the project's `mvnw`, else `mvn` on `PATH`. + pub home: String, + /// User `settings.xml` (`-s`); empty = `~/.m2/settings.xml`. + pub user_settings: String, + /// Local repository (`-Dmaven.repo.local`); empty = from `settings.xml`. + pub local_repository: String, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] diff --git a/crates/ide-sdk/src/apply.rs b/crates/ide-sdk/src/apply.rs index 50957d34..38226526 100644 --- a/crates/ide-sdk/src/apply.rs +++ b/crates/ide-sdk/src/apply.rs @@ -56,6 +56,42 @@ fn goroot(root: &Path, s: &Settings) -> Option { setting_path(root, &s.languages.go.goroot) } +/// The project JDK (`languages.java.jdk`): a JDK home, or its `bin/java`. +pub fn project_jdk(root: &Path, s: &Settings) -> Option { + let p = setting_path(root, &s.languages.java.jdk)?; + let jdk = crate::java::jdk_at(&p).or_else(|| crate::java::jdk_at(p.parent()?.parent()?)); + if jdk.is_none() { + tracing::warn!("project JDK {} is not a JDK home", p.display()); + } + jdk +} + +/// `mvn` of the configured Maven home (`languages.java.maven.home`): the +/// executable itself, `/bin/mvn` or `/mvn`. +pub fn maven_command(root: &Path, s: &Settings) -> Option { + let p = setting_path(root, &s.languages.java.maven.home)?; + let name = if cfg!(windows) { "mvn.cmd" } else { "mvn" }; + let found = [p.clone(), p.join("bin").join(name), p.join(name)].into_iter().find(|c| c.is_file()); + if found.is_none() { + tracing::warn!("Maven home {} has no {name}", p.display()); + } + found +} + +/// Maven options from settings, placed before the goals: `-s ` +/// and `-Dmaven.repo.local=`. +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(dir) = setting_path(root, &m.local_repository) { + out.push(format!("-Dmaven.repo.local={}", dir.to_string_lossy())); + } + out +} + /// TypeScript `lib` directory for the selected version; `None` = the server's bundled one. pub fn tsdk(root: &Path, s: &Settings) -> Option { match s.languages.typescript.version.trim() { @@ -206,7 +242,8 @@ const PY_RUNNERS: [&str; 3] = ["uv", "poetry", "pdm"]; /// Points run configurations at the selected toolchains: Python configs use the /// project interpreter, npm scripts the chosen package manager and Node.js, -/// cargo / go configs the selected toolchain. +/// cargo / go configs the selected toolchain, Maven / Gradle the project JDK +/// (and Maven the configured home and options). pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings) { let explicit_python = setting_path(root, &s.languages.python.interpreter); let python = python_interpreter(root, s); @@ -215,6 +252,9 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings let rust = rust_bin(root, s); let go = goroot(root, s); let gopath = setting_path(root, &s.languages.go.gopath); + let jdk = project_jdk(root, s); + let mvn = maven_command(root, s); + let mvn_options = maven_options(root, s); for c in configs.iter_mut() { let mut path_dirs: Vec = Vec::new(); @@ -281,6 +321,19 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings c.env.entry("GOPATH".into()).or_insert_with(|| gopath.to_string_lossy().into_owned()); } } + "maven" | "gradle" => { + if let Some(jdk) = &jdk { + c.env.entry("JAVA_HOME".into()).or_insert_with(|| jdk.home.to_string_lossy().into_owned()); + } + if c.kind == "maven" && c.detected { + if let Some(mvn) = &mvn { + c.command = mvn.to_string_lossy().into_owned(); + } + if !c.args.starts_with(&mvn_options) { + c.args.splice(0..0, mvn_options.iter().cloned()); + } + } + } _ => {} } if !path_dirs.is_empty() && !c.env.contains_key("PATH") { diff --git a/crates/ide-sdk/src/discover.rs b/crates/ide-sdk/src/discover.rs index d61124ee..20bea151 100644 --- a/crates/ide-sdk/src/discover.rs +++ b/crates/ide-sdk/src/discover.rs @@ -18,6 +18,8 @@ pub fn detect(kind: SdkKind, root: Option<&Path>) -> Vec { SdkKind::Rust => rust(), SdkKind::Go => go(), SdkKind::TypeScript => typescript(root), + SdkKind::Jdk => jdk(), + SdkKind::Maven => maven(), } } @@ -382,3 +384,100 @@ fn typescript(root: Option<&Path>) -> Vec { } out } + +// ------------------------------------------------------------------ jdk + +/// `KEY="value"` of a JDK `release` file. +fn release_value(release: &str, key: &str) -> Option { + let line = release.lines().find_map(|l| l.trim().strip_prefix(key)?.strip_prefix('='))?; + Some(line.trim().trim_matches('"').to_owned()).filter(|v| !v.is_empty()) +} + +fn jdk() -> Vec { + let java_home = std::env::var_os("JAVA_HOME").and_then(|h| dunce::canonicalize(h).ok()); + crate::java::rescan_jdks() + .iter() + .map(|j| { + let release = std::fs::read_to_string(j.home.join("release")).unwrap_or_default(); + let version = release_value(&release, "JAVA_VERSION").unwrap_or_else(|| j.major.to_string()); + let name = match release_value(&release, "IMPLEMENTOR") { + Some(vendor) => format!("{vendor} {version}"), + None => format!("JDK {version}"), + }; + let source = if java_home.as_ref() == Some(&j.home) { "JAVA_HOME" } else { "system" }; + Sdk { + path: j.home.clone().into(), + name, + version: Some(version), + source: source.to_owned(), + home: Some(j.home.clone().into()), + extra: None, + } + }) + .collect() +} + +// ------------------------------------------------------------------ maven + +/// Maven version from `lib/maven-core-.jar`. +fn maven_version(home: &Path) -> Option { + std::fs::read_dir(home.join("lib")).ok()?.flatten().find_map(|e| { + let name = e.file_name().to_string_lossy().into_owned(); + Some(name.strip_prefix("maven-core-")?.strip_suffix(".jar")?.to_owned()) + }) +} + +/// A Maven home (`bin/mvn` and `lib/`); Homebrew's formula keeps it in `libexec`. +fn maven_home(dir: &Path) -> Option { + let mvn = if cfg!(windows) { "mvn.cmd" } else { "mvn" }; + [dir.join("libexec"), dir.to_path_buf()] + .into_iter() + .find(|h| h.join("bin").join(mvn).is_file() && h.join("lib").is_dir()) +} + +fn maven() -> Vec { + let mut found: Vec<(PathBuf, &'static str)> = Vec::new(); + if let Some(mvn) = which("mvn").and_then(|p| dunce::canonicalize(p).ok()) + && let Some(home) = mvn.parent().and_then(Path::parent) + { + found.push((home.to_path_buf(), "path")); + } + for var in ["MAVEN_HOME", "M2_HOME"] { + if let Some(h) = std::env::var_os(var) { + found.push((PathBuf::from(h), "env")); + } + } + for p in ["/opt/homebrew/opt/maven", "/usr/local/opt/maven", "/usr/share/maven", "/opt/maven"] { + found.push((PathBuf::from(p), "system")); + } + if let Some(home) = home_dir() { + for d in subdirs(&home.join(".sdkman/candidates/maven")) { + found.push((d, "sdkman")); + } + // Distributions the Maven wrapper downloaded: `dists/apache-maven-3.9.6-bin//apache-maven-3.9.6`. + for dist in subdirs(&home.join(".m2/wrapper/dists")) { + for hash in subdirs(&dist) { + found.extend(subdirs(&hash).into_iter().map(|d| (d, "wrapper"))); + } + } + } + let mut seen = HashSet::new(); + let mut out = Vec::new(); + for (dir, source) in found { + let Some(home) = maven_home(&dir) else { continue }; + let home = dunce::canonicalize(&home).unwrap_or(home); + if !seen.insert(home.clone()) { + continue; + } + let version = maven_version(&home); + out.push(Sdk { + path: home.clone().into(), + name: format!("Maven {}", version.clone().unwrap_or_default()).trim().to_owned(), + version, + source: source.to_owned(), + home: Some(home.into()), + extra: None, + }); + } + out +} diff --git a/crates/ide-sdk/src/java.rs b/crates/ide-sdk/src/java.rs index 6a38e7d0..2eeaf733 100644 --- a/crates/ide-sdk/src/java.rs +++ b/crates/ide-sdk/src/java.rs @@ -25,7 +25,8 @@ pub fn parse_release(release: &str) -> Option { if first == 1 { parts.next()?.parse().ok() } else { Some(first) } } -fn jdk_at(home: &Path) -> Option { +/// The JDK at `home`, if it is one (a `release` file and `bin/java`). +pub fn jdk_at(home: &Path) -> Option { let release = std::fs::read_to_string(home.join("release")).ok()?; if !home.join("bin").join(crate::exe("java")).is_file() { return None; @@ -117,6 +118,11 @@ fn rescan(slot: &mut Option) -> Arc<[Jdk]> { slot.insert(Scan { at: Instant::now(), jdks: scan().into() }).jdks.clone() } +/// Scans for installed JDKs again (Settings › Detect again). +pub fn rescan_jdks() -> Arc<[Jdk]> { + rescan(&mut JDKS.lock().unwrap_or_else(|e| e.into_inner())) +} + /// Installed JDKs (scanned on first use; [`jdtls_jdks`] may refresh them). pub fn installed_jdks() -> Arc<[Jdk]> { let mut slot = JDKS.lock().unwrap_or_else(|e| e.into_inner()); @@ -258,12 +264,15 @@ const METADATA_PROPERTY: &str = "java.import.generatesMetadataFilesAtProjectRoot /// directory, one per server root, and no Eclipse metadata is written into /// the project; it runs on a JDK `min_java`+ (via `JAVA_HOME`, which its /// launcher reads), installed JDKs are registered as runtimes and `bundles` -/// (e.g. the java-debug plugin jar) are loaded into the server. +/// (e.g. the java-debug plugin jar) are loaded into the server. The project +/// JDK, when set, is the default runtime and preferred for running the server. +#[allow(clippy::too_many_arguments)] pub fn configure_jdtls( cfg: &mut LanguageServerConfig, server_root: &Path, data_dir: &Path, jdks: &[Jdk], + project_jdk: Option<&Jdk>, min_java: u32, bundles: &[PathBuf], lombok: Option<&Path>, @@ -294,6 +303,9 @@ pub fn configure_jdtls( { cfg.args.push(format!("--jvm-arg=-javaagent:{}", jar.to_string_lossy())); } + let jdks: Vec = + project_jdk.into_iter().chain(jdks.iter().filter(|j| Some(*j) != project_jdk)).cloned().collect(); + let jdks = jdks.as_slice(); if !cfg.env.contains_key("JAVA_HOME") { match jdtls_runtime(jdks, min_java) { Some(jdk) => { @@ -302,7 +314,15 @@ pub fn configure_jdtls( None => cfg.launch_error = Some(missing_runtime_message(jdks, min_java)), } } - let runtimes = runtimes(jdks); + let mut runtimes = runtimes(jdks); + if let Some(project) = project_jdk { + let home = project.home.to_string_lossy(); + for r in &mut runtimes { + if r["path"] == *home { + r["default"] = json!(true); + } + } + } if !runtimes.is_empty() { merge_init_options(cfg, json!({ "settings": { "java": { "configuration": { "runtimes": runtimes } } } })); } @@ -424,7 +444,16 @@ mod tests { Jdk { major: 8, home: PathBuf::from("/jdk8") }, ]; let bundles = [PathBuf::from("/tools/java-debug/bin/com.microsoft.java.debug.plugin.jar")]; - configure_jdtls(&mut cfg, Path::new("/p/demo"), Path::new("/data"), &jdks, JDTLS_MIN_JAVA, &bundles, None); + configure_jdtls( + &mut cfg, + Path::new("/p/demo"), + Path::new("/data"), + &jdks, + None, + JDTLS_MIN_JAVA, + &bundles, + None, + ); assert_eq!(cfg.args[0], "-data"); assert!(cfg.args[1].starts_with("/data/lsp/jdtls/"), "{}", cfg.args[1]); assert_eq!(cfg.args[2], "--jvm-arg=-Djava.import.generatesMetadataFilesAtProjectRoot=false"); @@ -439,11 +468,38 @@ mod tests { // Deterministic (reconfigure compares configs) and idempotent. let before = cfg.clone(); - configure_jdtls(&mut cfg, Path::new("/p/demo"), Path::new("/data"), &jdks, JDTLS_MIN_JAVA, &bundles, None); + configure_jdtls( + &mut cfg, + Path::new("/p/demo"), + Path::new("/data"), + &jdks, + None, + JDTLS_MIN_JAVA, + &bundles, + None, + ); assert_eq!(cfg, before); + // The project JDK is the default runtime, and runs the server when new enough. + let project = Jdk { major: 21, home: PathBuf::from("/jdk21b") }; + let mut cfg: LanguageServerConfig = toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap(); + configure_jdtls( + &mut cfg, + Path::new("/p"), + Path::new("/data"), + &jdks, + Some(&project), + JDTLS_MIN_JAVA, + &[], + None, + ); + let opts = serde_json::to_value(cfg.initialization_options.clone().unwrap()).unwrap(); + let rt = &opts["settings"]["java"]["configuration"]["runtimes"]; + assert_eq!(rt[1], json!({ "name": "JavaSE-21", "path": "/jdk21b", "default": true })); + assert_eq!(cfg.env["JAVA_HOME"], "/jdk21b"); + let mut other: LanguageServerConfig = toml::from_str("name = \"pyright\"\ncommand = \"x\"").unwrap(); - configure_jdtls(&mut other, Path::new("/p"), Path::new("/data"), &jdks, JDTLS_MIN_JAVA, &bundles, None); + configure_jdtls(&mut other, Path::new("/p"), Path::new("/data"), &jdks, None, JDTLS_MIN_JAVA, &bundles, None); assert!(other.args.is_empty() && other.initialization_options.is_none()); } @@ -452,23 +508,23 @@ mod tests { let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() }; let jar = Path::new("/m2/lombok-1.18.48.jar"); let mut cfg = jdtls(); - configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], Some(jar)); + configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], Some(jar)); let agent = format!("--jvm-arg=-javaagent:{}", jar.to_string_lossy()); assert!(cfg.args.contains(&agent), "{:?}", cfg.args); let mut plain = jdtls(); - configure_jdtls(&mut plain, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], None); + configure_jdtls(&mut plain, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], None); assert_ne!(cfg.args[1], plain.args[1], "a separate workspace, so everything is rebuilt with the agent"); let before = cfg.clone(); - configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], Some(jar)); + configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], Some(jar)); assert_eq!(cfg, before, "idempotent"); let mut cfg = jdtls(); cfg.args.push("--jvm-arg=-javaagent:/own/lombok.jar".into()); - configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], Some(jar)); + configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], Some(jar)); assert!(!cfg.args.contains(&agent), "the user's agent wins"); let mut own = jdtls(); own.args.push("--jvm-arg=-javaagent:/own/lombok.jar".into()); - configure_jdtls(&mut own, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], None); + configure_jdtls(&mut own, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], None); assert_eq!(cfg.args, own.args, "its workspace follows the agent in use"); } @@ -477,19 +533,19 @@ mod tests { let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() }; let old = [Jdk { major: 17, home: PathBuf::from("/jdk17") }, Jdk { major: 8, home: PathBuf::from("/jdk8") }]; let mut cfg = jdtls(); - configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, JDTLS_MIN_JAVA, &[], None); + configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, None, JDTLS_MIN_JAVA, &[], None); assert!(!cfg.env.contains_key("JAVA_HOME")); let err = cfg.launch_error.unwrap(); assert!(err.contains("JDK 21 or newer") && err.contains("found JDK 8, 17"), "{err}"); let mut cfg = jdtls(); - configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], JDTLS_MIN_JAVA, &[], None); + configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], None, JDTLS_MIN_JAVA, &[], None); assert!(cfg.launch_error.unwrap().contains("no JDK was found")); // An explicit JAVA_HOME is the user's call. let mut cfg = jdtls(); cfg.env.insert("JAVA_HOME".into(), "/custom".into()); - configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, JDTLS_MIN_JAVA, &[], None); + configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, None, JDTLS_MIN_JAVA, &[], None); assert_eq!((cfg.env["JAVA_HOME"].as_str(), cfg.launch_error), ("/custom", None)); let mixed = [old[0].clone(), Jdk { major: 21, home: PathBuf::from("/jdk21") }]; @@ -497,7 +553,7 @@ mod tests { // A jdtls whose bundles need 25 doesn't start on 21, and says what it needs. let mut cfg = jdtls(); - configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &mixed, 25, &[], None); + configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &mixed, None, 25, &[], None); let err = cfg.launch_error.unwrap(); assert!(err.contains("JDK 25 or newer") && err.contains("found JDK 17, 21"), "{err}"); } diff --git a/crates/ide-sdk/src/lib.rs b/crates/ide-sdk/src/lib.rs index faa821f5..296a7502 100644 --- a/crates/ide-sdk/src/lib.rs +++ b/crates/ide-sdk/src/lib.rs @@ -31,6 +31,10 @@ pub enum SdkKind { Rust, Go, TypeScript, + /// JDK homes. + Jdk, + /// Maven homes. + Maven, } /// One installed interpreter / toolchain. diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index 75d099f2..9c7cce5f 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -1,4 +1,7 @@ -use crate::apply::{apply_to_run_configs, configure_server, python_interpreter, terminal_activation, tsdk}; +use crate::apply::{ + apply_to_run_configs, configure_server, maven_command, maven_options, project_jdk, python_interpreter, + terminal_activation, tsdk, +}; use crate::python::parse_pip_list; use crate::{bin_dir, exe, venv_of}; use ide_core::settings::Settings; @@ -153,6 +156,52 @@ fn run_configs_use_selected_interpreter_and_package_manager() { assert_eq!(configs[4].command, "python3", "shell configs are left alone"); } +fn fake_jdk(dir: &Path) { + std::fs::create_dir_all(dir.join("bin")).unwrap(); + std::fs::write(dir.join("bin").join(exe("java")), "").unwrap(); + std::fs::write(dir.join("release"), "JAVA_VERSION=\"17.0.9\"\n").unwrap(); +} + +#[test] +fn project_jdk_and_maven_settings() { + if cfg!(windows) { + return; // Unix paths and shells + } + let tmp = tempfile::tempdir().unwrap(); + let jdk = tmp.path().join("jdk17"); + fake_jdk(&jdk); + let maven = tmp.path().join("maven"); + std::fs::create_dir_all(maven.join("bin")).unwrap(); + std::fs::write(maven.join("bin/mvn"), "").unwrap(); + + let mut s = Settings::default(); + assert_eq!(project_jdk(tmp.path(), &s), None); + assert_eq!(maven_command(tmp.path(), &s), None); + assert!(maven_options(tmp.path(), &s).is_empty()); + + // Project-relative paths, a JDK's `bin/java`, a Maven home. + 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.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"]); + + 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].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"); + + s.languages.java.jdk = "missing".into(); + assert_eq!(project_jdk(tmp.path(), &s), None); +} + #[test] fn terminal_activation_for_project_venv() { if cfg!(windows) { diff --git a/src-tauri/src/commands/build.rs b/src-tauri/src/commands/build.rs index 737ea4cc..40aec266 100644 --- a/src-tauri/src/commands/build.rs +++ b/src-tauri/src/commands/build.rs @@ -4,6 +4,7 @@ use crate::state::AppState; use ide_build::compile::{self, BuildEvent, BuildKind, BuildResult}; use ide_build::{BuildModel, BuildTool, SyncOptions, SyncProgress}; +use ide_core::settings::Settings; use ide_core::{IdeError, IdeEvent, IdeResult, RequestId}; use std::collections::BTreeMap; use std::path::Path; @@ -14,22 +15,32 @@ fn join_err(e: tauri::Error) -> IdeError { IdeError::internal(e.to_string()) } -/// `JAVA_HOME` of the installed JDK matching `release` (none when the project names no version). -fn java_env(release: Option) -> BTreeMap { +/// `JAVA_HOME`: the project JDK, else the installed JDK matching `release` (none +/// when the project names no version). +fn java_env(project_jdk: Option<&ide_sdk::java::Jdk>, release: Option) -> BTreeMap { let jdks = ide_sdk::java::installed_jdks(); - let jdk = release.and_then(|r| ide_sdk::java::pick_jdk(&jdks, Some(r))); + let jdk = project_jdk.or_else(|| release.and_then(|r| ide_sdk::java::pick_jdk(&jdks, Some(r)))); jdk.map(|j| BTreeMap::from([("JAVA_HOME".to_owned(), j.home.to_string_lossy().into_owned())])).unwrap_or_default() } -pub(crate) fn load(root: &Path, cache: &Path) -> BuildModel { +/// The build model with the launch settings applied: the JDK (`JAVA_HOME`), and +/// the Maven home and options from Settings › Maven. +pub(crate) fn load(root: &Path, cache: &Path, settings: &Settings) -> BuildModel { let mut model = ide_build::load(root, cache); + let jdk = ide_sdk::apply::project_jdk(root, settings); if let Some(m) = &mut model.maven { + let mvn = ide_sdk::apply::maven_command(root, settings); + let options = ide_sdk::apply::maven_options(root, settings); for p in &mut m.projects { - p.env = java_env(p.java_version); + p.env = java_env(jdk.as_ref(), p.java_version); + if let Some(mvn) = &mvn { + p.command = mvn.to_string_lossy().into_owned(); + } + p.options = options.clone(); } } if let Some(g) = &mut model.gradle { - g.env = java_env(g.java_version); + g.env = java_env(jdk.as_ref(), g.java_version); } model } @@ -39,8 +50,8 @@ pub(crate) fn load(root: &Path, cache: &Path) -> BuildModel { #[specta::specta] pub async fn build_model(window: Window, state: State<'_, AppState>) -> IdeResult { let project = state.project(window.label())?; - let (root, cache) = (project.root.clone(), project.build_cache_dir()); - tauri::async_runtime::spawn_blocking(move || load(&root, &cache)).await.map_err(join_err) + let (root, cache, settings) = (project.root.clone(), project.build_cache_dir(), project.settings.effective()); + tauri::async_runtime::spawn_blocking(move || load(&root, &cache, &settings)).await.map_err(join_err) } /// Runs Maven (`help:effective-pom dependency:tree`) or Gradle (`tasks --all`) in the @@ -58,10 +69,10 @@ pub async fn build_sync( on_progress: Channel, ) -> IdeResult { let project = state.project(window.label())?; - let (root, cache) = (project.root.clone(), project.build_cache_dir()); + let (root, cache, settings) = (project.root.clone(), project.build_cache_dir(), project.settings.effective()); let token = state.begin_request(request); let result = tauri::async_runtime::spawn_blocking(move || { - let current = load(&root, &cache); + let current = load(&root, &cache, &settings); let env = match tool { BuildTool::Maven => current .maven @@ -71,7 +82,13 @@ pub async fn build_sync( .unwrap_or_default(), BuildTool::Gradle => current.gradle.as_ref().map(|g| g.env.clone()).unwrap_or_default(), }; - let options = SyncOptions { offline, profiles, env }; + let options = SyncOptions { + offline, + profiles, + env, + maven_command: ide_sdk::apply::maven_command(&root, &settings).map(|p| p.to_string_lossy().into_owned()), + maven_options: ide_sdk::apply::maven_options(&root, &settings), + }; let progress = |p: SyncProgress| { let _ = on_progress.send(p); }; @@ -79,7 +96,7 @@ pub async fn build_sync( BuildTool::Maven => ide_build::sync_maven(&root, &cache, &options, &token, &progress)?, BuildTool::Gradle => ide_build::sync_gradle(&root, &cache, &options, &token, &progress)?, } - Ok(load(&root, &cache)) + Ok(load(&root, &cache, &settings)) }) .await .map_err(join_err); @@ -104,6 +121,7 @@ pub async fn build_run( ) -> IdeResult { let project = state.project(window.label())?; let (root, cache, sink) = (project.root.clone(), project.build_cache_dir(), project.sink.clone()); + let settings = project.settings.effective(); let token = state.begin_request(request); let result = tauri::async_runtime::spawn_blocking(move || { for path in clear { @@ -113,7 +131,7 @@ pub async fn build_run( items: Vec::new(), }); } - let plan = compile::plan(&root, &load(&root, &cache), kind, file.as_deref())?; + let plan = compile::plan(&root, &load(&root, &cache, &settings), kind, file.as_deref())?; let result = compile::run(&root, &plan, &token, &|e| { let _ = on_event.send(e); })?; diff --git a/src-tauri/src/commands/java.rs b/src-tauri/src/commands/java.rs index 8a52d20b..04b85eb3 100644 --- a/src-tauri/src/commands/java.rs +++ b/src-tauri/src/commands/java.rs @@ -22,10 +22,12 @@ pub async fn java_resolve_launch( ) -> IdeResult { let project = state.project(window.label())?; let module = project.root.join(module); + let settings = project.settings.effective(); let built = if build && compile::has_non_java_sources(&module) { - let (root, cache, module) = (project.root.clone(), project.build_cache_dir(), module.clone()); + let (root, cache, module, settings) = + (project.root.clone(), project.build_cache_dir(), module.clone(), settings.clone()); tauri::async_runtime::spawn_blocking(move || { - compile::build_for_launch(&root, &load(&root, &cache), &module, test, &CancellationToken::new()) + compile::build_for_launch(&root, &load(&root, &cache, &settings), &module, test, &CancellationToken::new()) }) .await .map_err(|e| IdeError::internal(e.to_string()))?? @@ -40,15 +42,17 @@ pub async fn java_resolve_launch( } } info.class_paths = class_paths; + // The project JDK, else the JDK jdtls compiles the module against, else one matching its Java version. + let project_jdk = ide_sdk::apply::project_jdk(&project.root, &settings); let from_jdtls = info.java_home.as_deref().map(ide_sdk::java::java_executable).filter(|p| p.is_file()); - let java = from_jdtls.or_else(|| { + let java = project_jdk.map(|j| ide_sdk::java::java_executable(&j.home)).or(from_jdtls).or_else(|| { let release = ide_sdk::java::project_java_version(&module); let jdks = ide_sdk::java::installed_jdks(); ide_sdk::java::pick_jdk(&jdks, release).map(|j| ide_sdk::java::java_executable(&j.home)) }); info.java_exec = Some(java.map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| "java".to_owned())); if info.error_count > 0 { - let vm_args = project.settings.effective().languages.java.jdtls_vm_args; + let vm_args = settings.languages.java.jdtls_vm_args; let lombok = ide_sdk::lombok::lombok_agent( &project.root, ide_sdk::lombok::agent_in_args(vm_args.split_whitespace()), diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index fde81826..02fd6273 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -242,7 +242,8 @@ impl AppState { let root = root.clone(); let data_dir = self.data_dir.clone(); lsp.set_config_hook(Box::new(move |cfg, server_root| { - ide_sdk::apply::configure_server(cfg, &root, server_root, &settings.effective()); + let effective = settings.effective(); + ide_sdk::apply::configure_server(cfg, &root, server_root, &effective); if cfg.name == "jdtls" { let bundles: Vec = ide_tools::managed_bundle(&data_dir, "java-debug").into_iter().collect(); @@ -261,6 +262,7 @@ impl AppState { server_root, &data_dir, &jdks, + ide_sdk::apply::project_jdk(&root, &effective).as_ref(), min_java, &bundles, lombok.jar(), diff --git a/src/bindings.ts b/src/bindings.ts index 41a76172..c5ce3d46 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -249,6 +249,7 @@ export const commands = { * follow-up command. Edits the server requests arrive via `workspace/applyEdit`. */ lspExecuteCommand: (doc: DocId, command: Command) => typedError(__TAURI_INVOKE("lsp_execute_command", { doc, command })), + /** Changes of every repository of the project. */ vcsStatus: () => typedError(__TAURI_INVOKE("vcs_status")), vcsBranchInfo: () => typedError(__TAURI_INVOKE("vcs_branch_info")), /** Gutter change markers for a file, computed against its current (unsaved) text. */ @@ -2341,11 +2342,18 @@ export type JavaLaunchInfo = { }; export type JavaSettings = { + /** + * Project JDK home; empty = per module, the installed JDK matching the + * Java version it targets. + */ + jdk?: string, /** * Extra JVM options for the Java language server (jdtls), space-separated * (VS Code's `java.jdt.ls.vmargs`). */ jdtlsVmArgs?: string, + /** Build Tools › Maven. */ + maven?: MavenSettings, }; /** Editor › Code Style › › Tabs and Indents (unset fields inherit). */ @@ -2635,10 +2643,22 @@ export type MavenProject = { javaVersion: number | null, /** Maven launcher: the nearest `mvnw` up to the project root (absolute), else `mvn`. */ command: string, + /** Options before the goals (`-s`, `-Dmaven.repo.local=`); filled in by the shell from settings. */ + options: string[], /** Environment for runs (`JAVA_HOME` of the matching JDK); filled in by the shell. */ env: { [key in string]: string }, }; +/** Build Tools › Maven. */ +export type MavenSettings = { + /** Maven home (or its `mvn` executable); empty = the project's `mvnw`, else `mvn` on `PATH`. */ + home?: string, + /** User `settings.xml` (`-s`); empty = `~/.m2/settings.xml`. */ + userSettings?: string, + /** Local repository (`-Dmaven.repo.local`); empty = from `settings.xml`. */ + localRepository?: string, +}; + export type MemoryCategory = /** The backend and its WebView processes. */ "app" | "languageServer" | @@ -3281,7 +3301,11 @@ export type Sdk = { /** What [`detect`] looks for. */ export type SdkKind = "python" | "node" | /** npm / pnpm / yarn executables. */ -"packageManager" | "rust" | "go" | "typeScript"; +"packageManager" | "rust" | "go" | "typeScript" | +/** JDK homes. */ +"jdk" | +/** Maven homes. */ +"maven"; export type SearchEvent = { type: "file"; file: FileMatches } | { type: "done"; summary: SearchSummary }; diff --git a/src/features/build-tools/actions.test.tsx b/src/features/build-tools/actions.test.tsx index acca068c..1ecd76bf 100644 --- a/src/features/build-tools/actions.test.tsx +++ b/src/features/build-tools/actions.test.tsx @@ -27,6 +27,7 @@ const mavenModel: BuildModel = { profiles: ["ci"], javaVersion: 21, command: "/p/mvnw", + options: [], env: {}, }, ], diff --git a/src/features/build-tools/index.ts b/src/features/build-tools/index.ts index b73260e8..f4c0fe93 100644 --- a/src/features/build-tools/index.ts +++ b/src/features/build-tools/index.ts @@ -95,6 +95,10 @@ export function activate(): () => void { if ([...e.created, ...e.modified, ...e.removed].some((p) => BUILD_FILE.test(p))) scheduleLoad(); }), onIdeEvent("projectRootsChanged", scheduleLoad), + // Project JDK / Maven home and options are applied to the model's launchers. + onIdeEvent("settingsChanged", (e) => { + if (e.key === "languages" || e.key.startsWith("languages.java")) scheduleLoad(); + }), registerActions([ { id: "maven.reload", diff --git a/src/features/build-tools/model.test.ts b/src/features/build-tools/model.test.ts index e9aaac5a..0d3f00e7 100644 --- a/src/features/build-tools/model.test.ts +++ b/src/features/build-tools/model.test.ts @@ -37,6 +37,7 @@ function project(over: Partial): MavenProject { profiles: [], javaVersion: 21, command: "/p/mvnw", + options: [], env: { JAVA_HOME: "/jdk21" }, ...over, }; @@ -184,6 +185,11 @@ describe("Maven runs", () => { const root = mavenGoalConfig(maven.projects[0]!, ["clean"], maven, { offline: false, skipTests: false, profiles: {} }); expect(root.cwd).toBeNull(); expect(root.isTest).toBe(false); + // Settings › Maven options go before the goals. + const configured = { ...core, options: ["-s", "/m2/settings.xml"] }; + expect(mavenGoalConfig(configured, ["test"], maven, { offline: false, skipTests: true, profiles: {} }).args).toEqual( + ["-s", "/m2/settings.xml", "test", "-DskipTests"], + ); }); it("parses typed command lines", () => { diff --git a/src/features/build-tools/model.ts b/src/features/build-tools/model.ts index 596eecb1..78ed6931 100644 --- a/src/features/build-tools/model.ts +++ b/src/features/build-tools/model.ts @@ -401,7 +401,7 @@ export function mavenGoalConfig( model: Pick, options: MavenRunOptions, ): RunConfig { - const args = mavenArgs(goals, model, options); + const args = [...project.options, ...mavenArgs(goals, model, options)]; return { id: `maven:goal:${project.pom}:${goals.join(" ")}`, name: mavenConfigName(project, goals), diff --git a/src/features/settings/__tests__/languages.test.ts b/src/features/settings/__tests__/languages.test.ts index 43c6ce68..9bacf231 100644 --- a/src/features/settings/__tests__/languages.test.ts +++ b/src/features/settings/__tests__/languages.test.ts @@ -21,7 +21,7 @@ describe("languages settings", () => { node: { packageManager: "pip" }, rust: { checkCommand: "clippy", checkOnSave: false }, typescript: { version: "" }, - java: { jdtlsVmArgs: " -Xmx2g " }, + java: { jdtlsVmArgs: " -Xmx2g ", jdk: 21, maven: { home: " /opt/maven " } }, }); expect(v.python).toEqual({ interpreter: "/usr/bin/python3", activateVirtualenv: true }); expect(v.node.packageManager).toBe(""); @@ -29,6 +29,8 @@ describe("languages settings", () => { expect(v.rust.checkOnSave).toBe(false); expect(v.typescript.version).toBe("bundled"); expect(v.java.jdtlsVmArgs).toBe("-Xmx2g"); + expect(v.java.jdk).toBe(""); + expect(v.java.maven).toEqual({ home: "/opt/maven", userSettings: "", localRepository: "" }); expect(validateSettings({}).languages).toEqual(DEFAULT_LANGUAGES); }); diff --git a/src/features/settings/languages/LanguagePages.tsx b/src/features/settings/languages/LanguagePages.tsx index 832b91b8..f6beda6d 100644 --- a/src/features/settings/languages/LanguagePages.tsx +++ b/src/features/settings/languages/LanguagePages.tsx @@ -1,7 +1,7 @@ /** - * Settings › Languages & Frameworks › Go / Java / Node.js / Rust / TypeScript: the - * toolchain each language's server and run configurations use (applied by - * `ide-sdk` when language servers start and when run configurations load). + * Settings › Languages & Frameworks › Go / Java / Node.js / Rust / TypeScript and + * Build Tools › Maven: the toolchain each language's server and run configurations + * use (applied by `ide-sdk` when language servers start and when run configurations load). */ import { useId, useMemo } from "react"; import { IconButton, Input, type SelectOption } from "@/ui"; @@ -16,12 +16,15 @@ function PathInput({ placeholder, hint, title, + file, }: { k: string; label: string; placeholder: string; hint?: string; title: string; + /** Browse for a file instead of a directory. */ + file?: boolean; }) { const [value, set] = useProjectSetting(k); const id = useId(); @@ -41,7 +44,7 @@ function PathInput({ size="small" title="Browse…" onClick={async () => { - const p = await choosePath({ directory: true, title, defaultPath: value || undefined }); + const p = await choosePath({ directory: !file, title, defaultPath: value || undefined }); if (p) set(p); }} /> @@ -217,6 +220,19 @@ export function JavaPage() { const id = useId(); return (
+
+ +
+
+ + + +
+
+ ); +} + // --------------------------------------------------------------------------- // TypeScript diff --git a/src/features/settings/pages/execution.tsx b/src/features/settings/pages/execution.tsx index f8d7b336..9bcbf46d 100644 --- a/src/features/settings/pages/execution.tsx +++ b/src/features/settings/pages/execution.tsx @@ -1,11 +1,12 @@ /** - * Build, Execution, Deployment pages: Console, Debugger (+ Data Views, + * Build, Execution, Deployment pages: Maven, Console, Debugger (+ Data Views, * Stepping), Python Debugger and Run/Debug. Consumed by the run and debugger * features (and `ide-run` for encoding / termination timeout). */ import { WORKFLOW_LIMITS } from "@/platform/workflowSettings"; import type { SelectOption } from "@/ui"; import { GlobListEditor, NumberInput, Row } from "../components/Form"; +import { MavenPage } from "../languages/LanguagePages"; import { useSetting } from "../components/context"; import type { PageSpec } from "./spec"; import { Indented, ToggleNumber } from "./workflowControls"; @@ -89,6 +90,15 @@ function TooltipDelayRow() { } export const EXECUTION_PAGES: PageSpec[] = [ + { + id: "build.maven", + title: "Maven", + group: BUILD_GROUP, + // IntelliJ IDEA: Build Tools › Maven, before Console / Debugger. + order: 359, + keywords: ["maven home", "mvn", "mvnw", "wrapper", "settings.xml", "user settings", "local repository", "m2"], + component: MavenPage, + }, { id: "build.console", title: "Console", diff --git a/src/features/settings/pages/languages.tsx b/src/features/settings/pages/languages.tsx index ab88dcc8..b6597b85 100644 --- a/src/features/settings/pages/languages.tsx +++ b/src/features/settings/pages/languages.tsx @@ -52,7 +52,17 @@ export const LANGUAGE_PAGES: PageSpec[] = [ title: "Java", group: LANGUAGES_GROUP, order: 395, - keywords: ["jdtls", "jvm options", "vmargs", "lombok", "javaagent", "eclipse jdt"], + keywords: [ + "jdk", + "project sdk", + "java home", + "jdtls", + "jvm options", + "vmargs", + "lombok", + "javaagent", + "eclipse jdt", + ], component: JavaPage, }, { diff --git a/src/platform/languageSettings.ts b/src/platform/languageSettings.ts index 89570066..f2743594 100644 --- a/src/platform/languageSettings.ts +++ b/src/platform/languageSettings.ts @@ -11,7 +11,11 @@ export interface LanguagesValid { rust: { toolchain: string; stdlib: string; checkOnSave: boolean; checkCommand: "check" | "clippy"; rustfmtArgs: string }; go: { goroot: string; gopath: string }; typescript: { version: string }; - java: { jdtlsVmArgs: string }; + java: { + jdk: string; + jdtlsVmArgs: string; + maven: { home: string; userSettings: string; localRepository: string }; + }; } export interface ProjectStructureValid { @@ -30,7 +34,7 @@ export const DEFAULT_LANGUAGES: LanguagesValid = { rust: { toolchain: "", stdlib: "", checkOnSave: true, checkCommand: "check", rustfmtArgs: "" }, go: { goroot: "", gopath: "" }, typescript: { version: "bundled" }, - java: { jdtlsVmArgs: "" }, + java: { jdk: "", jdtlsVmArgs: "", maven: { home: "", userSettings: "", localRepository: "" } }, }; export const DEFAULT_PROJECT_STRUCTURE: ProjectStructureValid = { @@ -76,6 +80,7 @@ export function validateLanguages(raw: unknown): LanguagesValid { const go = obj(l.go); const ts = obj(l.typescript); const java = obj(l.java); + const maven = obj(java.maven); return { python: { interpreter: text(py.interpreter, d.python.interpreter), @@ -94,7 +99,15 @@ export function validateLanguages(raw: unknown): LanguagesValid { }, go: { goroot: text(go.goroot, d.go.goroot), gopath: text(go.gopath, d.go.gopath) }, typescript: { version: text(ts.version, d.typescript.version) || d.typescript.version }, - java: { jdtlsVmArgs: text(java.jdtlsVmArgs, d.java.jdtlsVmArgs) }, + java: { + jdk: text(java.jdk, d.java.jdk), + jdtlsVmArgs: text(java.jdtlsVmArgs, d.java.jdtlsVmArgs), + maven: { + home: text(maven.home, d.java.maven.home), + userSettings: text(maven.userSettings, d.java.maven.userSettings), + localRepository: text(maven.localRepository, d.java.maven.localRepository), + }, + }, }; }