feat(usages): search Java usages in Project Files first, Project and Libraries on demand (#1837)

Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-06 21:46:29 +08:00
1 джерело ef95cd1716
коміт a6d17543fd
26 files changed
+963 -96

No files matched your search

+77 -2
Переглянути файл
@@ -5,12 +5,33 @@ vi.mock("@/workbench/contributions", async (orig) => ({
...(await orig<typeof import("@/workbench/contributions")>()),
toolWindowService: () => ({ show }),
}));
const referenceResponse = vi.fn();
const lspReferences = vi.fn(async (..._args: unknown[]) => ({ status: "ok", data: null }));
vi.mock("@/platform/ipc", async (orig) => {
const actual = await orig<typeof import("@/platform/ipc")>();
return { ...actual, commands: { ...actual.commands, lspReferences } };
});
vi.mock("./ipc", async (orig) => ({
...(await orig<typeof import("./ipc")>()),
readManyFileLines: async () => new Map([["/p/b.rs", ["", " used();"]]]),
caretContext: () => ({ doc: 1, path: "/p/a.rs", line: 0, col: 0, word: "used", view: null }),
lspRequest: (request: () => unknown) => {
request();
return referenceResponse();
},
readManyFileLines: async () => new Map([["/p/b.rs", ["", " used();", " used();"]]]),
}));
const channels: { onmessage: (refs: unknown) => void }[] = [];
vi.mock("@tauri-apps/api/core", async (orig) => ({
...(await orig<object>()),
Channel: class {
onmessage = () => {};
constructor() {
channels.push(this);
}
},
}));
const { activeUsageSearch, openUsageTab, pinUsageTab, resetUsages, showUsageResults, useUsages } =
const { activeUsageSearch, findUsages, openUsageTab, pinUsageTab, resetUsages, showUsageResults, useUsages } =
await import("./usages");
describe("showUsageResults", () => {
@@ -25,6 +46,19 @@ describe("showUsageResults", () => {
]);
});
it("shows the lines the server gave along for files it does not read", async () => {
const lib = {
path: "jdt://contents/lib/Lib.class",
range: { start: { line: 3, col: 4 }, end: { line: 3, col: 8 } },
};
await showUsageResults({
symbol: "used",
locations: [lib],
classified: [{ location: lib, kind: "unknown", text: " used();" }],
});
expect(activeUsageSearch(useUsages.getState())!.groups[0]!.usages[0]!.text).toBe(" used();");
});
it("ignores malformed arguments", async () => {
show.mockClear();
await showUsageResults({ symbol: "x" });
@@ -32,6 +66,47 @@ describe("showUsageResults", () => {
});
});
describe("findUsages", () => {
it("searches Project Files, then Project and Libraries on request", async () => {
const at = { path: "/p/b.rs", range: { start: { line: 1, col: 4 }, end: { line: 1, col: 8 } } };
lspReferences.mockClear();
referenceResponse.mockResolvedValueOnce({ locations: [at], incomplete: null, librariesSkipped: true });
await findUsages();
const project = activeUsageSearch(useUsages.getState())!;
expect(project).toMatchObject({ loading: false, scope: "project" });
expect(lspReferences.mock.lastCall![5]).toBe(false);
referenceResponse.mockResolvedValueOnce({ locations: [at], incomplete: null });
project.searchLibraries!();
await vi.waitFor(() => expect(activeUsageSearch(useUsages.getState())!.loading).toBe(false));
expect(lspReferences.mock.lastCall![5]).toBe(true);
const all = activeUsageSearch(useUsages.getState())!;
expect(all.scope).toBe("libraries");
// Rerun keeps the scope.
referenceResponse.mockResolvedValueOnce({ locations: [at], incomplete: null });
all.rerun!();
await vi.waitFor(() => expect(lspReferences).toHaveBeenCalledTimes(3));
expect(lspReferences.mock.lastCall![5]).toBe(true);
resetUsages();
});
it("shows the usages found first while it searches for the rest", async () => {
const at = (line: number) => ({ path: "/p/b.rs", range: { start: { line, col: 4 }, end: { line, col: 8 } } });
let resolve!: (refs: unknown) => void;
referenceResponse.mockReturnValueOnce(new Promise((r) => (resolve = r)));
const search = findUsages();
await vi.waitFor(() => expect(channels).not.toHaveLength(0));
channels.at(-1)!.onmessage({ locations: [at(1)], incomplete: null });
await vi.waitFor(() => expect(activeUsageSearch(useUsages.getState())!.groups).toHaveLength(1));
expect(activeUsageSearch(useUsages.getState())).toMatchObject({ loading: true, error: null });
resolve({ locations: [at(1), at(2)], incomplete: null });
await search;
const done = activeUsageSearch(useUsages.getState())!;
expect(done.loading).toBe(false);
expect(done.groups[0]!.usages.map((u) => u.line)).toEqual([1, 2]);
resetUsages();
});
});
describe("usage tabs", () => {
const search = (id: number) => ({
id,