mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
The official Kotlin LSP answers navigation into a dependency with a `jar:///…/lib.jar!/pkg/Name.class` URI but does not serve its contents through `workspace/textDocumentContent`, so opening the target failed with "no language server provides the contents of jar:…", locally and remotely. `jar:` URIs are now resolved by the backend, where the jars are: - An entry that is not a class (a sources jar entry, a resource) is read from its archive. - A class is shown as its source from the artifact's `-sources.jar` in the Maven repository or the Gradle cache (`Name.java`/`Name.kt`, an inner class's outer file, a `FileKt` facade's `File.kt`, a `@file:JvmName` file); Go to Declaration then lands on the declaration of the name it was invoked on. - Otherwise the server's `decompile` command is used when it offers one, and the error says that neither a source nor a decompiler is available. - Library tabs name the archive and package (`spring-boot-3.5.16.jar › org.springframework.boot`). Fixes #982
175 lines
7.2 KiB
Rust
175 lines
7.2 KiB
Rust
//! Virtual documents: read-only contents a language server provides for a
|
||
//! URI (library classes from `jdt://`, disk classes from `file://`, or
|
||
//! `workspace/textDocumentContent`).
|
||
|
||
use percent_encoding::percent_decode_str;
|
||
|
||
/// Recognize decompiler headers, without treating disassembler stubs as Java source.
|
||
pub fn is_decompiled(text: &str) -> bool {
|
||
let head: String = text.chars().take(600).collect();
|
||
head.lines()
|
||
.take(12)
|
||
.take_while(|line| {
|
||
let line = line.trim();
|
||
line.is_empty() || line.starts_with("//") || line.starts_with("/*") || line.starts_with('*')
|
||
})
|
||
.any(|line| {
|
||
let line = line.trim().trim_start_matches('/').trim_start_matches('*').trim().to_ascii_lowercase();
|
||
[
|
||
"source code is unavailable, and was generated by the fernflower decompiler.",
|
||
"source code is decompiled from a .class file using fernflower decompiler",
|
||
"decompiled with cfr",
|
||
"decompiled with procyon",
|
||
]
|
||
.iter()
|
||
.any(|marker| line.starts_with(marker))
|
||
})
|
||
}
|
||
|
||
/// `scheme` of a URI (`jdt` for `jdt://…`).
|
||
pub fn scheme(uri: &str) -> &str {
|
||
uri.split_once(':').map(|(s, _)| s).unwrap_or("")
|
||
}
|
||
|
||
/// The URI's path segments (without query / fragment), percent-decoded.
|
||
fn segments(uri: &str) -> Vec<String> {
|
||
let rest = uri.split_once(':').map(|(_, r)| r).unwrap_or(uri);
|
||
let rest = rest.split(['?', '#']).next().unwrap_or(rest);
|
||
let rest = rest.strip_prefix("//").unwrap_or(rest);
|
||
rest.split('/').filter(|s| !s.is_empty()).map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned()).collect()
|
||
}
|
||
|
||
/// Editor tab title: the last path segment; a class file whose real source
|
||
/// was found is shown as `.java` (IDEA: `String.java` vs `String.class`).
|
||
pub fn title(uri: &str, decompiled: bool) -> String {
|
||
let mut name = segments(uri).pop().unwrap_or_else(|| uri.to_owned());
|
||
// Recent jdtls versions use a .java path even for decompiled class handles.
|
||
if decompiled
|
||
&& scheme(uri) == "jdt"
|
||
&& name.ends_with(".java")
|
||
&& uri.split_once('?').is_some_and(|(_, q)| percent_decode_str(q).decode_utf8_lossy().contains(".class"))
|
||
{
|
||
name.truncate(name.len() - 5);
|
||
name.push_str(".class");
|
||
}
|
||
match name.strip_suffix(".class") {
|
||
Some(stem) if !decompiled && scheme(uri) == "jdt" => format!("{stem}.java"),
|
||
_ => name,
|
||
}
|
||
}
|
||
|
||
/// Where the document comes from, e.g. the jar path of a `jdt://` class.
|
||
pub fn description(uri: &str) -> Option<String> {
|
||
if scheme(uri) == "jar" {
|
||
return crate::library_source::parse(uri).map(|e| e.jar.to_string_lossy().into_owned());
|
||
}
|
||
if scheme(uri) != "jdt" {
|
||
return None;
|
||
}
|
||
let segs = segments(uri);
|
||
// jdt://contents/<jar or module>/<package>/<Class.class>?=<project>/<escaped jar path><package(Class.class
|
||
let container = segs.get(1).cloned();
|
||
let query = uri.split_once('?').map(|(_, q)| q).unwrap_or("");
|
||
let query = percent_decode_str(query).decode_utf8_lossy();
|
||
let jar = query
|
||
.split_once('/')
|
||
.map(|(_, rest)| rest.replace('\\', ""))
|
||
.and_then(|rest| rest.split(['<', '(', '`', '=']).next().map(str::to_owned))
|
||
.filter(|p| p.ends_with(".jar") || p.ends_with(".jmod"));
|
||
let package = segs.get(2).filter(|_| segs.len() >= 4).cloned();
|
||
match (jar, container) {
|
||
(Some(jar), _) if jar.ends_with("jrt-fs.jar") => segs.get(1).cloned(),
|
||
(Some(jar), _) => Some(jar),
|
||
(None, Some(c)) => Some(match package {
|
||
Some(p) => format!("{c} › {p}"),
|
||
None => c,
|
||
}),
|
||
(None, None) => None,
|
||
}
|
||
}
|
||
|
||
/// Check both the package and actual archive, so duplicate type names never pick another jar.
|
||
pub(crate) fn matches_class(uri: &str, jar: &std::path::Path, package: &str, name: &str) -> bool {
|
||
let segs = segments(uri);
|
||
scheme(uri) == "jdt"
|
||
&& (package.is_empty() && segs.len() == 3 || segs.get(2).is_some_and(|s| s == package))
|
||
&& segs.last().is_some_and(|s| s == &format!("{name}.class") || s == &format!("{name}.java"))
|
||
&& description(uri).is_some_and(|p| {
|
||
let path = std::path::Path::new(&p);
|
||
path == jar
|
||
|| dunce::canonicalize(path).ok().zip(dunce::canonicalize(jar).ok()).is_some_and(|(a, b)| a == b)
|
||
})
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
const JDT: &str = "jdt://contents/spring-boot-4.1.1.jar/org.springframework.boot/SpringApplication.class?=spring-demo/%5C/Users%5C/me%5C/.m2%5C/repository%5C/spring-boot-4.1.1.jar%3Corg.springframework.boot%28SpringApplication.class";
|
||
|
||
#[test]
|
||
fn jdt_titles() {
|
||
assert_eq!(title(JDT, true), "SpringApplication.class");
|
||
assert_eq!(title(JDT, false), "SpringApplication.java");
|
||
assert_eq!(title("untitled:foo/bar.txt", false), "bar.txt");
|
||
assert_eq!(scheme(JDT), "jdt");
|
||
}
|
||
|
||
#[test]
|
||
fn jdt_description() {
|
||
assert!(matches_class(
|
||
JDT,
|
||
std::path::Path::new("/Users/me/.m2/repository/spring-boot-4.1.1.jar"),
|
||
"org.springframework.boot",
|
||
"SpringApplication"
|
||
));
|
||
assert!(!matches_class(
|
||
JDT,
|
||
std::path::Path::new("/elsewhere/spring-boot-4.1.1.jar"),
|
||
"org.springframework.boot",
|
||
"SpringApplication"
|
||
));
|
||
assert!(!matches_class(
|
||
JDT,
|
||
std::path::Path::new("/Users/me/.m2/repository/spring-boot-4.1.1.jar"),
|
||
"other.package",
|
||
"SpringApplication"
|
||
));
|
||
assert!(matches_class(
|
||
"jdt://contents/a.jar//C.java?=p/%5C/m2%5C/a.jar%3C%28C.class",
|
||
std::path::Path::new("/m2/a.jar"),
|
||
"",
|
||
"C"
|
||
));
|
||
assert_eq!(description(JDT).as_deref(), Some("/Users/me/.m2/repository/spring-boot-4.1.1.jar"));
|
||
assert_eq!(
|
||
description(
|
||
"jdt://contents/java.base/java.lang/String.class?=p/%5C/jdk%5C/lib%5C/jrt-fs.jar%60java.base=x"
|
||
)
|
||
.as_deref(),
|
||
Some("java.base")
|
||
);
|
||
assert_eq!(description("jdt://contents/a.jar/p/C.class").as_deref(), Some("a.jar › p"));
|
||
assert_eq!(description("foo://x/y"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn detects_generated_code() {
|
||
assert!(is_decompiled(
|
||
"// Source code is decompiled from a .class file using FernFlower decompiler (from Intellij IDEA).\npackage a;"
|
||
));
|
||
assert!(!is_decompiled("package a;\n// generated by the fernflower decompiler"));
|
||
assert!(!is_decompiled("// Documentation about code generated by the Fernflower decompiler.\npackage a;"));
|
||
let recent = "jdt://contents/a.jar/p/C.java?=p/a.jar%3Cp%28C.class";
|
||
assert_eq!(title(recent, true), "C.class");
|
||
assert_eq!(title(recent, false), "C.java");
|
||
assert!(is_decompiled(
|
||
"// Source code is unavailable, and was generated by the Fernflower decompiler.\npackage a;"
|
||
));
|
||
assert!(!is_decompiled(
|
||
"// Failed to get sources. Instead, stub sources have been generated by the disassembler."
|
||
));
|
||
assert!(!is_decompiled("/*\n * Copyright 2012-2024 the original author or authors.\n */\npackage a;"));
|
||
}
|
||
}
|