Порівняти коміти

...
Автор SHA1 Повідомлення Дата
l0ng-ai 7cfe01122b fix(hierarchy): show no expand arrow on types without subtypes
The type hierarchy loaded a level only when its node was expanded, so every
type showed an expand arrow, also one without subtypes. The level below each
shown type is now loaded ahead, four requests at a time, so a leaf shows as
one. Call hierarchies stay lazy.

Fixes #2471
2026-11-10 23:34:28 +08:00
3 змінених файлів з 81 додано та 2 видалено

No files matched your search

+5
Переглянути файл
@@ -23,6 +23,7 @@ import {
hierarchyTitle,
hierarchyTarget,
loadChildren,
prefetchLevels,
refreshHierarchy,
noneFoundLabel,
setDirection,
@@ -222,6 +223,10 @@ export function HierarchyView() {
getChildren={getChildren}
isExpandable={isExpandable}
loadChildren={(n) => (n.kind === "item" ? loadChildren(n.node) : undefined)}
onExpandedChange={(_expanded, n, open) => {
const shown = open ? children.get(n.id) : undefined;
if (shown) prefetchLevels(shown);
}}
defaultExpanded={[ROOT_ID]}
handleRef={tree}
isOccurrence={() => true}
+37
Переглянути файл
@@ -55,6 +55,20 @@ const callHierarchy = vi.fn((): { items: HierarchyItem[]; incomplete: References
incomplete: null,
}));
const cancelRequest = vi.fn(async () => undefined);
// Subtypes by the line of the type asked about: the caret's type → Long (30), Striped64 (40); Striped64 → LongAdder (50).
const typeItem = (name: string, line: number): HierarchyItem => ({
...item(name),
kind: "class",
path: "/p/Types.java",
selectionRange: range(line, 0, line, name.length),
});
const typeHierarchy = vi.fn((pos: { line: number; col: number }): HierarchyItem[] =>
pos.line === 40
? [typeItem("LongAdder", 50)]
: pos.line < 30
? [typeItem("Long", 30), typeItem("Striped64", 40)]
: [],
);
// The positions a hierarchy request is answered at; anything else is "no symbol".
let servedAt = (pos: { line: number; col: number }) => pos.line === 2 && pos.col === 7;
@@ -67,6 +81,7 @@ vi.mock("@/platform/ipc", async (orig) => {
...real.commands,
docOutline: async () => OUTLINE,
cancelRequest,
lspTypeHierarchy: async (_doc: number, pos: { line: number; col: number }) => typeHierarchy(pos),
lspCallHierarchy: async (_request: number, _doc: number, pos: { line: number; col: number }) => {
// The server prepares a hierarchy only on the method name.
if (!servedAt(pos)) throw new real.IpcError({ kind: "notFound", message: "No symbol" });
@@ -169,6 +184,28 @@ describe("hierarchy", () => {
});
});
describe("type hierarchy", () => {
it("loads each shown type's level ahead, so a type without subtypes shows no expand arrow", async () => {
servedAt = () => true;
try {
useHierarchy.setState({ mode: "type", direction: "outgoing" });
await showHierarchy("type");
const [long, striped] = useHierarchy.getState().children.get(ROOT_ID) ?? [];
await vi.waitFor(() => {
const { children } = useHierarchy.getState();
expect(children.get(long!.id)).toEqual([]);
expect(children.get(striped!.id)?.map((n) => n.item.name)).toEqual(["LongAdder"]);
});
const { children } = useHierarchy.getState();
// Only the shown levels: LongAdder's subtypes wait until Striped64 is expanded.
expect(children.has(children.get(striped!.id)![0]!.id)).toBe(false);
} finally {
servedAt = (pos) => pos.line === 2 && pos.col === 7;
useHierarchy.setState({ mode: "call", direction: "incoming" });
}
});
});
describe("closing the hierarchy tab", () => {
it("clears the tree and hides the tool window", async () => {
const { closeHierarchy, useHierarchy } = await import("./hierarchy");
+39 -2
Переглянути файл
@@ -177,6 +177,7 @@ async function loadRoot(quiet = false): Promise<RootResult> {
request(s.mode, doc, root.line, root.col, s.direction),
);
setChildren(generation, ROOT_ID, [rootKey(root)], items, incomplete && incompleteNote(incomplete, root.name));
prefetchLevels(useHierarchy.getState().children.get(ROOT_ID) ?? []);
return "loaded";
} catch (e) {
if (useHierarchy.getState().generation !== generation) return "stale";
@@ -220,7 +221,8 @@ async function enclosingRoot(c: CaretContext, mode: HierarchyMode): Promise<Hier
return null;
}
}
export async function loadChildren(node: HierarchyNode) {
/** Loads `node`'s level. `ahead`: a look-ahead load (prefetchLevels), which reports nothing. */
export async function loadChildren(node: HierarchyNode, ahead = false) {
const { mode, direction, generation } = useHierarchy.getState();
const at = node.item.selectionRange.start;
try {
@@ -234,8 +236,9 @@ export async function loadChildren(node: HierarchyNode) {
items,
incomplete && incompleteNote(incomplete, node.item.name),
);
if (!ahead) prefetchLevels(useHierarchy.getState().children.get(node.id) ?? []);
} catch (e) {
if (useHierarchy.getState().generation !== generation) return;
if (ahead || useHierarchy.getState().generation !== generation) return;
useHierarchy.setState((st) => ({
failed: new Set(st.failed).add(node.id),
children: new Map(st.children).set(node.id, []),
@@ -244,6 +247,40 @@ export async function loadChildren(node: HierarchyNode) {
}
}
const AHEAD_CONCURRENCY = 4;
const ahead: HierarchyNode[] = [];
let aheadRunning = 0;
let aheadGeneration = -1;
/**
* IDEA's type hierarchy shows no expand arrow on a type without subtypes (or supertypes): the
* level below each shown node is loaded ahead, a few requests at a time. Call hierarchies stay
* lazy, as in IDEA.
*/
export function prefetchLevels(nodes: readonly HierarchyNode[]) {
const { mode, generation, children } = useHierarchy.getState();
if (mode !== "type") return;
if (aheadGeneration !== generation) {
ahead.length = 0;
aheadGeneration = generation;
}
for (const n of nodes) if (!n.recursion && !children.has(n.id) && !ahead.includes(n)) ahead.push(n);
pumpAhead();
}
function pumpAhead() {
while (aheadRunning < AHEAD_CONCURRENCY && ahead.length) {
const node = ahead.shift()!;
const { generation, children } = useHierarchy.getState();
if (generation !== aheadGeneration || children.has(node.id)) continue;
aheadRunning++;
void loadChildren(node, true).finally(() => {
aheadRunning--;
pumpAhead();
});
}
}
export async function showHierarchy(mode: HierarchyMode) {
const c = caretContext();
if (!c) {