Skip to content

Commit e290891

Browse files
Merge #8801
8801: feat: Allow viewing the crate graph in a webview r=jonas-schievink a=jonas-schievink This uses `dot` to render the crate graph as an SVD file, and displays it in a VS Code panel. For simple crate graphs, it works quite well: ![screenshot-2021-05-11-16:19:32](https://user-images.githubusercontent.com/1786438/117831361-c4a48980-b274-11eb-9276-240cdf6919aa.png) Unfortunately, on rust-analyzer itself (and most medium-sized dependency graphs), `dot` runs for around a minute and then produces this mess: ![screenshot-2021-05-11-16:41:37](https://user-images.githubusercontent.com/1786438/117834831-c754ae00-b277-11eb-850b-138495dbeba8.png) Co-authored-by: Jonas Schievink <[email protected]>
2 parents 6afd9b2 + d1aa6bb commit e290891

File tree

12 files changed

+166
-1
lines changed

12 files changed

+166
-1
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ide/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ oorandom = "11.1.2"
2020
pulldown-cmark-to-cmark = "6.0.0"
2121
pulldown-cmark = { version = "0.8.0", default-features = false }
2222
url = "2.1.1"
23+
dot = "0.1.4"
2324

2425
stdx = { path = "../stdx", version = "0.0.0" }
2526
syntax = { path = "../syntax", version = "0.0.0" }

crates/ide/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ mod syntax_tree;
4949
mod typing;
5050
mod markdown_remove;
5151
mod doc_links;
52+
mod view_crate_graph;
5253

5354
use std::sync::Arc;
5455

@@ -287,6 +288,10 @@ impl Analysis {
287288
self.with_db(|db| view_hir::view_hir(&db, position))
288289
}
289290

291+
pub fn view_crate_graph(&self) -> Cancelable<Result<String, String>> {
292+
self.with_db(|db| view_crate_graph::view_crate_graph(&db))
293+
}
294+
290295
pub fn expand_macro(&self, position: FilePosition) -> Cancelable<Option<ExpandedMacro>> {
291296
self.with_db(|db| expand_macro::expand_macro(db, position))
292297
}

crates/ide/src/view_crate_graph.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use std::{
2+
error::Error,
3+
io::{Read, Write},
4+
process::{Command, Stdio},
5+
sync::Arc,
6+
};
7+
8+
use dot::{Id, LabelText};
9+
use ide_db::{
10+
base_db::{CrateGraph, CrateId, Dependency, SourceDatabase, SourceDatabaseExt},
11+
RootDatabase,
12+
};
13+
use rustc_hash::FxHashSet;
14+
15+
// Feature: View Crate Graph
16+
//
17+
// Renders the currently loaded crate graph as an SVG graphic. Requires the `dot` tool, which
18+
// is part of graphviz, to be installed.
19+
//
20+
// Only workspace crates are included, no crates.io dependencies or sysroot crates.
21+
//
22+
// |===
23+
// | Editor | Action Name
24+
//
25+
// | VS Code | **Rust Analyzer: View Crate Graph**
26+
// |===
27+
pub(crate) fn view_crate_graph(db: &RootDatabase) -> Result<String, String> {
28+
let crate_graph = db.crate_graph();
29+
let crates_to_render = crate_graph
30+
.iter()
31+
.filter(|krate| {
32+
// Only render workspace crates
33+
let root_id = db.file_source_root(crate_graph[*krate].root_file_id);
34+
!db.source_root(root_id).is_library
35+
})
36+
.collect();
37+
let graph = DotCrateGraph { graph: crate_graph, crates_to_render };
38+
39+
let mut dot = Vec::new();
40+
dot::render(&graph, &mut dot).unwrap();
41+
42+
render_svg(&dot).map_err(|e| e.to_string())
43+
}
44+
45+
fn render_svg(dot: &[u8]) -> Result<String, Box<dyn Error>> {
46+
// We shell out to `dot` to render to SVG, as there does not seem to be a pure-Rust renderer.
47+
let child = Command::new("dot")
48+
.arg("-Tsvg")
49+
.stdin(Stdio::piped())
50+
.stdout(Stdio::piped())
51+
.spawn()
52+
.map_err(|err| format!("failed to spawn `dot`: {}", err))?;
53+
child.stdin.unwrap().write_all(&dot)?;
54+
55+
let mut svg = String::new();
56+
child.stdout.unwrap().read_to_string(&mut svg)?;
57+
Ok(svg)
58+
}
59+
60+
struct DotCrateGraph {
61+
graph: Arc<CrateGraph>,
62+
crates_to_render: FxHashSet<CrateId>,
63+
}
64+
65+
type Edge<'a> = (CrateId, &'a Dependency);
66+
67+
impl<'a> dot::GraphWalk<'a, CrateId, Edge<'a>> for DotCrateGraph {
68+
fn nodes(&'a self) -> dot::Nodes<'a, CrateId> {
69+
self.crates_to_render.iter().copied().collect()
70+
}
71+
72+
fn edges(&'a self) -> dot::Edges<'a, Edge<'a>> {
73+
self.crates_to_render
74+
.iter()
75+
.flat_map(|krate| {
76+
self.graph[*krate]
77+
.dependencies
78+
.iter()
79+
.filter(|dep| self.crates_to_render.contains(&dep.crate_id))
80+
.map(move |dep| (*krate, dep))
81+
})
82+
.collect()
83+
}
84+
85+
fn source(&'a self, edge: &Edge<'a>) -> CrateId {
86+
edge.0
87+
}
88+
89+
fn target(&'a self, edge: &Edge<'a>) -> CrateId {
90+
edge.1.crate_id
91+
}
92+
}
93+
94+
impl<'a> dot::Labeller<'a, CrateId, Edge<'a>> for DotCrateGraph {
95+
fn graph_id(&'a self) -> Id<'a> {
96+
Id::new("rust_analyzer_crate_graph").unwrap()
97+
}
98+
99+
fn node_id(&'a self, n: &CrateId) -> Id<'a> {
100+
Id::new(format!("_{}", n.0)).unwrap()
101+
}
102+
103+
fn node_shape(&'a self, _node: &CrateId) -> Option<LabelText<'a>> {
104+
Some(LabelText::LabelStr("box".into()))
105+
}
106+
107+
fn node_label(&'a self, n: &CrateId) -> LabelText<'a> {
108+
let name = self.graph[*n].display_name.as_ref().map_or("(unnamed crate)", |name| &*name);
109+
LabelText::LabelStr(name.into())
110+
}
111+
}

crates/rust-analyzer/src/handlers.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ pub(crate) fn handle_view_hir(
117117
Ok(res)
118118
}
119119

120+
pub(crate) fn handle_view_crate_graph(snap: GlobalStateSnapshot, (): ()) -> Result<String> {
121+
let _p = profile::span("handle_view_crate_graph");
122+
let res = snap.analysis.view_crate_graph()??;
123+
Ok(res)
124+
}
125+
120126
pub(crate) fn handle_expand_macro(
121127
snap: GlobalStateSnapshot,
122128
params: lsp_ext::ExpandMacroParams,

crates/rust-analyzer/src/lsp_ext.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ impl Request for ViewHir {
6161
const METHOD: &'static str = "rust-analyzer/viewHir";
6262
}
6363

64+
pub enum ViewCrateGraph {}
65+
66+
impl Request for ViewCrateGraph {
67+
type Params = ();
68+
type Result = String;
69+
const METHOD: &'static str = "rust-analyzer/viewCrateGraph";
70+
}
71+
6472
pub enum ExpandMacro {}
6573

6674
impl Request for ExpandMacro {

crates/rust-analyzer/src/main_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ impl GlobalState {
513513
.on::<lsp_ext::AnalyzerStatus>(handlers::handle_analyzer_status)
514514
.on::<lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
515515
.on::<lsp_ext::ViewHir>(handlers::handle_view_hir)
516+
.on::<lsp_ext::ViewCrateGraph>(handlers::handle_view_crate_graph)
516517
.on::<lsp_ext::ExpandMacro>(handlers::handle_expand_macro)
517518
.on::<lsp_ext::ParentModule>(handlers::handle_parent_module)
518519
.on::<lsp_ext::Runnables>(handlers::handle_runnables)

docs/dev/lsp-extensions.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!---
2-
lsp_ext.rs hash: 28a9d5a24b7ca396
2+
lsp_ext.rs hash: 6e57fc1b345b00e9
33
44
If you need to change the above hash to make the test pass, please check if you
55
need to adjust this doc as well and ping this issue:
@@ -486,6 +486,16 @@ Primarily for debugging, but very useful for all people working on rust-analyzer
486486
Returns a textual representation of the HIR of the function containing the cursor.
487487
For debugging or when working on rust-analyzer itself.
488488

489+
## View Crate Graph
490+
491+
**Method:** `rust-analyzer/viewCrateGraph`
492+
493+
**Request:** `null`
494+
495+
**Response:** `string`
496+
497+
Renders rust-analyzer's crate graph as an SVG image.
498+
489499
## Expand Macro
490500

491501
**Method:** `rust-analyzer/expandMacro`

editors/code/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@
109109
"title": "View Hir",
110110
"category": "Rust Analyzer"
111111
},
112+
{
113+
"command": "rust-analyzer.viewCrateGraph",
114+
"title": "View Crate Graph",
115+
"category": "Rust Analyzer"
116+
},
112117
{
113118
"command": "rust-analyzer.expandMacro",
114119
"title": "Expand macro recursively",

editors/code/src/commands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,14 @@ export function viewHir(ctx: Ctx): Cmd {
429429
};
430430
}
431431

432+
export function viewCrateGraph(ctx: Ctx): Cmd {
433+
return async () => {
434+
const panel = vscode.window.createWebviewPanel("rust-analyzer.crate-graph", "rust-analyzer crate graph", vscode.ViewColumn.Two);
435+
const svg = await ctx.client.sendRequest(ra.viewCrateGraph);
436+
panel.webview.html = svg;
437+
};
438+
}
439+
432440
// Opens the virtual file that will show the syntax tree
433441
//
434442
// The contents of the file come from the `TextDocumentContentProvider`

0 commit comments

Comments
 (0)