about summary refs log tree commit diff
path: root/editors/code/src
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-05-02 14:49:38 +0000
committerbors <bors@rust-lang.org>2023-05-02 14:49:38 +0000
commita48e0e14e15abf47feae17e54d149eb443729375 (patch)
treedfc55edf778735df9e4f3f276c75e707075204a4 /editors/code/src
parentcffc402c058f9d4d0675ed20bd920fa700c361ec (diff)
parentecfe7c04888a9c2567773370b127d2e6e1cdaa22 (diff)
downloadrust-a48e0e14e15abf47feae17e54d149eb443729375.tar.gz
rust-a48e0e14e15abf47feae17e54d149eb443729375.zip
Auto merge of #11557 - bruno-ortiz:rust-dependencies, r=bruno-ortiz
Creating rust dependencies tree explorer

Hello!

I tried to implement a tree view that shows the dependencies of a project.

It allows to see all dependencies to the project and it uses `cargo tree` for it. Also it allows to click and open the files, the viewtree tries its best to follow the openned file in the editor.

Here is an example:
![image](https://user-images.githubusercontent.com/5748995/155822183-1e227c7b-7929-4fc8-8eed-29ccfc5e14fe.png)

Any feedback is welcome since i have basically no professional experience with TS.
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/commands.ts75
-rw-r--r--editors/code/src/ctx.ts68
-rw-r--r--editors/code/src/dependencies_provider.ts144
-rw-r--r--editors/code/src/lsp_ext.ts32
-rw-r--r--editors/code/src/main.ts1
-rw-r--r--editors/code/src/util.ts13
6 files changed, 331 insertions, 2 deletions
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 2d5272d199d..98ccd50dc04 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -8,10 +8,18 @@ import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets";
 import { spawnSync } from "child_process";
 import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
 import { AstInspector } from "./ast_inspector";
-import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor } from "./util";
+import {
+    isRustDocument,
+    isCargoTomlDocument,
+    sleep,
+    isRustEditor,
+    RustEditor,
+    RustDocument,
+} from "./util";
 import { startDebugSession, makeDebugConfig } from "./debug";
 import { LanguageClient } from "vscode-languageclient/node";
 import { LINKED_COMMANDS } from "./client";
+import { DependencyId } from "./dependencies_provider";
 
 export * from "./ast_inspector";
 export * from "./run";
@@ -266,6 +274,71 @@ export function openCargoToml(ctx: CtxInit): Cmd {
     };
 }
 
+export function revealDependency(ctx: CtxInit): Cmd {
+    return async (editor: RustEditor) => {
+        if (!ctx.dependencies?.isInitialized()) {
+            return;
+        }
+        const documentPath = editor.document.uri.fsPath;
+        const dep = ctx.dependencies?.getDependency(documentPath);
+        if (dep) {
+            await ctx.treeView?.reveal(dep, { select: true, expand: true });
+        } else {
+            await revealParentChain(editor.document, ctx);
+        }
+    };
+}
+
+/**
+ * This function calculates the parent chain of a given file until it reaches it crate root contained in ctx.dependencies.
+ * This is need because the TreeView is Lazy, so at first it only has the root dependencies: For example if we have the following crates:
+ * - core
+ * - alloc
+ * - std
+ *
+ * if I want to reveal alloc/src/str.rs, I have to:
+
+ * 1. reveal every children of alloc
+ * - core
+ * - alloc\
+ * &emsp;|-beches\
+ * &emsp;|-src\
+ * &emsp;|- ...
+ * - std
+ * 2. reveal every children of src:
+ * core
+ * alloc\
+ * &emsp;|-beches\
+ * &emsp;|-src\
+ * &emsp;&emsp;|- lib.rs\
+ * &emsp;&emsp;|- str.rs <------- FOUND IT!\
+ * &emsp;&emsp;|- ...\
+ * &emsp;|- ...\
+ * std
+ */
+async function revealParentChain(document: RustDocument, ctx: CtxInit) {
+    let documentPath = document.uri.fsPath;
+    const maxDepth = documentPath.split(path.sep).length - 1;
+    const parentChain: DependencyId[] = [{ id: documentPath.toLowerCase() }];
+    do {
+        documentPath = path.dirname(documentPath);
+        parentChain.push({ id: documentPath.toLowerCase() });
+        if (parentChain.length >= maxDepth) {
+            // this is an odd case that can happen when we change a crate version but we'd still have
+            // a open file referencing the old version
+            return;
+        }
+    } while (!ctx.dependencies?.contains(documentPath));
+    parentChain.reverse();
+    for (const idx in parentChain) {
+        await ctx.treeView?.reveal(parentChain[idx], { select: true, expand: true });
+    }
+}
+
+export async function execRevealDependency(e: RustEditor): Promise<void> {
+    await vscode.commands.executeCommand("rust-analyzer.revealDependency", e);
+}
+
 export function ssr(ctx: CtxInit): Cmd {
     return async () => {
         const editor = vscode.window.activeTextEditor;
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index 567b9216bc1..8bed74b88ea 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -7,6 +7,7 @@ import { Config, prepareVSCodeConfig } from "./config";
 import { createClient } from "./client";
 import {
     executeDiscoverProject,
+    isDocumentInWorkspace,
     isRustDocument,
     isRustEditor,
     LazyOutputChannel,
@@ -14,6 +15,13 @@ import {
     RustEditor,
 } from "./util";
 import { ServerStatusParams } from "./lsp_ext";
+import {
+    Dependency,
+    DependencyFile,
+    RustDependenciesProvider,
+    DependencyId,
+} from "./dependencies_provider";
+import { execRevealDependency } from "./commands";
 import { PersistentState } from "./persistent_state";
 import { bootstrap } from "./bootstrap";
 import { ExecOptions } from "child_process";
@@ -84,11 +92,21 @@ export class Ctx {
     private commandFactories: Record<string, CommandFactory>;
     private commandDisposables: Disposable[];
     private unlinkedFiles: vscode.Uri[];
+    private _dependencies: RustDependenciesProvider | undefined;
+    private _treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> | undefined;
 
     get client() {
         return this._client;
     }
 
+    get treeView() {
+        return this._treeView;
+    }
+
+    get dependencies() {
+        return this._dependencies;
+    }
+
     constructor(
         readonly extCtx: vscode.ExtensionContext,
         commandFactories: Record<string, CommandFactory>,
@@ -101,7 +119,6 @@ export class Ctx {
         this.commandDisposables = [];
         this.commandFactories = commandFactories;
         this.unlinkedFiles = [];
-
         this.state = new PersistentState(extCtx.globalState);
         this.config = new Config(extCtx);
 
@@ -246,6 +263,53 @@ export class Ctx {
         }
         await client.start();
         this.updateCommands();
+        this.prepareTreeDependenciesView(client);
+    }
+
+    private prepareTreeDependenciesView(client: lc.LanguageClient) {
+        const ctxInit: CtxInit = {
+            ...this,
+            client: client,
+        };
+        this._dependencies = new RustDependenciesProvider(ctxInit);
+        this._treeView = vscode.window.createTreeView("rustDependencies", {
+            treeDataProvider: this._dependencies,
+            showCollapseAll: true,
+        });
+
+        this.pushExtCleanup(this._treeView);
+        vscode.window.onDidChangeActiveTextEditor(async (e) => {
+            // we should skip documents that belong to the current workspace
+            if (this.shouldRevealDependency(e)) {
+                try {
+                    await execRevealDependency(e);
+                } catch (reason) {
+                    await vscode.window.showErrorMessage(`Dependency error: ${reason}`);
+                }
+            }
+        });
+
+        this.treeView?.onDidChangeVisibility(async (e) => {
+            if (e.visible) {
+                const activeEditor = vscode.window.activeTextEditor;
+                if (this.shouldRevealDependency(activeEditor)) {
+                    try {
+                        await execRevealDependency(activeEditor);
+                    } catch (reason) {
+                        await vscode.window.showErrorMessage(`Dependency error: ${reason}`);
+                    }
+                }
+            }
+        });
+    }
+
+    private shouldRevealDependency(e: vscode.TextEditor | undefined): e is RustEditor {
+        return (
+            e !== undefined &&
+            isRustEditor(e) &&
+            !isDocumentInWorkspace(e.document) &&
+            (this.treeView?.visible || false)
+        );
     }
 
     async restart() {
@@ -348,6 +412,7 @@ export class Ctx {
                 statusBar.color = undefined;
                 statusBar.backgroundColor = undefined;
                 statusBar.command = "rust-analyzer.stopServer";
+                this.dependencies?.refresh();
                 break;
             case "warning":
                 if (status.message) {
@@ -410,4 +475,5 @@ export class Ctx {
 export interface Disposable {
     dispose(): void;
 }
+
 export type Cmd = (...args: any[]) => unknown;
diff --git a/editors/code/src/dependencies_provider.ts b/editors/code/src/dependencies_provider.ts
new file mode 100644
index 00000000000..74fbacbb3cd
--- /dev/null
+++ b/editors/code/src/dependencies_provider.ts
@@ -0,0 +1,144 @@
+import * as vscode from "vscode";
+import * as fspath from "path";
+import * as fs from "fs";
+import { CtxInit } from "./ctx";
+import * as ra from "./lsp_ext";
+import { FetchDependencyListResult } from "./lsp_ext";
+
+export class RustDependenciesProvider
+    implements vscode.TreeDataProvider<Dependency | DependencyFile>
+{
+    dependenciesMap: { [id: string]: Dependency | DependencyFile };
+    ctx: CtxInit;
+
+    constructor(ctx: CtxInit) {
+        this.dependenciesMap = {};
+        this.ctx = ctx;
+    }
+
+    private _onDidChangeTreeData: vscode.EventEmitter<
+        Dependency | DependencyFile | undefined | null | void
+    > = new vscode.EventEmitter<Dependency | undefined | null | void>();
+
+    readonly onDidChangeTreeData: vscode.Event<
+        Dependency | DependencyFile | undefined | null | void
+    > = this._onDidChangeTreeData.event;
+
+    getDependency(filePath: string): Dependency | DependencyFile | undefined {
+        return this.dependenciesMap[filePath.toLowerCase()];
+    }
+
+    contains(filePath: string): boolean {
+        return filePath.toLowerCase() in this.dependenciesMap;
+    }
+
+    isInitialized(): boolean {
+        return Object.keys(this.dependenciesMap).length !== 0;
+    }
+
+    refresh(): void {
+        this.dependenciesMap = {};
+        this._onDidChangeTreeData.fire();
+    }
+
+    getParent?(
+        element: Dependency | DependencyFile
+    ): vscode.ProviderResult<Dependency | DependencyFile> {
+        if (element instanceof Dependency) return undefined;
+        return element.parent;
+    }
+
+    getTreeItem(element: Dependency | DependencyFile): vscode.TreeItem | Thenable<vscode.TreeItem> {
+        if (element.id! in this.dependenciesMap) return this.dependenciesMap[element.id!];
+        return element;
+    }
+
+    getChildren(
+        element?: Dependency | DependencyFile
+    ): vscode.ProviderResult<Dependency[] | DependencyFile[]> {
+        return new Promise((resolve, _reject) => {
+            if (!vscode.workspace.workspaceFolders) {
+                void vscode.window.showInformationMessage("No dependency in empty workspace");
+                return Promise.resolve([]);
+            }
+            if (element) {
+                const files = fs.readdirSync(element.dependencyPath).map((fileName) => {
+                    const filePath = fspath.join(element.dependencyPath, fileName);
+                    const collapsibleState = fs.lstatSync(filePath).isDirectory()
+                        ? vscode.TreeItemCollapsibleState.Collapsed
+                        : vscode.TreeItemCollapsibleState.None;
+                    const dep = new DependencyFile(fileName, filePath, element, collapsibleState);
+                    this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep;
+                    return dep;
+                });
+                return resolve(files);
+            } else {
+                return resolve(this.getRootDependencies());
+            }
+        });
+    }
+
+    private async getRootDependencies(): Promise<Dependency[]> {
+        const dependenciesResult: FetchDependencyListResult = await this.ctx.client.sendRequest(
+            ra.fetchDependencyList,
+            {}
+        );
+        const crates = dependenciesResult.crates;
+
+        return crates.map((crate) => {
+            const dep = this.toDep(crate.name || "unknown", crate.version || "", crate.path);
+            this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep;
+            return dep;
+        });
+    }
+
+    private toDep(moduleName: string, version: string, path: string): Dependency {
+        return new Dependency(
+            moduleName,
+            version,
+            vscode.Uri.parse(path).fsPath,
+            vscode.TreeItemCollapsibleState.Collapsed
+        );
+    }
+}
+
+export class Dependency extends vscode.TreeItem {
+    constructor(
+        public readonly label: string,
+        private version: string,
+        readonly dependencyPath: string,
+        public readonly collapsibleState: vscode.TreeItemCollapsibleState
+    ) {
+        super(label, collapsibleState);
+        this.resourceUri = vscode.Uri.file(dependencyPath);
+        this.id = this.resourceUri.fsPath.toLowerCase();
+        this.description = this.version;
+        if (this.version) {
+            this.tooltip = `${this.label}-${this.version}`;
+        } else {
+            this.tooltip = this.label;
+        }
+    }
+}
+
+export class DependencyFile extends vscode.TreeItem {
+    constructor(
+        readonly label: string,
+        readonly dependencyPath: string,
+        readonly parent: Dependency | DependencyFile,
+        public readonly collapsibleState: vscode.TreeItemCollapsibleState
+    ) {
+        super(vscode.Uri.file(dependencyPath), collapsibleState);
+        this.id = this.resourceUri!.fsPath.toLowerCase();
+        const isDir = fs.lstatSync(this.resourceUri!.fsPath).isDirectory();
+        if (!isDir) {
+            this.command = {
+                command: "vscode.open",
+                title: "Open File",
+                arguments: [this.resourceUri],
+            };
+        }
+    }
+}
+
+export type DependencyId = { id: string };
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 82955acf25e..b72804e510c 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -70,6 +70,38 @@ export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void>
 
 export type AnalyzerStatusParams = { textDocument?: lc.TextDocumentIdentifier };
 
+export interface FetchDependencyListParams {}
+
+export interface FetchDependencyListResult {
+    crates: {
+        name: string | undefined;
+        version: string | undefined;
+        path: string;
+    }[];
+}
+
+export const fetchDependencyList = new lc.RequestType<
+    FetchDependencyListParams,
+    FetchDependencyListResult,
+    void
+>("rust-analyzer/fetchDependencyList");
+
+export interface FetchDependencyGraphParams {}
+
+export interface FetchDependencyGraphResult {
+    crates: {
+        name: string;
+        version: string;
+        path: string;
+    }[];
+}
+
+export const fetchDependencyGraph = new lc.RequestType<
+    FetchDependencyGraphParams,
+    FetchDependencyGraphResult,
+    void
+>("rust-analyzer/fetchDependencyGraph");
+
 export type ExpandMacroParams = {
     textDocument: lc.TextDocumentIdentifier;
     position: lc.Position;
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 7ae8fa8ca28..be9bc9d363c 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -190,5 +190,6 @@ function createCommands(): Record<string, CommandFactory> {
         showReferences: { enabled: commands.showReferences },
         triggerParameterHints: { enabled: commands.triggerParameterHints },
         openLogs: { enabled: commands.openLogs },
+        revealDependency: { enabled: commands.revealDependency },
     };
 }
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 922fbcbcf35..b6b779e2660 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -112,6 +112,19 @@ export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
     return isRustDocument(editor.document);
 }
 
+export function isDocumentInWorkspace(document: RustDocument): boolean {
+    const workspaceFolders = vscode.workspace.workspaceFolders;
+    if (!workspaceFolders) {
+        return false;
+    }
+    for (const folder of workspaceFolders) {
+        if (document.uri.fsPath.startsWith(folder.uri.fsPath)) {
+            return true;
+        }
+    }
+    return false;
+}
+
 export function isValidExecutable(path: string): boolean {
     log.debug("Checking availability of a binary at", path);