diff --git a/crates/ide-sdk/src/apply.rs b/crates/ide-sdk/src/apply.rs index 6fe598550..98a1fbf44 100644 --- a/crates/ide-sdk/src/apply.rs +++ b/crates/ide-sdk/src/apply.rs @@ -523,6 +523,63 @@ pub fn maven_local_repository(root: &Path, s: &Settings) -> Option { }) } +fn xml_escape(text: &str) -> String { + text.replace('&', "&").replace('<', "<").replace('>', ">") +} + +/// Inserts `` right after the `` open tag. +fn inject_local_repository(settings_xml: &str, repo: &str) -> Option { + let start = settings_xml.find("')? + start; + let mut out = String::with_capacity(settings_xml.len() + repo.len() + 40); + out.push_str(&settings_xml[..=end]); + out.push_str(""); + out.push_str(&xml_escape(repo)); + out.push_str(""); + out.push_str(&settings_xml[end + 1..]); + Some(out) +} + +fn write_m2e_settings(root: &Path, text: &str) -> Option { + let file = ide_core::project_dir::path(root, "maven-settings.xml"); + if let Some(parent) = file.parent() { + std::fs::create_dir_all(parent).ok()?; + } + match std::fs::read_to_string(&file) { + Ok(existing) if existing == text => {} + _ => std::fs::write(&file, text).ok()?, + } + Some(file) +} + +/// The user settings file m2e (the JDTLS Maven import) should read: the +/// configured `settings.xml` when it already declares ``, +/// else a derived copy under `/.openidea/` with the configured local +/// repository injected. m2e resolves the local repository from the settings +/// XML, not from `-Dmaven.repo.local`, so without this the import downloads +/// into `~/.m2/repository` even when the IDE's own `mvn` runs honor the +/// configured path (#1739). `None` = m2e's default applies. +pub fn maven_user_settings_for_m2e(root: &Path, s: &Settings) -> Option { + let m = &s.languages.java.maven; + if let Some(file) = setting_path(root, &m.user_settings) { + let text = std::fs::read_to_string(&file).ok()?; + if crate::java::xml_value(&crate::lombok::strip_xml_comments(&text), "localRepository").is_some() { + return Some(file); + } + if let Some(repo) = setting_path(root, &m.local_repository) { + let merged = inject_local_repository(&text, &repo.to_string_lossy())?; + if let Some(derived) = write_m2e_settings(root, &merged) { + return Some(derived); + } + } + return Some(file); + } + let repo = setting_path(root, &m.local_repository)?; + let minimal = + format!("{}", xml_escape(&repo.to_string_lossy())); + write_m2e_settings(root, &minimal) +} + /// 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() { @@ -861,18 +918,22 @@ pub fn configure_server(cfg: &mut LanguageServerConfig, root: &Path, server_root // The jdtls launcher forwards `--jvm-arg=` values to the JVM it starts. let args = s.languages.java.jdtls_vm_args.split_whitespace().map(|a| format!("--jvm-arg={a}")); cfg.args.extend(args); - // m2e (the Maven import) reads mirrors, servers and `` from it. - if let Some(file) = setting_path(root, &s.languages.java.maven.user_settings) { + // m2e (the Maven import) reads mirrors, servers and `` + // from the settings file — never from `-Dmaven.repo.local`. The + // derived file carries the configured local repository so the import + // downloads where the IDE's own `mvn` runs do (#1739). + if let Some(file) = + maven_user_settings_for_m2e(root, s).or_else(|| setting_path(root, &s.languages.java.maven.user_settings)) + { let file = file.to_string_lossy(); merge_init_options( cfg, json!({ "settings": { "java": { "configuration": { "maven": { "userSettings": file } } } } }), ); } - // m2e resolves and downloads into `maven.repo.local`; without it the - // embedded Maven falls back to ~/.m2 even when the IDE's own `mvn` - // runs honor the configured local repository. A `-Dmaven.repo.local` - // the user put in their own jdtls VM options wins. + // The JVM system property is still forwarded for JVM-level consumers, + // but it is the settings file above that m2e honors. A + // `-Dmaven.repo.local` the user put in their own jdtls VM options wins. if let Some(repo) = maven_local_repository(root, s) { let arg = format!("-Dmaven.repo.local={}", repo.to_string_lossy()); let taken = cfg.args.iter().enumerate().any(|(i, a)| { diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index 9d1761dc9..48aa2dd80 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -208,6 +208,53 @@ fn jdtls_honors_the_configured_maven_local_repository() { assert!(cfg.args.iter().all(|a| !a.contains("maven.repo.local"))); } +#[test] +fn jdtls_m2e_gets_effective_settings_with_local_repository() { + fn user_settings(cfg: &LanguageServerConfig) -> Option { + let opts = serde_json::to_value(cfg.initialization_options.clone()?).ok()?; + opts.pointer("/settings/java/configuration/maven/userSettings")?.as_str().map(std::path::PathBuf::from) + } + + // Configured repository, no user settings: a derived file is passed (#1739). + let tmp = tempfile::tempdir().unwrap(); + let repo = tmp.path().join("m2-repo"); + let mut s = Settings::default(); + s.languages.java.maven.local_repository = repo.to_string_lossy().into_owned(); + let mut cfg = server("jdtls"); + configure_server(&mut cfg, tmp.path(), tmp.path(), &s); + let derived = tmp.path().join(".openidea").join("maven-settings.xml"); + assert_eq!(user_settings(&cfg), Some(derived.clone())); + let text = std::fs::read_to_string(&derived).unwrap(); + assert!(text.contains(&format!("{}", repo.to_string_lossy()))); + // Stable across runs (no rewrite churn). + let mut cfg = server("jdtls"); + configure_server(&mut cfg, tmp.path(), tmp.path(), &s); + assert_eq!(user_settings(&cfg), Some(derived.clone())); + + // User settings without ``: the derived copy keeps the + // user's mirrors and gains the configured repository. + let settings = tmp.path().join("mine.xml"); + std::fs::write(&settings, "m").unwrap(); + s.languages.java.maven.user_settings = settings.to_string_lossy().into_owned(); + let mut cfg = server("jdtls"); + configure_server(&mut cfg, tmp.path(), tmp.path(), &s); + assert_eq!(user_settings(&cfg), Some(derived.clone())); + let text = std::fs::read_to_string(&derived).unwrap(); + assert!(text.contains("m")); + assert!(text.contains(&format!("{}", repo.to_string_lossy()))); + + // User settings already declaring ``: passed through untouched. + std::fs::write(&settings, "/user/repo").unwrap(); + let mut cfg = server("jdtls"); + configure_server(&mut cfg, tmp.path(), tmp.path(), &s); + assert_eq!(user_settings(&cfg), Some(settings.clone())); + + // Neither configured: no userSettings at all. + let mut cfg = server("jdtls"); + configure_server(&mut cfg, tmp.path(), tmp.path(), &Settings::default()); + assert_eq!(user_settings(&cfg), None); +} + #[test] fn rust_analyzer_check_command_and_toolchain() { if cfg!(windows) {