@@ -2,8 +2,8 @@ import * as vscode from 'vscode';
22import { generateCallGraph } from './generateCallGraph' ;
33
44interface ArgusSettings {
5- includeAll : boolean ; // formerly --all
6- includeDeps : boolean ; // formerly --libs
5+ includeAll : boolean ; // formerly --all
6+ includeDeps : boolean ; // formerly --libs
77}
88
99/**
@@ -12,14 +12,14 @@ interface ArgusSettings {
1212 * Later we will integrate the real processing pipeline from processor.ts (processCompilerOutput) adapted for single-file focus.
1313 */
1414export class ArgusCallGraphEditorProvider implements vscode . CustomTextEditorProvider {
15- public static readonly viewType = 'recon.argusCallGraph' ;
15+ public static readonly viewType = 'recon.argusCallGraph' ;
1616
17- constructor ( private readonly context : vscode . ExtensionContext ) { }
17+ constructor ( private readonly context : vscode . ExtensionContext ) { }
1818
19- async resolveCustomTextEditor ( document : vscode . TextDocument , webviewPanel : vscode . WebviewPanel ) : Promise < void > {
20- webviewPanel . webview . options = {
21- enableScripts : true ,
22- } ;
19+ async resolveCustomTextEditor ( document : vscode . TextDocument , webviewPanel : vscode . WebviewPanel ) : Promise < void > {
20+ webviewPanel . webview . options = {
21+ enableScripts : true ,
22+ } ;
2323
2424 const settings : ArgusSettings = { includeAll : false , includeDeps : false } ;
2525 let genToken = 0 ;
@@ -33,39 +33,42 @@ export class ArgusCallGraphEditorProvider implements vscode.CustomTextEditorProv
3333 includeAll : settings . includeAll ,
3434 includeDeps : settings . includeDeps
3535 } ) ;
36- if ( token !== genToken ) return ; // stale generation
36+ if ( token !== genToken ) { return ; } // stale generation
3737 lastPrimaryContract = result . primaryContractName || lastPrimaryContract ;
38- webviewPanel . webview . html = this . getHtml ( webviewPanel . webview , document , settings , result . html ) ;
38+ webviewPanel . webview . html = this . getHtml ( webviewPanel . webview , document , settings , result . html ) ;
3939 } ;
4040 const scheduleUpdate = debounce ( updateWebview , 300 ) ;
4141
42- // Listen for document changes to refresh preview (future: incremental regen)
43- const changeSub = vscode . workspace . onDidChangeTextDocument ( e => {
44- if ( e . document . uri . toString ( ) === document . uri . toString ( ) ) {
42+ // Listen for document changes to refresh preview (future: incremental regen)
43+ const changeSub = vscode . workspace . onDidChangeTextDocument ( e => {
44+ if ( e . document . uri . toString ( ) === document . uri . toString ( ) ) {
4545 scheduleUpdate ( ) ;
46- }
47- } ) ;
48- webviewPanel . onDidDispose ( ( ) => changeSub . dispose ( ) ) ;
46+ }
47+ } ) ;
48+ webviewPanel . onDidDispose ( ( ) => changeSub . dispose ( ) ) ;
4949
50- // Handle messages from the webview
51- webviewPanel . webview . onDidReceiveMessage ( msg => {
52- switch ( msg . type ) {
53- case 'updateSetting' :
54- if ( msg . key in settings ) {
50+ // Handle messages from the webview
51+ webviewPanel . webview . onDidReceiveMessage ( msg => {
52+ switch ( msg . type ) {
53+ case 'updateSetting' :
54+ if ( msg . key in settings ) {
5555 ( settings as any ) [ msg . key ] = ! ! msg . value ;
5656 scheduleUpdate ( ) ;
57- }
58- break ;
57+ }
58+ break ;
5959 case 'runBuild' :
6060 // Show interim building message
6161 webviewPanel . webview . postMessage ?.( { } ) ; // no-op safeguard
62- webviewPanel . webview . html = `<div style="font-family:var(--vscode-font-family);padding:16px;">` +
63- `<strong>Building project (forge build --build-info)...</strong><br/><br/>` +
64- `Open the <em>Recon</em> output channel to watch progress. The call graph will refresh automatically when done.` +
62+ webviewPanel . webview . html = `<div style="font-family:var(--vscode-font-family);padding:16px;">` +
63+ `<strong>Building project (forge build --build-info)...</strong><br/><br/>` +
64+ `Open the <em>Recon</em> output channel to watch progress. The call graph will refresh automatically when done.` +
6565 `</div>` ;
66- vscode . commands . executeCommand ( 'recon.buildWithInfo' ) . then ( ( ) => {
67- scheduleUpdate ( ) ;
68- } ) ;
66+ // Await the build command; our command now returns a promise that resolves when build finishes
67+ Promise . resolve ( vscode . commands . executeCommand ( 'recon.buildWithInfo' ) )
68+ . finally ( ( ) => {
69+ // Refresh regardless of success/failure/cancel so the page doesn't get stuck
70+ scheduleUpdate ( ) ;
71+ } ) ;
6972 break ;
7073 case 'copyToClipboard' :
7174 if ( typeof msg . text === 'string' && msg . text . length > 0 ) {
@@ -95,25 +98,25 @@ export class ArgusCallGraphEditorProvider implements vscode.CustomTextEditorProv
9598 : pathMod . dirname ( document . uri . fsPath ) ;
9699 const baseDirUri = vscode . Uri . file ( baseDirFs ) ;
97100 const inferredName = lastPrimaryContract ? `${ lastPrimaryContract } -callgraph.png` : 'callgraph.png' ;
98- const fileBase = ( suggested || inferredName ) . replace ( / [ ^ a - z 0 - 9 _ . - ] / gi, '_' ) ;
101+ const fileBase = ( suggested || inferredName ) . replace ( / [ ^ a - z 0 - 9 _ . - ] / gi, '_' ) ;
99102 let targetName = fileBase ;
100103 let attempt = 0 ;
101104 while ( attempt < 50 ) {
102105 const candidate = pathMod . join ( baseDirFs , targetName ) ;
103- console . log ( '[Argus] exportImage attempt' , attempt + 1 , 'candidate' , candidate ) ;
106+ console . log ( '[Argus] exportImage attempt' , attempt + 1 , 'candidate' , candidate ) ;
104107 if ( ! fs . existsSync ( candidate ) ) {
105108 const uri = vscode . Uri . file ( candidate ) ;
106109 await vscode . workspace . fs . writeFile ( uri , buffer ) ;
107110 const rel = workspaceRoot ? pathMod . relative ( workspaceRoot , uri . fsPath ) : uri . fsPath ;
108- vscode . window . showInformationMessage ( `Argus call graph image saved at workspace root: ${ rel } ` , 'Open' ) . then ( sel => {
111+ vscode . window . showInformationMessage ( `Argus call graph image saved at workspace root: ${ rel } ` , 'Open' ) . then ( sel => {
109112 if ( sel === 'Open' ) { vscode . commands . executeCommand ( 'vscode.open' , uri ) ; }
110113 } ) ;
111114 webviewPanel . webview . postMessage ( { type : 'exportImageResult' , ok : true , file : uri . fsPath } ) ;
112115 console . log ( '[Argus] exportImage success' , uri . fsPath ) ;
113116 return ;
114117 }
115118 attempt ++ ;
116- const stem = fileBase . replace ( / \. p n g $ / i, '' ) ;
119+ const stem = fileBase . replace ( / \. p n g $ / i, '' ) ;
117120 targetName = `${ stem } -${ attempt } .png` ;
118121 }
119122 vscode . window . showWarningMessage ( 'Unable to save image: too many existing versions.' ) ;
@@ -127,44 +130,44 @@ export class ArgusCallGraphEditorProvider implements vscode.CustomTextEditorProv
127130 } ) ( ) ;
128131 break ;
129132 }
130- }
131- } ) ;
133+ }
134+ } ) ;
132135
133136 updateWebview ( ) ;
134- }
137+ }
135138 private getLoadingHtml ( document : vscode . TextDocument , _settings : ArgusSettings ) : string {
136139 const fileName = vscode . workspace . asRelativePath ( document . uri ) ;
137140 return `<div style="font-family:var(--vscode-font-family);padding:16px;">Generating Argus Call Graph for <code>${ escapeHtml ( vscode . workspace . asRelativePath ( document . uri ) ) } </code>...</div>` ;
138141 }
139142
140143 private getHtml ( webview : vscode . Webview , document : vscode . TextDocument , settings : ArgusSettings , body : string ) : string {
141- const nonce = getNonce ( ) ;
142- const fileName = vscode . workspace . asRelativePath ( document . uri ) ;
143- const html2canvasUri = webview . asWebviewUri ( vscode . Uri . joinPath ( this . context . extensionUri , 'node_modules' , 'html2canvas' , 'dist' , 'html2canvas.min.js' ) ) ;
144+ const nonce = getNonce ( ) ;
145+ const fileName = vscode . workspace . asRelativePath ( document . uri ) ;
146+ const html2canvasUri = webview . asWebviewUri ( vscode . Uri . joinPath ( this . context . extensionUri , 'node_modules' , 'html2canvas' , 'dist' , 'html2canvas.min.js' ) ) ;
144147 // Extract inner <body> content if a full HTML document was returned to avoid nested <html> issues
145- let fragment = body ;
146- const bodyMatch = body . match ( / < b o d y [ ^ > ] * > ( [ \s \S ] * ?) < \/ b o d y > / i) ;
147- if ( bodyMatch ) fragment = bodyMatch [ 1 ] ;
148- // Collect any style tags from original HTML (head or body) to preserve design
149- const styleTags : string [ ] = [ ] ;
150- const styleRegex = / < s t y l e [ ^ > ] * > [ \s \S ] * ?< \/ s t y l e > / gi;
151- let m : RegExpExecArray | null ;
152- while ( ( m = styleRegex . exec ( body ) ) ) { styleTags . push ( m [ 0 ] ) ; }
153- const collectedStyles = styleTags . join ( '\n' ) ;
148+ let fragment = body ;
149+ const bodyMatch = body . match ( / < b o d y [ ^ > ] * > ( [ \s \S ] * ?) < \/ b o d y > / i) ;
150+ if ( bodyMatch ) { fragment = bodyMatch [ 1 ] ; }
151+ // Collect any style tags from original HTML (head or body) to preserve design
152+ const styleTags : string [ ] = [ ] ;
153+ const styleRegex = / < s t y l e [ ^ > ] * > [ \s \S ] * ?< \/ s t y l e > / gi;
154+ let m : RegExpExecArray | null ;
155+ while ( ( m = styleRegex . exec ( body ) ) ) { styleTags . push ( m [ 0 ] ) ; }
156+ const collectedStyles = styleTags . join ( '\n' ) ;
154157 // Ensure any <script> tags inside the fragment receive the nonce so CSP allows execution
155- const bodyWithNonce = fragment
156- . replace ( / < s c r i p t (? ! [ ^ > ] * n o n c e = ) / g, `<script nonce="${ nonce } "` )
157- . replace ( / < s t y l e (? ! [ ^ > ] * n o n c e = ) / g, `<style nonce="${ nonce } "` ) ;
158+ const bodyWithNonce = fragment
159+ . replace ( / < s c r i p t (? ! [ ^ > ] * n o n c e = ) / g, `<script nonce="${ nonce } "` )
160+ . replace ( / < s t y l e (? ! [ ^ > ] * n o n c e = ) / g, `<style nonce="${ nonce } "` ) ;
158161
159162 // Prism resource URIs (mirror working implementation in logToFoundryView)
160- const prismCore = webview . asWebviewUri ( vscode . Uri . joinPath ( this . context . extensionUri , 'node_modules' , 'prismjs' , 'prism.js' ) ) ;
161- const prismSolidity = webview . asWebviewUri ( vscode . Uri . joinPath ( this . context . extensionUri , 'node_modules' , 'prismjs' , 'components' , 'prism-solidity.min.js' ) ) ;
162- const prismTheme = webview . asWebviewUri ( vscode . Uri . joinPath ( this . context . extensionUri , 'node_modules' , 'prismjs' , 'themes' , 'prism-tomorrow.css' ) ) ;
163- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" />
163+ const prismCore = webview . asWebviewUri ( vscode . Uri . joinPath ( this . context . extensionUri , 'node_modules' , 'prismjs' , 'prism.js' ) ) ;
164+ const prismSolidity = webview . asWebviewUri ( vscode . Uri . joinPath ( this . context . extensionUri , 'node_modules' , 'prismjs' , 'components' , 'prism-solidity.min.js' ) ) ;
165+ const prismTheme = webview . asWebviewUri ( vscode . Uri . joinPath ( this . context . extensionUri , 'node_modules' , 'prismjs' , 'themes' , 'prism-tomorrow.css' ) ) ;
166+ return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" />
164167<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline' ${ this . getCspSource ( ) } ; script-src 'nonce-${ nonce } ' ${ this . getCspSource ( ) } ;" />
165168<title>Argus Call Graph Preview</title>
166169<link rel="stylesheet" href="${ prismTheme } " />
167- ${ collectedStyles . replace ( / < s t y l e / gi, `<style nonce="${ nonce } "` ) . replace ( / < s c r i p t / gi, '<!-- stripped-script' ) }
170+ ${ collectedStyles . replace ( / < s t y l e / gi, `<style nonce="${ nonce } "` ) . replace ( / < s c r i p t / gi, '<!-- stripped-script' ) }
168171<style nonce="${ nonce } ">
169172/* Header layout & logo (inline) */
170173header.argus-header { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:12px; }
@@ -356,30 +359,30 @@ window.addEventListener('message', function(event){
356359 else { btn.innerHTML='❌ Save Failed'; setTimeout(function(){ btn.innerHTML='📷 Export as Image'; btn.disabled=false; }, 2200); }
357360});
358361</script></body></html>` ;
359- }
362+ }
360363
361- private getCspSource ( ) : string {
362- return this . context . extensionUri . scheme === 'vscode-file' ? 'vscode-file:' : 'vscode-resource:' ;
363- }
364+ private getCspSource ( ) : string {
365+ return this . context . extensionUri . scheme === 'vscode-file' ? 'vscode-file:' : 'vscode-resource:' ;
366+ }
364367}
365368
366369function escapeHtml ( str : string ) : string {
367- return str . replace ( / [ & < > ' " ] / g, s => ( { '&' :'&' , '<' :'<' , '>' :'>' , '"' :'"' , '\'' :''' } [ s ] as string ) ) ;
370+ return str . replace ( / [ & < > ' " ] / g, s => ( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , '\'' : ''' } [ s ] as string ) ) ;
368371}
369372
370373function getNonce ( ) {
371- let text = '' ;
372- const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ;
373- for ( let i = 0 ; i < 32 ; i ++ ) {
374- text += possible . charAt ( Math . floor ( Math . random ( ) * possible . length ) ) ;
375- }
376- return text ;
374+ let text = '' ;
375+ const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ;
376+ for ( let i = 0 ; i < 32 ; i ++ ) {
377+ text += possible . charAt ( Math . floor ( Math . random ( ) * possible . length ) ) ;
378+ }
379+ return text ;
377380}
378381
379382function debounce < T extends ( ...args : any [ ] ) => unknown > ( fn : T , wait : number ) {
380383 let handle : NodeJS . Timeout | undefined ;
381384 return ( ...args : Parameters < T > ) => {
382- if ( handle ) clearTimeout ( handle ) ;
385+ if ( handle ) { clearTimeout ( handle ) ; }
383386 handle = setTimeout ( ( ) => fn ( ...args ) , wait ) ;
384387 } ;
385388}
0 commit comments