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

This commit is contained in:
Jaw authored and GitHub committed 2026-11-07 08:51:43 +08:00
1 parent 61b12269a1
commit fc5a7d8c91
9 files changed
+149

No files matched your search

+1
View File
@@ -78,5 +78,6 @@ the [official plugins](plugins/official). The plugin API is still a preview.
Database tool window notes, including Oracle Instant Client setup, are in
[`docs/databases.md`](docs/databases.md).
Java language server choices and their capabilities are in [`docs/java.md`](docs/java.md).
<sub>Not affiliated with JetBrains. IntelliJ IDEA is a trademark of JetBrains s.r.o.</sub>
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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());
+21
View File
@@ -0,0 +1,21 @@
# Java language servers
Settings › Languages & Frameworks › Language Servers lists Eclipse JDT Language
Server (`jdtls`) and [kmp-lsp](https://github.com/Hessesian/kmp-lsp). Select either
tool and use **Language server for Java** to choose one. The choice can be global
or limited to the current project. **Automatic** prefers jdtls and falls back to
kmp-lsp when jdtls is not installed; **None** disables both.
Install kmp-lsp from that page, or point its executable path at an existing
installation. The IDE downloads a platform distribution containing both kmp-lsp
and its native JAR indexer. Reinstall and uninstall manage both together.
kmp-lsp offers syntax-based Java code insight without running a JVM. It does not
provide JDT's compiler analysis, Lombok integration, or JDT-specific refactorings.
Java Application builds, classpath discovery and the Microsoft Java debugger still
require jdtls; use jdtls for those flows. Maven and Gradle build tasks can run
independently of the selected code-insight server.
This built-in choice applies to Java. Kotlin and Swift keep their existing server
choices. To use another server, add it with **New Language Server**, assign its
languages, and disable the servers it replaces.
+7
View File
@@ -58,3 +58,10 @@ generatesMetadataFilesAtProjectRoot = false
exclusions = ["**/node_modules/**", "**/.metadata/**", "**/archetype-resources/**", "**/META-INF/maven/**"]
maven = { enabled = true }
gradle = { enabled = true, wrapper = { enabled = true } }
[[language-servers]]
name = "kmp-lsp"
fallback = true
command = "kmp-lsp"
root-markers = ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "build.xml", ".project"]
install-hint = "Install kmp-lsp in Settings › Languages & Frameworks › Language Servers (https://github.com/Hessesian/kmp-lsp)"