about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--crates/ide/src/fetch_crates.rs37
-rw-r--r--crates/ide/src/lib.rs8
-rw-r--r--crates/paths/src/lib.rs7
-rw-r--r--crates/project-model/src/tests.rs13
-rw-r--r--crates/rust-analyzer/src/handlers.rs5
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs57
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs28
-rw-r--r--crates/rust-analyzer/src/main_loop.rs1
-rw-r--r--crates/vfs/src/vfs_path.rs5
-rw-r--r--docs/dev/lsp-extensions.md25
-rw-r--r--editors/code/package.json16
-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
17 files changed, 522 insertions, 13 deletions
diff --git a/crates/ide/src/fetch_crates.rs b/crates/ide/src/fetch_crates.rs
new file mode 100644
index 00000000000..d326b7c2ccc
--- /dev/null
+++ b/crates/ide/src/fetch_crates.rs
@@ -0,0 +1,37 @@
+use ide_db::{
+    base_db::{CrateOrigin, FileId, SourceDatabase},
+    FxIndexSet, RootDatabase,
+};
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct CrateInfo {
+    pub name: Option<String>,
+    pub version: Option<String>,
+    pub root_file_id: FileId,
+}
+
+// Feature: Show Dependency Tree
+//
+// Shows a view tree with all the dependencies of this project
+//
+// |===
+// image::https://user-images.githubusercontent.com/5748995/229394139-2625beab-f4c9-484b-84ed-ad5dee0b1e1a.png[]
+pub(crate) fn fetch_crates(db: &RootDatabase) -> FxIndexSet<CrateInfo> {
+    let crate_graph = db.crate_graph();
+    crate_graph
+        .iter()
+        .map(|crate_id| &crate_graph[crate_id])
+        .filter(|&data| !matches!(data.origin, CrateOrigin::Local { .. }))
+        .map(|data| crate_info(data))
+        .collect()
+}
+
+fn crate_info(data: &ide_db::base_db::CrateData) -> CrateInfo {
+    let crate_name = crate_name(data);
+    let version = data.version.clone();
+    CrateInfo { name: crate_name, version, root_file_id: data.root_file_id }
+}
+
+fn crate_name(data: &ide_db::base_db::CrateData) -> Option<String> {
+    data.display_name.as_ref().map(|it| it.canonical_name().to_owned())
+}
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index e3900fa0d63..24e2aed65a5 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -59,16 +59,18 @@ mod view_mir;
 mod interpret_function;
 mod view_item_tree;
 mod shuffle_crate_graph;
+mod fetch_crates;
 
 use std::sync::Arc;
 
 use cfg::CfgOptions;
+use fetch_crates::CrateInfo;
 use ide_db::{
     base_db::{
         salsa::{self, ParallelDatabase},
         CrateOrigin, Env, FileLoader, FileSet, SourceDatabase, VfsPath,
     },
-    symbol_index, FxHashMap, LineIndexDatabase,
+    symbol_index, FxHashMap, FxIndexSet, LineIndexDatabase,
 };
 use syntax::SourceFile;
 
@@ -331,6 +333,10 @@ impl Analysis {
         self.with_db(|db| view_crate_graph::view_crate_graph(db, full))
     }
 
+    pub fn fetch_crates(&self) -> Cancellable<FxIndexSet<CrateInfo>> {
+        self.with_db(|db| fetch_crates::fetch_crates(db))
+    }
+
     pub fn expand_macro(&self, position: FilePosition) -> Cancellable<Option<ExpandedMacro>> {
         self.with_db(|db| expand_macro::expand_macro(db, position))
     }
diff --git a/crates/paths/src/lib.rs b/crates/paths/src/lib.rs
index ac09121aedf..083dfcf43d9 100644
--- a/crates/paths/src/lib.rs
+++ b/crates/paths/src/lib.rs
@@ -184,6 +184,13 @@ impl AbsPath {
         self.0.ends_with(&suffix.0)
     }
 
+    pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
+        Some((
+            self.file_stem()?.to_str()?,
+            self.extension().and_then(|extension| extension.to_str()),
+        ))
+    }
+
     // region:delegate-methods
 
     // Note that we deliberately don't implement `Deref<Target = Path>` here.
diff --git a/crates/project-model/src/tests.rs b/crates/project-model/src/tests.rs
index 9acf60bf4a8..c3c654ddb6f 100644
--- a/crates/project-model/src/tests.rs
+++ b/crates/project-model/src/tests.rs
@@ -102,6 +102,18 @@ fn replace_root(s: &mut String, direction: bool) {
     }
 }
 
+fn replace_fake_sys_root(s: &mut String) {
+    let fake_sysroot_path = get_test_path("fake-sysroot");
+    let fake_sysroot_path = if cfg!(windows) {
+        let normalized_path =
+            fake_sysroot_path.to_str().expect("expected str").replace(r#"\"#, r#"\\"#);
+        format!(r#"{}\\"#, normalized_path)
+    } else {
+        format!("{}/", fake_sysroot_path.to_str().expect("expected str"))
+    };
+    *s = s.replace(&fake_sysroot_path, "$FAKESYSROOT$")
+}
+
 fn get_test_path(file: &str) -> PathBuf {
     let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
     base.join("test_data").join(file)
@@ -140,6 +152,7 @@ fn to_crate_graph(project_workspace: ProjectWorkspace) -> (CrateGraph, ProcMacro
 fn check_crate_graph(crate_graph: CrateGraph, expect: ExpectFile) {
     let mut crate_graph = format!("{crate_graph:#?}");
     replace_root(&mut crate_graph, false);
+    replace_fake_sys_root(&mut crate_graph);
     expect.assert_eq(&crate_graph);
 }
 
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index a00d0fba7c4..c19be196544 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -3,7 +3,8 @@
 //! `ide` crate.
 
 use ide::AssistResolveStrategy;
-use lsp_types::{Diagnostic, DiagnosticTag, NumberOrString};
+use lsp_types::{Diagnostic, DiagnosticTag, NumberOrString, Url};
+
 use vfs::FileId;
 
 use crate::{global_state::GlobalStateSnapshot, to_proto, Result};
@@ -27,7 +28,7 @@ pub(crate) fn publish_diagnostics(
             severity: Some(to_proto::diagnostic_severity(d.severity)),
             code: Some(NumberOrString::String(d.code.as_str().to_string())),
             code_description: Some(lsp_types::CodeDescription {
-                href: lsp_types::Url::parse(&format!(
+                href: Url::parse(&format!(
                     "https://rust-analyzer.github.io/manual.html#{}",
                     d.code.as_str()
                 ))
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index 03e08d9cdfc..f25dc74a142 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -2,6 +2,7 @@
 //! Protocol. This module specifically handles requests.
 
 use std::{
+    fs,
     io::Write as _,
     process::{self, Stdio},
     sync::Arc,
@@ -29,7 +30,7 @@ use project_model::{ManifestPath, ProjectWorkspace, TargetKind};
 use serde_json::json;
 use stdx::{format_to, never};
 use syntax::{algo, ast, AstNode, TextRange, TextSize};
-use vfs::{AbsPath, AbsPathBuf};
+use vfs::{AbsPath, AbsPathBuf, VfsPath};
 
 use crate::{
     cargo_target_spec::CargoTargetSpec,
@@ -38,7 +39,10 @@ use crate::{
     from_proto,
     global_state::{GlobalState, GlobalStateSnapshot},
     line_index::LineEndings,
-    lsp_ext::{self, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams},
+    lsp_ext::{
+        self, CrateInfoResult, FetchDependencyListParams, FetchDependencyListResult,
+        PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams,
+    },
     lsp_utils::{all_edits_are_disjoint, invalid_params_error},
     to_proto, LspError, Result,
 };
@@ -1881,3 +1885,52 @@ fn run_rustfmt(
         Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text))))
     }
 }
+
+pub(crate) fn fetch_dependency_list(
+    state: GlobalStateSnapshot,
+    _params: FetchDependencyListParams,
+) -> Result<FetchDependencyListResult> {
+    let crates = state.analysis.fetch_crates()?;
+    let crate_infos = crates
+        .into_iter()
+        .filter_map(|it| {
+            let root_file_path = state.file_id_to_file_path(it.root_file_id);
+            crate_path(root_file_path).and_then(to_url).map(|path| CrateInfoResult {
+                name: it.name,
+                version: it.version,
+                path,
+            })
+        })
+        .collect();
+    Ok(FetchDependencyListResult { crates: crate_infos })
+}
+
+/// Searches for the directory of a Rust crate given this crate's root file path.
+///
+/// # Arguments
+///
+/// * `root_file_path`: The path to the root file of the crate.
+///
+/// # Returns
+///
+/// An `Option` value representing the path to the directory of the crate with the given
+/// name, if such a crate is found. If no crate with the given name is found, this function
+/// returns `None`.
+fn crate_path(root_file_path: VfsPath) -> Option<VfsPath> {
+    let mut current_dir = root_file_path.parent();
+    while let Some(path) = current_dir {
+        let cargo_toml_path = path.join("../Cargo.toml")?;
+        if fs::metadata(cargo_toml_path.as_path()?).is_ok() {
+            let crate_path = cargo_toml_path.parent()?;
+            return Some(crate_path);
+        }
+        current_dir = path.parent();
+    }
+    None
+}
+
+fn to_url(path: VfsPath) -> Option<Url> {
+    let path = path.as_path()?;
+    let str_path = path.as_os_str().to_str()?;
+    Url::from_file_path(str_path).ok()
+}
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 625ffe0763c..69e7d824680 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -4,11 +4,11 @@ use std::{collections::HashMap, path::PathBuf};
 
 use ide_db::line_index::WideEncoding;
 use lsp_types::request::Request;
-use lsp_types::PositionEncodingKind;
 use lsp_types::{
     notification::Notification, CodeActionKind, DocumentOnTypeFormattingParams,
     PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
 };
+use lsp_types::{PositionEncodingKind, Url};
 use serde::{Deserialize, Serialize};
 
 use crate::line_index::PositionEncoding;
@@ -27,6 +27,31 @@ pub struct AnalyzerStatusParams {
     pub text_document: Option<TextDocumentIdentifier>,
 }
 
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct CrateInfoResult {
+    pub name: Option<String>,
+    pub version: Option<String>,
+    pub path: Url,
+}
+pub enum FetchDependencyList {}
+
+impl Request for FetchDependencyList {
+    type Params = FetchDependencyListParams;
+    type Result = FetchDependencyListResult;
+    const METHOD: &'static str = "rust-analyzer/fetchDependencyList";
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct FetchDependencyListParams {}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct FetchDependencyListResult {
+    pub crates: Vec<CrateInfoResult>,
+}
+
 pub enum MemoryUsage {}
 
 impl Request for MemoryUsage {
@@ -359,6 +384,7 @@ impl Request for CodeActionRequest {
 }
 
 pub enum CodeActionResolveRequest {}
+
 impl Request for CodeActionResolveRequest {
     type Params = CodeAction;
     type Result = CodeAction;
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index dc0ea0b17e0..d3cfc5e40d6 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -660,6 +660,7 @@ impl GlobalState {
             .on_sync::<lsp_ext::OnEnter>(handlers::handle_on_enter)
             .on_sync::<lsp_types::request::SelectionRangeRequest>(handlers::handle_selection_range)
             .on_sync::<lsp_ext::MatchingBrace>(handlers::handle_matching_brace)
+            .on::<lsp_ext::FetchDependencyList>(handlers::fetch_dependency_list)
             .on::<lsp_ext::AnalyzerStatus>(handlers::handle_analyzer_status)
             .on::<lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
             .on::<lsp_ext::ViewHir>(handlers::handle_view_hir)
diff --git a/crates/vfs/src/vfs_path.rs b/crates/vfs/src/vfs_path.rs
index 38501a8ba5a..d327f2edf14 100644
--- a/crates/vfs/src/vfs_path.rs
+++ b/crates/vfs/src/vfs_path.rs
@@ -107,10 +107,7 @@ impl VfsPath {
     /// Returns `self`'s base name and file extension.
     pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
         match &self.0 {
-            VfsPathRepr::PathBuf(p) => Some((
-                p.file_stem()?.to_str()?,
-                p.extension().and_then(|extension| extension.to_str()),
-            )),
+            VfsPathRepr::PathBuf(p) => p.name_and_extension(),
             VfsPathRepr::VirtualPath(p) => p.name_and_extension(),
         }
     }
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 42f58fee30e..a4ad3e5a553 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
 <!---
-lsp_ext.rs hash: 31ca513a249753ab
+lsp_ext.rs hash: fdf1afd34548abbc
 
 If you need to change the above hash to make the test pass, please check if you
 need to adjust this doc as well and ping this issue:
@@ -851,3 +851,26 @@ export interface Diagnostic {
         rendered?: string;
     };
 }
+```
+
+## Dependency Tree
+
+**Method:** `rust-analyzer/fetchDependencyList`
+
+**Request:**
+
+```typescript
+export interface FetchDependencyListParams {}
+```
+
+**Response:** 
+```typescript
+export interface FetchDependencyListResult {
+    crates: {
+        name: string;
+        version: string;
+        path: string;
+    }[];
+}
+```
+Returns all crates from this workspace, so it can be used create a viewTree to help navigate the dependency tree.
\ No newline at end of file
diff --git a/editors/code/package.json b/editors/code/package.json
index f36e34b6a1b..ca00da9f361 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -284,6 +284,14 @@
                 "command": "rust-analyzer.clearFlycheck",
                 "title": "Clear flycheck diagnostics",
                 "category": "rust-analyzer"
+            },
+            {
+                "command": "rust-analyzer.revealDependency",
+                "title": "Reveal File"
+            },
+            {
+                "command": "rust-analyzer.revealDependency",
+                "title": "Reveal File"
             }
         ],
         "keybindings": [
@@ -1956,6 +1964,14 @@
                 }
             ]
         },
+        "views": {
+            "explorer": [
+                {
+                    "id": "rustDependencies",
+                    "name": "Rust Dependencies"
+                }
+            ]
+        },
         "jsonValidation": [
             {
                 "fileMatch": "rust-project.json",
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);