From 695803a219b7308527c9d50eec94b7f129528481 Mon Sep 17 00:00:00 2001 From: Ufuk Kayserilioglu Date: Thu, 1 Feb 2024 22:33:42 +0200 Subject: [PATCH 1/5] Add Dependencies view declarations Co-authored-by: Vinicius Stock --- package.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b80cb450..819599bb 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "activationEvents": [ "onLanguage:ruby", "workspaceContains:Gemfile.lock", - "workspaceContains:gems.locked" + "workspaceContains:gems.locked", + "onView:dependencies" ], "main": "./out/extension.js", "contributes": { @@ -315,6 +316,17 @@ } } }, + "views": { + "explorer": [ + { + "id": "dependencies", + "name": "Dependencies", + "icon": "$(package)", + "description": "View and manage dependencies", + "contextualTitle": "Dependencies" + } + ] + }, "breakpoints": [ { "language": "ruby" From f122262a3904f9022a590236335b5e034cc864b6 Mon Sep 17 00:00:00 2001 From: Ufuk Kayserilioglu Date: Thu, 1 Feb 2024 22:51:21 +0200 Subject: [PATCH 2/5] Add DependenciesTree view code Co-authored-by: Vinicius Stock --- src/common.ts | 5 ++ src/dependenciesTree.ts | 154 ++++++++++++++++++++++++++++++++++++++++ src/rubyLsp.ts | 4 ++ 3 files changed, 163 insertions(+) create mode 100644 src/dependenciesTree.ts 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(); From 8345c5462f9460f5deb70e0ff157f5e1a5715c29 Mon Sep 17 00:00:00 2001 From: Ufuk Kayserilioglu Date: Thu, 1 Feb 2024 23:56:55 +0200 Subject: [PATCH 3/5] Fix up all the fake client interfaces in tests Co-authored-by: Vinicius Stock --- src/test/suite/status.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/suite/status.test.ts b/src/test/suite/status.test.ts index 679b29fc..382f83b6 100644 --- a/src/test/suite/status.test.ts +++ b/src/test/suite/status.test.ts @@ -35,6 +35,7 @@ suite("StatusItems", () => { state: State.Running, formatter: "none", serverVersion: "1.0.0", + sendRequest: () => Promise.resolve([] as T), }, error: false, }; @@ -70,6 +71,7 @@ suite("StatusItems", () => { state: State.Running, formatter: "none", serverVersion: "1.0.0", + sendRequest: () => Promise.resolve([] as T), }, error: false, }; @@ -127,6 +129,7 @@ suite("StatusItems", () => { state: State.Running, formatter, serverVersion: "1.0.0", + sendRequest: () => Promise.resolve([] as T), }, error: false, }; @@ -154,6 +157,7 @@ suite("StatusItems", () => { state: State.Running, formatter: "none", serverVersion: "1.0.0", + sendRequest: () => Promise.resolve([] as T), }, error: false, }; @@ -186,6 +190,7 @@ suite("StatusItems", () => { state: State.Running, formatter: "none", serverVersion: "1.0.0", + sendRequest: () => Promise.resolve([] as T), }, error: false, }; @@ -224,6 +229,7 @@ suite("StatusItems", () => { state: State.Running, formatter: "none", serverVersion: "1.0.0", + sendRequest: () => Promise.resolve([] as T), }, error: false, }; @@ -290,6 +296,7 @@ suite("StatusItems", () => { state: State.Running, formatter: "auto", serverVersion: "1.0.0", + sendRequest: () => Promise.resolve([] as T), }, error: false, }; From dc50a504f7affd9b70618587310f409988dfcc62 Mon Sep 17 00:00:00 2001 From: Ufuk Kayserilioglu Date: Fri, 2 Feb 2024 01:31:17 +0200 Subject: [PATCH 4/5] Make `DependenciesTree` disposable Co-authored-by: Vinicius Stock --- src/dependenciesTree.ts | 20 ++++++++++++++------ src/rubyLsp.ts | 6 ++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/dependenciesTree.ts b/src/dependenciesTree.ts index e555429c..780ca720 100644 --- a/src/dependenciesTree.ts +++ b/src/dependenciesTree.ts @@ -9,7 +9,7 @@ interface DependenciesNode { type BundlerTreeNode = Dependency | GemDirectoryPath | GemFilePath; export class DependenciesTree - implements vscode.TreeDataProvider + implements vscode.TreeDataProvider, vscode.Disposable { private readonly _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); @@ -19,9 +19,16 @@ export class DependenciesTree this._onDidChangeTreeData.event; private currentWorkspace: WorkspaceInterface | undefined; + private readonly treeView: vscode.TreeView; + private readonly workspaceListener: vscode.Disposable; - constructor(context: vscode.ExtensionContext) { - STATUS_EMITTER.event((workspace) => { + constructor() { + this.treeView = vscode.window.createTreeView("dependencies", { + treeDataProvider: this, + showCollapseAll: true, + }); + + this.workspaceListener = STATUS_EMITTER.event((workspace) => { if (!workspace) { return; } @@ -29,10 +36,11 @@ export class DependenciesTree this.currentWorkspace = workspace; this.refresh(); }); + } - context.subscriptions.push( - vscode.window.createTreeView("dependencies", { treeDataProvider: this }), - ); + dispose(): void { + this.workspaceListener.dispose(); + this.treeView.dispose(); } getTreeItem( diff --git a/src/rubyLsp.ts b/src/rubyLsp.ts index 132a1c6f..a5a6f083 100644 --- a/src/rubyLsp.ts +++ b/src/rubyLsp.ts @@ -23,7 +23,6 @@ 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; @@ -34,12 +33,11 @@ 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(); - 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) => { From 0064dad3009c921029cdd79fc903f6b4be9ea25d Mon Sep 17 00:00:00 2001 From: Ufuk Kayserilioglu Date: Fri, 2 Feb 2024 01:31:59 +0200 Subject: [PATCH 5/5] Stop triggering a refresh if the workspace hasn't changed Co-authored-by: Vinicius Stock --- src/dependenciesTree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dependenciesTree.ts b/src/dependenciesTree.ts index 780ca720..ba042377 100644 --- a/src/dependenciesTree.ts +++ b/src/dependenciesTree.ts @@ -29,7 +29,7 @@ export class DependenciesTree }); this.workspaceListener = STATUS_EMITTER.event((workspace) => { - if (!workspace) { + if (!workspace || workspace === this.currentWorkspace) { return; }