Skip to content

Commit 9520bda

Browse files
committed
add graphviz-based code map
1 parent 4c4b20a commit 9520bda

File tree

4 files changed

+119
-4
lines changed

4 files changed

+119
-4
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ This is a VSCode extension containing developer utilities built on the [DebugAda
88

99
Whenever the debugger stops execution the elapsed time since the last stop is displayed after the new code line.
1010

11+
## [CodeMaps](https://docs.microsoft.com/en-us/visualstudio/modeling/use-code-maps-to-debug-your-applications)
12+
13+
![screenshot](https://user-images.githubusercontent.com/404623/183659131-d23fc42b-ecb8-480a-a10b-2a8128a05e4c.gif)
14+
15+
The extension adds a command to open a code map which gets incrementally built while stepping through the code.
16+
1117
## Known Issues
1218

1319
- It has only been tested with the C++ and C# debuggers.

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "vscode-debug-utils",
3-
"displayName": "VSCode PerfTips",
3+
"displayName": "VSCode PerfTips/CodeMap",
44
"author": {
55
"name": "Andreas Hollandt"
66
},
77
"repository": {
88
"url": "https://github.com/Trass3r/vscode-debug-utils"
99
},
10-
"description": "Implements debugger PerfTips as known from Visual Studio",
10+
"description": "Implements debugger PerfTips and CodeMap as known from Visual Studio",
1111
"publisher": "trass3r",
1212
"license": "MIT",
1313
"version": "0.0.3",
@@ -33,7 +33,11 @@
3333
"test": "node ./out/test/runTest.js"
3434
},
3535
"extensionDependencies": [
36+
"tintinweb.graphviz-interactive-preview"
3637
],
38+
"dependencies": {
39+
"ts-graphviz": "^0.16.0"
40+
},
3741
"devDependencies": {
3842
"@types/glob": "^7.2.0",
3943
"@types/mocha": "^9.1.1",

src/codemap.ts

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

src/extension.ts

Lines changed: 14 additions & 2 deletions
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)