Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow viewing the crate graph in a webview #8801

Merged
merged 6 commits into from
May 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ide/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
5 changes: 5 additions & 0 deletions crates/ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mod syntax_tree;
mod typing;
mod markdown_remove;
mod doc_links;
mod view_crate_graph;

use std::sync::Arc;

Expand Down Expand Up @@ -287,6 +288,10 @@ impl Analysis {
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))
}
Expand Down
111 changes: 111 additions & 0 deletions crates/ide/src/view_crate_graph.rs
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to consider investigating the different layout options. For example dot -Kneato. (all options for dot -K are: circo dot fdp neato nop nop1 nop2 osage patchwork sfdp twopi) Be aware that some option are extremely slow for large graphs though. You can also use options like -Gnslimit=1, -Glheight=1000 and -Gmaxiter=1 to increase the svg height for clearer graphs or reduce the amount of effort used to layout, increasing speed.

let child = Command::new("dot")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the layering is slightly off here, we generally don’t do IO, including IPC, in the ide layer and below. I think it’d be more orthogonal to return a string in a .dot format from here, and let the client (rust-analyzer binary) render it to svg.

.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())
}
}
6 changes: 6 additions & 0 deletions crates/rust-analyzer/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions crates/rust-analyzer/src/lsp_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ impl GlobalState {
.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)
Expand Down
12 changes: 11 additions & 1 deletion docs/dev/lsp-extensions.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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`
Expand Down
5 changes: 5 additions & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions editors/code/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 2 additions & 0 deletions editors/code/src/lsp_ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions editors/code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down