fix(java): Run jdtls with a Lombok agent and show build errors at launch (#9)

* fix(java): Run jdtls with a Lombok agent and show build errors at launch

jdtls compiles with ECJ, which only generates Lombok members when the
server JVM runs -javaagent:lombok.jar. Nothing put it there, so @Data /
@Setter classes lost their accessors and Run launched stale classes
with "Unresolved compilation problem" stubs and no explanation.

- Projects whose build files mention Lombok get the newest jar: the
  IDE-managed one (new "Lombok (jdtls agent)" tool, latest release from
  Maven Central), else ~/.m2 (incl. settings.xml localRepository) or the
  Gradle cache. Each agent gets its own jdtls workspace so everything is
  rebuilt with it.
- Settings › Languages & Frameworks › Java › JVM options
  (java.jdt.ls.vmargs); a Lombok agent there wins.
- Run (and Debug, as a notification) list the first compile errors when
  the build has errors, plus a hint when Lombok is missing or older than
  1.18.34.

Fixes #3

* fix(java): Base the Lombok hint on the agent jdtls actually runs

A Lombok agent in Settings › Java › JVM options wins over the managed
and cached jars, but the launch hint still looked only at those: with
no cached jar it said Lombok was missing, and with an old cached one it
blamed that version although jdtls ran the user's. The hint and the
startup warning now use the user's agent (version from its file name).
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-23 18:46:13 +08:00
1 джерело 555ea8901f
коміт acf6931017
24 files changed
+687 -19

No files matched your search

+9
Переглянути файл
@@ -335,6 +335,7 @@ pub struct LanguagesSettings {
pub rust: RustSettings,
pub go: GoSettings,
pub typescript: TypeScriptSettings,
pub java: JavaSettings,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
@@ -397,6 +398,14 @@ pub struct GoSettings {
pub gopath: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
#[serde(default, rename_all = "camelCase")]
pub struct JavaSettings {
/// Extra JVM options for the Java language server (jdtls), space-separated
/// (VS Code's `java.jdt.ls.vmargs`).
pub jdtls_vm_args: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
#[serde(default, rename_all = "camelCase")]
pub struct TypeScriptSettings {
+82
Переглянути файл
@@ -24,6 +24,10 @@ const BUILD_TIMEOUT: Duration = Duration::from_secs(300);
const COMMAND_TIMEOUT: Duration = Duration::from_secs(120);
/// How long to retry while the project is still being imported.
const IMPORT_WAIT: Duration = Duration::from_secs(90);
/// Compile errors listed with a launch; the rest are only counted.
const MAX_LAUNCH_ERRORS: usize = 10;
/// Build markers can be published just after `java/buildWorkspace` answers.
const ERRORS_WAIT: Duration = Duration::from_secs(2);
/// Result of `java/buildWorkspace` (jdtls `BuildWorkspaceStatus`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
@@ -70,6 +74,43 @@ pub struct JavaLaunchInfo {
pub java_home: Option<ide_core::IpcPath>,
/// The `java` launcher to use (filled in by the caller from the installed JDKs).
pub java_exec: Option<String>,
/// First compile errors of the workspace when the build had errors.
pub errors: Vec<JavaCompileError>,
/// All compile errors (`errors` is capped).
pub error_count: u32,
/// Likely causes of the errors worth telling the user (e.g. a missing Lombok agent).
pub hints: Vec<String>,
}
/// One error diagnostic; `line` / `column` are 1-based for display.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct JavaCompileError {
pub path: ide_core::IpcPath,
pub line: u32,
pub column: u32,
pub message: String,
}
/// Error diagnostics sorted by file and position, capped at `limit`, and their total.
pub fn compile_errors(
diagnostics: Vec<(PathBuf, lsp_types::Diagnostic)>,
limit: usize,
) -> (Vec<JavaCompileError>, u32) {
let mut errors: Vec<JavaCompileError> = diagnostics
.into_iter()
.filter(|(_, d)| d.severity == Some(lsp_types::DiagnosticSeverity::ERROR))
.map(|(path, d)| JavaCompileError {
path: path.into(),
line: d.range.start.line + 1,
column: d.range.start.character + 1,
message: d.message.lines().next().unwrap_or_default().to_owned(),
})
.collect();
errors.sort_by(|a, b| (&a.path, a.line, a.column).cmp(&(&b.path, b.line, b.column)));
let count = u32::try_from(errors.len()).unwrap_or(u32::MAX);
errors.truncate(limit);
(errors, count)
}
/// `java.project.getClasspaths` result.
@@ -210,6 +251,10 @@ impl LspManager {
/// Builds (when asked), then resolves the classpath and JDK of the module at `module`.
pub async fn java_launch(&self, module: &Path, test: bool, build: bool) -> IdeResult<JavaLaunchInfo> {
let build = if build { Some(self.java_build(module).await?) } else { None };
let (errors, error_count) = match build {
Some(JavaBuildStatus::WithErrors | JavaBuildStatus::Failed) => self.java_compile_errors(module).await,
_ => (Vec::new(), 0),
};
let cp = self.java_classpath(module, test).await?;
let java_home = match self.java_vm_location(module).await {
Ok(home) => home,
@@ -225,9 +270,24 @@ impl LspManager {
module_paths: cp.module_paths,
java_home: java_home.map(Into::into),
java_exec: None,
errors,
error_count,
hints: Vec::new(),
})
}
/// Compile errors jdtls reports for its workspace (all modules it imported).
async fn java_compile_errors(&self, module: &Path) -> (Vec<JavaCompileError>, u32) {
let Ok((server, _)) = self.jdtls(module).await else { return (Vec::new(), 0) };
let collect = || compile_errors(server.diagnostics_snapshot(), MAX_LAUNCH_ERRORS);
let found = collect();
if found.1 > 0 {
return found;
}
let _ = tokio::time::timeout(ERRORS_WAIT, server.published_notify.notified()).await;
collect()
}
/// Starts (or reuses) java-debug's DAP server inside jdtls and returns its port.
pub async fn java_debug_server(&self, path: &Path) -> IdeResult<u16> {
let v = self.jdtls_command(path, "vscode.java.startDebugSession", Vec::new()).await.map_err(|e| {
@@ -256,6 +316,28 @@ impl LspManager {
mod tests {
use super::*;
#[test]
fn launch_errors_are_sorted_and_capped() {
let diag = |line: u32, severity, message: &str| lsp_types::Diagnostic {
range: lsp_types::Range::new(lsp_types::Position::new(line, 4), lsp_types::Position::new(line, 9)),
severity: Some(severity),
message: message.into(),
..Default::default()
};
let (errors, count) = compile_errors(
vec![
(PathBuf::from("/p/B.java"), diag(1, lsp_types::DiagnosticSeverity::ERROR, "b")),
(PathBuf::from("/p/A.java"), diag(9, lsp_types::DiagnosticSeverity::ERROR, "a2\nmore")),
(PathBuf::from("/p/A.java"), diag(2, lsp_types::DiagnosticSeverity::WARNING, "warning")),
(PathBuf::from("/p/A.java"), diag(3, lsp_types::DiagnosticSeverity::ERROR, "a1")),
],
2,
);
assert_eq!(count, 3);
let shown: Vec<(u32, u32, &str)> = errors.iter().map(|e| (e.line, e.column, e.message.as_str())).collect();
assert_eq!(shown, [(4, 5, "a1"), (10, 5, "a2")]);
}
#[test]
fn classpaths() {
let v = json!({
+6
Переглянути файл
@@ -330,6 +330,12 @@ impl Server {
self.diagnostics.lock().get(path).cloned().unwrap_or_default()
}
/// Every published diagnostic, by file.
pub fn diagnostics_snapshot(&self) -> Vec<(PathBuf, lsp::Diagnostic)> {
let d = self.diagnostics.lock();
d.iter().flat_map(|(p, items)| items.iter().map(move |i| (p.clone(), i.clone()))).collect()
}
pub fn take_diagnostic_paths(&self) -> Vec<PathBuf> {
self.published.lock().clear();
self.diagnostics.lock().drain().map(|(p, _)| p).collect()
+6
Переглянути файл
@@ -166,6 +166,12 @@ pub fn configure_server(cfg: &mut LanguageServerConfig, root: &Path, server_root
}
}
if name == "jdtls" {
// 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);
}
if let Some(lib) = tsdk(server_root, s).or_else(|| tsdk(root, s)) {
let lib_s = lib.to_string_lossy().into_owned();
match name {
+49 -9
Переглянути файл
@@ -266,12 +266,22 @@ pub fn configure_jdtls(
jdks: &[Jdk],
min_java: u32,
bundles: &[PathBuf],
lombok: Option<&Path>,
) {
if cfg.name != "jdtls" {
return;
}
// A Lombok agent from the user's JVM arguments wins.
let own_agent = crate::lombok::agent_in_args(cfg.args.iter().map(String::as_str));
let agent = own_agent.as_deref().or(lombok);
if !cfg.args.iter().any(|a| a == "-data") {
let dir = data_dir.join("lsp").join("jdtls").join(path_key(server_root));
// ECJ builds incrementally: classes compiled without (or with another) Lombok
// agent would be kept, so each agent gets its own workspace and a full build.
let key = match agent {
Some(jar) => format!("{}-{}", path_key(server_root), path_key(jar)),
None => path_key(server_root),
};
let dir = data_dir.join("lsp").join("jdtls").join(key);
cfg.args.extend(["-data".to_owned(), dir.to_string_lossy().into_owned()]);
}
// jdtls reads this one as a system property (its file-system bundle
@@ -279,6 +289,11 @@ pub fn configure_jdtls(
if !cfg.args.iter().any(|a| a.contains(METADATA_PROPERTY)) {
cfg.args.push(format!("--jvm-arg=-D{METADATA_PROPERTY}=false"));
}
if own_agent.is_none()
&& let Some(jar) = lombok
{
cfg.args.push(format!("--jvm-arg=-javaagent:{}", jar.to_string_lossy()));
}
if !cfg.env.contains_key("JAVA_HOME") {
match jdtls_runtime(jdks, min_java) {
Some(jdk) => {
@@ -369,7 +384,7 @@ pub fn project_java_version(module_dir: &Path) -> Option<u32> {
None
}
fn xml_value(text: &str, tag: &str) -> Option<String> {
pub(crate) fn xml_value(text: &str, tag: &str) -> Option<String> {
let open = format!("<{tag}>");
let start = text.find(&open)? + open.len();
let end = text[start..].find('<')?;
@@ -409,7 +424,7 @@ 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);
configure_jdtls(&mut cfg, Path::new("/p/demo"), Path::new("/data"), &jdks, 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");
@@ -424,32 +439,57 @@ 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);
configure_jdtls(&mut cfg, Path::new("/p/demo"), Path::new("/data"), &jdks, JDTLS_MIN_JAVA, &bundles, None);
assert_eq!(cfg, before);
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);
configure_jdtls(&mut other, Path::new("/p"), Path::new("/data"), &jdks, JDTLS_MIN_JAVA, &bundles, None);
assert!(other.args.is_empty() && other.initialization_options.is_none());
}
#[test]
fn jdtls_lombok_agent() {
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));
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);
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));
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));
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);
assert_eq!(cfg.args, own.args, "its workspace follows the agent in use");
}
#[test]
fn jdtls_needs_a_new_enough_jdk_to_run() {
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, &[]);
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, 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, &[]);
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &[], 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, &[]);
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, 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") }];
@@ -457,7 +497,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, &[]);
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &mixed, 25, &[], None);
let err = cfg.launch_error.unwrap();
assert!(err.contains("JDK 25 or newer") && err.contains("found JDK 17, 21"), "{err}");
}
+1
Переглянути файл
@@ -10,6 +10,7 @@ mod discover;
mod exec;
pub mod java;
pub mod locate;
pub mod lombok;
pub mod python;
use serde::{Deserialize, Serialize};
+272
Переглянути файл
@@ -0,0 +1,272 @@
//! Lombok for jdtls: ECJ only generates Lombok members when the language
//! server's JVM runs `-javaagent:lombok.jar`, so a project using Lombok gets the
//! newest jar available, managed (installed by the IDE) or in the Maven / Gradle caches.
use crate::home_dir;
use std::path::{Path, PathBuf};
/// Oldest Lombok whose ECJ handlers work in current jdtls builds (Eclipse 2024-06 support);
/// older ones throw `NoSuchMethodError` and drop every annotation of the compilation unit.
pub const LOMBOK_MIN: [u32; 3] = [1, 18, 34];
/// What a jdtls instance gets for Lombok.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LombokAgent {
/// The project doesn't use Lombok.
Unused,
/// The project uses Lombok but no jar was found.
Missing,
Found {
jar: PathBuf,
/// `None` for the managed jar (the latest release when installed).
version: Option<String>,
},
}
impl LombokAgent {
pub fn jar(&self) -> Option<&Path> {
match self {
Self::Found { jar, .. } => Some(jar),
_ => None,
}
}
/// Too old for current jdtls.
pub fn outdated(&self) -> bool {
matches!(self, Self::Found { version: Some(v), .. } if parse_version(v).is_some_and(|v| v < LOMBOK_MIN))
}
/// Why Lombok-generated members may be missing, if they may be.
pub fn problem(&self) -> Option<String> {
const INSTALL: &str =
"Install Lombok in Settings › Languages & Frameworks › Language Servers (jdtls restarts with it)";
match self {
Self::Missing => Some(format!(
"The project uses Lombok, but no lombok.jar was found for the Java language server, so Lombok-generated \
members are missing. {INSTALL}."
)),
Self::Found { version: Some(v), .. } if self.outdated() => Some(format!(
"Lombok {v} is too old for the Java language server (needs {}.{}.{} or newer): classes using it may \
miss generated members. {INSTALL}.",
LOMBOK_MIN[0], LOMBOK_MIN[1], LOMBOK_MIN[2]
)),
_ => None,
}
}
}
/// The Lombok jar of a `-javaagent:` among JVM arguments (bare or as `--jvm-arg=`).
pub fn agent_in_args<'a>(args: impl IntoIterator<Item = &'a str>) -> Option<PathBuf> {
args.into_iter().find_map(|a| {
let jar = a.split_once("-javaagent:")?.1;
jar.to_ascii_lowercase().contains("lombok").then(|| PathBuf::from(jar))
})
}
/// 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 {
if let Some(jar) = own {
let version = jar
.file_stem()
.and_then(|s| s.to_str()?.strip_prefix("lombok-"))
.filter(|v| parse_version(v).is_some())
.map(str::to_owned);
return LombokAgent::Found { jar, version };
}
if !uses_lombok(root) {
return LombokAgent::Unused;
}
if let Some(jar) = managed {
return LombokAgent::Found { jar, version: None };
}
let home = home_dir();
let mut dirs = Vec::new();
if let Some(repo) = maven_repository(home.as_deref()) {
dirs.push(repo.join("org").join("projectlombok").join("lombok"));
}
if let Some(gradle) =
std::env::var_os("GRADLE_USER_HOME").map(PathBuf::from).or_else(|| home.map(|h| h.join(".gradle")))
{
dirs.push(gradle.join("caches").join("modules-2").join("files-2.1").join("org.projectlombok").join("lombok"));
}
match newest_cached(&dirs) {
Some((version, jar)) => LombokAgent::Found { jar, version: Some(version) },
None => LombokAgent::Missing,
}
}
/// Build files under `root` (a few levels, for multi-module builds) mentioning Lombok.
pub fn uses_lombok(root: &Path) -> bool {
const FILES: [&str; 4] = ["pom.xml", "build.gradle", "build.gradle.kts", "libs.versions.toml"];
const SKIP: [&str; 7] = ["target", "build", "node_modules", "out", "bin", "src", ".gradle"];
fn walk(dir: &Path, depth: usize) -> bool {
if dir.join("lombok.config").is_file() {
return true;
}
let Ok(entries) = std::fs::read_dir(dir) else { return false };
let mut subdirs = Vec::new();
for e in entries.flatten() {
let name = e.file_name();
let name = name.to_string_lossy();
let Ok(ty) = e.file_type() else { continue };
if ty.is_dir() {
if depth > 0 && !name.starts_with('.') && !SKIP.contains(&name.as_ref()) {
subdirs.push(e.path());
}
} else if FILES.contains(&name.as_ref())
&& std::fs::read_to_string(e.path()).is_ok_and(|t| t.contains("lombok"))
{
return true;
}
}
subdirs.iter().any(|d| walk(d, depth - 1))
}
// `gradle/libs.versions.toml` sits one level down, modules usually no deeper than three.
walk(root, 3)
}
/// `<localRepository>` of `~/.m2/settings.xml`, else `~/.m2/repository`.
fn maven_repository(home: Option<&Path>) -> Option<PathBuf> {
let m2 = home?.join(".m2");
let custom = std::fs::read_to_string(m2.join("settings.xml"))
.ok()
.and_then(|s| crate::java::xml_value(&strip_xml_comments(&s), "localRepository"))
.filter(|v| !v.is_empty() && !v.contains("${"))
.map(PathBuf::from);
Some(custom.unwrap_or_else(|| m2.join("repository")))
}
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("<!--") {
out.push_str(&rest[..i]);
rest = rest[i..].find("-->").map_or("", |j| &rest[i + j + 3..]);
}
out.push_str(rest);
out
}
/// Highest release `<dir>/<version>/…/lombok-<version>.jar` (Maven: directly in
/// the version directory; Gradle: one hash directory below it).
fn newest_cached(dirs: &[PathBuf]) -> Option<(String, PathBuf)> {
let mut best: Option<([u32; 3], String, PathBuf)> = None;
for dir in dirs {
let Ok(entries) = std::fs::read_dir(dir) else { continue };
for e in entries.flatten() {
let version = e.file_name().to_string_lossy().into_owned();
let Some(parsed) = parse_version(&version) else { continue };
if best.as_ref().is_some_and(|(b, ..)| *b >= parsed) {
continue;
}
let name = format!("lombok-{version}.jar");
let direct = e.path().join(&name);
let jar = if direct.is_file() {
Some(direct)
} else {
std::fs::read_dir(e.path())
.into_iter()
.flatten()
.flatten()
.map(|h| h.path().join(&name))
.find(|p| p.is_file())
};
if let Some(jar) = jar {
best = Some((parsed, version, jar));
}
}
}
best.map(|(_, v, jar)| (v, jar))
}
/// `1.18.34` → `[1, 18, 34]`; snapshots and other qualifiers don't count as releases.
fn parse_version(v: &str) -> Option<[u32; 3]> {
let mut parts = v.split('.').map(|p| p.parse::<u32>().ok());
let out = [parts.next()??, parts.next()??, parts.next().unwrap_or(Some(0))?];
parts.next().is_none().then_some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn detects_lombok_in_build_files() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fs::write(root.join("pom.xml"), "<project><modules><module>api</module></modules></project>").unwrap();
assert!(!uses_lombok(root));
fs::create_dir_all(root.join("api/src/main")).unwrap();
fs::write(root.join("api/src/main/pom.xml"), "<artifactId>lombok</artifactId>").unwrap();
assert!(!uses_lombok(root), "sources are not scanned");
fs::write(root.join("api/pom.xml"), "<artifactId>lombok</artifactId>").unwrap();
assert!(uses_lombok(root));
let gradle = tempfile::tempdir().unwrap();
fs::create_dir_all(gradle.path().join("gradle")).unwrap();
fs::write(gradle.path().join("gradle/libs.versions.toml"), "lombok = \"org.projectlombok:lombok:1.18.30\"")
.unwrap();
assert!(uses_lombok(gradle.path()));
}
#[test]
fn picks_the_newest_cached_release() {
let tmp = tempfile::tempdir().unwrap();
let m2 = tmp.path().join("m2");
let gradle = tmp.path().join("gradle");
for v in ["1.18.26", "1.18.36", "1.18.99-SNAPSHOT"] {
fs::create_dir_all(m2.join(v)).unwrap();
fs::write(m2.join(v).join(format!("lombok-{v}.jar")), b"PK").unwrap();
}
fs::create_dir_all(m2.join("1.18.40")).unwrap(); // only a .pom: not usable
fs::create_dir_all(gradle.join("1.18.38/abc123")).unwrap();
fs::write(gradle.join("1.18.38/abc123/lombok-1.18.38.jar"), b"PK").unwrap();
let (v, jar) = newest_cached(&[m2.clone(), gradle.clone()]).unwrap();
assert_eq!(v, "1.18.38");
assert_eq!(jar, gradle.join("1.18.38/abc123/lombok-1.18.38.jar"));
assert_eq!(newest_cached(std::slice::from_ref(&m2)).unwrap().0, "1.18.36");
assert_eq!(newest_cached(&[tmp.path().join("none")]), None);
}
#[test]
fn old_versions_are_reported() {
let found = |v: Option<&str>| LombokAgent::Found { jar: "/l.jar".into(), version: v.map(Into::into) };
assert!(found(Some("1.18.26")).outdated());
assert!(found(Some("1.18.26")).problem().unwrap().contains("1.18.26 is too old"));
assert!(!found(Some("1.18.34")).outdated());
assert_eq!(found(None).problem(), None);
assert!(LombokAgent::Missing.problem().is_some());
assert_eq!(LombokAgent::Unused.problem(), None);
assert_eq!(parse_version("1.18"), Some([1, 18, 0]));
assert_eq!(parse_version("edge-SNAPSHOT"), None);
}
#[test]
fn the_users_agent_wins() {
let tmp = tempfile::tempdir().unwrap();
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()));
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);
assert_eq!(plain.problem(), None, "a user agent is never reported missing");
}
#[test]
fn maven_settings_local_repository() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path();
assert_eq!(maven_repository(Some(home)), Some(home.join(".m2/repository")));
fs::create_dir_all(home.join(".m2")).unwrap();
fs::write(
home.join(".m2/settings.xml"),
"<settings><!-- <localRepository>/old</localRepository> --><localRepository>/repo</localRepository></settings>",
)
.unwrap();
assert_eq!(maven_repository(Some(home)), Some(PathBuf::from("/repo")));
}
}
+10
Переглянути файл
@@ -74,6 +74,16 @@ fn pyright_gets_python_path_and_venv_env() {
assert!(cfg.env["PATH"].starts_with(&*tmp.path().join(".venv").join(bin_dir()).to_string_lossy()));
}
#[test]
fn jdtls_vm_args() {
let tmp = tempfile::tempdir().unwrap();
let mut s = Settings::default();
s.languages.java.jdtls_vm_args = " -Xmx2g -javaagent:/opt/lombok.jar ".into();
let mut cfg = server("jdtls");
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
assert_eq!(cfg.args, ["--jvm-arg=-Xmx2g", "--jvm-arg=-javaagent:/opt/lombok.jar"]);
}
#[test]
fn rust_analyzer_check_command_and_toolchain() {
if cfg!(windows) {
+8
Переглянути файл
@@ -216,6 +216,10 @@ fn usages(langs: &[LanguageConfig]) -> BTreeMap<String, Usage> {
add(&d.adapter, &d.command, ToolKind::Debugger, d.install_hint.as_ref(), &l.name);
}
}
for (plugin, host) in registry::PLUGIN_HOSTS {
let languages = map.get(*host).map(|u| u.languages.clone()).unwrap_or_default();
map.entry(plugin.to_string()).or_default().languages = languages;
}
map
}
@@ -734,6 +738,10 @@ impl Tools {
let json = http.get_text(&sources::openvsx_latest_url(namespace, extension), cancel)?;
sources::openvsx_pick(&json, assets, self.platform)
}
ReleaseSource::MavenCentral { group, artifact } => {
let xml = http.get_text(&sources::maven_metadata_url(group, artifact), cancel)?;
sources::maven_pick(group, artifact, &xml)
}
}
}
+36 -2
Переглянути файл
@@ -10,6 +10,8 @@ use specta::Type;
pub enum ToolKind {
LanguageServer,
Debugger,
/// Loaded into another tool's process, e.g. a jdtls agent.
Plugin,
}
/// Where release archives come from.
@@ -23,6 +25,8 @@ pub enum ReleaseSource {
EclipseJdtls,
/// Latest version of a VS Code extension (`.vsix`) on open-vsx.org.
OpenVsx { namespace: &'static str, extension: &'static str },
/// Latest release of a Maven Central artifact (the `.jar` itself).
MavenCentral { group: &'static str, artifact: &'static str },
}
#[derive(Clone, Copy, Debug)]
@@ -56,6 +60,7 @@ impl Method {
Method::Release { source: ReleaseSource::HashiCorp { .. }, .. } => "HashiCorp release",
Method::Release { source: ReleaseSource::EclipseJdtls, .. } => "Eclipse download",
Method::Release { source: ReleaseSource::OpenVsx { .. }, .. } => "Open VSX",
Method::Release { source: ReleaseSource::MavenCentral { .. }, .. } => "Maven Central",
Method::Release { .. } => "GitHub release",
}
}
@@ -86,7 +91,7 @@ impl ToolSpec {
use Arch::{Arm64, X64};
use Os::{Linux, Mac, Windows};
use ToolKind::{Debugger, LanguageServer};
use ToolKind::{Debugger, LanguageServer, Plugin};
const V: Option<(&str, &[&str])> = None;
const fn ver(cmd: &'static str) -> Option<(&'static str, &'static [&'static str])> {
@@ -453,6 +458,24 @@ pub static TOOLS: &[ToolSpec] = &[
version: V,
homepage: "https://github.com/microsoft/java-debug",
},
ToolSpec {
name: "lombok",
display: "Lombok (jdtls agent)",
kind: Plugin,
commands: &["lombok.jar"],
// Newer than the Lombok a project pins: old ones fail inside current ECJ.
method: Method::Release {
source: ReleaseSource::MavenCentral { group: "org.projectlombok", artifact: "lombok" },
assets: &[
any_arch(Mac, "lombok-*.jar"),
any_arch(Linux, "lombok-*.jar"),
any_arch(Windows, "lombok-*.jar"),
],
locate: &["lombok.jar"],
},
version: V,
homepage: "https://projectlombok.org",
},
ToolSpec {
name: "kotlin-lsp",
display: "Kotlin LSP (JetBrains)",
@@ -478,6 +501,9 @@ pub static TOOLS: &[ToolSpec] = &[
},
];
/// Plugin tools and the tool that loads them.
pub const PLUGIN_HOSTS: &[(&str, &str)] = &[("lombok", "jdtls")];
pub fn find(name: &str) -> Option<&'static ToolSpec> {
TOOLS.iter().find(|t| t.name == name)
}
@@ -507,7 +533,7 @@ mod tests {
known.insert((d.adapter.clone(), d.command.clone()));
}
}
for t in TOOLS {
for t in TOOLS.iter().filter(|t| t.kind != Plugin) {
assert!(
known.contains(&(t.name.to_string(), t.commands[0].to_string())),
"{} / {} is not referenced by languages/*.toml",
@@ -517,6 +543,14 @@ mod tests {
}
}
#[test]
fn plugins_have_a_known_host() {
for t in TOOLS.iter().filter(|t| t.kind == Plugin) {
let host = PLUGIN_HOSTS.iter().find(|(p, _)| *p == t.name).map(|(_, h)| *h);
assert!(host.and_then(find).is_some(), "{} has no known host", t.name);
}
}
#[test]
fn every_release_tool_covers_mac_linux_windows() {
for t in TOOLS {
+50
Переглянути файл
@@ -209,6 +209,39 @@ pub fn openvsx_pick(json: &str, rules: &[AssetRule], platform: Platform) -> IdeR
})
}
const MAVEN_CENTRAL: &str = "https://repo1.maven.org/maven2";
fn maven_dir(group: &str, artifact: &str) -> String {
format!("{MAVEN_CENTRAL}/{}/{artifact}", group.replace('.', "/"))
}
pub fn maven_metadata_url(group: &str, artifact: &str) -> String {
format!("{}/maven-metadata.xml", maven_dir(group, artifact))
}
/// The `<release>` jar of a `maven-metadata.xml`. Central publishes only SHA-1
/// sums for most artifacts, so the download is not checksum-verified.
pub fn maven_pick(group: &str, artifact: &str, metadata: &str) -> IdeResult<Download> {
let tag = |t: &str| {
let start = metadata.find(&format!("<{t}>"))? + t.len() + 2;
let end = start + metadata[start..].find(&format!("</{t}>"))?;
Some(metadata[start..end].trim())
};
let version = tag("release")
.or_else(|| tag("latest"))
.filter(|v| !v.is_empty() && !v.contains(['/', '\\']))
.ok_or_else(|| proto(format!("no release in the Maven metadata of {group}:{artifact}")))?;
let name = format!("{artifact}-{version}.jar");
Ok(Download {
version: version.to_string(),
url: format!("{}/{version}/{name}", maven_dir(group, artifact)),
name,
size: None,
sha256: None,
checksum_url: None,
})
}
/// Finds the hash for `file_name` in a checksum file: either a bare hex digest
/// or `sha256sum` lines (`<hex> <name>`).
pub fn parse_checksum(text: &str, file_name: &str) -> Option<String> {
@@ -322,6 +355,23 @@ mod tests {
assert!(openvsx_pick(&other, rules, LINUX).is_err());
}
#[test]
fn maven_central() {
let xml =
"<metadata><versioning><latest>1.18.49-SNAPSHOT</latest><release>1.18.48</release></versioning></metadata>";
assert_eq!(
maven_metadata_url("org.projectlombok", "lombok"),
"https://repo1.maven.org/maven2/org/projectlombok/lombok/maven-metadata.xml"
);
let d = maven_pick("org.projectlombok", "lombok", xml).unwrap();
assert_eq!(d.version, "1.18.48");
assert_eq!(d.name, "lombok-1.18.48.jar");
assert_eq!(d.url, "https://repo1.maven.org/maven2/org/projectlombok/lombok/1.18.48/lombok-1.18.48.jar");
assert!(select_asset(&[d.name.as_str()], find("lombok").unwrap().asset_rules(), LINUX).is_some());
assert!(maven_pick("g", "a", "<metadata/>").is_err());
assert!(maven_pick("g", "a", "<release>../x</release>").is_err());
}
#[test]
fn checksums() {
let h = "a".repeat(64);
+9
Переглянути файл
@@ -47,6 +47,15 @@ pub async fn java_resolve_launch(
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 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"),
);
info.hints.extend(lombok.problem());
}
Ok(info)
}
+17 -1
Переглянути файл
@@ -248,7 +248,23 @@ impl AppState {
ide_tools::managed_bundle(&data_dir, "java-debug").into_iter().collect();
let min_java = ide_sdk::java::jdtls_min_java(cfg);
let jdks = ide_sdk::java::jdtls_jdks(min_java);
ide_sdk::java::configure_jdtls(cfg, server_root, &data_dir, &jdks, min_java, &bundles);
let lombok = ide_sdk::lombok::lombok_agent(
server_root,
ide_sdk::lombok::agent_in_args(cfg.args.iter().map(String::as_str)),
ide_tools::managed_bundle(&data_dir, "lombok"),
);
if let Some(problem) = lombok.problem() {
tracing::warn!("{problem}");
}
ide_sdk::java::configure_jdtls(
cfg,
server_root,
&data_dir,
&jdks,
min_java,
&bundles,
lombok.jar(),
);
}
}));
}
+26 -1
Переглянути файл
@@ -2301,6 +2301,14 @@ export type JavaBuildStatus = "failed" | "succeeded" |
/** Compiled, but some project has compile errors. */
"withErrors" | "cancelled";
/** One error diagnostic; `line` / `column` are 1-based for display. */
export type JavaCompileError = {
path: IpcPath,
line: number,
column: number,
message: string,
};
/** Everything needed to launch a class of a module: build outcome, classpath and JDK. */
export type JavaLaunchInfo = {
/** `None` when no build was requested. */
@@ -2312,6 +2320,20 @@ export type JavaLaunchInfo = {
javaHome: IpcPath | null,
/** The `java` launcher to use (filled in by the caller from the installed JDKs). */
javaExec: string | null,
/** First compile errors of the workspace when the build had errors. */
errors: JavaCompileError[],
/** All compile errors (`errors` is capped). */
errorCount: number,
/** Likely causes of the errors worth telling the user (e.g. a missing Lombok agent). */
hints: string[],
};
export type JavaSettings = {
/**
* Extra JVM options for the Java language server (jdtls), space-separated
* (VS Code's `java.jdt.ls.vmargs`).
*/
jdtlsVmArgs?: string,
};
/** Editor › Code Style › <Language> › Tabs and Indents (unset fields inherit). */
@@ -2371,6 +2393,7 @@ export type LanguagesSettings = {
rust?: RustSettings,
go?: GoSettings,
typescript?: TypeScriptSettings,
java?: JavaSettings,
};
/** What a freshly opened window should do once its frontend is up. */
@@ -3778,7 +3801,9 @@ export type ToolInfo = {
homepage: string | null,
};
export type ToolKind = "languageServer" | "debugger";
export type ToolKind = "languageServer" | "debugger" |
/** Loaded into another tool's process, e.g. a jdtls agent. */
"plugin";
export type ToolStatus =
/** Installed by the IDE (managed tools dir or a toolchain component it manages). */
+3
Переглянути файл
@@ -18,6 +18,9 @@ vi.mock("@/platform/ipc", async (orig) => {
modulePaths: [],
javaHome: "/jdk21",
javaExec: "/jdk21/bin/java",
errors: [],
errorCount: 0,
hints: [],
});
},
javaDebugServer: (...a: unknown[]) => {
+15 -1
Переглянути файл
@@ -6,7 +6,13 @@
*/
import { executeAction } from "@/platform/actions";
import { call, commands, IpcError, type LaunchConfig, type RunConfig } from "@/platform/ipc";
import { isJvmApplication, joinJavaArgs, parseJavaArgs, resolveJavaCommand } from "@/platform/javaLaunch";
import {
formatCompileErrors,
isJvmApplication,
joinJavaArgs,
parseJavaArgs,
resolveJavaCommand,
} from "@/platform/javaLaunch";
import { notifications } from "@/workbench/contributions";
export const JAVA_ADAPTER = "java-debug";
@@ -52,6 +58,14 @@ export async function javaLaunchConfig(rc: RunConfig, root: string): Promise<Lau
const parsed = parseJavaArgs(rc.args);
if (!parsed.mainClass) throw new Error(`No main class in '${rc.name}'`);
const info = await call(commands.javaResolveLaunch(module, parsed.classpath === "test", true));
if (info.errorCount > 0)
notifications().notify({
group: "run",
level: "warning",
key: "debug.java-compile-errors",
title: `'${rc.name}' has compile errors; debugging the last compiled classes`,
body: formatCompileErrors(info).trimEnd(),
});
const resolved = resolveJavaCommand(rc, info);
const port = await debugServer(module);
const classPaths = parsed.explicitClasspath ? parsed.explicitClasspath.split(/[:;]/) : info.classPaths;
+29
Переглянути файл
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it } from "vitest";
import type { JavaLaunchInfo, RunConfig } from "@/platform/ipc";
import {
formatCompileErrors,
joinJavaArgs,
jdwpListeningPort,
parseJavaArgs,
@@ -170,6 +171,31 @@ describe("application configurations", () => {
});
});
describe("java compile errors", () => {
const base: Omit<JavaLaunchInfo, "errors" | "errorCount" | "hints"> = {
build: "withErrors",
projectRoot: "/p",
classPaths: [],
modulePaths: [],
javaHome: null,
javaExec: null,
};
it("lists the first errors, counts the rest and adds hints", () => {
const report = formatCompileErrors({
...base,
errors: [{ path: "/p/src/A.java", line: 72, column: 5, message: "The method getUsername() is undefined" }],
errorCount: 3,
hints: ["Install Lombok"],
});
expect(report).toBe(
"/p/src/A.java:72:5: error: The method getUsername() is undefined\n" +
"… and 2 more errors (see the Problems tool window)\nInstall Lombok\n",
);
expect(formatCompileErrors({ ...base, errors: [], errorCount: 0, hints: [] })).toBe("");
});
});
describe("java command lines", () => {
const info: JavaLaunchInfo = {
build: "succeeded",
@@ -178,6 +204,9 @@ describe("java command lines", () => {
modulePaths: [],
javaHome: "/jdk21",
javaExec: "/jdk21/bin/java",
errors: [],
errorCount: 0,
hints: [],
};
it("parses VM options, main class and program arguments", () => {
+3 -1
Переглянути файл
@@ -6,7 +6,7 @@
import { create } from "zustand";
import { Channel } from "@tauri-apps/api/core";
import { call, commands, isTauri, type OutputChunk, type RunConfig, type RunId, type TestCaseReport } from "@/platform/ipc";
import { isJvmApplication, parseJavaArgs, resolveJavaCommand } from "@/platform/javaLaunch";
import { formatCompileErrors, isJvmApplication, parseJavaArgs, resolveJavaCommand } from "@/platform/javaLaunch";
import { useProject } from "@/platform/project";
import { workflowSettings } from "@/platform/workflowSettings";
import { maybeEditorService, notifications, toolWindowService } from "@/workbench/contributions";
@@ -449,6 +449,8 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom
const info = await call(commands.javaResolveLaunch(config.cwd ?? "", parsed.classpath === "test", true));
if (info.build === "withErrors") say("Build completed with errors; launching with the last compiled classes.\n", "stderr");
if (info.build === "failed") say("Build failed; launching with the last compiled classes.\n", "stderr");
const compileReport = formatCompileErrors(info);
if (compileReport) say(compileReport, "stderr");
if (!info.classPaths.length) {
say("The classpath is empty: is the module imported by the Java language server?\n", "stderr");
return null;
+3 -1
Переглянути файл
@@ -21,12 +21,14 @@ describe("languages settings", () => {
node: { packageManager: "pip" },
rust: { checkCommand: "clippy", checkOnSave: false },
typescript: { version: "" },
java: { jdtlsVmArgs: " -Xmx2g " },
});
expect(v.python).toEqual({ interpreter: "/usr/bin/python3", activateVirtualenv: true });
expect(v.node.packageManager).toBe("");
expect(v.rust.checkCommand).toBe("clippy");
expect(v.rust.checkOnSave).toBe(false);
expect(v.typescript.version).toBe("bundled");
expect(v.java.jdtlsVmArgs).toBe("-Xmx2g");
expect(validateSettings({}).languages).toEqual(DEFAULT_LANGUAGES);
});
@@ -48,7 +50,7 @@ describe("language pages", () => {
const tree = buildPageTree(LANGUAGE_PAGES.map((p) => ({ ...p, component: p.component! })));
expect(tree.map((n) => n.title)).toEqual(["Project Structure", "Languages & Frameworks"]);
const langs = tree.find((n) => n.title === "Languages & Frameworks")!;
expect(langs.children.map((n) => n.title)).toEqual(["Go", "Node.js", "Python Interpreter", "Rust", "TypeScript"]);
expect(langs.children.map((n) => n.title)).toEqual(["Go", "Java", "Node.js", "Python Interpreter", "Rust", "TypeScript"]);
});
it("filters packages and shortens home paths", () => {
+29 -1
Переглянути файл
@@ -1,5 +1,5 @@
/**
* Settings › Languages & Frameworks › Go / Node.js / Rust / TypeScript: the
* 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).
*/
@@ -209,6 +209,34 @@ export function GoPage() {
);
}
// ---------------------------------------------------------------------------
// Java
export function JavaPage() {
const [vmArgs, setVmArgs] = useProjectSetting<string>("languages.java.jdtlsVmArgs");
const id = useId();
return (
<div className={styles.page}>
<Section title="Language Server">
<Row
label="JVM options"
htmlFor={id}
hint="Passed to the JVM running jdtls (java.jdt.ls.vmargs), e.g. -Xmx2g. A Lombok agent is added automatically when the project uses Lombok."
>
<Input
id={id}
mono
className={styles.pathInput}
value={vmArgs}
placeholder="-Xmx2g"
onChange={(e) => setVmArgs(e.target.value)}
/>
</Row>
</Section>
</div>
);
}
// ---------------------------------------------------------------------------
// TypeScript
+10 -2
Переглянути файл
@@ -1,9 +1,9 @@
/**
* PyCharm's top-level "Python" group (Interpreter), "Project Structure", and
* Languages & Frameworks › Go / Node.js / Rust / TypeScript. Pages live in
* Languages & Frameworks › Go / Java / Node.js / Rust / TypeScript. Pages live in
* `../languages/`; settings are `languages.*` / `projectStructure.*`.
*/
import { GoPage, NodePage, RustPage, TypeScriptPage } from "../languages/LanguagePages";
import { GoPage, JavaPage, NodePage, RustPage, TypeScriptPage } from "../languages/LanguagePages";
import { ProjectStructurePage } from "../languages/ProjectStructurePage";
import { PythonInterpreterPage } from "../languages/PythonInterpreterPage";
import type { PageSpec } from "./spec";
@@ -47,6 +47,14 @@ export const LANGUAGE_PAGES: PageSpec[] = [
keywords: ["goroot", "gopath", "go sdk", "gopls"],
component: GoPage,
},
{
id: "languages.java",
title: "Java",
group: LANGUAGES_GROUP,
order: 395,
keywords: ["jdtls", "jvm options", "vmargs", "lombok", "javaagent", "eclipse jdt"],
component: JavaPage,
},
{
id: "languages.node",
title: "Node.js",
+1
Переглянути файл
@@ -186,6 +186,7 @@ export function LanguageServersPage() {
<div className={styles.toolName}>
<span className={styles.display}>{t.displayName}</span>
{t.kind === "debugger" && <span className={styles.kindTag}>Debugger</span>}
{t.kind === "plugin" && <span className={styles.kindTag}>Plugin</span>}
</div>
{(t.path ?? (t.command !== t.displayName ? t.command : null)) && (
<div className={styles.sub} title={t.path ?? undefined}>
+9
Переглянути файл
@@ -81,6 +81,15 @@ export function resolveJavaCommand(config: RunConfig, info: JavaLaunchInfo, path
return { ...config, command, args };
}
/** Console report of a launch's compile errors (`path:line:col: error: …`) and their likely causes; "" when none. */
export function formatCompileErrors(info: JavaLaunchInfo): string {
const lines = info.errors.map((e) => `${e.path}:${e.line}:${e.column}: error: ${e.message}`);
const more = info.errorCount - info.errors.length;
if (more > 0) lines.push(`… and ${more} more error${more === 1 ? "" : "s"} (see the Problems tool window)`);
lines.push(...info.hints);
return lines.length ? `${lines.join("\n")}\n` : "";
}
/** Joins arguments into one string for adapters that take `args` / `vmArgs` as text (java-debug). */
export function joinJavaArgs(args: readonly string[]): string {
return args.map((a) => (a === "" ? '""' : /[\s"]/.test(a) ? `"${a.replace(/(["\\])/g, "\\$1")}"` : a)).join(" ");
+4
Переглянути файл
@@ -11,6 +11,7 @@ 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 };
}
export interface ProjectStructureValid {
@@ -29,6 +30,7 @@ export const DEFAULT_LANGUAGES: LanguagesValid = {
rust: { toolchain: "", stdlib: "", checkOnSave: true, checkCommand: "check", rustfmtArgs: "" },
go: { goroot: "", gopath: "" },
typescript: { version: "bundled" },
java: { jdtlsVmArgs: "" },
};
export const DEFAULT_PROJECT_STRUCTURE: ProjectStructureValid = {
@@ -73,6 +75,7 @@ export function validateLanguages(raw: unknown): LanguagesValid {
const rust = obj(l.rust);
const go = obj(l.go);
const ts = obj(l.typescript);
const java = obj(l.java);
return {
python: {
interpreter: text(py.interpreter, d.python.interpreter),
@@ -91,6 +94,7 @@ 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) },
};
}