дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
Find Usages mixed reads and writes of a Java field, including Lombok getter and setter calls, because jdtls has no read/write classification in its references. The usages popup and Find window now have Read and Write toggles. Read-write accesses stay visible in either mode and unclassified results are kept. Classification comes from a jdtls extension in `plugins/java-usages`, embedded as a jar and installed beside the other bundles. The checked-in jar was rebuilt from source with ECJ and matches byte for byte. Any extension failure falls back to standard references, so a jdtls upgrade can lose the filter but not Find Usages; rebuilding the jar on an upgrade is manual and described in the plugin README. Older installed jars are removed. Other languages keep their plain result count. Co-authored-by: NingYuanLin <[email protected]>
143 рядки
5.9 KiB
TypeScript
143 рядки
5.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { groupUsages, trimPreview, usageCount } from "./usages";
|
|
import { serversSummary, type ServerEntry } from "./servers";
|
|
import { buildProblemNodes } from "./ProblemsView";
|
|
import { mergeDiagnostics, sortProblems } from "./problemsStore";
|
|
import { toggleExcludedUsages } from "./UsagesView";
|
|
import { childId, directionLabel } from "./hierarchy";
|
|
|
|
const loc = (path: string, line: number, col: number, endCol: number) => ({
|
|
path,
|
|
range: { start: { line, col }, end: { line, col: endCol } },
|
|
});
|
|
|
|
describe("usages", () => {
|
|
it("groups by file with previews, sorted and de-duplicated", () => {
|
|
const files = new Map<string, string[] | null>([
|
|
["/p/b.ts", ["", " const x = foo();"]],
|
|
["/p/a.ts", null],
|
|
]);
|
|
const groups = groupUsages(
|
|
[loc("/p/b.ts", 1, 12, 15), loc("/p/a.ts", 4, 0, 3), loc("/p/b.ts", 1, 12, 15), loc("/p/b.ts", 0, 0, 0)],
|
|
files,
|
|
);
|
|
expect(groups.map((g) => [g.path, g.usages.map((u) => u.line)])).toEqual([
|
|
["/p/a.ts", [4]],
|
|
["/p/b.ts", [0, 1]],
|
|
]);
|
|
expect(usageCount(groups)).toBe(3);
|
|
expect(groups[0]!.usages[0]!.text).toBe("");
|
|
expect(trimPreview(groups[1]!.usages[1]!)).toEqual({ text: "const x = foo();", textCol: 2, from: 10, to: 13 });
|
|
});
|
|
});
|
|
|
|
describe("serversSummary", () => {
|
|
const s = (name: string, status: ServerEntry["status"]): ServerEntry => ({
|
|
id: Math.random(),
|
|
name,
|
|
status,
|
|
message: null,
|
|
languages: [],
|
|
root: null,
|
|
});
|
|
it("summarises names and the worst state", () => {
|
|
expect(serversSummary([])).toBeNull();
|
|
expect(serversSummary([s("rust-analyzer", "ready")])).toEqual({ text: "rust-analyzer", tone: "ok" });
|
|
expect(serversSummary([s("vtsls", "ready"), s("eslint", "starting")])).toEqual({ text: "vtsls +1", tone: "busy" });
|
|
expect(serversSummary([s("gopls", "missing")])!.tone).toBe("error");
|
|
expect(serversSummary([s("jdtls", "misconfigured")])!.tone).toBe("error");
|
|
});
|
|
});
|
|
|
|
describe("buildProblemNodes", () => {
|
|
it("lists files with errors first and applies the severity filter", () => {
|
|
const diag = (severity: "error" | "warning" | "hint") => ({
|
|
range: { start: { line: 0, col: 0 }, end: { line: 0, col: 1 } },
|
|
severity,
|
|
message: severity,
|
|
source: null,
|
|
code: null,
|
|
});
|
|
let map = mergeDiagnostics(new Map(), "/a", "s", [diag("warning")]);
|
|
map = mergeDiagnostics(map, "/b", "s", [diag("error"), diag("hint")]);
|
|
const all = buildProblemNodes(map, ["/a", "/b"], { error: true, warning: true, info: true });
|
|
expect(all.map((n) => n.key)).toEqual(["/b", "/a"]);
|
|
const errorsOnly = buildProblemNodes(map, ["/a", "/b"], { error: true, warning: false, info: false });
|
|
expect(errorsOnly.map((n) => n.key)).toEqual(["/b"]);
|
|
expect(errorsOnly[0]!.kind === "file" && errorsOnly[0]!.items.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("hierarchy", () => {
|
|
it("labels directions and builds unique child ids", () => {
|
|
expect(directionLabel("call", "incoming")).toBe("Callers");
|
|
expect(directionLabel("type", "outgoing")).toBe("Subtypes");
|
|
const item = {
|
|
name: "f",
|
|
kind: "function" as const,
|
|
detail: null,
|
|
path: "/a.rs",
|
|
range: { start: { line: 1, col: 0 }, end: { line: 3, col: 1 } },
|
|
selectionRange: { start: { line: 1, col: 3 }, end: { line: 1, col: 4 } },
|
|
fromRanges: [],
|
|
};
|
|
expect(childId("root", item)).toBe("root//a.rs:1:3:f");
|
|
});
|
|
});
|
|
|
|
describe("sortProblems", () => {
|
|
const d = (severity: "error" | "warning", line: number, message: string) => ({
|
|
range: { start: { line, col: 0 }, end: { line, col: 1 } },
|
|
severity,
|
|
message,
|
|
source: null,
|
|
code: null,
|
|
});
|
|
const items = [d("warning", 1, "b"), d("error", 5, "c"), d("warning", 0, "z"), d("error", 2, "a")];
|
|
const lines = (order: { bySeverity: boolean; byName: boolean }) =>
|
|
sortProblems(items, order).map((x) => x.range.start.line);
|
|
|
|
it("orders by severity, then position (IDEA's default)", () => {
|
|
expect(lines({ bySeverity: true, byName: false })).toEqual([2, 5, 0, 1]);
|
|
});
|
|
it("orders by position or by name when asked", () => {
|
|
expect(lines({ bySeverity: false, byName: false })).toEqual([0, 1, 2, 5]);
|
|
expect(lines({ bySeverity: false, byName: true })).toEqual([2, 1, 5, 0]);
|
|
});
|
|
});
|
|
|
|
describe("toggleExcludedUsages", () => {
|
|
const u = (line: number) => ({ path: "/a", line, col: 0, endCol: 1, text: "" });
|
|
it("excludes a group unless all of it is excluded already", () => {
|
|
const one = toggleExcludedUsages(new Set(), [u(1)]);
|
|
expect([...one]).toEqual(["/a:1:0:1:1"]);
|
|
const both = toggleExcludedUsages(one, [u(1), u(2)]);
|
|
expect([...both].sort()).toEqual(["/a:1:0:1:1", "/a:2:0:2:1"]);
|
|
expect(toggleExcludedUsages(both, [u(1), u(2)]).size).toBe(0);
|
|
});
|
|
});
|
|
|
|
it("preserves semantic categories for separate ranges on the same source line", () => {
|
|
const getter = { path: "/Bean.java", range: { start: { line: 0, col: 12 }, end: { line: 0, col: 19 } } };
|
|
const setter = { path: "/Bean.java", range: { start: { line: 0, col: 2 }, end: { line: 0, col: 9 } } };
|
|
const groups = groupUsages([getter, setter], new Map([["/Bean.java", ["b.setName(b.getName())"]]]), [
|
|
{ location: getter, kind: "getter" },
|
|
{ location: setter, kind: "setter" },
|
|
]);
|
|
expect(groups[0]!.usages.map((u) => [u.col, u.kind])).toEqual([
|
|
[2, "setter"],
|
|
[12, "getter"],
|
|
]);
|
|
});
|
|
|
|
it("retains nested references sharing a start and their exact multiline range", () => {
|
|
const inner = { path: "/Bean.java", range: { start: { line: 0, col: 0 }, end: { line: 0, col: 9 } } };
|
|
const outer = { path: "/Bean.java", range: { start: { line: 0, col: 0 }, end: { line: 1, col: 10 } } };
|
|
const groups = groupUsages([inner, outer], new Map([["/Bean.java", ["b.value()", ".value(x);"]]]), [
|
|
{ location: inner, kind: "getter" },
|
|
{ location: outer, kind: "setter" },
|
|
]);
|
|
expect(groups[0]!.usages).toHaveLength(2);
|
|
expect(groups[0]!.usages[1]!.sourceLocation).toEqual(outer);
|
|
});
|