9828: Remove dependency on the system graphviz when rendering crate graph r=lnicola a=p32blo


This PR removes the need for having `graphviz` installed on the user system by using the `d3-graphviz` npm package.

The responsibility of rendering the svg output is moved to the extension while the rust side only handles the generation of the dot file.

This change also brings the following additional features:
- Allow zooming the view
- Ctrl+click to reset the zoom 
- Adjust the color scheme to dark themes
- Works on any platform without installing graphviz locally

---

I’m not sure if this fits what you had in mind for the crates graph feature but I decided to submit it anyway to see if this is useful to anyone else. 

A potential downside might be that it increases the extension size ( haven’t checked) but this feature already required the installation of graphviz on the user side, so the cost is just moved explicitly to the extension.

Feel free to make any suggestion or comments. 

Co-authored-by: André Oliveira <p32blo@gmail.com>
This commit is contained in:
bors[bot] 2021-08-11 08:30:53 +00:00 committed by GitHub
commit 6c80c42c57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1025 additions and 19 deletions

View File

@ -3,8 +3,8 @@
//! `ide` crate.
use std::{
io::{Read, Write as _},
process::{self, Command, Stdio},
io::Write as _,
process::{self, Stdio},
};
use ide::{
@ -132,19 +132,7 @@ pub(crate) fn handle_view_crate_graph(
) -> Result<String> {
let _p = profile::span("handle_view_crate_graph");
let dot = snap.analysis.view_crate_graph(params.full)??;
// 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.as_bytes())?;
let mut svg = String::new();
child.stdout.unwrap().read_to_string(&mut svg)?;
Ok(svg)
Ok(dot)
}
pub(crate) fn handle_expand_macro(

View File

@ -3,6 +3,10 @@
!out
out/**
!out/src
!node_modules/d3/dist/d3.min.js
!node_modules/@hpcc-js/wasm/dist/index.min.js
!node_modules/@hpcc-js/wasm/dist/graphvizlib.wasm
!node_modules/d3-graphviz/build/d3-graphviz.min.js
!package.json
!package-lock.json
!ra_syntax_tree.tmGrammar.json

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,9 @@
"dependencies": {
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.1",
"vscode-languageclient": "^7.1.0-next.5"
"vscode-languageclient": "^7.1.0-next.5",
"d3": "^7.0.0",
"d3-graphviz": "^4.0.0"
},
"devDependencies": {
"@types/glob": "^7.1.4",

View File

@ -1,6 +1,7 @@
import * as vscode from 'vscode';
import * as lc from 'vscode-languageclient';
import * as ra from './lsp_ext';
import * as path from 'path';
import { Ctx, Cmd } from './ctx';
import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets';
@ -472,12 +473,59 @@ export function viewItemTree(ctx: Ctx): Cmd {
function crateGraph(ctx: Ctx, full: boolean): Cmd {
return async () => {
const panel = vscode.window.createWebviewPanel("rust-analyzer.crate-graph", "rust-analyzer crate graph", vscode.ViewColumn.Two);
const nodeModulesPath = vscode.Uri.file(path.join(ctx.extensionPath, "node_modules"));
const panel = vscode.window.createWebviewPanel("rust-analyzer.crate-graph", "rust-analyzer crate graph", vscode.ViewColumn.Two, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [nodeModulesPath]
});
const params = {
full: full,
};
const svg = await ctx.client.sendRequest(ra.viewCrateGraph, params);
panel.webview.html = svg;
const dot = await ctx.client.sendRequest(ra.viewCrateGraph, params);
const uri = panel.webview.asWebviewUri(nodeModulesPath);
const html = `
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
/* Fill the entire view */
html, body { margin:0; padding:0; overflow:hidden }
svg { position:fixed; top:0; left:0; height:100%; width:100% }
/* Disable the graphviz backgroud and fill the polygons */
.graph > polygon { display:none; }
:is(.node,.edge) polygon { fill: white; }
/* Invert the line colours for dark themes */
body:not(.vscode-light) .edge path { stroke: white; }
</style>
</head>
<body>
<script type="text/javascript" src="${uri}/d3/dist/d3.min.js"></script>
<script type="javascript/worker" src="${uri}/@hpcc-js/wasm/dist/index.min.js"></script>
<script type="text/javascript" src="${uri}/d3-graphviz/build/d3-graphviz.min.js"></script>
<div id="graph"></div>
<script>
let graph = d3.select("#graph")
.graphviz()
.fit(true)
.zoomScaleExtent([0.1, Infinity])
.renderDot(\`${dot}\`);
d3.select(window).on("click", (event) => {
if (event.ctrlKey) {
graph.resetZoom(d3.transition().duration(100));
}
});
</script>
</body>
`;
panel.webview.html = html;
};
}

View File

@ -68,6 +68,10 @@ export class Ctx {
this.pushCleanup(d);
}
get extensionPath(): string {
return this.extCtx.extensionPath;
}
get globalState(): vscode.Memento {
return this.extCtx.globalState;
}