diff --git a/src/common.ts b/src/common.ts index eb968cb8..5d78139f 100644 --- a/src/common.ts +++ b/src/common.ts @@ -34,6 +34,11 @@ export interface ClientInterface { state: State; formatter: string; serverVersion?: string; + sendRequest( + method: string, + param: any, + token?: vscode.CancellationToken, + ): Promise; } export interface WorkspaceInterface { diff --git a/src/dependenciesTree.ts b/src/dependenciesTree.ts new file mode 100644 index 00000000..e555429c --- /dev/null +++ b/src/dependenciesTree.ts @@ -0,0 +1,154 @@ +import * as vscode from "vscode"; + +import { STATUS_EMITTER, WorkspaceInterface } from "./common"; + +interface DependenciesNode { + getChildren(): BundlerTreeNode[] | undefined | Thenable; +} + +type BundlerTreeNode = Dependency | GemDirectoryPath | GemFilePath; + +export class DependenciesTree + implements vscode.TreeDataProvider +{ + private readonly _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter(); + + // eslint-disable-next-line @typescript-eslint/member-ordering + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; + + private currentWorkspace: WorkspaceInterface | undefined; + + constructor(context: vscode.ExtensionContext) { + STATUS_EMITTER.event((workspace) => { + if (!workspace) { + return; + } + + this.currentWorkspace = workspace; + this.refresh(); + }); + + context.subscriptions.push( + vscode.window.createTreeView("dependencies", { treeDataProvider: this }), + ); + } + + getTreeItem( + element: BundlerTreeNode, + ): vscode.TreeItem | Thenable { + return element; + } + + getChildren( + element?: BundlerTreeNode | undefined, + ): vscode.ProviderResult { + if (element) { + return element.getChildren(); + } else { + return this.fetchDependencies(); + } + } + + refresh(): void { + this.fetchDependencies(); + this._onDidChangeTreeData.fire(undefined); + } + + private async fetchDependencies(): Promise { + 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; + } +} diff --git a/src/rubyLsp.ts b/src/rubyLsp.ts index 48fca8d3..132a1c6f 100644 --- a/src/rubyLsp.ts +++ b/src/rubyLsp.ts @@ -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 @@ -22,6 +23,7 @@ export class RubyLsp { private readonly statusItems: StatusItems; private readonly testController: TestController; private readonly debug: Debugger; + // private readonly dependenciesTree: DependenciesTree; constructor(context: vscode.ExtensionContext) { this.context = context; @@ -32,6 +34,8 @@ export class RubyLsp { this.currentActiveWorkspace.bind(this), ); this.debug = new Debugger(context, this.workspaceResolver.bind(this)); + // eslint-disable-next-line no-new + new DependenciesTree(context); this.registerCommands(context); this.statusItems = new StatusItems();