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 <jonasschievink@gmail.com>
This commit is contained in:
bors[bot] 2021-05-11 18:02:31 +00:00 committed by GitHub
commit e290891dd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 166 additions and 1 deletions

7
Cargo.lock generated
View File

@ -319,6 +319,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4b29f4b9bb94bf267d57269fd0706d343a160937108e9619fe380645428abb"
[[package]]
name = "dot"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74b6c4d4a1cff5f454164363c16b72fa12463ca6b31f4b5f2035a65fa3d5906"
[[package]]
name = "drop_bomb"
version = "0.1.5"
@ -588,6 +594,7 @@ version = "0.0.0"
dependencies = [
"cfg",
"cov-mark",
"dot",
"either",
"expect-test",
"hir",

View File

@ -20,6 +20,7 @@ oorandom = "11.1.2"
pulldown-cmark-to-cmark = "6.0.0"
pulldown-cmark = { version = "0.8.0", default-features = false }
url = "2.1.1"
dot = "0.1.4"
stdx = { path = "../stdx", version = "0.0.0" }
syntax = { path = "../syntax", version = "0.0.0" }

View File

@ -49,6 +49,7 @@ macro_rules! eprintln {
mod typing;
mod markdown_remove;
mod doc_links;
mod view_crate_graph;
use std::sync::Arc;
@ -287,6 +288,10 @@ pub fn view_hir(&self, position: FilePosition) -> Cancelable<String> {
self.with_db(|db| view_hir::view_hir(&db, position))
}
pub fn view_crate_graph(&self) -> Cancelable<Result<String, String>> {
self.with_db(|db| view_crate_graph::view_crate_graph(&db))
}
pub fn expand_macro(&self, position: FilePosition) -> Cancelable<Option<ExpandedMacro>> {
self.with_db(|db| expand_macro::expand_macro(db, position))
}

View File

@ -0,0 +1,111 @@
use std::{
error::Error,
io::{Read, Write},
process::{Command, Stdio},
sync::Arc,
};
use dot::{Id, LabelText};
use ide_db::{
base_db::{CrateGraph, CrateId, Dependency, SourceDatabase, SourceDatabaseExt},
RootDatabase,
};
use rustc_hash::FxHashSet;
// Feature: View Crate Graph
//
// Renders the currently loaded crate graph as an SVG graphic. Requires the `dot` tool, which
// is part of graphviz, to be installed.
//
// Only workspace crates are included, no crates.io dependencies or sysroot crates.
//
// |===
// | Editor | Action Name
//
// | VS Code | **Rust Analyzer: View Crate Graph**
// |===
pub(crate) fn view_crate_graph(db: &RootDatabase) -> Result<String, String> {
let crate_graph = db.crate_graph();
let crates_to_render = crate_graph
.iter()
.filter(|krate| {
// Only render workspace crates
let root_id = db.file_source_root(crate_graph[*krate].root_file_id);
!db.source_root(root_id).is_library
})
.collect();
let graph = DotCrateGraph { graph: crate_graph, crates_to_render };
let mut dot = Vec::new();
dot::render(&graph, &mut dot).unwrap();
render_svg(&dot).map_err(|e| e.to_string())
}
fn render_svg(dot: &[u8]) -> Result<String, Box<dyn Error>> {
// We shell out to `dot` to render to SVG, as there does not seem to be a pure-Rust renderer.
let child = Command::new("dot")
.arg("-Tsvg")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.map_err(|err| format!("failed to spawn `dot`: {}", err))?;
child.stdin.unwrap().write_all(&dot)?;
let mut svg = String::new();
child.stdout.unwrap().read_to_string(&mut svg)?;
Ok(svg)
}
struct DotCrateGraph {
graph: Arc<CrateGraph>,
crates_to_render: FxHashSet<CrateId>,
}
type Edge<'a> = (CrateId, &'a Dependency);
impl<'a> dot::GraphWalk<'a, CrateId, Edge<'a>> for DotCrateGraph {
fn nodes(&'a self) -> dot::Nodes<'a, CrateId> {
self.crates_to_render.iter().copied().collect()
}
fn edges(&'a self) -> dot::Edges<'a, Edge<'a>> {
self.crates_to_render
.iter()
.flat_map(|krate| {
self.graph[*krate]
.dependencies
.iter()
.filter(|dep| self.crates_to_render.contains(&dep.crate_id))
.map(move |dep| (*krate, dep))
})
.collect()
}
fn source(&'a self, edge: &Edge<'a>) -> CrateId {
edge.0
}
fn target(&'a self, edge: &Edge<'a>) -> CrateId {
edge.1.crate_id
}
}
impl<'a> dot::Labeller<'a, CrateId, Edge<'a>> for DotCrateGraph {
fn graph_id(&'a self) -> Id<'a> {
Id::new("rust_analyzer_crate_graph").unwrap()
}
fn node_id(&'a self, n: &CrateId) -> Id<'a> {
Id::new(format!("_{}", n.0)).unwrap()
}
fn node_shape(&'a self, _node: &CrateId) -> Option<LabelText<'a>> {
Some(LabelText::LabelStr("box".into()))
}
fn node_label(&'a self, n: &CrateId) -> LabelText<'a> {
let name = self.graph[*n].display_name.as_ref().map_or("(unnamed crate)", |name| &*name);
LabelText::LabelStr(name.into())
}
}

View File

@ -117,6 +117,12 @@ pub(crate) fn handle_view_hir(
Ok(res)
}
pub(crate) fn handle_view_crate_graph(snap: GlobalStateSnapshot, (): ()) -> Result<String> {
let _p = profile::span("handle_view_crate_graph");
let res = snap.analysis.view_crate_graph()??;
Ok(res)
}
pub(crate) fn handle_expand_macro(
snap: GlobalStateSnapshot,
params: lsp_ext::ExpandMacroParams,

View File

@ -61,6 +61,14 @@ impl Request for ViewHir {
const METHOD: &'static str = "rust-analyzer/viewHir";
}
pub enum ViewCrateGraph {}
impl Request for ViewCrateGraph {
type Params = ();
type Result = String;
const METHOD: &'static str = "rust-analyzer/viewCrateGraph";
}
pub enum ExpandMacro {}
impl Request for ExpandMacro {

View File

@ -513,6 +513,7 @@ fn on_request(&mut self, request_received: Instant, req: Request) -> Result<()>
.on::<lsp_ext::AnalyzerStatus>(handlers::handle_analyzer_status)
.on::<lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
.on::<lsp_ext::ViewHir>(handlers::handle_view_hir)
.on::<lsp_ext::ViewCrateGraph>(handlers::handle_view_crate_graph)
.on::<lsp_ext::ExpandMacro>(handlers::handle_expand_macro)
.on::<lsp_ext::ParentModule>(handlers::handle_parent_module)
.on::<lsp_ext::Runnables>(handlers::handle_runnables)

View File

@ -1,5 +1,5 @@
<!---
lsp_ext.rs hash: 28a9d5a24b7ca396
lsp_ext.rs hash: 6e57fc1b345b00e9
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:
@ -486,6 +486,16 @@ Primarily for debugging, but very useful for all people working on rust-analyzer
Returns a textual representation of the HIR of the function containing the cursor.
For debugging or when working on rust-analyzer itself.
## View Crate Graph
**Method:** `rust-analyzer/viewCrateGraph`
**Request:** `null`
**Response:** `string`
Renders rust-analyzer's crate graph as an SVG image.
## Expand Macro
**Method:** `rust-analyzer/expandMacro`

View File

@ -109,6 +109,11 @@
"title": "View Hir",
"category": "Rust Analyzer"
},
{
"command": "rust-analyzer.viewCrateGraph",
"title": "View Crate Graph",
"category": "Rust Analyzer"
},
{
"command": "rust-analyzer.expandMacro",
"title": "Expand macro recursively",

View File

@ -429,6 +429,14 @@ export function viewHir(ctx: Ctx): Cmd {
};
}
export function viewCrateGraph(ctx: Ctx): Cmd {
return async () => {
const panel = vscode.window.createWebviewPanel("rust-analyzer.crate-graph", "rust-analyzer crate graph", vscode.ViewColumn.Two);
const svg = await ctx.client.sendRequest(ra.viewCrateGraph);
panel.webview.html = svg;
};
}
// Opens the virtual file that will show the syntax tree
//
// The contents of the file come from the `TextDocumentContentProvider`

View File

@ -27,6 +27,8 @@ export const syntaxTree = new lc.RequestType<SyntaxTreeParams, string, void>("ru
export const viewHir = new lc.RequestType<lc.TextDocumentPositionParams, string, void>("rust-analyzer/viewHir");
export const viewCrateGraph = new lc.RequestType0<string, void>("rust-analyzer/viewCrateGraph");
export interface ExpandMacroParams {
textDocument: lc.TextDocumentIdentifier;
position: lc.Position;

View File

@ -106,6 +106,7 @@ async function tryActivate(context: vscode.ExtensionContext) {
ctx.registerCommand('parentModule', commands.parentModule);
ctx.registerCommand('syntaxTree', commands.syntaxTree);
ctx.registerCommand('viewHir', commands.viewHir);
ctx.registerCommand('viewCrateGraph', commands.viewCrateGraph);
ctx.registerCommand('expandMacro', commands.expandMacro);
ctx.registerCommand('run', commands.run);
ctx.registerCommand('copyRunCommandLine', commands.copyRunCommandLine);