Файли
openidea/src/platform/virtualDocs.ts
T
l0ng-ai d54ef98a59 fix(lsp): open library classes behind jar: URIs (#1048)
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:04:05 +08:00

82 рядки
2.9 KiB
TypeScript
Неформатований Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
/**
* 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;
}