Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add dependency treeview #647

Merged
merged 9 commits into from
Jul 6, 2021
27 changes: 27 additions & 0 deletions src/explorer/model/DependenciesMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import * as vscode from "vscode";
import { parseRawDependencyDataHandler } from "../../handlers/parseRawDependencyDataHandler";
import { Dependency } from "./Dependency";
import { ITreeItem } from "./ITreeItem";
import { MavenProject } from "./MavenProject";
import { Menu } from "./Menu";

export class DependenciesMenu extends Menu implements ITreeItem {
constructor(project: MavenProject) {
super(project);
this.name = "Dependencies";
}

public async getChildren() : Promise<Dependency[]> {
const treeNodes = await parseRawDependencyDataHandler(this.project);
return Promise.resolve(treeNodes);
}

public getTreeItem(): vscode.TreeItem | Thenable<vscode.TreeItem> {
const treeItem: vscode.TreeItem = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed);
treeItem.iconPath = new vscode.ThemeIcon("library");
return treeItem;
}
}
52 changes: 52 additions & 0 deletions src/explorer/model/Dependency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import * as vscode from "vscode";
import { ITreeItem } from "./ITreeItem";
import { TreeNode } from "./TreeNode";

const DUPLICATE_INDICATOR: string = "omitted for duplicate";
const CONFLICT_INDICATOR: string = "omitted for conflict";

export class Dependency extends TreeNode implements ITreeItem {
private label: string = ""; // groupId:artifactId:version:scope
private description: string = "";
constructor(value: string) {
super(value);
const indexCut: number = value.indexOf("(");
if (indexCut !== -1) {
this.description = value.substr(indexCut);
this.label = value.substr(0, indexCut);
} else {
this.label = value;
}
}

public getContextValue(): string {
return "Dependencies";
}

public async getChildren(): Promise<Dependency[] | undefined> {
return Promise.resolve(<Dependency[]> this.children);
}

public getTreeItem(): vscode.TreeItem | Thenable<vscode.TreeItem> {
const treeItem: vscode.TreeItem = new vscode.TreeItem(this.label);
if (this.children.length !== 0) {
treeItem.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
} else {
treeItem.collapsibleState = vscode.TreeItemCollapsibleState.None;
}

if (this.description.indexOf(DUPLICATE_INDICATOR) !== -1) {
treeItem.iconPath = new vscode.ThemeIcon("trash");
treeItem.description = this.description;
} else if (this.description.indexOf(CONFLICT_INDICATOR) !== -1) {
treeItem.iconPath = new vscode.ThemeIcon("warning");
treeItem.description = this.description;
} else {
treeItem.iconPath = new vscode.ThemeIcon("library");
}
return treeItem;
}
}
4 changes: 4 additions & 0 deletions src/explorer/model/MavenProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getPathToExtensionRoot } from "../../utils/contextUtils";
import { Utils } from "../../utils/Utils";
import { EffectivePomProvider } from "../EffectivePomProvider";
import { mavenExplorerProvider } from "../mavenExplorerProvider";
import { DependenciesMenu} from "./DependenciesMenu";
import { IEffectivePom } from "./IEffectivePom";
import { ITreeItem } from "./ITreeItem";
import { LifecycleMenu } from "./LifecycleMenu";
Expand Down Expand Up @@ -146,6 +147,9 @@ export class MavenProject implements ITreeItem {
const ret: ITreeItem[] = [];
ret.push(new LifecycleMenu(this));
ret.push(new PluginsMenu(this));
if (this.packaging !== "pom") { // hide the "Dependencies" item for "pom" project
ret.push(new DependenciesMenu(this));
}
if (this.moduleNames.length > 0 && Settings.viewType() === "hierarchical") {
const projects: MavenProject[] = <MavenProject[]>this.modules.map(m => mavenExplorerProvider.getMavenProject(m)).filter(Boolean);
ret.push(...projects);
Expand Down
27 changes: 27 additions & 0 deletions src/explorer/model/TreeNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

export class TreeNode {
protected value: string;
protected children: TreeNode[] = [];
protected parent?: TreeNode | undefined;
constructor(value: string) {
this.value = value;
}

public addChild(valueOrNode: string | TreeNode): void {
let child: TreeNode;
if (typeof valueOrNode === "string") {
child = new TreeNode(valueOrNode);
} else {
child = valueOrNode;
}
child.parent = this;
this.children.push(child);
}

public addChildren(nodes: TreeNode[]): void {
nodes.forEach(node => node.parent = this);
this.children = this.children.concat(nodes);
}
}
50 changes: 50 additions & 0 deletions src/handlers/parseRawDependencyDataHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import { Dependency } from "../explorer/model/Dependency";
import { MavenProject } from "../explorer/model/MavenProject";
import { getDependencyTree } from "../handlers/showDependenciesHandler";

export async function parseRawDependencyDataHandler(project: MavenProject): Promise<Dependency[]> {
const dependencyTree: string | undefined = await getDependencyTree(project.pomPath);
if (dependencyTree === undefined) {
throw new Error("Failed to generate dependency tree.");
}
let treeContent: string = dependencyTree.slice(0, -1); // delete last "\n"
treeContent = treeContent.replace(/\|/g, " ");
treeContent = treeContent.replace(/\\/g, "+");
treeContent = treeContent.replace(/\n/g, "\r\n");
// handle the version switch in conflict
// input = (groupId:artifactId:)(version1)(:scope (omitted for conflict): (version2))
// output = (groupId:artifactId:)(version2)(:scope (omitted for conflict) with (version1))
const re = /([\w.]+:[\w.-]+:)([\w.-]+)(:[\w/.(\s]+):\s([\w.-]+)\)/gm;
treeContent = treeContent.replace(re, "$1$4$3 with $2)");

const eol: string = "\r\n";
const indent: string = " "; // three spaces
const separator: string = "\r\n";
const starter: string = "+- ";
return parseTreeNodes(treeContent, separator, indent, starter, eol);
}

function parseTreeNodes(treecontent: string, separator: string, indent: string, starter: string, eol: string): Dependency[] {
const treeNodes: Dependency[] = [];
if (treecontent) {
const treeChildren: string[] = treecontent.split(`${separator}${starter}`).splice(1); // delete first line
const toTreeNode = (treeChild: string): Dependency => {
let curNode: Dependency;
if (treeChild.indexOf(eol) === -1) {
curNode = new Dependency(treeChild);
} else {
const curValue: string = treeChild.split(separator, 1)[0];
curNode = new Dependency(curValue);
const nextSeparator = `${separator}${indent}`;
const childrenNodes: Dependency[] = parseTreeNodes(treeChild, nextSeparator, indent, starter, eol);
curNode.addChildren(childrenNodes);
}
return curNode;
};
treeChildren.forEach(treeChild => treeNodes.push(toTreeNode(treeChild)));
}
return treeNodes;
}
2 changes: 1 addition & 1 deletion src/handlers/showDependenciesHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function showDependenciesHandler(project: MavenProject): Promise<vo
await vscode.window.showTextDocument(document, { viewColumn: vscode.ViewColumn.Active, preview: false });
}

async function getDependencyTree(pomPathOrMavenProject: string | MavenProject): Promise<string | undefined> {
export async function getDependencyTree(pomPathOrMavenProject: string | MavenProject): Promise<string | undefined> {
let pomPath: string;
let name: string;
if (typeof pomPathOrMavenProject === "object" && pomPathOrMavenProject instanceof MavenProject) {
Expand Down
11 changes: 6 additions & 5 deletions src/utils/mavenUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ export async function rawEffectivePom(pomPath: string, options?: {cacheOnly?: bo
return await readFileIfExists(outputPath);
}

export async function rawDependencyTree(pomPath: string): Promise<string | undefined> {
const outputPath: string = getTempFolder(pomPath);
await executeInBackground(`dependency:tree -Dverbose -DoutputFile="${outputPath}"`, pomPath);
return await readFileIfExists(outputPath);
export async function rawDependencyTree(pomPath: string): Promise<any> {
const outputDirectory: string = path.dirname(pomPath);
const outputFileName: string = "dependency-graph.txt";
await executeInBackground(`com.github.ferstl:depgraph-maven-plugin:graph -DgraphFormat=text -DshowDuplicates -DshowConflicts -DshowVersions -DshowGroupIds -DoutputDirectory="${outputDirectory}" -DoutputFileName="${outputFileName}"`, pomPath);
return await readFileIfExists(path.join(outputDirectory, outputFileName));
}

export async function pluginDescription(pluginId: string, pomPath: string): Promise<string | undefined> {
Expand Down Expand Up @@ -83,7 +84,7 @@ async function executeInBackground(mvnArgs: string, pomfile?: string): Promise<a
if (code === 0) {
resolve(code);
} else {
reject(new Error(`Background process terminated with code ${code}.`));
reject(new Error(`Background process terminated with code ${code}`));
}
} else {
reject(new Error(`Background process killed by signal ${signal}.`));
Expand Down