Skip to content
This repository has been archived by the owner on May 30, 2024. It is now read-only.

Add Dependencies editor view #1018

Merged
merged 5 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"activationEvents": [
"onLanguage:ruby",
"workspaceContains:Gemfile.lock",
"workspaceContains:gems.locked"
"workspaceContains:gems.locked",
"onView:dependencies"
],
"main": "./out/extension.js",
"contributes": {
Expand Down Expand Up @@ -315,6 +316,17 @@
}
}
},
"views": {
"explorer": [
{
"id": "dependencies",
"name": "Dependencies",
"icon": "$(package)",
"description": "View and manage dependencies",
"contextualTitle": "Dependencies"
}
]
},
"breakpoints": [
{
"language": "ruby"
Expand Down
5 changes: 5 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export interface ClientInterface {
state: State;
formatter: string;
serverVersion?: string;
sendRequest<T>(
method: string,
param: any,
token?: vscode.CancellationToken,
): Promise<T>;
}

export interface WorkspaceInterface {
Expand Down
162 changes: 162 additions & 0 deletions src/dependenciesTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import * as vscode from "vscode";

import { STATUS_EMITTER, WorkspaceInterface } from "./common";

interface DependenciesNode {
getChildren(): BundlerTreeNode[] | undefined | Thenable<BundlerTreeNode[]>;
}

type BundlerTreeNode = Dependency | GemDirectoryPath | GemFilePath;

export class DependenciesTree
implements vscode.TreeDataProvider<BundlerTreeNode>, vscode.Disposable
{
private readonly _onDidChangeTreeData: vscode.EventEmitter<any> =
new vscode.EventEmitter<any>();

// eslint-disable-next-line @typescript-eslint/member-ordering
readonly onDidChangeTreeData: vscode.Event<any> =
this._onDidChangeTreeData.event;

private currentWorkspace: WorkspaceInterface | undefined;
private readonly treeView: vscode.TreeView<BundlerTreeNode>;
private readonly workspaceListener: vscode.Disposable;

constructor() {
this.treeView = vscode.window.createTreeView("dependencies", {
treeDataProvider: this,
showCollapseAll: true,
});

this.workspaceListener = STATUS_EMITTER.event((workspace) => {
if (!workspace || workspace === this.currentWorkspace) {
return;
}

this.currentWorkspace = workspace;
this.refresh();
});
}

dispose(): void {
this.workspaceListener.dispose();
this.treeView.dispose();
}

getTreeItem(
element: BundlerTreeNode,
): vscode.TreeItem | Thenable<vscode.TreeItem> {
return element;
}

getChildren(
element?: BundlerTreeNode | undefined,
): vscode.ProviderResult<BundlerTreeNode[]> {
if (element) {
return element.getChildren();
} else {
return this.fetchDependencies();
}
}

refresh(): void {
this.fetchDependencies();
this._onDidChangeTreeData.fire(undefined);
}

private async fetchDependencies(): Promise<BundlerTreeNode[]> {
if (!this.currentWorkspace) {
return [];
}

const resp = (await this.currentWorkspace.lspClient?.sendRequest(
"rubyLsp/workspace/dependencies",
{},
)) as [
{ name: string; version: string; path: string; dependency: boolean },
];

return resp
.sort((left, right) => {
if (left.dependency === right.dependency) {
// if the two dependencies are the same, sort by name
return left.name.localeCompare(right.name);
} else {
// otherwise, direct dependencies sort before transitive dependencies
return right.dependency ? 1 : -1;
}
})
.map((dep) => {
return new Dependency(dep.name, dep.version, vscode.Uri.file(dep.path));
});
}
}

class Dependency extends vscode.TreeItem implements DependenciesNode {
constructor(
name: string,
version: string,
public readonly resourceUri: vscode.Uri,
) {
super(`${name} (${version})`, vscode.TreeItemCollapsibleState.Collapsed);
this.contextValue = "dependency";
this.iconPath = new vscode.ThemeIcon("ruby");
}

async getChildren() {
const dir = this.resourceUri;
const entries = await vscode.workspace.fs.readDirectory(dir);

return entries.map(([name, type]) => {
if (type === vscode.FileType.Directory) {
return new GemDirectoryPath(vscode.Uri.joinPath(dir, name));
} else {
return new GemFilePath(vscode.Uri.joinPath(dir, name));
}
});
}
}

class GemDirectoryPath extends vscode.TreeItem implements DependenciesNode {
constructor(public readonly resourceUri: vscode.Uri) {
super(resourceUri, vscode.TreeItemCollapsibleState.Collapsed);
this.contextValue = "gem-directory-path";
this.description = true;

this.command = {
command: "list.toggleExpand",
title: "Toggle",
};
}

async getChildren() {
const dir = this.resourceUri;
const entries = await vscode.workspace.fs.readDirectory(dir);

return entries.map(([name, type]) => {
if (type === vscode.FileType.Directory) {
return new GemDirectoryPath(vscode.Uri.joinPath(dir, name));
} else {
return new GemFilePath(vscode.Uri.joinPath(dir, name));
}
});
}
}

class GemFilePath extends vscode.TreeItem implements DependenciesNode {
constructor(public readonly resourceUri: vscode.Uri) {
super(resourceUri, vscode.TreeItemCollapsibleState.None);
this.contextValue = "gem-file-path";
this.description = true;

this.command = {
command: "vscode.open",
title: "Open",
arguments: [resourceUri],
};
}

getChildren() {
return undefined;
}
}
4 changes: 3 additions & 1 deletion src/rubyLsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { VersionManager } from "./ruby";
import { StatusItems } from "./status";
import { TestController } from "./testController";
import { Debugger } from "./debugger";
import { DependenciesTree } from "./dependenciesTree";

// The RubyLsp class represents an instance of the entire extension. This should only be instantiated once at the
// activation event. One instance of this class controls all of the existing workspaces, telemetry and handles all
Expand All @@ -35,7 +36,8 @@ export class RubyLsp {
this.registerCommands(context);

this.statusItems = new StatusItems();
context.subscriptions.push(this.statusItems, this.debug);
const dependenciesTree = new DependenciesTree();
context.subscriptions.push(this.statusItems, this.debug, dependenciesTree);

// Switch the status items based on which workspace is currently active
vscode.window.onDidChangeActiveTextEditor((editor) => {
Expand Down
7 changes: 7 additions & 0 deletions src/test/suite/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ suite("StatusItems", () => {
state: State.Running,
formatter: "none",
serverVersion: "1.0.0",
sendRequest: <T>() => Promise.resolve([] as T),
},
error: false,
};
Expand Down Expand Up @@ -70,6 +71,7 @@ suite("StatusItems", () => {
state: State.Running,
formatter: "none",
serverVersion: "1.0.0",
sendRequest: <T>() => Promise.resolve([] as T),
},
error: false,
};
Expand Down Expand Up @@ -127,6 +129,7 @@ suite("StatusItems", () => {
state: State.Running,
formatter,
serverVersion: "1.0.0",
sendRequest: <T>() => Promise.resolve([] as T),
},
error: false,
};
Expand Down Expand Up @@ -154,6 +157,7 @@ suite("StatusItems", () => {
state: State.Running,
formatter: "none",
serverVersion: "1.0.0",
sendRequest: <T>() => Promise.resolve([] as T),
},
error: false,
};
Expand Down Expand Up @@ -186,6 +190,7 @@ suite("StatusItems", () => {
state: State.Running,
formatter: "none",
serverVersion: "1.0.0",
sendRequest: <T>() => Promise.resolve([] as T),
},
error: false,
};
Expand Down Expand Up @@ -224,6 +229,7 @@ suite("StatusItems", () => {
state: State.Running,
formatter: "none",
serverVersion: "1.0.0",
sendRequest: <T>() => Promise.resolve([] as T),
},
error: false,
};
Expand Down Expand Up @@ -290,6 +296,7 @@ suite("StatusItems", () => {
state: State.Running,
formatter: "auto",
serverVersion: "1.0.0",
sendRequest: <T>() => Promise.resolve([] as T),
},
error: false,
};
Expand Down
Loading