mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-08-19 10:00:27 +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 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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
))
|
))
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
|
@ -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",
|
||||||
|
|
|
@ -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\
|
||||||
|
*  |-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 {
|
export function ssr(ctx: CtxInit): Cmd {
|
||||||
return async () => {
|
return async () => {
|
||||||
const editor = vscode.window.activeTextEditor;
|
const editor = vscode.window.activeTextEditor;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
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 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;
|
||||||
|
|
|
@ -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 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue