fix(java): pass m2e an effective settings file carrying the configured local repository (#1739) (#2301)

m2e resolves the local repository from the Maven settings XML, never from
-Dmaven.repo.local, so JDTLS imports kept downloading into ~/.m2/repository
even when the IDE's own mvn runs honored the configured path. When a local
repository is configured but the user settings lack <localRepository>, derive
an effective settings file under <root>/.openidea/ and pass it as
java.configuration.maven.userSettings. Settings that already declare
<localRepository> are passed through untouched.
Цей коміт міститься в:
arookieofc authored and GitHub committed 2026-11-09 16:16:38 +08:00
1 джерело 2dd57d095f
коміт cb2f249db5
2 files changed
+114 -6

No files matched your search

+67 -6
Переглянути файл
@@ -523,6 +523,63 @@ pub fn maven_local_repository(root: &Path, s: &Settings) -> Option<PathBuf> {
})
}
fn xml_escape(text: &str) -> String {
text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
}
/// Inserts `<localRepository>` right after the `<settings>` open tag.
fn inject_local_repository(settings_xml: &str, repo: &str) -> Option<String> {
let start = settings_xml.find("<settings")?;
let end = settings_xml[start..].find('>')? + start;
let mut out = String::with_capacity(settings_xml.len() + repo.len() + 40);
out.push_str(&settings_xml[..=end]);
out.push_str("<localRepository>");
out.push_str(&xml_escape(repo));
out.push_str("</localRepository>");
out.push_str(&settings_xml[end + 1..]);
Some(out)
}
fn write_m2e_settings(root: &Path, text: &str) -> Option<PathBuf> {
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 `<localRepository>`,
/// else a derived copy under `<root>/.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<PathBuf> {
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!("<settings><localRepository>{}</localRepository></settings>", 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<PathBuf> {
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 `<localRepository>` from it.
if let Some(file) = setting_path(root, &s.languages.java.maven.user_settings) {
// m2e (the Maven import) reads mirrors, servers and `<localRepository>`
// 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)| {
+47
Переглянути файл
@@ -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<std::path::PathBuf> {
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!("<localRepository>{}</localRepository>", 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 `<localRepository>`: the derived copy keeps the
// user's mirrors and gains the configured repository.
let settings = tmp.path().join("mine.xml");
std::fs::write(&settings, "<settings><mirrors><mirror><id>m</id></mirror></mirrors></settings>").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("<mirror><id>m</id></mirror>"));
assert!(text.contains(&format!("<localRepository>{}</localRepository>", repo.to_string_lossy())));
// User settings already declaring `<localRepository>`: passed through untouched.
std::fs::write(&settings, "<settings><localRepository>/user/repo</localRepository></settings>").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) {