Skip to content

Commit 5cadeb6

Browse files
committed
add graphviz-based code map
1 parent bd57dc1 commit 5cadeb6

File tree

3 files changed

+104
-3
lines changed

3 files changed

+104
-3
lines changed

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@
3333
"test": "node ./out/test/runTest.js"
3434
},
3535
"extensionDependencies": [
36-
"eamodio.gitlens"
36+
"eamodio.gitlens",
37+
"tintinweb.graphviz-interactive-preview"
3738
],
39+
"dependencies": {
40+
"ts-graphviz": "^0.13.1"
41+
},
3842
"devDependencies": {
3943
"@types/glob": "^7.1.3",
4044
"@types/mocha": "^8.0.0",

src/codemap.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as vs from 'vscode';
2+
import { DebugProtocol as dap } from 'vscode-debugprotocol';
3+
import * as gv from "ts-graphviz";
4+
5+
export class CodeMapProvider implements vs.DebugAdapterTracker, vs.TextDocumentContentProvider {
6+
dotDocument?: vs.TextDocument;
7+
graph: gv.Digraph;
8+
9+
constructor() {
10+
this.graph = gv.digraph('Call Graph', { splines: true });
11+
}
12+
13+
provideTextDocumentContent(_uri: vs.Uri, _token: vs.CancellationToken): vs.ProviderResult<string> {
14+
return gv.toDot(this.graph);
15+
}
16+
17+
onDidChangeEmitter = new vs.EventEmitter<vs.Uri>();
18+
19+
async onWillStartSession() {
20+
this.dotDocument = await vs.workspace.openTextDocument(vs.Uri.parse('dot:1.dot', true));
21+
this.graph = gv.digraph('Call Graph', { splines: true }); // reset the graph
22+
23+
// used for debugging
24+
vs.window.showTextDocument(this.dotDocument, { preview: false });
25+
26+
const args = {
27+
document: this.dotDocument,
28+
callback: (webpanel: any) => {
29+
// The callback function receives the newly created webPanel.
30+
// Overload webPanel.handleMessage(message) to receive message events like onClick and onDblClick
31+
//console.log(JSON.stringify(webpanel, undefined, 2));
32+
}
33+
};
34+
35+
vs.commands.executeCommand("graphviz-interactive-preview.preview.beside", args);
36+
}
37+
38+
onWillStopSession() {
39+
this.dotDocument = undefined;
40+
this.graph.clear();
41+
}
42+
43+
private getOrCreateNode(name: string) {
44+
return this.graph.getNode(name) ?? this.graph.createNode(name, { shape: "box" });
45+
}
46+
47+
private static wordwrap(str : string, width : number, brk : string = '\n', cut : boolean = false) {
48+
if (!str)
49+
return str;
50+
51+
const regex = '.{1,' + width + '}(\s|$)' + (cut ? '|.{' + width + '}|.+$' : '|\S+?(\s|$)');
52+
return str.match(RegExp(regex, 'g'))!.join(brk);
53+
}
54+
55+
private async onStackTraceResponse(r: dap.StackTraceResponse) {
56+
if (!r.success || r.body.stackFrames.length < 1)
57+
return;
58+
59+
for (const f of this.graph.nodes)
60+
f.attributes.delete("color");
61+
62+
let lastNode = this.getOrCreateNode(CodeMapProvider.wordwrap(r.body.stackFrames[0].name, 64));
63+
lastNode.attributes.set("color", "red");
64+
for (let i = 1; i < r.body.stackFrames.length; ++i) {
65+
const nodeName = CodeMapProvider.wordwrap(r.body.stackFrames[i].name, 64);
66+
const node = this.getOrCreateNode(nodeName);
67+
if (!this.graph.edges.find(e => {
68+
return (e.targets[0] as gv.INode).id === nodeName &&
69+
(e.targets[1] as gv.INode).id === lastNode.id;
70+
}))
71+
this.graph.createEdge([node, lastNode]);
72+
lastNode = node;
73+
}
74+
this.onDidChangeEmitter.fire(this.dotDocument!.uri);
75+
}
76+
77+
// @override
78+
onDidSendMessage(msg: dap.ProtocolMessage) {
79+
console.log(`< ${typeof msg} ${JSON.stringify(msg, undefined, 2)}`);
80+
if (msg.type !== "response" || (msg as dap.Response).command !== "stackTrace")
81+
return;
82+
83+
this.onStackTraceResponse(msg as dap.StackTraceResponse);
84+
}
85+
}

src/extension.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import * as vs from 'vscode';
22
import { PerfTipsProvider } from "./perftips";
3+
import { CodeMapProvider } from "./codemap";
34

45
// this method is called when your extension is activated
56
export function activate(context: vs.ExtensionContext) {
6-
const tracker = new PerfTipsProvider();
7+
const perftips = new PerfTipsProvider();
78
let disposable = vs.debug.registerDebugAdapterTrackerFactory("*", {
89
createDebugAdapterTracker(_session: vs.DebugSession) {
9-
return tracker;
10+
return perftips;
1011
}
1112
});
1213
context.subscriptions.push(disposable);
1314

15+
const codemap = new CodeMapProvider();
16+
disposable = vs.debug.registerDebugAdapterTrackerFactory("*", {
17+
createDebugAdapterTracker(_session: vs.DebugSession) {
18+
return codemap;
19+
}
20+
});
21+
context.subscriptions.push(disposable);
22+
23+
disposable = vs.workspace.registerTextDocumentContentProvider("dot", codemap);
24+
context.subscriptions.push(disposable);
25+
1426
const logDAP = vs.workspace.getConfiguration('debug-utils').get('logDAP');
1527
if (logDAP) {
1628
const outputChannel = vs.window.createOutputChannel("PerfTips");

0 commit comments

Comments
 (0)