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.
This commit is contained in:
bors 2023-05-02 14:49:38 +00:00
commit a48e0e14e1
17 changed files with 522 additions and 13 deletions

View file

@ -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())
}

View file

@ -59,16 +59,18 @@ mod view_mir;
mod interpret_function; mod interpret_function;
mod view_item_tree; mod view_item_tree;
mod shuffle_crate_graph; mod shuffle_crate_graph;
mod fetch_crates;
use std::sync::Arc; use std::sync::Arc;
use cfg::CfgOptions; use cfg::CfgOptions;
use fetch_crates::CrateInfo;
use ide_db::{ use ide_db::{
base_db::{ base_db::{
salsa::{self, ParallelDatabase}, salsa::{self, ParallelDatabase},
CrateOrigin, Env, FileLoader, FileSet, SourceDatabase, VfsPath, CrateOrigin, Env, FileLoader, FileSet, SourceDatabase, VfsPath,
}, },
symbol_index, FxHashMap, LineIndexDatabase, symbol_index, FxHashMap, FxIndexSet, LineIndexDatabase,
}; };
use syntax::SourceFile; use syntax::SourceFile;
@ -331,6 +333,10 @@ impl Analysis {
self.with_db(|db| view_crate_graph::view_crate_graph(db, full)) 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>> { pub fn expand_macro(&self, position: FilePosition) -> Cancellable<Option<ExpandedMacro>> {
self.with_db(|db| expand_macro::expand_macro(db, position)) self.with_db(|db| expand_macro::expand_macro(db, position))
} }

View file

@ -184,6 +184,13 @@ impl AbsPath {
self.0.ends_with(&suffix.0) 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 // region:delegate-methods
// Note that we deliberately don't implement `Deref<Target = Path>` here. // Note that we deliberately don't implement `Deref<Target = Path>` here.

View file

@ -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 { fn get_test_path(file: &str) -> PathBuf {
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
base.join("test_data").join(file) 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) { fn check_crate_graph(crate_graph: CrateGraph, expect: ExpectFile) {
let mut crate_graph = format!("{crate_graph:#?}"); let mut crate_graph = format!("{crate_graph:#?}");
replace_root(&mut crate_graph, false); replace_root(&mut crate_graph, false);
replace_fake_sys_root(&mut crate_graph);
expect.assert_eq(&crate_graph); expect.assert_eq(&crate_graph);
} }

View file

@ -3,7 +3,8 @@
//! `ide` crate. //! `ide` crate.
use ide::AssistResolveStrategy; use ide::AssistResolveStrategy;
use lsp_types::{Diagnostic, DiagnosticTag, NumberOrString}; use lsp_types::{Diagnostic, DiagnosticTag, NumberOrString, Url};
use vfs::FileId; use vfs::FileId;
use crate::{global_state::GlobalStateSnapshot, to_proto, Result}; 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)), severity: Some(to_proto::diagnostic_severity(d.severity)),
code: Some(NumberOrString::String(d.code.as_str().to_string())), code: Some(NumberOrString::String(d.code.as_str().to_string())),
code_description: Some(lsp_types::CodeDescription { code_description: Some(lsp_types::CodeDescription {
href: lsp_types::Url::parse(&format!( href: Url::parse(&format!(
"https://rust-analyzer.github.io/manual.html#{}", "https://rust-analyzer.github.io/manual.html#{}",
d.code.as_str() d.code.as_str()
)) ))

View file

@ -2,6 +2,7 @@
//! Protocol. This module specifically handles requests. //! Protocol. This module specifically handles requests.
use std::{ use std::{
fs,
io::Write as _, io::Write as _,
process::{self, Stdio}, process::{self, Stdio},
sync::Arc, sync::Arc,
@ -29,7 +30,7 @@ use project_model::{ManifestPath, ProjectWorkspace, TargetKind};
use serde_json::json; use serde_json::json;
use stdx::{format_to, never}; use stdx::{format_to, never};
use syntax::{algo, ast, AstNode, TextRange, TextSize}; use syntax::{algo, ast, AstNode, TextRange, TextSize};
use vfs::{AbsPath, AbsPathBuf}; use vfs::{AbsPath, AbsPathBuf, VfsPath};
use crate::{ use crate::{
cargo_target_spec::CargoTargetSpec, cargo_target_spec::CargoTargetSpec,
@ -38,7 +39,10 @@ use crate::{
from_proto, from_proto,
global_state::{GlobalState, GlobalStateSnapshot}, global_state::{GlobalState, GlobalStateSnapshot},
line_index::LineEndings, 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}, lsp_utils::{all_edits_are_disjoint, invalid_params_error},
to_proto, LspError, Result, to_proto, LspError, Result,
}; };
@ -1881,3 +1885,52 @@ fn run_rustfmt(
Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text)))) 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()
}

View file

@ -4,11 +4,11 @@ use std::{collections::HashMap, path::PathBuf};
use ide_db::line_index::WideEncoding; use ide_db::line_index::WideEncoding;
use lsp_types::request::Request; use lsp_types::request::Request;
use lsp_types::PositionEncodingKind;
use lsp_types::{ use lsp_types::{
notification::Notification, CodeActionKind, DocumentOnTypeFormattingParams, notification::Notification, CodeActionKind, DocumentOnTypeFormattingParams,
PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams, PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
}; };
use lsp_types::{PositionEncodingKind, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::line_index::PositionEncoding; use crate::line_index::PositionEncoding;
@ -27,6 +27,31 @@ pub struct AnalyzerStatusParams {
pub text_document: Option<TextDocumentIdentifier>, 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 {} pub enum MemoryUsage {}
impl Request for MemoryUsage { impl Request for MemoryUsage {
@ -359,6 +384,7 @@ impl Request for CodeActionRequest {
} }
pub enum CodeActionResolveRequest {} pub enum CodeActionResolveRequest {}
impl Request for CodeActionResolveRequest { impl Request for CodeActionResolveRequest {
type Params = CodeAction; type Params = CodeAction;
type Result = CodeAction; type Result = CodeAction;

View file

@ -660,6 +660,7 @@ impl GlobalState {
.on_sync::<lsp_ext::OnEnter>(handlers::handle_on_enter) .on_sync::<lsp_ext::OnEnter>(handlers::handle_on_enter)
.on_sync::<lsp_types::request::SelectionRangeRequest>(handlers::handle_selection_range) .on_sync::<lsp_types::request::SelectionRangeRequest>(handlers::handle_selection_range)
.on_sync::<lsp_ext::MatchingBrace>(handlers::handle_matching_brace) .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::AnalyzerStatus>(handlers::handle_analyzer_status)
.on::<lsp_ext::SyntaxTree>(handlers::handle_syntax_tree) .on::<lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
.on::<lsp_ext::ViewHir>(handlers::handle_view_hir) .on::<lsp_ext::ViewHir>(handlers::handle_view_hir)

View file

@ -107,10 +107,7 @@ impl VfsPath {
/// Returns `self`'s base name and file extension. /// Returns `self`'s base name and file extension.
pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> { pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
match &self.0 { match &self.0 {
VfsPathRepr::PathBuf(p) => Some(( VfsPathRepr::PathBuf(p) => p.name_and_extension(),
p.file_stem()?.to_str()?,
p.extension().and_then(|extension| extension.to_str()),
)),
VfsPathRepr::VirtualPath(p) => p.name_and_extension(), VfsPathRepr::VirtualPath(p) => p.name_and_extension(),
} }
} }

View file

@ -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 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: need to adjust this doc as well and ping this issue:
@ -851,3 +851,26 @@ export interface Diagnostic {
rendered?: string; 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.

View file

@ -284,6 +284,14 @@
"command": "rust-analyzer.clearFlycheck", "command": "rust-analyzer.clearFlycheck",
"title": "Clear flycheck diagnostics", "title": "Clear flycheck diagnostics",
"category": "rust-analyzer" "category": "rust-analyzer"
},
{
"command": "rust-analyzer.revealDependency",
"title": "Reveal File"
},
{
"command": "rust-analyzer.revealDependency",
"title": "Reveal File"
} }
], ],
"keybindings": [ "keybindings": [
@ -1956,6 +1964,14 @@
} }
] ]
}, },
"views": {
"explorer": [
{
"id": "rustDependencies",
"name": "Rust Dependencies"
}
]
},
"jsonValidation": [ "jsonValidation": [
{ {
"fileMatch": "rust-project.json", "fileMatch": "rust-project.json",

View file

@ -8,10 +8,18 @@ import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets";
import { spawnSync } from "child_process"; import { spawnSync } from "child_process";
import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run"; import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
import { AstInspector } from "./ast_inspector"; 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 { startDebugSession, makeDebugConfig } from "./debug";
import { LanguageClient } from "vscode-languageclient/node"; import { LanguageClient } from "vscode-languageclient/node";
import { LINKED_COMMANDS } from "./client"; import { LINKED_COMMANDS } from "./client";
import { DependencyId } from "./dependencies_provider";
export * from "./ast_inspector"; export * from "./ast_inspector";
export * from "./run"; 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 { export function ssr(ctx: CtxInit): Cmd {
return async () => { return async () => {
const editor = vscode.window.activeTextEditor; const editor = vscode.window.activeTextEditor;

View file

@ -7,6 +7,7 @@ import { Config, prepareVSCodeConfig } from "./config";
import { createClient } from "./client"; import { createClient } from "./client";
import { import {
executeDiscoverProject, executeDiscoverProject,
isDocumentInWorkspace,
isRustDocument, isRustDocument,
isRustEditor, isRustEditor,
LazyOutputChannel, LazyOutputChannel,
@ -14,6 +15,13 @@ import {
RustEditor, RustEditor,
} from "./util"; } from "./util";
import { ServerStatusParams } from "./lsp_ext"; import { ServerStatusParams } from "./lsp_ext";
import {
Dependency,
DependencyFile,
RustDependenciesProvider,
DependencyId,
} from "./dependencies_provider";
import { execRevealDependency } from "./commands";
import { PersistentState } from "./persistent_state"; import { PersistentState } from "./persistent_state";
import { bootstrap } from "./bootstrap"; import { bootstrap } from "./bootstrap";
import { ExecOptions } from "child_process"; import { ExecOptions } from "child_process";
@ -84,11 +92,21 @@ export class Ctx {
private commandFactories: Record<string, CommandFactory>; private commandFactories: Record<string, CommandFactory>;
private commandDisposables: Disposable[]; private commandDisposables: Disposable[];
private unlinkedFiles: vscode.Uri[]; private unlinkedFiles: vscode.Uri[];
private _dependencies: RustDependenciesProvider | undefined;
private _treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> | undefined;
get client() { get client() {
return this._client; return this._client;
} }
get treeView() {
return this._treeView;
}
get dependencies() {
return this._dependencies;
}
constructor( constructor(
readonly extCtx: vscode.ExtensionContext, readonly extCtx: vscode.ExtensionContext,
commandFactories: Record<string, CommandFactory>, commandFactories: Record<string, CommandFactory>,
@ -101,7 +119,6 @@ export class Ctx {
this.commandDisposables = []; this.commandDisposables = [];
this.commandFactories = commandFactories; this.commandFactories = commandFactories;
this.unlinkedFiles = []; this.unlinkedFiles = [];
this.state = new PersistentState(extCtx.globalState); this.state = new PersistentState(extCtx.globalState);
this.config = new Config(extCtx); this.config = new Config(extCtx);
@ -246,6 +263,53 @@ export class Ctx {
} }
await client.start(); await client.start();
this.updateCommands(); 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() { async restart() {
@ -348,6 +412,7 @@ export class Ctx {
statusBar.color = undefined; statusBar.color = undefined;
statusBar.backgroundColor = undefined; statusBar.backgroundColor = undefined;
statusBar.command = "rust-analyzer.stopServer"; statusBar.command = "rust-analyzer.stopServer";
this.dependencies?.refresh();
break; break;
case "warning": case "warning":
if (status.message) { if (status.message) {
@ -410,4 +475,5 @@ export class Ctx {
export interface Disposable { export interface Disposable {
dispose(): void; dispose(): void;
} }
export type Cmd = (...args: any[]) => unknown; export type Cmd = (...args: any[]) => unknown;

View file

@ -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 };

View file

@ -70,6 +70,38 @@ export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void>
export type AnalyzerStatusParams = { textDocument?: lc.TextDocumentIdentifier }; 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 = { export type ExpandMacroParams = {
textDocument: lc.TextDocumentIdentifier; textDocument: lc.TextDocumentIdentifier;
position: lc.Position; position: lc.Position;

View file

@ -190,5 +190,6 @@ function createCommands(): Record<string, CommandFactory> {
showReferences: { enabled: commands.showReferences }, showReferences: { enabled: commands.showReferences },
triggerParameterHints: { enabled: commands.triggerParameterHints }, triggerParameterHints: { enabled: commands.triggerParameterHints },
openLogs: { enabled: commands.openLogs }, openLogs: { enabled: commands.openLogs },
revealDependency: { enabled: commands.revealDependency },
}; };
} }

View file

@ -112,6 +112,19 @@ export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
return isRustDocument(editor.document); 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 { export function isValidExecutable(path: string): boolean {
log.debug("Checking availability of a binary at", path); log.debug("Checking availability of a binary at", path);