mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-08-19 01:50:32 +00:00
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:  Any feedback is welcome since i have basically no professional experience with TS.
This commit is contained in:
commit
a48e0e14e1
17 changed files with 522 additions and 13 deletions
37
crates/ide/src/fetch_crates.rs
Normal file
37
crates/ide/src/fetch_crates.rs
Normal 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())
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
144
editors/code/src/dependencies_provider.ts
Normal file
144
editors/code/src/dependencies_provider.ts
Normal 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 };
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue