Skip to content
Draft
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
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
//"debugWebWorkerHost": true,
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
"--extensionDevelopmentPath=${workspaceFolder}",
//"--extensionDevelopmentKind=web"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ This is a VSCode extension containing developer utilities built on the [DebugAda

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

## [CodeMaps](https://docs.microsoft.com/en-us/visualstudio/modeling/use-code-maps-to-debug-your-applications)

![screenshot](https://user-images.githubusercontent.com/404623/183659131-d23fc42b-ecb8-480a-a10b-2a8128a05e4c.gif)

The extension adds a command to open a code map which gets incrementally built while stepping through the code.

## Known Issues

- It has only been tested with the C++ and C# debuggers.
Expand Down
29 changes: 23 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "vscode-debug-utils",
"displayName": "VSCode PerfTips",
"displayName": "VSCode PerfTips/CodeMap",
"author": {
"name": "Andreas Hollandt"
},
"repository": {
"url": "https://github.com/Trass3r/vscode-debug-utils"
},
"description": "Implements debugger PerfTips as known from Visual Studio",
"description": "Implements debugger PerfTips and CodeMap as known from Visual Studio",
"publisher": "trass3r",
"license": "MIT",
"version": "0.0.3",
Expand All @@ -23,17 +23,26 @@
],
"main": "./out/extension.js",
"scripts": {
"bundle": "./node_modules/.bin/esbuild src/extension.ts --bundle --target=es2018 --minify --sourcemap --external:vscode --outfile=./out/extension.js",
"vscode:prepublish": "npm run compile",
"package": "node_modules/.bin/vsce package",
"bundle": "esbuild src/extension.ts --bundle --target=es2018 --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
"vscode:prepublish": "npm run bundle -- --minify",
"esbuild": "npm run bundle -- --sourcemap",
"esbuild-watch": "npm run bundle -- --sourcemap --watch",
"esbuild-web": "npm run bundle -- --sourcemap --platform=browser",
"package": "vsce package",
"publish": "vsce publish",
"compile": "tsc -p ./",
"lint": "eslint src --ext ts",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"test": "node ./out/test/runTest.js"
"test": "node ./out/test/runTest.js",
"open-in-browser": "npm run esbuild-web && vscode-test-web --browserType=chromium --open-devtools --extensionDevelopmentPath=. --extensionId tintinweb.graphviz-interactive-preview --extensionId ms-vscode.mock-debug /tmp"
},
"extensionDependencies": [
"tintinweb.graphviz-interactive-preview"
],
"dependencies": {
"ts-graphviz": "^0.16.0"
},
"devDependencies": {
"@types/glob": "^7.2.0",
"@types/mocha": "^9.1.1",
Expand All @@ -43,6 +52,7 @@
"@typescript-eslint/parser": "^5.33.0",
"@vscode/debugprotocol": "^1.57.0",
"@vscode/test-electron": "^2.1.5",
"@vscode/test-web": "*",
"esbuild": "^0.14.54",
"eslint": "^8.21.0",
"glob": "^8.0.3",
Expand Down Expand Up @@ -80,6 +90,13 @@
"highContrast": "#99999999"
}
}
],
"commands": [
{
"command": "debug-utils.showCodeMap",
"title": "Build a Code Map during debugging",
"when": "inDebugMode"
}
]
}
}
129 changes: 129 additions & 0 deletions src/codemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as vs from 'vscode';
import { DebugProtocol as dap } from '@vscode/debugprotocol';
import * as gv from "ts-graphviz";

/** @sealed */
export class CodeMapProvider implements vs.DebugAdapterTracker, vs.TextDocumentContentProvider {
dotDocument?: vs.TextDocument;
graph: gv.Digraph;
active = false;

async activate() : Promise<void> {
this.graph = gv.digraph('Call Graph', { splines: true }); // reset the graph

this.dotDocument = await vs.workspace.openTextDocument(vs.Uri.parse('dot:callgraph.dot', true));
this.active = true;

// save the current editor
const activeEditor = vs.window.activeTextEditor;
// show the `dot` source
vs.window.showTextDocument(this.dotDocument, vs.ViewColumn.Beside, true);

const args = {
document: this.dotDocument,
callback: (panel: any /* PreviewPanel */) => {
const graphvizConfig = vs.workspace.getConfiguration('graphviz-interactive-preview');
const preserveFocus = graphvizConfig.get('preserveFocus');
if (preserveFocus)
return;

// we have to switch back to the original editor group to prevent issues
const webpanel: vs.WebviewPanel = panel.panel;
const disposable = webpanel.onDidChangeViewState(e => {
if (activeEditor)
vs.window.showTextDocument(activeEditor.document, activeEditor.viewColumn, false);
disposable.dispose();
});
// handle user closing the graphviz preview
webpanel.onDidDispose(e => {
this.active = false;
// there's no way to close a document, only this
// FIXME: if the editor is not in the active view column, this opens a new one and closes it
vs.window.showTextDocument(this.dotDocument!, vs.ViewColumn.Active, false)
.then(() => {
return vs.commands.executeCommand('workbench.action.closeActiveEditor');
});
this.graph.clear();
this.onDidChangeEmitter.fire(this.dotDocument!.uri);
this.dotDocument = undefined; // FIXME: does not delete the document
});
},
allowMultiplePanels: false,
title: 'Call Graph',
};

const graphvizConfig = vs.workspace.getConfiguration('graphviz-interactive-preview');
const openAutomatically = graphvizConfig.get('openAutomatically');
if (!openAutomatically)
vs.commands.executeCommand("graphviz-interactive-preview.preview.beside", args);
}

/** @override TextDocumentContentProvider */
provideTextDocumentContent(uri: vs.Uri, token: vs.CancellationToken): vs.ProviderResult<string> {
// here we update the source .dot document
if (uri.path != 'callgraph.dot')
return;
return gv.toDot(this.graph);
}

// https://code.visualstudio.com/api/extension-guides/virtual-documents#update-virtual-documents
onDidChangeEmitter = new vs.EventEmitter<vs.Uri>();
onDidChange = this.onDidChangeEmitter.event; // without this the emitter silently doesn't work

/** @override DebugAdapterTracker */
// onWillStartSession() {}

/** @override DebugAdapterTracker */
// onWillStopSession() {}

private getOrCreateNode(name: string) {
return this.graph.getNode(name) ?? this.graph.createNode(name, { shape: "box" });
}

private static wordwrap(str : string, width : number, brk : string = '\n', cut : boolean = false) {
if (!str)
return str;

const regex = '.{1,' + width + '}(\s|$)' + (cut ? '|.{' + width + '}|.+$' : '|\S+?(\s|$)');
return str.match(RegExp(regex, 'g'))!.join(brk);
}

private async onStackTraceResponse(r: dap.StackTraceResponse) {
if (!r.success || r.body.stackFrames.length < 1)
return;

let lastNode = this.getOrCreateNode(CodeMapProvider.wordwrap(r.body.stackFrames[0].name, 64));
// prevent re-rendering if we're still in the same function
if (lastNode.attributes.get("color"))
return;

// mark the current function with a red border
for (const f of this.graph.nodes)
f.attributes.delete("color");
lastNode.attributes.set("color", "red");

// walk up the stack and create nodes/edges
for (let i = 1; i < r.body.stackFrames.length; ++i) {
const nodeName = CodeMapProvider.wordwrap(r.body.stackFrames[i].name, 64);
const node = this.getOrCreateNode(nodeName);
if (!this.graph.edges.find(e => {
return (e.targets[0] as gv.INode).id === nodeName &&
(e.targets[1] as gv.INode).id === lastNode.id;
}))
this.graph.createEdge([node, lastNode]);
lastNode = node;
}
this.onDidChangeEmitter.fire(this.dotDocument!.uri);
}

/** @override DebugAdapterTracker */
onDidSendMessage(msg: dap.ProtocolMessage) {
if (!this.active)
return;

if (msg.type !== "response" || (msg as dap.Response).command !== "stackTrace")
return;

this.onStackTraceResponse(msg as dap.StackTraceResponse);
}
}
23 changes: 21 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import * as vs from 'vscode';
import { PerfTipsProvider } from "./perftips";
import { CodeMapProvider } from "./codemap";

// this method is called when your extension is activated
export function activate(context: vs.ExtensionContext) {
const tracker = new PerfTipsProvider();
const perftips = new PerfTipsProvider();
let disposable = vs.debug.registerDebugAdapterTrackerFactory("*", {
createDebugAdapterTracker(_session: vs.DebugSession) {
return tracker;
return perftips;
}
});
context.subscriptions.push(disposable);

// when the command is run a debug session is already active
// then it'd be too late to register the tracker, so do it eagerly
const codemap = new CodeMapProvider();
disposable = vs.commands.registerCommand("debug-utils.showCodeMap", () => {
codemap.activate();
});
context.subscriptions.push(disposable);

disposable = vs.debug.registerDebugAdapterTrackerFactory("*", {
createDebugAdapterTracker(_session: vs.DebugSession) {
return codemap; // null is also possible
}
});
context.subscriptions.push(disposable);

disposable = vs.workspace.registerTextDocumentContentProvider("dot", codemap);
context.subscriptions.push(disposable);

const logDAP = vs.workspace.getConfiguration('debug-utils').get('logDAP');
if (logDAP) {
const outputChannel = vs.window.createOutputChannel("PerfTips");
Expand Down