diff options
| -rw-r--r-- | crates/ide/src/fetch_crates.rs | 37 | ||||
| -rw-r--r-- | crates/ide/src/lib.rs | 8 | ||||
| -rw-r--r-- | crates/paths/src/lib.rs | 7 | ||||
| -rw-r--r-- | crates/project-model/src/tests.rs | 13 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/handlers.rs | 5 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/handlers/request.rs | 57 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/lsp_ext.rs | 28 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/main_loop.rs | 1 | ||||
| -rw-r--r-- | crates/vfs/src/vfs_path.rs | 5 | ||||
| -rw-r--r-- | docs/dev/lsp-extensions.md | 25 | ||||
| -rw-r--r-- | editors/code/package.json | 16 | ||||
| -rw-r--r-- | editors/code/src/commands.ts | 75 | ||||
| -rw-r--r-- | editors/code/src/ctx.ts | 68 | ||||
| -rw-r--r-- | editors/code/src/dependencies_provider.ts | 144 | ||||
| -rw-r--r-- | editors/code/src/lsp_ext.ts | 32 | ||||
| -rw-r--r-- | editors/code/src/main.ts | 1 | ||||
| -rw-r--r-- | editors/code/src/util.ts | 13 |
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\ + *  |-beches\ + *  |-src\ + *  |- ... + * - std + * 2. reveal every children of src: + * core + * alloc\ + *  |-beches\ + *  |-src\ + *   |- lib.rs\ + *   |- str.rs <------- FOUND IT!\ + *   |- ...\ + *  |- ...\ + * 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); |
