дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(project-view): reveal library classes opened from jars in External Libraries (#2378)
- Map jdt://, jar: and decompiled class URIs to <archive>!/<entry> paths inside the library archive - revealPath handles them in one place, so Select Opened File and autoscroll both work Co-authored-by: LU XIANGRUI <[email protected]>
Цей коміт міститься в:
1 джерело
e839e5dfb7
коміт
394e9919c7
4 files changed
+168
-4
No files matched your search
@@ -102,3 +102,50 @@ describe("project view with a scratches root", () => {
|
||||
expect(scratchLabels()).toEqual(["scratch.py"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("project view with External Libraries", () => {
|
||||
const JAR = "/m2/spring-boot-4.1.1.jar";
|
||||
const CLASS = `${JAR}!/org/springframework/boot/SpringApplication.class`;
|
||||
let unregister = () => {};
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
setProjectFsForTests(new MemoryFs({ "/p/pom.xml": "x", [CLASS]: "x" }, "/p"), "/p");
|
||||
initProjectTree("/p", "proj");
|
||||
unregister = registerProjectViewRoot({
|
||||
id: "externalLibraries",
|
||||
label: "External Libraries",
|
||||
icon: "library",
|
||||
readOnly: true,
|
||||
children: [
|
||||
{
|
||||
label: "< Maven: spring-boot >",
|
||||
icon: "maven-project",
|
||||
children: [{ label: "spring-boot-4.1.1.jar", icon: "file-archive", path: `${JAR}!` }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
unregister();
|
||||
setProjectFsForTests(null);
|
||||
initProjectTree(null);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["the archive entry", CLASS],
|
||||
[
|
||||
"a jdtls class",
|
||||
"jdt://contents/spring-boot-4.1.1.jar/org.springframework.boot/SpringApplication.class?=demo/%5C/m2%5C/spring-boot-4.1.1.jar%3Corg.springframework.boot%28SpringApplication.class",
|
||||
],
|
||||
["a Kotlin LSP class", `jar:///m2/spring-boot-4.1.1.jar!/org/springframework/boot/SpringApplication.class`],
|
||||
])("reveals %s inside its jar", (_, path) => {
|
||||
expect(revealPath(path)).toBe(true);
|
||||
const { expanded, revealRequest } = useProjectTree.getState();
|
||||
expect(revealRequest?.path).toBe(CLASS);
|
||||
expect([...expanded].some((k) => k.endsWith(`${JAR}!/org/springframework/boot`))).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves classes of unlisted jars alone", () => {
|
||||
expect(revealPath("jar:///elsewhere/x.jar!/p/C.class")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { call } from "@/platform/ipc";
|
||||
import { commands, type FileEntry, type OutlineItem } from "@/bindings";
|
||||
import { withDocument } from "@/editor/lsp/files";
|
||||
import { IpcError } from "@/platform/ipc";
|
||||
import { libraryEntryPaths } from "@/platform/virtualDocs";
|
||||
import {
|
||||
DEFAULT_TREE_OPTIONS,
|
||||
GROUP_PREFIX,
|
||||
@@ -570,11 +571,15 @@ export function dropOutlines(paths: readonly string[]) {
|
||||
let revealSeq = 0;
|
||||
|
||||
/** Expands the ancestors of `path` and selects it once visible. */
|
||||
export function revealPath(path: string, focus = false) {
|
||||
export function revealPath(requested: string, focus = false) {
|
||||
const { expanded } = get();
|
||||
const project = get().root;
|
||||
const root = project ? (memberRootOf(path) ?? (isSameOrInside(path, project) ? project : extraRootOf(path))) : null;
|
||||
if (!root) return false;
|
||||
if (!project) return false;
|
||||
const rootOf = (p: string) => memberRootOf(p) ?? (isSameOrInside(p, project) ? project : extraRootOf(p));
|
||||
// Library documents (`jdt://`, `jar:` URIs) are shown as entries of their archive under External Libraries.
|
||||
const path = [requested, ...libraryEntryPaths(requested)].find((p) => rootOf(p) !== null);
|
||||
if (!path) return false;
|
||||
const root = rootOf(path)!;
|
||||
// Library folders sit below group nodes (External Libraries › SDK), keyed in their own namespace.
|
||||
const model = currentModel();
|
||||
const rootKey = root === project ? root : (model.keyOfPath.get(root) ?? root);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isVirtualUri, virtualContainer, virtualName, virtualParent } from "./virtualDocs";
|
||||
import { isVirtualUri, libraryEntryPaths, virtualContainer, virtualName, virtualParent } from "./virtualDocs";
|
||||
import { basename, dirname, relativeDir, relativePath } from "./project";
|
||||
|
||||
const JDT =
|
||||
@@ -38,3 +38,57 @@ describe("virtual document URIs", () => {
|
||||
expect(relativePath("/p", "/p/src/A.java")).toBe("src/A.java");
|
||||
});
|
||||
});
|
||||
|
||||
describe("libraryEntryPaths", () => {
|
||||
const M2 = "/Users/me/.m2/repository/org/springframework/boot/spring-boot/4.1.1";
|
||||
it.each([
|
||||
// The reported case: Go to Declaration into a Maven jar through jdtls.
|
||||
[
|
||||
"jdtls class in a Maven jar",
|
||||
`jdt://contents/spring-boot-4.1.1.jar/org.springframework.boot/SpringApplication.class?=demo/${encodeURIComponent(
|
||||
`${M2}/spring-boot-4.1.1.jar`.replace(/\//g, "\\/"),
|
||||
)}%3Corg.springframework.boot%28SpringApplication.class`,
|
||||
[`${M2}/spring-boot-4.1.1.jar!/org/springframework/boot/SpringApplication.class`],
|
||||
],
|
||||
[
|
||||
"jdtls class in a Gradle jar, default package",
|
||||
"jdt://contents/a.jar//C.class?=p/%5C/g%5C/files-2.1%5C/a.jar%3C%28C.class",
|
||||
["/g/files-2.1/a.jar!/C.class"],
|
||||
],
|
||||
[
|
||||
"jdtls JDK class (module in lib/src.zip)",
|
||||
"jdt://contents/java.base/java.lang/String.class?=p/%5C/jdk%5C/lib%5C/jrt-fs.jar%60java.base=/javadoc_location=/x=/%3Cjava.lang%28String.class",
|
||||
["/jdk/lib/src.zip!/java.base/java/lang/String.java"],
|
||||
],
|
||||
[
|
||||
"jdtls JDK 8 class (rt.jar, src.zip in the home)",
|
||||
"jdt://contents/rt.jar/java.lang/String.class?=p/%5C/jdk8%5C/jre%5C/lib%5C/rt.jar%3Cjava.lang%28String.class",
|
||||
["/jdk8/jre/lib/rt.jar!/java/lang/String.class", "/jdk8/src.zip!/java/lang/String.java"],
|
||||
],
|
||||
[
|
||||
"jdtls class on Windows",
|
||||
"jdt://contents/a.jar/p/C.class?=x/%5C/C:%5C/m2%5C/a.jar%3Cp%28C.class",
|
||||
["C:/m2/a.jar!/p/C.class"],
|
||||
],
|
||||
[
|
||||
"Kotlin LSP class (jar:///)",
|
||||
"jar:///home/me/.gradle/a-1.0.jar!/org/x/Y.class",
|
||||
["/home/me/.gradle/a-1.0.jar!/org/x/Y.class"],
|
||||
],
|
||||
[
|
||||
"sources jar entry (jar:file:///), binary jar listed instead",
|
||||
"jar:file:///m2/my%20lib/a-1.0-sources.jar!/org/x/Y.kt",
|
||||
["/m2/my lib/a-1.0-sources.jar!/org/x/Y.kt", "/m2/my lib/a-1.0.jar!/org/x/Y.class"],
|
||||
],
|
||||
["decompiled class on disk", "file:///lib/classes/p/C.class", ["/lib/classes/p/C.class"]],
|
||||
// Plain paths (src.zip entries, Go modules, node_modules, crates, site-packages) are revealed as they are.
|
||||
["archive entry path", "/jdk/lib/src.zip!/java.base/java/lang/String.java", []],
|
||||
["Go module file", "/Users/me/go/pkg/mod/github.com/x/[email protected]/y.go", []],
|
||||
["node_modules file", "/p/node_modules/react/index.js", []],
|
||||
["Cargo crate file", "/Users/me/.cargo/registry/src/index.crates.io-1/serde-1.0.200/src/lib.rs", []],
|
||||
["site-packages file", "/p/.venv/lib/python3.12/site-packages/requests/api.py", []],
|
||||
["other virtual document", "untitled:Untitled-1", []],
|
||||
])("%s", (_, uri, expected) => {
|
||||
expect(libraryEntryPaths(uri)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -79,3 +79,61 @@ export function virtualDocument(uri: string): Promise<VirtualDocument> {
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const decode = (s: string) => {
|
||||
try {
|
||||
return decodeURIComponent(s);
|
||||
} catch {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
/** `/C:/x` (a drive path inside a URI) → `C:/x`. */
|
||||
const uriFsPath = (p: string) => (/^\/[A-Za-z]:\//.test(p) ? p.slice(1) : p);
|
||||
|
||||
const parentDir = (p: string, levels = 1) => p.split("/").slice(0, -levels).join("/");
|
||||
|
||||
/** `(jar, entry without extension)` → the class in its jar (the binary jar for a `-sources.jar`). */
|
||||
const jarCandidates = (jar: string, stem: string) => [
|
||||
`${jar.replace(/-sources\.jar$/, ".jar")}!/${stem}.class`,
|
||||
// JDK 8: `<home>/jre/lib/rt.jar` with its sources in `<home>/src.zip`.
|
||||
...(jar.endsWith("/rt.jar") ? [`${parentDir(jar, 3)}/src.zip!/${stem}.java`] : []),
|
||||
];
|
||||
|
||||
/**
|
||||
* Where a library document URI lies in the Project view's External Libraries,
|
||||
* as `<archive>!/<entry>` paths, most specific first: jdtls' `jdt://contents/…`
|
||||
* classes (dependency jars, the JDK's modules in `lib/src.zip`), `jar:` entries
|
||||
* (classes and sources jars) and decompiled `file:` classes. Empty for anything
|
||||
* else; a missing entry still leads to its package folder.
|
||||
*/
|
||||
export function libraryEntryPaths(uri: string): string[] {
|
||||
const base = uri.split(/[?#]/)[0]!;
|
||||
const scheme = base.slice(0, Math.max(base.indexOf(":"), 0)).toLowerCase();
|
||||
if (scheme === "file") return isClassFile(base) ? [uriFsPath(decode(base.replace(/^file:(\/\/)?/, "")))] : [];
|
||||
if (scheme === "jar") {
|
||||
const m = /^jar:(?:file:)?(?:\/\/)?(.*?)!\/(.*)$/.exec(base);
|
||||
if (!m) return [];
|
||||
const jar = uriFsPath(decode(m[1]!));
|
||||
const entry = decode(m[2]!);
|
||||
const exact = `${jar}!/${entry}`;
|
||||
const stem = entry.replace(/\.[^./]*$/, "");
|
||||
return [exact, ...jarCandidates(jar, stem).filter((p) => p !== exact)];
|
||||
}
|
||||
if (scheme !== "jdt") return [];
|
||||
// jdt://contents/<jar or module>/<package>/<Class.class>?=<project>/<escaped jar path><package(Class.class
|
||||
const segs = base
|
||||
.replace(/^jdt:\/\/contents\//, "")
|
||||
.split("/")
|
||||
.map(decode);
|
||||
if (segs.length !== 3) return [];
|
||||
const [module, pkg, file] = segs as [string, string, string];
|
||||
const stem = [...(pkg ? pkg.split(".") : []), file.replace(/\.(class|java)$/, "")].join("/");
|
||||
const query = decode(uri.split("?")[1] ?? "");
|
||||
const rest = query.slice(query.indexOf("/") + 1);
|
||||
const jar = query.includes("/") ? uriFsPath(rest.replace(/\\/g, "").split(/[<(`=]/)[0]!) : "";
|
||||
if (!jar) return [];
|
||||
// JDK 9+: `<home>/lib/jrt-fs.jar`, sources per module in `<home>/lib/src.zip`.
|
||||
if (jar.endsWith("/jrt-fs.jar")) return [`${parentDir(jar)}/src.zip!/${module}/${stem}.java`];
|
||||
return jarCandidates(jar, stem);
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача