Introduce separate tree item types in the methods usage panel

This creates new tree item types for methods and usages such that these
can contain references to their parent and children. This allows us to
easily find the parent of a usage and to find the children of a method.
This removes an expensive `find` call in `getParent`.
This commit is contained in:
Koen Vlaswinkel
2023-10-16 11:18:27 +02:00
parent 39a9f4ce1e
commit d715ceea10
5 changed files with 93 additions and 40 deletions

View File

@@ -27,7 +27,7 @@ export class MethodsUsageDataProvider
private methods: readonly Method[] = []; private methods: readonly Method[] = [];
// sortedMethods is a separate field so we can check if the methods have changed // sortedMethods is a separate field so we can check if the methods have changed
// by reference, which is faster than checking if the methods have changed by value. // by reference, which is faster than checking if the methods have changed by value.
private sortedMethods: readonly Method[] = []; private sortedTreeItems: readonly MethodTreeViewItem[] = [];
private databaseItem: DatabaseItem | undefined = undefined; private databaseItem: DatabaseItem | undefined = undefined;
private sourceLocationPrefix: string | undefined = undefined; private sourceLocationPrefix: string | undefined = undefined;
private hideModeledMethods: boolean = INITIAL_HIDE_MODELED_METHODS_VALUE; private hideModeledMethods: boolean = INITIAL_HIDE_MODELED_METHODS_VALUE;
@@ -72,7 +72,9 @@ export class MethodsUsageDataProvider
this.modifiedMethodSignatures !== modifiedMethodSignatures this.modifiedMethodSignatures !== modifiedMethodSignatures
) { ) {
this.methods = methods; this.methods = methods;
this.sortedMethods = sortMethodsInGroups(methods, mode); this.sortedTreeItems = createTreeItems(
sortMethodsInGroups(methods, mode),
);
this.databaseItem = databaseItem; this.databaseItem = databaseItem;
this.sourceLocationPrefix = this.sourceLocationPrefix =
await this.databaseItem.getSourceLocationPrefix(this.cliServer); await this.databaseItem.getSourceLocationPrefix(this.cliServer);
@@ -86,7 +88,7 @@ export class MethodsUsageDataProvider
} }
getTreeItem(item: MethodsUsageTreeViewItem): TreeItem { getTreeItem(item: MethodsUsageTreeViewItem): TreeItem {
if (isExternalApiUsage(item)) { if (isMethodTreeViewItem(item)) {
return { return {
label: `${item.packageName}.${item.typeName}.${item.methodName}${item.methodParameters}`, label: `${item.packageName}.${item.typeName}.${item.methodName}${item.methodParameters}`,
collapsibleState: TreeItemCollapsibleState.Collapsed, collapsibleState: TreeItemCollapsibleState.Collapsed,
@@ -94,7 +96,7 @@ export class MethodsUsageDataProvider
}; };
} else { } else {
const method = this.getParent(item); const method = this.getParent(item);
if (!method || !isExternalApiUsage(method)) { if (!method || !isMethodTreeViewItem(method)) {
throw new Error("Parent not found for tree item"); throw new Error("Parent not found for tree item");
} }
return { return {
@@ -144,12 +146,12 @@ export class MethodsUsageDataProvider
getChildren(item?: MethodsUsageTreeViewItem): MethodsUsageTreeViewItem[] { getChildren(item?: MethodsUsageTreeViewItem): MethodsUsageTreeViewItem[] {
if (item === undefined) { if (item === undefined) {
if (this.hideModeledMethods) { if (this.hideModeledMethods) {
return this.sortedMethods.filter((api) => !api.supported); return this.sortedTreeItems.filter((api) => !api.supported);
} else { } else {
return [...this.sortedMethods]; return [...this.sortedTreeItems];
} }
} else if (isExternalApiUsage(item)) { } else if (isMethodTreeViewItem(item)) {
return [...item.usages]; return item.children;
} else { } else {
return []; return [];
} }
@@ -158,29 +160,42 @@ export class MethodsUsageDataProvider
getParent( getParent(
item: MethodsUsageTreeViewItem, item: MethodsUsageTreeViewItem,
): MethodsUsageTreeViewItem | undefined { ): MethodsUsageTreeViewItem | undefined {
if (isExternalApiUsage(item)) { if (isMethodTreeViewItem(item)) {
return undefined; return undefined;
} else { } else {
return this.methods.find((e) => e.usages.includes(item)); return item.parent;
} }
} }
public resolveCanonicalUsage(usage: Usage): Usage | undefined { public resolveUsageTreeViewItem(
for (const method of this.methods) { methodSignature: string,
for (const u of method.usages) { usage: Usage,
if (usagesAreEqual(u, usage)) { ): UsageTreeViewItem | undefined {
return u; const method = this.sortedTreeItems.find(
} (m) => m.signature === methodSignature,
} );
if (!method) {
return undefined;
} }
return undefined;
return method.children.find((u) => usagesAreEqual(u, usage));
} }
} }
export type MethodsUsageTreeViewItem = Method | Usage; type MethodTreeViewItem = Method & {
children: UsageTreeViewItem[];
};
function isExternalApiUsage(item: MethodsUsageTreeViewItem): item is Method { type UsageTreeViewItem = Usage & {
return (item as any).usages !== undefined; parent: MethodTreeViewItem;
};
export type MethodsUsageTreeViewItem = MethodTreeViewItem | UsageTreeViewItem;
function isMethodTreeViewItem(
item: MethodsUsageTreeViewItem,
): item is MethodTreeViewItem {
return "children" in item && "usages" in item;
} }
function usagesAreEqual(u1: Usage, u2: Usage): boolean { function usagesAreEqual(u1: Usage, u2: Usage): boolean {
@@ -206,3 +221,20 @@ function sortMethodsInGroups(methods: readonly Method[], mode: Mode): Method[] {
return sortMethods(group); return sortMethods(group);
}); });
} }
function createTreeItems(methods: readonly Method[]): MethodTreeViewItem[] {
return methods.map((method) => {
const newMethod: MethodTreeViewItem = {
...method,
children: [],
};
newMethod.children = method.usages.map((usage) => ({
...usage,
// This needs to be a reference to the parent method, not a copy of it.
parent: newMethod,
}));
return newMethod;
});
}

View File

@@ -56,10 +56,16 @@ export class MethodsUsagePanel extends DisposableObject {
}; };
} }
public async revealItem(usage: Usage): Promise<void> { public async revealItem(
const canonicalUsage = this.dataProvider.resolveCanonicalUsage(usage); methodSignature: string,
if (canonicalUsage !== undefined) { usage: Usage,
await this.treeView.reveal(canonicalUsage); ): Promise<void> {
const usageTreeViewItem = this.dataProvider.resolveUsageTreeViewItem(
methodSignature,
usage,
);
if (usageTreeViewItem !== undefined) {
await this.treeView.reveal(usageTreeViewItem);
} }
} }

View File

@@ -102,7 +102,7 @@ export class ModelEditorModule extends DisposableObject {
method: Method, method: Method,
usage: Usage, usage: Usage,
): Promise<void> { ): Promise<void> {
await this.methodsUsagePanel.revealItem(usage); await this.methodsUsagePanel.revealItem(method.signature, usage);
await this.methodModelingPanel.setMethod(databaseItem, method); await this.methodModelingPanel.setMethod(databaseItem, method);
await showResolvableLocation(usage.url, databaseItem, this.app.logger); await showResolvableLocation(usage.url, databaseItem, this.app.logger);
} }

View File

@@ -3,7 +3,10 @@ import {
CallClassification, CallClassification,
Method, Method,
} from "../../../../../src/model-editor/method"; } from "../../../../../src/model-editor/method";
import { MethodsUsageDataProvider } from "../../../../../src/model-editor/methods-usage/methods-usage-data-provider"; import {
MethodsUsageDataProvider,
MethodsUsageTreeViewItem,
} from "../../../../../src/model-editor/methods-usage/methods-usage-data-provider";
import { DatabaseItem } from "../../../../../src/databases/local-databases"; import { DatabaseItem } from "../../../../../src/databases/local-databases";
import { import {
createMethod, createMethod,
@@ -242,13 +245,23 @@ describe("MethodsUsageDataProvider", () => {
const usage = createUsage({}); const usage = createUsage({});
const methodTreeItem: MethodsUsageTreeViewItem = {
...supportedMethod,
children: [],
};
const usageTreeItem: MethodsUsageTreeViewItem = {
...usage,
parent: methodTreeItem,
};
methodTreeItem.children = [usageTreeItem];
it("should return [] if item is a usage", async () => { it("should return [] if item is a usage", async () => {
expect(dataProvider.getChildren(usage)).toEqual([]); expect(dataProvider.getChildren(usageTreeItem)).toEqual([]);
}); });
it("should return usages if item is external api usage", async () => { it("should return usages if item is method", async () => {
const method = createMethod({ usages: [usage] }); expect(dataProvider.getChildren(methodTreeItem)).toEqual([usageTreeItem]);
expect(dataProvider.getChildren(method)).toEqual([usage]);
}); });
it("should show all methods if hideModeledMethods is false and looking at the root", async () => { it("should show all methods if hideModeledMethods is false and looking at the root", async () => {

View File

@@ -68,11 +68,10 @@ describe("MethodsUsagePanel", () => {
}); });
it("should reveal the correct item in the tree view", async () => { it("should reveal the correct item in the tree view", async () => {
const methods = [ const method = createMethod({
createMethod({ usages: [usage],
usages: [usage], });
}), const methods = [method];
];
const panel = new MethodsUsagePanel(modelingStore, mockCliServer); const panel = new MethodsUsagePanel(modelingStore, mockCliServer);
await panel.setState( await panel.setState(
@@ -84,13 +83,16 @@ describe("MethodsUsagePanel", () => {
modifiedMethodSignatures, modifiedMethodSignatures,
); );
await panel.revealItem(usage); await panel.revealItem(method.signature, usage);
expect(mockTreeView.reveal).toHaveBeenCalledWith(usage); expect(mockTreeView.reveal).toHaveBeenCalledWith(
expect.objectContaining(usage),
);
}); });
it("should do nothing if usage cannot be found", async () => { it("should do nothing if usage cannot be found", async () => {
const methods = [createMethod({})]; const method = createMethod({});
const methods = [method];
const panel = new MethodsUsagePanel(modelingStore, mockCliServer); const panel = new MethodsUsagePanel(modelingStore, mockCliServer);
await panel.setState( await panel.setState(
methods, methods,
@@ -101,7 +103,7 @@ describe("MethodsUsagePanel", () => {
modifiedMethodSignatures, modifiedMethodSignatures,
); );
await panel.revealItem(usage); await panel.revealItem(method.signature, usage);
expect(mockTreeView.reveal).not.toHaveBeenCalled(); expect(mockTreeView.reveal).not.toHaveBeenCalled();
}); });