fix(java): Apply the Maven settings to jdtls and Lombok, find more Maven installs

Review fixes for the Project JDK / Maven settings:

- jdtls gets the user settings.xml as java.configuration.maven.userSettings, so the
  editor resolves dependencies through the same mirrors and servers as sync and builds.
- The configured local repository (else <localRepository> of the chosen settings.xml)
  is where the static POM model looks for parent POMs / BOMs and where the Lombok
  agent is looked for.
- Maven discovery finds mvn.cmd on PATH on Windows and the homes Maven Wrapper 3.3+
  downloads (dists/<name>/<hash> without a nested folder).
- A Project JDK pointing at the JDK's bin directory is accepted.
- Detected run configurations reload when language settings change, so Maven and
  Gradle runs use the new JDK, Maven home and options.
- Chinese text for the new settings.
This commit is contained in:
l0ng-ai committed 2026-10-23 23:01:07 +08:00
1 parent 825f190030
commit 48ebbe15e7
12 files changed
+78 -21

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ fn main() {
eprintln!("synced in {:?}: {r:?}", started.elapsed());
}
let started = std::time::Instant::now();
let model = ide_build::load(&root, &cache);
let model = ide_build::load(&root, &cache, None);
println!("{}", serde_json::to_string_pretty(&model).unwrap());
eprintln!("loaded in {:?}", started.elapsed());
}
+2 -2
View File
@@ -672,7 +672,7 @@ mod tests {
);
write(&root, "core/pom.xml", "<project><artifactId>core</artifactId></project>");
write(&root, "core/src/main/java/A.java", "class A {}");
let model = crate::load(&root, &tmp.path().join("cache"));
let model = crate::load(&root, &tmp.path().join("cache"), None);
let p = plan(&root, &model, BuildKind::Build, None).unwrap();
assert_eq!(p.system, BuildSystem::Maven);
assert_eq!(p.title, "demo");
@@ -686,7 +686,7 @@ mod tests {
let g = tmp.path().join("g");
write(&g, "settings.gradle", "include 'app'");
write(&g, "app/build.gradle", "apply plugin: 'java'");
let model = crate::load(&g, &tmp.path().join("cache2"));
let model = crate::load(&g, &tmp.path().join("cache2"), None);
let p = plan(&g, &model, BuildKind::Build, Some(&g.join("app/src/A.java"))).unwrap();
assert_eq!(p.system, BuildSystem::Gradle);
assert_eq!(p.steps[0].args, ["--console=plain", ":app:classes", ":app:testClasses"]);
+6 -5
View File
@@ -18,7 +18,7 @@ pub use sync::{SyncOptions, SyncProgress, sync_gradle, sync_maven};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::path::Path;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
@@ -36,9 +36,10 @@ pub struct BuildModel {
}
/// Static model of the builds under `root`, merged with the sync results cached in `cache_dir`.
pub fn load(root: &Path, cache_dir: &Path) -> BuildModel {
/// Parent POMs are looked up in `maven_repo` (the configured local repository), else the default one.
pub fn load(root: &Path, cache_dir: &Path, maven_repo: Option<PathBuf>) -> BuildModel {
let maven = maven::is_maven_project(root).then(|| {
let mut m = maven::load(root);
let mut m = maven::load_with_repo(root, maven_repo.or_else(maven::default_local_repo));
sync::apply_maven_cache(root, cache_dir, &mut m);
m
});
@@ -68,10 +69,10 @@ mod tests {
let cache = tmp.path().join("cache");
let root = tmp.path().join("p");
std::fs::create_dir_all(root.join("svc")).unwrap();
assert_eq!(load(&root, &cache), BuildModel::default());
assert_eq!(load(&root, &cache, None), BuildModel::default());
std::fs::write(root.join("svc/pom.xml"), "<project><artifactId>svc</artifactId></project>").unwrap();
std::fs::write(root.join("build.gradle"), "").unwrap();
let m = load(&root, &cache);
let m = load(&root, &cache, None);
assert_eq!(m.maven.unwrap().projects[0].pom, "svc/pom.xml");
assert_eq!(m.gradle.unwrap().projects[0].path, ":");
assert!(affects_build(Path::new("a/pom.xml")) && affects_build(Path::new("settings.gradle.kts")));
+1 -1
View File
@@ -412,7 +412,7 @@ struct Resolver {
}
/// `~/.m2/repository`, or `<localRepository>` from `~/.m2/settings.xml`.
fn default_local_repo() -> Option<PathBuf> {
pub(crate) fn default_local_repo() -> Option<PathBuf> {
let m2 = dirs::home_dir()?.join(".m2");
if let Some(settings) = std::fs::read_to_string(m2.join("settings.xml")).ok().and_then(|t| xml::parse(&t))
&& let Some(repo) = settings.text_of("localRepository")
+23 -2
View File
@@ -56,10 +56,10 @@ fn goroot(root: &Path, s: &Settings) -> Option<PathBuf> {
setting_path(root, &s.languages.go.goroot)
}
/// The project JDK (`languages.java.jdk`): a JDK home, or its `bin/java`.
/// The project JDK (`languages.java.jdk`): a JDK home, its `bin` or its `bin/java`.
pub fn project_jdk(root: &Path, s: &Settings) -> Option<crate::java::Jdk> {
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()?));
let jdk = p.ancestors().take(3).find_map(crate::java::jdk_at);
if jdk.is_none() {
tracing::warn!("project JDK {} is not a JDK home", p.display());
}
@@ -92,6 +92,19 @@ pub fn maven_options(root: &Path, s: &Settings) -> Vec<String> {
out
}
/// The Maven local repository from settings: the configured one, else
/// `<localRepository>` of the configured user `settings.xml`; `None` = Maven's default.
pub fn maven_local_repository(root: &Path, s: &Settings) -> Option<PathBuf> {
let m = &s.languages.java.maven;
setting_path(root, &m.local_repository).or_else(|| {
let text = std::fs::read_to_string(setting_path(root, &m.user_settings)?).ok()?;
let repo = crate::java::xml_value(&crate::lombok::strip_xml_comments(&text), "localRepository")?;
let home = crate::home_dir().map(|h| h.to_string_lossy().into_owned()).unwrap_or_default();
let repo = repo.replace("${user.home}", &home);
(!repo.is_empty() && !repo.contains("${")).then(|| PathBuf::from(repo))
})
}
/// 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() {
@@ -206,6 +219,14 @@ 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) {
let file = file.to_string_lossy();
merge_init_options(
cfg,
json!({ "settings": { "java": { "configuration": { "maven": { "userSettings": file } } } } }),
);
}
}
if let Some(lib) = tsdk(server_root, s).or_else(|| tsdk(root, s)) {
+10 -2
View File
@@ -437,7 +437,13 @@ fn maven_home(dir: &Path) -> Option<PathBuf> {
fn maven() -> Vec<Sdk> {
let mut found: Vec<(PathBuf, &'static str)> = Vec::new();
if let Some(mvn) = which("mvn").and_then(|p| dunce::canonicalize(p).ok())
// Windows' Maven ships `mvn.cmd` only (`which` looks for `mvn.exe`).
let on_path = if cfg!(windows) {
std::env::split_paths(&search_path()).map(|d| d.join("mvn.cmd")).find(|p| p.is_file())
} else {
which("mvn")
};
if let Some(mvn) = on_path.and_then(|p| dunce::canonicalize(p).ok())
&& let Some(home) = mvn.parent().and_then(Path::parent)
{
found.push((home.to_path_buf(), "path"));
@@ -454,10 +460,12 @@ fn maven() -> Vec<Sdk> {
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/<hash>/apache-maven-3.9.6`.
// Distributions the Maven wrapper downloaded: `dists/apache-maven-3.9.6-bin/<hash>/apache-maven-3.9.6`,
// or since wrapper 3.3 the home itself, `dists/apache-maven-3.9.6/<hash>`.
for dist in subdirs(&home.join(".m2/wrapper/dists")) {
for hash in subdirs(&dist) {
found.extend(subdirs(&hash).into_iter().map(|d| (d, "wrapper")));
found.push((hash, "wrapper"));
}
}
}
+12 -6
View File
@@ -64,8 +64,14 @@ pub fn agent_in_args<'a>(args: impl IntoIterator<Item = &'a str>) -> Option<Path
}
/// The Lombok agent for the project at `root`: the user's own (`own`, from the JVM
/// options) wins, then `managed` (the IDE-installed jar), then the build caches.
pub fn lombok_agent(root: &Path, own: Option<PathBuf>, managed: Option<PathBuf>) -> LombokAgent {
/// options) wins, then `managed` (the IDE-installed jar), then the build caches
/// (`maven_repo`, the configured Maven local repository, else the default one).
pub fn lombok_agent(
root: &Path,
own: Option<PathBuf>,
managed: Option<PathBuf>,
maven_repo: Option<PathBuf>,
) -> LombokAgent {
if let Some(jar) = own {
let version = jar
.file_stem()
@@ -82,7 +88,7 @@ pub fn lombok_agent(root: &Path, own: Option<PathBuf>, managed: Option<PathBuf>)
}
let home = home_dir();
let mut dirs = Vec::new();
if let Some(repo) = maven_repository(home.as_deref()) {
if let Some(repo) = maven_repo.or_else(|| maven_repository(home.as_deref())) {
dirs.push(repo.join("org").join("projectlombok").join("lombok"));
}
if let Some(gradle) =
@@ -137,7 +143,7 @@ fn maven_repository(home: Option<&Path>) -> Option<PathBuf> {
Some(custom.unwrap_or_else(|| m2.join("repository")))
}
fn strip_xml_comments(text: &str) -> String {
pub(crate) fn strip_xml_comments(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(i) = rest.find("<!--") {
@@ -249,10 +255,10 @@ mod tests {
let own = agent_in_args(["--jvm-arg=-Xmx2g", "--jvm-arg=-javaagent:/opt/lombok-1.18.26.jar"]);
assert_eq!(own.as_deref(), Some(Path::new("/opt/lombok-1.18.26.jar")));
assert_eq!(agent_in_args(["-javaagent:/opt/other.jar"]), None);
let agent = lombok_agent(tmp.path(), own, Some("/managed/lombok.jar".into()));
let agent = lombok_agent(tmp.path(), own, Some("/managed/lombok.jar".into()), None);
assert_eq!(agent.jar(), Some(Path::new("/opt/lombok-1.18.26.jar")));
assert!(agent.outdated(), "its version is read from the file name");
let plain = lombok_agent(tmp.path(), agent_in_args(["-javaagent:/opt/lombok.jar"]), None);
let plain = lombok_agent(tmp.path(), agent_in_args(["-javaagent:/opt/lombok.jar"]), None, None);
assert_eq!(plain.problem(), None, "a user agent is never reported missing");
}
+1 -1
View File
@@ -26,7 +26,7 @@ fn java_env(project_jdk: Option<&ide_sdk::java::Jdk>, release: Option<u32>) -> B
/// 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 mut model = ide_build::load(root, cache, ide_sdk::apply::maven_local_repository(root, settings));
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);
+2 -1
View File
@@ -52,11 +52,12 @@ pub async fn java_resolve_launch(
});
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 = settings.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()),
ide_tools::managed_bundle(&state.data_dir, "lombok"),
ide_sdk::apply::maven_local_repository(&project.root, &settings),
);
info.hints.extend(lombok.problem());
}
+1
View File
@@ -254,6 +254,7 @@ impl AppState {
server_root,
ide_sdk::lombok::agent_in_args(cfg.args.iter().map(String::as_str)),
ide_tools::managed_bundle(&data_dir, "lombok"),
ide_sdk::apply::maven_local_repository(&root, &effective),
);
if let Some(problem) = lombok.problem() {
tracing::warn!("{problem}");
+4
View File
@@ -386,6 +386,10 @@ export function activate(): () => void {
const paths = [...e.created, ...e.modified, ...e.removed];
if (paths.some((p) => CONFIG_FILES.test(p))) scheduleReload();
}),
// Detected configs carry the selected toolchains (interpreter, JDK, Maven home and options).
onIdeEvent("settingsChanged", (e) => {
if (e.key === "languages" || e.key.startsWith("languages.")) scheduleReload();
}),
);
return () => {
+15
View File
@@ -345,6 +345,7 @@
"Compare with Clipboard": "与剪贴板比较",
"Compare with Local": "与本地比较",
"Compilation failed: fix the errors and reload again.": "编译失败:请修复错误后重新加载。",
"Compiles and runs the project: the default jdtls runtime, Run/Debug, and JAVA_HOME for Maven and Gradle.": "用于编译和运行项目:jdtls 的默认运行时、运行/调试,以及 Maven 和 Gradle 的 JAVA_HOME。",
"Compiles the module of the current file": "编译当前文件所在的模块",
"Compiles the project's sources and tests with its build system": "使用项目的构建系统编译其源代码和测试",
"Completions": "补全",
@@ -766,6 +767,7 @@
"Fragment": "片段",
"Frames": "帧",
"From Template…": "从模板…",
"From settings.xml, else ~/.m2/repository": "取自 settings.xml,否则为 ~/.m2/repository",
"Function": "函数",
"Function component": "函数组件",
"Function with return type": "带返回类型的函数",
@@ -1001,6 +1003,7 @@
"Local History of all projects will be deleted.": "所有项目的本地历史记录都将被删除。",
"Local History: Put Label…": "本地历史记录:添加标签…",
"Local History: Show History": "本地历史记录:显示历史记录",
"Local repository": "本地仓库",
"Locate Duplicates": "查找重复项",
"Locate Duplicates…": "查找重复项…",
"Location": "位置",
@@ -1036,8 +1039,10 @@
"Matches": "匹配项",
"Maven actions": "Maven 操作",
"Maven command line": "Maven 命令行",
"Maven home path": "Maven 主路径",
"Maven projects": "Maven 项目",
"Maven sync finished with errors": "Maven 同步完成,但有错误",
"Maven wrapper, else mvn on PATH": "Maven wrapper,否则使用 PATH 中的 mvn",
"Maximize": "最大化",
"Maximize Tool Window": "最大化工具窗口",
"Maximum local history size per project": "每个项目的最大本地历史记录大小",
@@ -1301,6 +1306,8 @@
"Parses (hit / incr. / full)": "解析(命中/增量/完整)",
"Partially staged: only staged hunks are committed": "部分暂存:仅提交已暂存的块",
"Passed": "已通过",
"Passed as -Dmaven.repo.local.": "以 -Dmaven.repo.local 传入。",
"Passed as -s (mirrors, servers, proxies).": "以 -s 传入(镜像、服务器、代理)。",
"Passed to rustfmt when reformatting, e.g. --edition 2024": "重新格式化时传递给 rustfmt,例如 --edition 2024",
"Passed to the JVM running jdtls (java.jdt.ls.vmargs), e.g. -Xmx2g. A Lombok agent is added automatically when the project uses Lombok.": "传递给运行 jdtls 的 JVM (java.jdt.ls.vmargs),例如 -Xmx2g。项目使用 Lombok 时会自动添加 Lombok 代理。",
"Password": "密码",
@@ -1394,6 +1401,7 @@
"Project Errors": "项目错误",
"Project Files": "项目文件",
"Project GOPATH": "项目 GOPATH",
"Project JDK": "项目 JDK",
"Project Production Files": "项目生产文件",
"Project Settings": "项目设置",
"Project Structure": "项目结构",
@@ -1669,6 +1677,7 @@
"Runs Maven with -o": "使用 -o 运行 Maven",
"Runs Optimize Imports when the file has no errors, unless it would change the caret line.": "文件没有错误时运行“优化导入”,除非这会更改光标所在行。",
"Runs npm scripts and Node-based language servers (vtsls, eslint, pyright…).": "运行 npm 脚本和基于 Node 的语言服务器(vtsls、eslint、pyright…)。",
"Runs sync, builds and Maven run configurations. A chosen home is used instead of the project's mvnw.": "用于同步、构建和 Maven 运行配置。选定后将代替项目的 mvnw。",
"Runs the language server's Organize Imports action.": "运行语言服务器的“整理导入”操作。",
"Runs the language server's formatter before an explicit save (Actions on Save).": "在显式保存前运行语言服务器的格式化程序(保存时操作)。",
"Runs this project": "运行此项目",
@@ -1727,6 +1736,10 @@
"Select Base Interpreter": "选择基础解释器",
"Select Folders…": "选择文件夹…",
"Select GOPATH": "选择 GOPATH",
"Select JDK Home Directory": "选择 JDK 主目录",
"Select Local Repository": "选择本地仓库",
"Select Maven Home Directory": "选择 Maven 主目录",
"Select Maven settings.xml": "选择 Maven settings.xml",
"Select Next Tab": "选择下一个标签页",
"Select Opened File": "选择已打开的文件",
"Select Previous Tab": "选择上一个标签页",
@@ -2140,6 +2153,7 @@
"Used for new files.": "用于新文件。",
"User": "用户",
"User name or email": "用户名或电子邮件",
"User settings file": "用户设置文件",
"User:": "用户:",
"Uses HostName, User, Port, IdentityFile and ProxyJump from ~/.ssh/config, and keys of the SSH agent.": "使用 ~/.ssh/config 中的 HostName、User、Port、IdentityFile 和 ProxyJump,以及 SSH 代理中的密钥。",
"VCS Operations": "VCS 操作",
@@ -2251,6 +2265,7 @@
"occurrences ·": "个匹配项 ·",
"or click a line number to set a breakpoint.": "或点击行号设置断点。",
"or pick one in the toolbar.": "或在工具栏中选择一个。",
"per module, the JDK of its Java version": "按模块使用与其 Java 版本匹配的 JDK",
"pip output": "pip 输出",
"places the caret. A line with": "放置光标。包含",
"println! with Debug": "使用 Debug 的 println!",