feat(java): add kmp-lsp as an alternative language server (#1891)

Цей коміт міститься в:
Jaw authored and GitHub committed 2026-11-07 08:51:43 +08:00
1 джерело 61b12269a1
коміт fc5a7d8c91
9 files changed
+149

No files matched your search

+1
Переглянути файл
@@ -639,6 +639,7 @@ mod tests {
for (lang, servers) in [
("python", vec!["basedpyright", "pyright", "ty", "pyrefly"]),
("typescript", vec!["vtsls", "typescript-language-server"]),
("java", vec!["jdtls", "kmp-lsp"]),
] {
let cfg = reg.get(&LanguageId::new(lang)).unwrap();
let got: Vec<(&str, bool)> = cfg
+12
Переглянути файл
@@ -1673,6 +1673,18 @@ async fn library_classes_in_jars_open_as_their_source_or_decompiled() {
assert_eq!(asked[0], json!({ "command": "decompile", "arguments": [thing] }));
}
#[tokio::test(flavor = "multi_thread")]
async fn alternative_java_server_uses_lsp_without_jdt_commands() {
let h = harness_for("java", "java", &[("kmp-lsp", incremental_caps())], &[]);
let (doc, path) = h.open("Sample.java", "class Sample {}");
h.wait_status("kmp-lsp", ServerStatus::Ready).await;
assert!(h.mgr.hover(doc, Pos::new(0, 7), Default::default()).await.is_ok());
let error = h.mgr.java_debug_server(&path).await.unwrap_err();
assert_eq!(error.kind, ide_core::error::ErrorKind::Unavailable);
assert!(error.message.contains("jdtls"));
assert!(h.launcher.state("kmp-lsp").requested("workspace/executeCommand").is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn fallback_servers_only_run_when_the_preferred_one_is_missing() {
// Preferred installed: the alternative is never started.
+49
Переглянути файл
@@ -63,6 +63,55 @@ fn lang(id: &str, ext: &str, server: LanguageServerConfig) -> LanguageConfig {
}
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires kmp-lsp on PATH (or OPENIDEA_TEST_KMP_LSP)"]
async fn kmp_lsp_java_end_to_end() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
let path = root.join("Sample.java");
let source = "class Sample {\n static String greet() { return \"Hello\"; }\n void run() { greet(); }\n}\n";
std::fs::write(&path, source).unwrap();
let mut java = LanguageRegistry::load(None).unwrap().get(&LanguageId::new("java")).unwrap();
java.language_servers.retain(|s| s.name == "kmp-lsp");
assert_eq!(java.language_servers.len(), 1);
if let Ok(bin) = std::env::var("OPENIDEA_TEST_KMP_LSP") {
java.language_servers[0].command = bin;
}
let (mgr, events) = manager(&root, LanguageRegistry::from_configs(vec![java]));
let doc = DocId::next();
mgr.did_open(doc, &path, &LanguageId::new("java"), 1, source);
retry("kmp-lsp ready", Duration::from_secs(30), || {
let status = last_status(&events, "kmp-lsp");
if let Some((ServerStatus::Crashed | ServerStatus::Missing | ServerStatus::Misconfigured, msg)) = &status {
panic!("kmp-lsp failed: {msg:?}");
}
async move { matches!(status, Some((ServerStatus::Ready | ServerStatus::Busy, _))).then_some(()) }
})
.await;
let hover = retry("Java hover", Duration::from_secs(30), || async {
mgr.hover(doc, Pos::new(2, 18), Default::default()).await.ok().flatten()
})
.await;
assert!(hover.contents.value.contains("greet"), "{}", hover.contents.value);
let defs = retry("Java definition", Duration::from_secs(30), || async {
mgr.goto(doc, Pos::new(2, 18), GotoKind::Definition).await.ok().filter(|d| !d.is_empty())
})
.await;
assert_eq!(defs[0].path, path);
assert_eq!(defs[0].range.start.line, 1);
retry("Java completion", Duration::from_secs(30), || async {
let list = mgr.completion(doc, Pos::new(2, 20), None, Default::default()).await.ok()?;
list.items.iter().any(|i| i.label.starts_with("greet")).then_some(())
})
.await;
let tokens = mgr.semantic_tokens(doc).await.unwrap().unwrap();
assert!(!tokens.data.is_empty() && tokens.data.len() % 5 == 0);
let error = mgr.java_debug_server(&path).await.unwrap_err();
assert_eq!(error.kind, ide_core::error::ErrorKind::Unavailable);
assert!(error.message.contains("jdtls"));
tokio::time::timeout(Duration::from_secs(10), mgr.shutdown_all()).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires jdtls on PATH and a compatible JDK"]
async fn intellij_java_formatter_preserves_manual_breaks_and_wraps_long_lines() {
+30
Переглянути файл
@@ -151,6 +151,36 @@ mod tests {
assert_eq!(select_asset(&names, rules, WIN), Some(names[4]));
}
#[test]
fn kmp_lsp_assets_include_the_native_jar_indexer() {
let names = [
"kmp-lsp-darwin-aarch64.gz",
"kmp-lsp-darwin-aarch64.tar.gz",
"kmp-lsp-darwin-x86_64.tar.gz",
"kmp-lsp-linux-aarch64.tar.gz",
"kmp-lsp-linux-x86_64.tar.gz",
"kmp-lsp-windows-x86_64.zip",
"kmp-lsp-windows-aarch64.zip",
"kmp-lsp-v0.27.0.vsix",
"kmp-lsp-win32-x64-v0.27.0.vsix",
"kmp-lsp-win32-arm64-v0.27.0.vsix",
];
let rules = crate::registry::find("kmp-lsp").unwrap().asset_rules();
for (os, arch, expected) in [
(Os::Mac, Arch::Arm64, names[1]),
(Os::Mac, Arch::X64, names[2]),
(Os::Linux, Arch::Arm64, names[3]),
(Os::Linux, Arch::X64, names[4]),
(Os::Windows, Arch::X64, names[8]),
(Os::Windows, Arch::Arm64, names[9]),
] {
assert_eq!(select_asset(&names, rules, Platform { os, arch }), Some(expected));
}
// The plain gzip, Windows zip and universal VSIX omit the sidecar or server.
assert_eq!(select_asset(&[names[0]], rules, MAC_ARM), None);
assert_eq!(select_asset(&names[5..8], rules, WIN), None);
}
#[test]
fn rosetta_fallback_and_missing() {
let names = ["zls-x86_64-macos.tar.xz", "zls-x86_64-linux.tar.xz", "zls-x86_64-windows.zip"];
+24
Переглянути файл
@@ -618,6 +618,30 @@ pub static TOOLS: &[ToolSpec] = &[
homepage: "https://github.com/eclipse-jdtls/eclipse.jdt.ls",
needs: &[],
},
ToolSpec {
name: "kmp-lsp",
display: "kmp-lsp",
kind: LanguageServer,
commands: &["kmp-lsp"],
method: Method::Release {
source: ReleaseSource::GitHub { repo: "Hessesian/kmp-lsp" },
// These distributions include the native JAR indexer next to the server.
// The single-binary .gz and Windows .zip assets do not.
assets: &[
rule(Mac, Arm64, "kmp-lsp-darwin-aarch64.tar.gz"),
rule(Mac, X64, "kmp-lsp-darwin-x86_64.tar.gz"),
rule(Linux, Arm64, "kmp-lsp-linux-aarch64.tar.gz"),
rule(Linux, X64, "kmp-lsp-linux-x86_64.tar.gz"),
rule(Windows, Arm64, "kmp-lsp-win32-arm64-*.vsix"),
rule(Windows, X64, "kmp-lsp-win32-x64-*.vsix"),
],
locate: &["kmp-lsp"],
},
fallback: None,
version: ver("kmp-lsp"),
homepage: "https://github.com/Hessesian/kmp-lsp",
needs: &[],
},
ToolSpec {
name: "java-debug",
display: "Java Debugger (Microsoft java-debug)",
+4
Переглянути файл
@@ -170,6 +170,10 @@ fn interchangeable_servers_list_their_alternatives() {
assert_eq!(find(&list, name).alternatives, python);
}
assert_eq!(find(&list, "vtsls").alternatives, ["vtsls", "typescript-language-server"]);
for name in ["jdtls", "kmp-lsp"] {
assert_eq!(find(&list, name).alternatives, ["jdtls", "kmp-lsp"]);
assert_eq!(find(&list, name).languages, ["Java"]);
}
// A regular server after a fallback starts no group with it.
assert!(find(&list, "angular-language-server").alternatives.is_empty());
assert!(find(&list, "ruff").alternatives.is_empty());