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+ }
0 commit comments