дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
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.
Цей коміт міститься в:
1 джерело
086c89df5b
коміт
401b4a9441
16 files changed
+441
-61
No files matched your search
@@ -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,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");
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Заблокувати користувача