mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 00:07:29 +00:00
feat(java): Add Project JDK and Maven home / settings.xml / local repository settings
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
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -92,6 +92,8 @@ pub struct MavenProject {
|
||||
pub java_version: Option<u32>,
|
||||
/// 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<String>,
|
||||
/// Environment for runs (`JAVA_HOME` of the matching JDK); filled in by the shell.
|
||||
pub env: BTreeMap<String, String>,
|
||||
}
|
||||
@@ -924,6 +926,7 @@ pub(crate) fn load_with_repo(root: &Path, local_repo: Option<PathBuf>) -> 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() {
|
||||
|
||||
@@ -29,6 +29,10 @@ pub struct SyncOptions {
|
||||
pub profiles: Vec<String>,
|
||||
/// Extra environment (`JAVA_HOME`).
|
||||
pub env: BTreeMap<String, String>,
|
||||
/// Maven launcher replacing the project's `mvnw` / `mvn` (Settings › Maven home).
|
||||
pub maven_command: Option<String>,
|
||||
/// Maven options before the goals (`-s`, `-Dmaven.repo.local=`).
|
||||
pub maven_options: Vec<String>,
|
||||
}
|
||||
|
||||
#[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<String> = 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) });
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -56,6 +56,42 @@ 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`.
|
||||
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()?));
|
||||
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, `<home>/bin/mvn` or `<bin>/mvn`.
|
||||
pub fn maven_command(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||||
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 <settings.xml>`
|
||||
/// and `-Dmaven.repo.local=<dir>`.
|
||||
pub fn maven_options(root: &Path, s: &Settings) -> Vec<String> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> = 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") {
|
||||
|
||||
@@ -18,6 +18,8 @@ pub fn detect(kind: SdkKind, root: Option<&Path>) -> Vec<Sdk> {
|
||||
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<Sdk> {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ jdk
|
||||
|
||||
/// `KEY="value"` of a JDK `release` file.
|
||||
fn release_value(release: &str, key: &str) -> Option<String> {
|
||||
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<Sdk> {
|
||||
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-<version>.jar`.
|
||||
fn maven_version(home: &Path) -> Option<String> {
|
||||
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<PathBuf> {
|
||||
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<Sdk> {
|
||||
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/<hash>/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
|
||||
}
|
||||
|
||||
+71
-15
@@ -25,7 +25,8 @@ pub fn parse_release(release: &str) -> Option<u32> {
|
||||
if first == 1 { parts.next()?.parse().ok() } else { Some(first) }
|
||||
}
|
||||
|
||||
fn jdk_at(home: &Path) -> Option<Jdk> {
|
||||
/// The JDK at `home`, if it is one (a `release` file and `bin/java`).
|
||||
pub fn jdk_at(home: &Path) -> Option<Jdk> {
|
||||
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<Scan>) -> 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<Jdk> =
|
||||
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}");
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ pub enum SdkKind {
|
||||
Rust,
|
||||
Go,
|
||||
TypeScript,
|
||||
/// JDK homes.
|
||||
Jdk,
|
||||
/// Maven homes.
|
||||
Maven,
|
||||
}
|
||||
|
||||
/// One installed interpreter / toolchain.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<u32>) -> BTreeMap<String, String> {
|
||||
/// `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<u32>) -> BTreeMap<String, String> {
|
||||
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<BuildModel> {
|
||||
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<SyncProgress>,
|
||||
) -> IdeResult<BuildModel> {
|
||||
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<BuildResult> {
|
||||
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);
|
||||
})?;
|
||||
|
||||
@@ -22,10 +22,12 @@ pub async fn java_resolve_launch(
|
||||
) -> IdeResult<JavaLaunchInfo> {
|
||||
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()),
|
||||
|
||||
@@ -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<PathBuf> =
|
||||
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(),
|
||||
|
||||
+25
-1
@@ -249,6 +249,7 @@ export const commands = {
|
||||
* follow-up command. Edits the server requests arrive via `workspace/applyEdit`.
|
||||
*/
|
||||
lspExecuteCommand: (doc: DocId, command: Command) => typedError<null, IdeError>(__TAURI_INVOKE("lsp_execute_command", { doc, command })),
|
||||
/** Changes of every repository of the project. */
|
||||
vcsStatus: () => typedError<FileStatus[], IdeError>(__TAURI_INVOKE("vcs_status")),
|
||||
vcsBranchInfo: () => typedError<BranchInfo, IdeError>(__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 › <Language> › 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 };
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const mavenModel: BuildModel = {
|
||||
profiles: ["ci"],
|
||||
javaVersion: 21,
|
||||
command: "/p/mvnw",
|
||||
options: [],
|
||||
env: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -37,6 +37,7 @@ function project(over: Partial<MavenProject>): 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", () => {
|
||||
|
||||
@@ -401,7 +401,7 @@ export function mavenGoalConfig(
|
||||
model: Pick<MavenModel, "profiles">,
|
||||
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),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string>(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 (
|
||||
<div className={styles.page}>
|
||||
<Section title="Project JDK">
|
||||
<SdkRow
|
||||
k="languages.java.jdk"
|
||||
label="SDK"
|
||||
kind="jdk"
|
||||
emptyLabel="Auto"
|
||||
emptyDetail="per module, the JDK of its Java version"
|
||||
directory
|
||||
browseTitle="Select JDK Home Directory"
|
||||
aria-label="Project JDK"
|
||||
hint="Compiles and runs the project: the default jdtls runtime, Run/Debug, and JAVA_HOME for Maven and Gradle."
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Language Server">
|
||||
<Row
|
||||
label="JVM options"
|
||||
@@ -237,6 +253,44 @@ export function JavaPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build Tools › Maven
|
||||
|
||||
export function MavenPage() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Section>
|
||||
<SdkRow
|
||||
k="languages.java.maven.home"
|
||||
label="Maven home path"
|
||||
kind="maven"
|
||||
emptyLabel="Auto"
|
||||
emptyDetail="Maven wrapper, else mvn on PATH"
|
||||
directory
|
||||
browseTitle="Select Maven Home Directory"
|
||||
aria-label="Maven home path"
|
||||
hint="Runs sync, builds and Maven run configurations. A chosen home is used instead of the project's mvnw."
|
||||
/>
|
||||
<PathInput
|
||||
k="languages.java.maven.userSettings"
|
||||
label="User settings file"
|
||||
placeholder="~/.m2/settings.xml"
|
||||
title="Select Maven settings.xml"
|
||||
file
|
||||
hint="Passed as -s (mirrors, servers, proxies)."
|
||||
/>
|
||||
<PathInput
|
||||
k="languages.java.maven.localRepository"
|
||||
label="Local repository"
|
||||
placeholder="From settings.xml, else ~/.m2/repository"
|
||||
title="Select Local Repository"
|
||||
hint="Passed as -Dmaven.repo.local."
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TypeScript
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user