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
77 changes: 77 additions & 0 deletions src/explorer/model/Dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

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

const DUPLICATE_VALUE: string = "omitted for duplicate";
const CONFLICT_VALUE: string = "omitted for conflict";
ShihanMeng618 marked this conversation as resolved.
Show resolved Hide resolved

export class Dependencies implements ITreeItem {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Dependency or MavenDependency instead of Dependencies

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I imagine:

  • basic properties of a Dependency should at least include groupId, artifactId, version, scope, which are parsed from plaintext from your implementation.
  • in getTreeItem, you contruct an TreeItem using above props.

public dependency: string;
public pomPath: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the pomPath for a dependency/artifact ?

private curDep: string;
ShihanMeng618 marked this conversation as resolved.
Show resolved Hide resolved
private separator: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's it for?

private collapsedState: vscode.TreeItemCollapsibleState;
ShihanMeng618 marked this conversation as resolved.
Show resolved Hide resolved

constructor(dependency: string, separator: string, collapsedState: vscode.TreeItemCollapsibleState, pomPath: string) {
this.dependency = dependency;
this.separator = `${separator} `; // three spaces
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why adding 3 whitespaces?

this.collapsedState = collapsedState;
this.pomPath = pomPath;
}

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

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

public getTreeItem(): vscode.TreeItem | Thenable<vscode.TreeItem> {
this.curDep = this.dependency.split(this.separator, 1)[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. parsing work should be extracted into another utility.
  2. during parsing with some complicated regular expression, it's better to add comments about i) a sample input and ii) the matched parts.

//handle the version switch in conflict
if (this.curDep.indexOf(CONFLICT_VALUE) !== -1) {
const re = /([\w.]+:[\w.-]+:)([\w.-]+)(:[\w/.(\s]+):\s([\w.-]+)\)/gm;
this.curDep = this.curDep.replace(re, "$1$4$3 with $2)");
}
const indexCut: number = this.curDep.indexOf("(");
let depLabel: string;
let depDescription: string = "";
if (indexCut === -1) {
depLabel = this.curDep;
} else {
depLabel = this.curDep.substr(0, indexCut);
depDescription = this.curDep.substr(indexCut);
}
//highlight for conflict and description for duplicate
const treeItem: vscode.TreeItem = new vscode.TreeItem(depLabel, this.collapsedState);
if (this.curDep.indexOf(DUPLICATE_VALUE) !== -1) {
treeItem.iconPath = new vscode.ThemeIcon("trash");
treeItem.description = depDescription;
} else if (this.curDep.indexOf(CONFLICT_VALUE) !== -1) {
treeItem.iconPath = new vscode.ThemeIcon("warning");
treeItem.description = depDescription;
} else {
treeItem.iconPath = new vscode.ThemeIcon("library");
}
return treeItem;
}

private getDepsInString(treecontent: string): Dependencies[] {
if (treecontent) {
const treeChildren: string[] = treecontent.split(`${this.separator}+-`).splice(1); //delelte first line
const toDep = (treeChild: string): Dependencies => {
if (treeChild.indexOf("\r\n") === -1) {
return new Dependencies(treeChild, this.separator, vscode.TreeItemCollapsibleState.None, this.pomPath);
} else {
return new Dependencies(treeChild, this.separator, vscode.TreeItemCollapsibleState.Collapsed, this.pomPath);
}
};
return treeChildren.map(toDep);
} else {
return [];
}
}
}
50 changes: 50 additions & 0 deletions src/explorer/model/DependenciesMenu.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 * as vscode from "vscode";
import { getDependencyTree } from "../../handlers/showDependenciesHandler";
import { Dependencies } from "./Dependencies";
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<Dependencies[]> {
const dependencyTree: string | undefined = await getDependencyTree( this.project.pomPath);
if (dependencyTree === undefined) {
throw new Error("Failed to generate dependency tree.");
}
let treeContent: string = dependencyTree.slice(0, -1); //delete last "\n"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you tried on OS with different EOL, e.g. Linux/Mac?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modification of treeContent should be part of the parsing functionality.

treeContent = treeContent.replace(/\|/g, " ");
treeContent = treeContent.replace(/\\/g, "+");
treeContent = treeContent.replace(/\n/g, "\r\n");
return Promise.resolve(this.getDepsInString(treeContent));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getDepsInString => parseDependencies

}

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;
}

private getDepsInString(treecontent: string): Dependencies[] {
if (treecontent) {
const treeChildren: string[] = treecontent.split(`\r\n+-`).splice(1); // delete first line
const toDep = (treeChild: string): Dependencies => {
if (treeChild.indexOf("\r\n") === -1) {
return new Dependencies(treeChild, "\r\n", vscode.TreeItemCollapsibleState.None, this.project.pomPath);
} else {
return new Dependencies(treeChild, "\r\n", vscode.TreeItemCollapsibleState.Collapsed, this.project.pomPath);
}
};
return treeChildren.map(toDep);
} else {
return [];
}
}
}
4 changes: 4 additions & 0 deletions src/explorer/model/MavenProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Settings } from "../../Settings";
import { taskExecutor } from "../../taskExecutor";
import { getPathToExtensionRoot } from "../../utils/contextUtils";
import { Utils } from "../../utils/Utils";
import { DependenciesMenu} from "./DependenciesMenu";
import { EffectivePomProvider } from "../EffectivePomProvider";
import { mavenExplorerProvider } from "../mavenExplorerProvider";
import { IEffectivePom } from "./IEffectivePom";
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") { // parent pom does not have dependencies
ShihanMeng618 marked this conversation as resolved.
Show resolved Hide resolved
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
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
16 changes: 12 additions & 4 deletions src/utils/mavenUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,17 @@ 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);
// export async function rawDependencyTree(pomPath: string): Promise<string | undefined> {
ShihanMeng618 marked this conversation as resolved.
Show resolved Hide resolved
// 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> {
let outputPath: string;
{
outputPath = `${path.dirname(pomPath)}\\target\\dependency-graph.txt`;
ShihanMeng618 marked this conversation as resolved.
Show resolved Hide resolved
await executeInBackground(`depgraph:graph -DgraphFormat=text -DshowDuplicates -DshowConflicts -DshowVersions -DshowGroupIds`, pomPath);
}
return await readFileIfExists(outputPath);
}

Expand Down Expand Up @@ -83,7 +91,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} and the failed command can be found in output channel.`));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra information/guidance should be provided outside.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave it to another PR, which improves overall UX of error handling

}
} else {
reject(new Error(`Background process killed by signal ${signal}.`));
Expand Down