fix(java): list dependencies' implementations after the project's in Go to Implementation (#1644)

Go to Implementation on a type or method declared in a dependency listed only the project's implementations once the project had one. openidea.java.implementations (java-usages 1.5.0) now answers {locations, complete}, complete when the type is declared in the sources, and also searches the Kotlin bridge's classes. When not complete, the chooser opens with the project's implementations (searching…) through the new lsp_goto_implementation channel and fills in the dependencies' when the standard request ends; closing it cancels the search.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-06 08:16:13 +08:00
1 джерело 086c89df5b
коміт 401b4a9441
16 files changed
+441 -61

No files matched your search

+6
Переглянути файл
@@ -409,6 +409,12 @@ export const commands = {
* `cancel_request(request)`.
*/
lspImplementations: (request: RequestId, doc: DocId, positions: Pos[]) => typedError<Location[][], IdeError>(__TAURI_INVOKE("lsp_implementations", { request, doc, positions })),
/**
* Go to Implementation of the symbol at `pos`. Implementations found before a
* slower search for the rest arrive on `on_partial`; the result has them all.
* Cancel with `cancel_request(request)`.
*/
lspGotoImplementation: (request: RequestId, doc: DocId, pos: Pos, onPartial: Channel<Location[]>) => typedError<Location[], IdeError>(__TAURI_INVOKE("lsp_goto_implementation", { request, doc, pos, onPartial })),
/**
* Contents of a read-only language-server document, including JDT library
* classes and disk class files. Cached per project.
+102 -4
Переглянути файл
@@ -15,13 +15,17 @@ import {
type Tooltip,
type ViewUpdate,
} from "@codemirror/view";
import { Channel } from "@tauri-apps/api/core";
import {
commands,
isCancelled,
nextRequestId,
type DocId,
type GotoKind,
type Highlight,
type Location,
type Pos,
type RequestId,
type WebReferenceKind,
} from "@/platform/ipc";
import {
@@ -48,7 +52,7 @@ import {
} from "./syntaxFallback";
import { showHint, showPendingHint } from "./hint";
import { outsideProjectReason } from "./outsideProject";
import { caretAnchor, openPopup, type ListPopupItem } from "./popupStore";
import { caretAnchor, openPopup, updatePopup, type ListPopupItem } from "./popupStore";
import { readManyFileLines, targetLabel } from "./files";
import {
documentLinks,
@@ -134,9 +138,89 @@ export async function locationItems(locs: readonly DeclarationTarget[]): Promise
/** "Choose Implementation of foo (3 found)". */
export function chooserTitle(kind: GotoKind, name: string | null, count: number, fromSyntax = false): string {
if (kind === "implementation" && !fromSyntax)
return name ? tx`Choose Implementation of ${name} (${count} found)` : tx`Choose Implementation (${count} found)`;
return `${TITLES[kind]}${name ? ` of ${name}` : ""} (${count} found${fromSyntax ? ", based on syntax" : ""})`;
}
/** The chooser's title while the server is still searching for more. */
function searchingTitle(name: string | null, count: number): string {
return name
? tx`Choose Implementation of ${name} (${count} found, searching…)`
: tx`Choose Implementation (${count} found, searching…)`;
}
function nameAt(view: EditorView, offset: number): string | null {
const word = view.state.wordAt(offset);
return word ? view.state.sliceDoc(word.from, word.to) : null;
}
/**
* Go to Implementation whose chooser opens with the implementations the server finds first
* (Java: the project's) while it searches on (the dependencies'), and fills in when it ends.
* Closing the chooser cancels the search.
*/
function streamedImplementations(view: EditorView, offset: number, onOpen: () => void) {
const name = nameAt(view, offset);
let current: RequestId | null = null;
let finished = false;
let opened = false;
let closed = false;
let first: DeclarationTarget[] = [];
let showing: Promise<void> = Promise.resolve();
return {
request(doc: DocId, pos: Pos): Promise<DeclarationTarget[]> {
const request = (current = nextRequestId());
finished = false;
const channel = new Channel<Location[]>();
channel.onmessage = (locs) => {
if (finished || opened || current !== request || !locs.length) return;
opened = true;
onOpen();
first = dedupeLocations(locs);
showing = locationItems(first).then((items) => {
// All of them arrived meanwhile: shown as usual.
if (finished) {
opened = false;
return;
}
openPopup({
kind: "list",
anchor: caretAnchor(view, offset),
title: searchingTitle(name, first.length),
items,
pin: pinToFind(name ?? "", first),
onClose: () => {
closed = true;
if (!finished) void commands.cancelRequest(request).catch(() => {});
},
});
});
};
return lspRequest(() => commands.lspGotoImplementation(request, doc, pos, channel)).finally(() => {
finished = true;
});
},
/** Whether the chooser opened with the first ones (and so the search's result goes there). */
async opened(): Promise<boolean> {
await showing;
return opened;
},
/** Fills the opened chooser with all of them, after those it shows. */
async complete(locs: readonly DeclarationTarget[]) {
if (closed) return;
const all = dedupeLocations([...first, ...locs]);
const items = await locationItems(all);
if (closed) return;
updatePopup("list", {
title: chooserTitle("implementation", name, all.length),
items,
pin: pinToFind(name ?? "", all),
});
},
};
}
/** Pin button of the targets chooser: the targets as a Find tool window tab (code-insight). */
export function pinToFind(symbol: string, locations: readonly Location[]) {
return {
@@ -175,14 +259,25 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
let fromSyntax = false;
const declaration = kind === "definition" || kind === "declaration";
let serverDown = false;
let streamed: ReturnType<typeof streamedImplementations> | null = null;
try {
const done = showPendingHint(view, tx`Finding navigation target…`);
const stream = kind === "implementation" ? streamedImplementations(view, offset, done) : null;
streamed = stream;
const request = stream
? () => stream.request(doc.doc, pos)
: () => lspRequest(() => commands.lspGoto(doc.doc, pos, kind));
try {
locs = dedupeLocations(await gotoAfterEdits(view, () => lspRequest(() => commands.lspGoto(doc.doc, pos, kind))));
locs = dedupeLocations(await gotoAfterEdits(view, request));
} finally {
done();
}
} catch (e) {
// The chooser shows the first ones: closing it cancelled the search, or the rest failed.
if (streamed && (await streamed.opened())) {
await streamed.complete([]);
return;
}
if (isCancelled(e) || !declaration) {
reportLspError(view, e, true);
return;
@@ -199,6 +294,10 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
locs = fb;
fromSyntax = true;
}
if (streamed && (await streamed.opened())) {
await streamed.complete(locs);
return;
}
if (!locs.length) {
// The server has no definition here: a link (`href`, `#include`, Markdown) or a file path.
if (
@@ -234,8 +333,7 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
}, 150);
return;
}
const word = view.state.wordAt(offset);
const name = word ? view.state.sliceDoc(word.from, word.to) : null;
const name = nameAt(view, offset);
openPopup({
kind: "list",
anchor: caretAnchor(view, offset),
+10 -1
Переглянути файл
@@ -10,8 +10,17 @@ import { registerAction } from "@/platform/actions";
const lspGoto = vi.hoisted(() => vi.fn<typeof commands.lspGoto>());
vi.mock("@/platform/ipc", async (orig) => {
const real = await orig<typeof import("@/platform/ipc")>();
return { ...real, isTauri: () => true, commands: { ...real.commands, lspGoto } };
// Go to Implementation streams (`lspGotoImplementation`); here it answers all at once.
const lspGotoImplementation: typeof commands.lspGotoImplementation = (_request, doc, pos) =>
lspGoto(doc, pos, "implementation");
return { ...real, isTauri: () => true, commands: { ...real.commands, lspGoto, lspGotoImplementation } };
});
vi.mock("@tauri-apps/api/core", async (orig) => ({
...(await orig<object>()),
Channel: class {
onmessage = () => {};
},
}));
const { gotoTargets, navigationExtension } = await import("./navigation");
const { lspDoc } = await import("./lsp");
+99 -6
Переглянути файл
@@ -1,16 +1,35 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import type { commands } from "@/platform/ipc";
import type { commands, Location } from "@/platform/ipc";
const lspGoto = vi.hoisted(() => vi.fn<typeof commands.lspGoto>());
const { lspGoto, lspGotoImplementation, cancelRequest } = vi.hoisted(() => ({
lspGoto: vi.fn<typeof commands.lspGoto>(),
lspGotoImplementation: vi.fn<typeof commands.lspGotoImplementation>(),
cancelRequest: vi.fn<typeof commands.cancelRequest>(async () => {}),
}));
vi.mock("@/platform/ipc", async (orig) => {
const real = await orig<typeof import("@/platform/ipc")>();
return { ...real, isTauri: () => true, commands: { ...real.commands, lspGoto } };
const fsReadText: typeof commands.fsReadText = async () => ({
status: "error",
error: { kind: "notFound", message: "not found" },
});
return {
...real,
isTauri: () => true,
commands: { ...real.commands, lspGoto, lspGotoImplementation, cancelRequest, fsReadText },
};
});
vi.mock("@tauri-apps/api/core", async (orig) => ({
...(await orig<object>()),
Channel: class {
onmessage = () => {};
},
}));
const { gotoTargets } = await import("./navigation");
const { lspDoc } = await import("./lsp");
const { useLspPopup, closePopup } = await import("./popupStore");
const { provideEditorService } = await import("@/workbench/contributions");
const openFile = vi.fn(async () => {});
provideEditorService({ openFile } as never);
@@ -50,13 +69,87 @@ describe("slow navigation feedback", () => {
});
it("replaces progress with the server error", async () => {
let resolve!: (result: Awaited<ReturnType<typeof commands.lspGoto>>) => void;
lspGoto.mockImplementationOnce(() => new Promise((r) => (resolve = r)));
const search = slowImplementations();
const pending = gotoTargets(view, "implementation");
await vi.advanceTimersByTimeAsync(250);
resolve({ status: "error", error: { kind: "protocol", message: "Server timed out" } });
search.resolve({ status: "error", error: { kind: "protocol", message: "Server timed out" } });
await pending;
expect(document.querySelector(".cm-lsp-hint")?.textContent).toBe("Server timed out");
expect(openFile).not.toHaveBeenCalled();
});
});
const at = (path: string, line: number): Location => ({
path,
range: { start: { line, col: 0 }, end: { line, col: 1 } },
uri: null,
});
/** A Go to Implementation request answered when the test says: the first ones, then all. */
function slowImplementations() {
let channel!: { onmessage: (locs: Location[]) => void };
let resolve!: (result: Awaited<ReturnType<typeof commands.lspGotoImplementation>>) => void;
lspGotoImplementation.mockImplementationOnce((_request, _doc, _pos, onPartial) => {
channel = onPartial as unknown as typeof channel;
return new Promise((r) => (resolve = r));
});
return {
first: (locs: Location[]) => channel.onmessage(locs),
resolve: (r: Parameters<typeof resolve>[0]) => resolve(r),
};
}
const listPopup = () => {
const popup = useLspPopup.getState().popup;
return popup?.kind === "list" ? popup : null;
};
describe("Go to Implementation while the search goes on", () => {
afterEach(() => {
closePopup();
cancelRequest.mockClear();
lspGotoImplementation.mockReset();
});
it("opens the chooser with the implementations found first, then adds the rest", async () => {
const search = slowImplementations();
const pending = gotoTargets(view, "implementation");
await vi.advanceTimersByTimeAsync(250);
search.first([at("/p/Own.java", 3)]);
await vi.waitFor(() => expect(listPopup()?.title).toBe("Choose Implementation of run (1 found, searching…)"));
expect(listPopup()!.items.map((i) => i.label)).toEqual(["Own.java"]);
expect(document.querySelector(".cm-lsp-hint")).toBeNull();
search.resolve({ status: "ok", data: [at("/p/Library.java", 8), at("/p/Own.java", 3)] });
await pending;
expect(listPopup()?.title).toBe("Choose Implementation of run (2 found)");
expect(listPopup()!.items.map((i) => i.label)).toEqual(["Own.java", "Library.java"]);
expect(openFile).not.toHaveBeenCalled();
expect(cancelRequest).not.toHaveBeenCalled();
});
it("cancels the search when the chooser closes", async () => {
const search = slowImplementations();
const pending = gotoTargets(view, "implementation");
await vi.waitFor(() => expect(lspGotoImplementation).toHaveBeenCalled());
search.first([at("/p/Own.java", 3)]);
await vi.waitFor(() => expect(listPopup()).not.toBeNull());
closePopup();
expect(cancelRequest).toHaveBeenCalledExactlyOnceWith(lspGotoImplementation.mock.lastCall![0]);
search.resolve({ status: "error", error: { kind: "cancelled", message: "cancelled" } });
await pending;
expect(listPopup()).toBeNull();
expect(document.querySelector(".cm-lsp-hint")).toBeNull();
});
it("goes to the only implementation when none came first", async () => {
const search = slowImplementations();
const pending = gotoTargets(view, "implementation");
await vi.waitFor(() => expect(lspGotoImplementation).toHaveBeenCalled());
search.resolve({ status: "ok", data: [at("/p/Library.java", 8)] });
await pending;
expect(listPopup()).toBeNull();
expect(openFile).toHaveBeenCalledWith("/p/Library.java", { selection: { line: 8, col: 0 }, focus: true });
});
});
+3
Переглянути файл
@@ -4048,6 +4048,9 @@
"Choose Super Class or Interface ({count} found)": "选择超类或接口(找到 {count} 个)",
"Choose Super Method of {name} ({count} found)": "选择 {name} 的超方法(找到 {count} 个)",
"Choose Implementation of {name} ({count} found)": "选择 {name} 的实现(找到 {count} 个)",
"Choose Implementation ({count} found)": "选择实现(找到 {count} 个)",
"Choose Implementation of {name} ({count} found, searching…)": "选择 {name} 的实现(已找到 {count} 个,仍在搜索…)",
"Choose Implementation ({count} found, searching…)": "选择实现(已找到 {count} 个,仍在搜索…)",
"Choose Subclass of {name} ({count} found)": "选择 {name} 的子类(找到 {count} 个)",
"Choose Overriding Method of {name} ({count} found)": "选择 {name} 的重写方法(找到 {count} 个)",
"Is implemented by {names}": "由 {names} 实现",