diff --git a/package-lock.json b/package-lock.json index 72f03f93..c2cc3352 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1399,7 +1399,7 @@ }, "d": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "dev": true, "requires": { @@ -5621,7 +5621,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -5807,7 +5807,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, @@ -6173,7 +6173,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -6246,7 +6246,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -6310,7 +6310,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, @@ -7073,7 +7073,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -7506,7 +7506,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -7564,7 +7564,7 @@ }, "through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, @@ -8380,7 +8380,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xtend": { diff --git a/package.json b/package.json index 5e826cfb..a3acc90d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,15 @@ "dark": "images/dark/icon-refresh.svg", "light": "images/light/icon-refresh.svg" } + }, + { + "command": "java.view.package.changeRepresentation", + "title": "Change package representation", + "category": "Java", + "icon": { + "dark": "images/dark/package.svg", + "light": "images/light/package.svg" + } } ], "configuration": { @@ -62,6 +71,15 @@ "type": "boolean", "description": "Synchronize dependency viewer selection with folder explorer", "default": true + }, + "java.dependency.packagePresentation": { + "type": "string", + "enum": [ + "flat", + "hierarchical" + ], + "description": "Package presentation mode: flat or hierarchical", + "default": "flat" } } }, @@ -70,6 +88,11 @@ { "command": "java.view.package.refresh", "when": "view == javaDependencyExplorer", + "group": "navigation@1" + }, + { + "command": "java.view.package.changeRepresentation", + "when": "view == javaDependencyExplorer", "group": "navigation@0" } ] diff --git a/src/commands.ts b/src/commands.ts index 64feac68..7f981852 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -10,6 +10,8 @@ export namespace Commands { */ export const EXECUTE_WORKSPACE_COMMAND = "java.execute.workspaceCommand"; + export const VIEW_PACKAGE_CHANGEREPRESENTATION = "java.view.package.changeRepresentation"; + export const VIEW_PACKAGE_REFRESH = "java.view.package.refresh"; export const VIEW_PACKAGE_OPEN_FILE = "java.view.package.openFile"; diff --git a/src/java/hierachicalPackageNodeData.ts b/src/java/hierachicalPackageNodeData.ts new file mode 100644 index 00000000..51bfbc46 --- /dev/null +++ b/src/java/hierachicalPackageNodeData.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { INodeData, NodeKind } from "./nodeData"; + +export class HierachicalPackageNodeData implements INodeData { + + public static createHierachicalNodeDataByPackageList(packageList: INodeData[]): HierachicalPackageNodeData { + const result = new HierachicalPackageNodeData("", ""); + packageList.forEach((nodeData) => result.addSubPackage(nodeData.name.split("."), nodeData)); + result.compressTree(); + return result; + } + + public name: string; + public children = []; + public displayName: string; + private nodeData: INodeData = null; + + public get uri() { + return this.nodeData.uri; + } + + public get moduleName() { + return this.nodeData.moduleName; + } + + public get path() { + return this.nodeData.path; + } + + public get kind() { + return this.nodeData ? this.nodeData.kind : NodeKind.Package; + } + + public get isPackage() { + return this.nodeData !== null; + } + + private constructor(displayName: string, parentName: string) { + this.displayName = displayName; + this.name = parentName === "" ? displayName : parentName + "." + displayName; + } + + private compressTree(): void { + // Don't compress the root node + while (this.name !== "" && this.children.length === 1 && !this.isPackage) { + const child = this.children[0]; + this.name = this.name + "." + child.displayName; + this.displayName = this.displayName + "." + child.displayName; + this.children = child.children; + this.nodeData = child.nodeData; + } + this.children.forEach((child) => child.compressTree()); + } + + private addSubPackage(packages: string[], nodeData: INodeData): void { + if (!packages.length) { + this.nodeData = nodeData; + return; + } + const subPackageDisplayName = packages.shift(); + const childNode = this.children.find((child) => child.displayName === subPackageDisplayName); + if (childNode) { + childNode.addSubPackage(packages); + } else { + const newNode = new HierachicalPackageNodeData(subPackageDisplayName, this.name); + newNode.addSubPackage(packages, nodeData); + this.children.push(newNode); + } + } +} diff --git a/src/settings.ts b/src/settings.ts index 361e3ee6..520c0fd0 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { commands, ConfigurationChangeEvent, ExtensionContext, workspace, WorkspaceConfiguration } from "vscode"; +import { instrumentOperation } from "vscode-extension-telemetry-wrapper"; import { Commands } from "./commands"; export class Settings { @@ -12,12 +13,21 @@ export class Settings { return; } const updatedConfig = workspace.getConfiguration("java.dependency"); - if (updatedConfig.showOutline !== this._depdendencyConfig.showOutline) { + if (updatedConfig.showOutline !== this._depdendencyConfig.showOutline + || updatedConfig.packagePresentation !== this._depdendencyConfig.packagePresentation) { commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH); } this._depdendencyConfig = updatedConfig; })); + + const instrumented = instrumentOperation(Commands.VIEW_PACKAGE_CHANGEREPRESENTATION, Settings.changePackageRepresentation); + context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_CHANGEREPRESENTATION, instrumented)); + } + + public static changePackageRepresentation(): void { + const representationSetting = Settings.isHierarchicalView() ? PackagePresentation.Flat : PackagePresentation.Hierarchical; + workspace.getConfiguration().update("java.dependency.packagePresentation", representationSetting, false); } public static showOutline(): boolean { @@ -28,5 +38,14 @@ export class Settings { return this._depdendencyConfig.get("syncWithFolderExplorer"); } + public static isHierarchicalView(): boolean { + return this._depdendencyConfig.get("packagePresentation") === PackagePresentation.Hierarchical; + } + private static _depdendencyConfig: WorkspaceConfiguration = workspace.getConfiguration("java.dependency"); } + +enum PackagePresentation { + Flat = "flat", + Hierarchical = "hierarchical", +} diff --git a/src/views/containerNode.ts b/src/views/containerNode.ts index 6ac7c002..5cc1ffd5 100644 --- a/src/views/containerNode.ts +++ b/src/views/containerNode.ts @@ -5,7 +5,7 @@ import { Jdtls } from "../java/jdtls"; import { INodeData, NodeKind } from "../java/nodeData"; import { DataNode } from "./dataNode"; import { ExplorerNode } from "./explorerNode"; -import { PackageRootNode } from "./packageRootNode"; +import { NodeFactory } from "./nodeFactory"; import { ProjectNode } from "./projectNode"; export class ContainerNode extends DataNode { @@ -21,7 +21,7 @@ export class ContainerNode extends DataNode { if (this.nodeData.children && this.nodeData.children.length) { this.sort(); this.nodeData.children.forEach((classpathNode) => { - result.push(new PackageRootNode(classpathNode, this, this._project)); + result.push(NodeFactory.createPackageRootNode(classpathNode, this, this._project)); }); } return result; diff --git a/src/views/dataNode.ts b/src/views/dataNode.ts index ac43cac1..b65ce8a2 100644 --- a/src/views/dataNode.ts +++ b/src/views/dataNode.ts @@ -7,7 +7,7 @@ import { Telemetry } from "../telemetry"; import { ExplorerNode } from "./explorerNode"; export abstract class DataNode extends ExplorerNode { - constructor(private _nodeData: INodeData, parent: DataNode) { + constructor(protected _nodeData: INodeData, parent: DataNode) { super(parent); } @@ -32,6 +32,13 @@ export abstract class DataNode extends ExplorerNode { return this._nodeData.path; } + public async revealPaths(paths: INodeData[]): Promise { + const childNodeData = paths.shift(); + const childs: ExplorerNode[] = await this.getChildren(); + const childNode = childs.find((child: DataNode) => child.nodeData.name === childNodeData.name && child.path === childNodeData.path); + return childNode === null ? null : (paths.length ? childNode.revealPaths(paths) : childNode); + } + public getChildren(): ProviderResult { if (!this._nodeData.children) { return this.loadData().then((res) => { diff --git a/src/views/dependencyDataProvider.ts b/src/views/dependencyDataProvider.ts index 2a955e77..b27935e1 100644 --- a/src/views/dependencyDataProvider.ts +++ b/src/views/dependencyDataProvider.ts @@ -10,6 +10,7 @@ import { Commands } from "../commands"; import { Jdtls } from "../java/jdtls"; import { INodeData, NodeKind } from "../java/nodeData"; import { Telemetry } from "../telemetry"; +import { DataNode } from "./dataNode"; import { ExplorerNode } from "./explorerNode"; import { ProjectNode } from "./projectNode"; import { WorkspaceNode } from "./workspaceNode"; @@ -67,6 +68,28 @@ export class DependencyDataProvider implements TreeDataProvider { return element.getParent(); } + public async revealPaths(paths: INodeData[]): Promise { + const projectNodeData = paths.shift(); + const projects = await this.getRootProjects(); + const correspondProject = projects.find((node: DataNode) => + node.path === projectNodeData.path && node.nodeData.name === projectNodeData.name); + return correspondProject.revealPaths(paths); + } + + private async getRootProjects(): Promise { + const rootElements = this._rootItems ? this._rootItems : await this.getChildren(); + if (rootElements[0] instanceof ProjectNode) { + return rootElements; + } else { + let result = []; + for (const singleworkspace of rootElements) { + const projects = await singleworkspace.getChildren(); + result = result.concat(projects); + } + return result; + } + } + private getRootNodes(): Thenable { return new Promise((resolve, reject) => { this._rootItems = new Array(); diff --git a/src/views/dependencyExplorer.ts b/src/views/dependencyExplorer.ts index 4c80ee89..0b9fd734 100644 --- a/src/views/dependencyExplorer.ts +++ b/src/views/dependencyExplorer.ts @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { ExtensionContext, ProviderResult, TextEditor, TreeView, TreeViewVisibilityChangeEvent, Uri, window } from "vscode"; +import { ExtensionContext, TextEditor, TreeView, TreeViewVisibilityChangeEvent, Uri, window } from "vscode"; import { Jdtls } from "../java/jdtls"; import { INodeData } from "../java/nodeData"; import { Settings } from "../settings"; -import { Utility } from "../utility"; import { DataNode } from "./dataNode"; import { DependencyDataProvider } from "./dependencyDataProvider"; import { ExplorerNode } from "./explorerNode"; @@ -39,44 +38,14 @@ export class DependencyExplorer { public dispose(): void { } - public reveal(uri: Uri): void { - Jdtls.resolvePath(uri.toString()).then((paths: INodeData[]) => { - this.revealPath(this._dataProvider, paths); - }); - } - - private revealPath(current: { getChildren: (element?: ExplorerNode) => ProviderResult }, paths: INodeData[]) { - if (!current) { - return; - } + public async reveal(uri: Uri): Promise { + const paths: INodeData[] = await Jdtls.resolvePath(uri.toString()); + const node = await this._dataProvider.revealPaths(paths); - const res = current.getChildren(); - if (Utility.isThenable(res)) { - res.then((children: DataNode[]) => { - this.visitChildren(children, paths); - }); + if (this._dependencyViewer.visible) { + this._dependencyViewer.reveal(node); } else { - this.visitChildren(res, paths); - } - } - - private visitChildren(children: DataNode[], paths: INodeData[]): void { - if (children && paths) { - for (const c of children) { - if (paths[0] && c.path === paths[0].path && c.nodeData.name === paths[0].name) { - if (paths.length === 1) { - if (this._dependencyViewer.visible) { - this._dependencyViewer.reveal(c); - } else { - this._selectionWhenHidden = c; - } - } else { - paths.shift(); - this.revealPath(c, paths); - } - break; - } - } + this._selectionWhenHidden = node; } } } diff --git a/src/views/hierachicalPackageNode.ts b/src/views/hierachicalPackageNode.ts new file mode 100644 index 00000000..3cf9d373 --- /dev/null +++ b/src/views/hierachicalPackageNode.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { ProviderResult, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { HierachicalPackageNodeData } from "../java/hierachicalPackageNodeData"; +import { INodeData, NodeKind } from "../java/nodeData"; +import { Telemetry } from "../telemetry"; +import { DataNode } from "./dataNode"; +import { ExplorerNode } from "./explorerNode"; +import { FileNode } from "./fileNode"; +import { PackageNode } from "./packageNode"; +import { ProjectNode } from "./projectNode"; +import { TypeRootNode } from "./typeRootNode"; + +export class HierachicalPackageNode extends PackageNode { + + constructor(nodeData: INodeData, parent: DataNode, protected _project: ProjectNode, protected _rootNode: DataNode) { + super(nodeData, parent, _project, _rootNode); + } + + public getTreeItem(): TreeItem | Promise { + if (this._nodeData) { + const item = new TreeItem(this.getHierarchicalNodeData().displayName, + this.hasChildren() ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None); + item.iconPath = this.iconPath; + item.command = this.command; + return item; + } + } + + public getChildren(): ProviderResult { + return this.loadData().then((res) => { + if (!res) { + Telemetry.sendEvent("load data get undefined result", { node_kind: this.nodeData.kind.toString() }); + } else { + // Combine hierachical children and normal packagenode children + res.forEach((node) => this.nodeData.children.push(node)); + } + return this.createChildNodeList(); + }); + } + + public async revealPaths(paths: INodeData[]): Promise { + const hierachicalNodeData = paths[0]; + if (hierachicalNodeData.name === this.nodeData.name) { + paths.shift(); + // reveal as a package node + return super.revealPaths(paths); + } else { + // reveal as a package root node + const childs: ExplorerNode[] = await this.getChildren(); + const childNode = childs.find((child: DataNode) => + child instanceof HierachicalPackageNode && hierachicalNodeData.name.startsWith(child.nodeData.name)); + return childNode === null ? null : childNode.revealPaths(paths); + } + } + + protected loadData(): Thenable { + // Load data only when current node is a package + return this.getHierarchicalNodeData().isPackage ? super.loadData() : Promise.resolve([]); + } + + protected createChildNodeList(): ExplorerNode[] { + const result = []; + if (this.nodeData.children && this.nodeData.children.length) { + this.sort(); + this.nodeData.children.forEach((nodeData) => { + if (nodeData.kind === NodeKind.File) { + result.push(new FileNode(nodeData, this)); + } else if (nodeData instanceof HierachicalPackageNodeData) { + result.push(new HierachicalPackageNode(nodeData, this, this._project, this._rootNode)); + } else { + result.push(new TypeRootNode(nodeData, this)); + } + }); + } + return result; + } + + private getHierarchicalNodeData(): HierachicalPackageNodeData { + return this.nodeData; + } +} diff --git a/src/views/hierachicalPackageRootNode.ts b/src/views/hierachicalPackageRootNode.ts new file mode 100644 index 00000000..3d3946b6 --- /dev/null +++ b/src/views/hierachicalPackageRootNode.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { HierachicalPackageNodeData } from "../java/hierachicalPackageNodeData"; +import { INodeData, NodeKind } from "../java/nodeData"; +import { DataNode } from "./dataNode"; +import { ExplorerNode } from "./explorerNode"; +import { FileNode } from "./fileNode"; +import { FolderNode } from "./folderNode"; +import { HierachicalPackageNode } from "./hierachicalPackageNode"; +import { PackageRootNode } from "./packageRootNode"; +import { ProjectNode } from "./projectNode"; +import { TypeRootNode } from "./typeRootNode"; + +export class HierachicalPackageRootNode extends PackageRootNode { + + constructor(nodeData: INodeData, parent: DataNode, _project: ProjectNode) { + super(nodeData, parent, _project); + } + + public async revealPaths(paths: INodeData[]): Promise { + const hierachicalNodeData = paths[0]; + const childs: ExplorerNode[] = await this.getChildren(); + const childNode = childs.find((child: DataNode) => + child instanceof HierachicalPackageNode && hierachicalNodeData.name.startsWith(child.nodeData.name)); + return childNode === null ? null : childNode.revealPaths(paths); + } + + protected createChildNodeList(): ExplorerNode[] { + const result = []; + if (this.nodeData.children && this.nodeData.children.length) { + this.sort(); + this.nodeData.children.forEach((data) => { + if (data.kind === NodeKind.File) { + result.push(new FileNode(data, this)); + } else if (data.kind === NodeKind.Folder) { + result.push(new FolderNode(data, this, this._project, this)); + } else if (data.kind === NodeKind.TypeRoot) { + result.push(new TypeRootNode(data, this)); + } + }); + } + return this.getHierarchicalPackageNodes().concat(result); + } + + protected getHierarchicalPackageNodes(): ExplorerNode[] { + const hierachicalPackageNodeData = this.getHierarchicalPackageNodeData(); + return hierachicalPackageNodeData === null ? [] : hierachicalPackageNodeData.children.map((hierachicalChildrenNode) => + new HierachicalPackageNode(hierachicalChildrenNode, this, this._project, this)); + } + + private getHierarchicalPackageNodeData(): HierachicalPackageNodeData { + if (this.nodeData.children && this.nodeData.children.length) { + const nodeDataList = this.nodeData.children + .filter((child) => child.kind === NodeKind.Package); + return HierachicalPackageNodeData.createHierachicalNodeDataByPackageList(nodeDataList); + } else { + return null; + } + } +} diff --git a/src/views/nodeFactory.ts b/src/views/nodeFactory.ts new file mode 100644 index 00000000..ad5ae537 --- /dev/null +++ b/src/views/nodeFactory.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { INodeData } from "../java/nodeData"; +import { Settings } from "../settings"; +import { DataNode } from "./dataNode"; +import { HierachicalPackageRootNode } from "./hierachicalPackageRootNode"; +import { PackageRootNode } from "./packageRootNode"; +import { ProjectNode } from "./projectNode"; + +export class NodeFactory { + public static createPackageRootNode(nodeData: INodeData, parent: DataNode, project: ProjectNode): PackageRootNode { + return Settings.isHierarchicalView() ? + new HierachicalPackageRootNode(nodeData, parent, project) : new PackageRootNode(nodeData, parent, project); + } +} diff --git a/src/views/packageNode.ts b/src/views/packageNode.ts index a453f84b..9756e79f 100644 --- a/src/views/packageNode.ts +++ b/src/views/packageNode.ts @@ -9,7 +9,7 @@ import { FileNode } from "./fileNode"; import { TypeRootNode } from "./typeRootNode"; export class PackageNode extends DataNode { - constructor(nodeData: INodeData, parent: DataNode, private _project: DataNode, private _rootNode: DataNode) { + constructor(nodeData: INodeData, parent: DataNode, protected _project: DataNode, protected _rootNode: DataNode) { super(nodeData, parent); } diff --git a/src/views/packageRootNode.ts b/src/views/packageRootNode.ts index be1e712a..0f7e9c2e 100644 --- a/src/views/packageRootNode.ts +++ b/src/views/packageRootNode.ts @@ -14,7 +14,7 @@ import { TypeRootNode } from "./typeRootNode"; export class PackageRootNode extends DataNode { - constructor(nodeData: INodeData, parent: DataNode, private _project: ProjectNode) { + constructor(nodeData: INodeData, parent: DataNode, protected _project: ProjectNode) { super(nodeData, parent); } diff --git a/src/views/projectNode.ts b/src/views/projectNode.ts index 49346377..29f345d7 100644 --- a/src/views/projectNode.ts +++ b/src/views/projectNode.ts @@ -8,7 +8,7 @@ import { Telemetry } from "../telemetry"; import { ContainerNode } from "./containerNode"; import { DataNode } from "./dataNode"; import { ExplorerNode } from "./explorerNode"; -import { PackageRootNode } from "./packageRootNode"; +import { NodeFactory } from "./nodeFactory"; export class ProjectNode extends DataNode { @@ -54,7 +54,7 @@ export class ProjectNode extends DataNode { if (data.kind === NodeKind.Container) { result.push(new ContainerNode(data, this, this)); } else if (data.kind === NodeKind.PackageRoot) { - result.push(new PackageRootNode(data, this, this)); + result.push(NodeFactory.createPackageRootNode(data, this, this)); } }); }