Merge pull request #2976 from github/koesie10/methods-usage-panel-parent

Introduce separate tree item types in the methods usage panel
This commit is contained in:
Koen Vlaswinkel
2023-10-16 13:25:05 +02:00
committed by GitHub
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

@@ -103,7 +103,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();
}); });