Files
openidea/src/platform/virtualDocs.ts
T
l0ng-ai 1300317730 fix(lsp): open library classes behind jar: URIs
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
2026-11-03 22:03:51 +08:00

82 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Virtual documents: read-only contents a language server provides for a
* non-`file:` URI (library classes from jdtls' `jdt://contents/…`, decompiled
* code). Locations into them carry the URI as their `path`; the editor opens
* them as `virtual` editor inputs instead of files.
*/
import { isClassFile } from "./classFiles";
import { call, commands, type VirtualDocument } from "./ipc";
/** Server document URIs, including decompiled disk classes; drive letters remain paths. */
export function isVirtualUri(s: string | null | undefined): boolean {
const m = /^([a-zA-Z][a-zA-Z0-9+.-]+):/.exec(s ?? "");
return !!m && (m[1]!.toLowerCase() !== "file" || isClassFile(s!));
}
/** Last path segment of a URI (without query / fragment), e.g. `SpringApplication.class`. */
export function virtualName(uri: string): string {
const rest = uri.replace(/^[^:]+:(\/\/)?/, "").split(/[?#]/)[0]!;
const seg = rest.split("/").filter(Boolean).pop() ?? uri;
try {
return decodeURIComponent(seg);
} catch {
return seg;
}
}
/** The URI's path without its last segment, e.g. `jdt://contents/x.jar/org.example`. */
export function virtualParent(uri: string): string {
const base = uri.split(/[?#]/)[0]!;
const i = base.lastIndexOf("/");
return i > base.indexOf("://") + 2 ? base.slice(0, i) : base;
}
/**
* Short human location of a virtual document, e.g. `spring-boot-4.1.1.jar › org.springframework.boot`
* for `jdt://contents/spring-boot-4.1.1.jar/org.springframework.boot/SpringApplication.class?=…`.
*/
export function virtualContainer(uri: string): string {
const scheme = uri.slice(0, uri.indexOf(":"));
// `jar:///…/spring-boot-3.5.16.jar!/org/springframework/boot/X.class` → `spring-boot-3.5.16.jar › org.springframework.boot`
const jar = scheme === "jar" ? /^jar:(?:file:)?(?:\/\/)?(.*?)!\/(.*)$/.exec(uri.split(/[?#]/)[0]!) : null;
if (jar) {
const decode = (s: string) => {
try {
return decodeURIComponent(s);
} catch {
return s;
}
};
const archive = decode(jar[1]!.split("/").pop() ?? jar[1]!);
const pkg = jar[2]!.split("/").slice(0, -1).map(decode).join(".");
return pkg ? `${archive} › ${pkg}` : archive;
}
const rest = uri.replace(/^[^:]+:(\/\/)?/, "").split(/[?#]/)[0]!;
const segs = rest
.split("/")
.filter(Boolean)
.map((s) => {
try {
return decodeURIComponent(s);
} catch {
return s;
}
});
segs.pop();
if (scheme === "jdt" && segs[0] === "contents") segs.shift();
return segs.join(" › ");
}
const cache = new Map<string, Promise<VirtualDocument>>();
/** Contents of a virtual document (cached; failures are retried on the next call). */
export function virtualDocument(uri: string): Promise<VirtualDocument> {
let p = cache.get(uri);
if (!p) {
p = call(commands.lspVirtualDocument(uri));
p.catch(() => cache.delete(uri));
cache.set(uri, p);
}
return p;
}