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