@@ -70,7 +70,7 @@ const tabsReducer = (state: ITabsState, action: ITabsAction) => {
70
70
return state
71
71
}
72
72
}
73
- const PlayExtList = [ 'js' , 'ts' , 'sol' , 'circom' , 'vy' , 'nr' ]
73
+ const PlayExtList = [ 'js' , 'ts' , 'sol' , 'circom' , 'vy' , 'nr' , 'yul' ]
74
74
75
75
export const TabsUI = ( props : TabsUIProps ) => {
76
76
@@ -83,6 +83,10 @@ export const TabsUI = (props: TabsUIProps) => {
83
83
tabs . current = props . tabs // we do this to pass the tabs list to the onReady callbacks
84
84
const appContext = useContext ( AppContext )
85
85
86
+ const compileSeq = useRef ( 0 )
87
+ const compileWatchdog = useRef < number | null > ( null )
88
+ const settledSeqRef = useRef < number > ( 0 )
89
+
86
90
const [ compileState , setCompileState ] = useState < 'idle' | 'compiling' | 'compiled' > ( 'idle' )
87
91
88
92
useEffect ( ( ) => {
@@ -193,7 +197,7 @@ export const TabsUI = (props: TabsUIProps) => {
193
197
setFileDecorations
194
198
} )
195
199
return ( ) => {
196
- tabsElement . current . removeEventListener ( 'wheel' , transformScroll )
200
+ if ( tabsElement . current ) tabsElement . current . removeEventListener ( 'wheel' , transformScroll )
197
201
}
198
202
} , [ ] )
199
203
@@ -208,6 +212,25 @@ export const TabsUI = (props: TabsUIProps) => {
208
212
setCompileState ( 'idle' )
209
213
} , [ tabsState . selectedIndex ] )
210
214
215
+ useEffect ( ( ) => {
216
+ if ( ! props . plugin || tabsState . selectedIndex < 0 ) return
217
+
218
+ const currentPath = props . tabs [ tabsState . selectedIndex ] ?. name
219
+ if ( ! currentPath ) return
220
+
221
+ const listener = ( path : string ) => {
222
+ if ( currentPath . endsWith ( path ) ) {
223
+ setCompileState ( 'idle' )
224
+ }
225
+ }
226
+
227
+ props . plugin . on ( 'editor' , 'contentChanged' , listener )
228
+
229
+ return ( ) => {
230
+ props . plugin . off ( 'editor' , 'contentChanged' )
231
+ }
232
+ } , [ tabsState . selectedIndex , props . plugin , props . tabs ] )
233
+
211
234
const handleCompileAndPublish = async ( storageType : 'ipfs' | 'swarm' ) => {
212
235
setCompileState ( 'compiling' )
213
236
await props . plugin . call ( 'notification' , 'toast' , `Switching to Solidity Compiler to publish...` )
@@ -307,17 +330,97 @@ export const TabsUI = (props: TabsUIProps) => {
307
330
}
308
331
}
309
332
333
+ const waitForFreshCompilationResult = async (
334
+ mySeq : number ,
335
+ targetPath : string ,
336
+ startMs : number ,
337
+ maxWaitMs = 1500 ,
338
+ intervalMs = 120
339
+ ) => {
340
+ const norm = ( p : string ) => p . replace ( / ^ \/ + / , '' )
341
+ const fileName = norm ( targetPath ) . split ( '/' ) . pop ( ) || norm ( targetPath )
342
+
343
+ const hasFile = ( res : any ) => {
344
+ if ( ! res ) return false
345
+ const byContracts =
346
+ res . contracts && typeof res . contracts === 'object' &&
347
+ Object . keys ( res . contracts ) . some ( k => k . endsWith ( fileName ) || norm ( k ) === norm ( targetPath ) )
348
+ const bySources =
349
+ res . sources && typeof res . sources === 'object' &&
350
+ Object . keys ( res . sources ) . some ( k => k . endsWith ( fileName ) || norm ( k ) === norm ( targetPath ) )
351
+ return byContracts || bySources
352
+ }
353
+
354
+ let last : any = null
355
+ const until = startMs + maxWaitMs
356
+ while ( Date . now ( ) < until ) {
357
+ if ( mySeq !== compileSeq . current ) return null
358
+ try {
359
+ const res = await props . plugin . call ( 'solidity' , 'getCompilationResult' )
360
+ last = res
361
+ const ts = ( res && ( res . timestamp || res . timeStamp || res . time || res . generatedAt ) ) || null
362
+ const isFreshTime = typeof ts === 'number' ? ts >= startMs : true
363
+ if ( res && hasFile ( res ) && isFreshTime ) return res
364
+ } catch { }
365
+ await new Promise ( r => setTimeout ( r , intervalMs ) )
366
+ }
367
+ return last
368
+ }
369
+
370
+ const attachCompilationListener = ( compilerName : string , mySeq : number , path : string , startedAt : number ) => {
371
+ try { props . plugin . off ( compilerName , 'compilationFinished' ) } catch { }
372
+
373
+ const onFinished = async ( _success : boolean ) => {
374
+ if ( mySeq !== compileSeq . current || settledSeqRef . current === mySeq ) return
375
+
376
+ if ( compileWatchdog . current ) {
377
+ clearTimeout ( compileWatchdog . current )
378
+ compileWatchdog . current = null
379
+ }
380
+
381
+ const fresh = await waitForFreshCompilationResult ( mySeq , path , startedAt )
382
+
383
+ if ( ! fresh ) {
384
+ setCompileState ( 'idle' )
385
+ props . plugin . call ( 'notification' , 'toast' , 'Compilation failed (no result). See compiler output.' )
386
+ } else {
387
+ const errs = Array . isArray ( fresh . errors ) ? fresh . errors . filter ( ( e : any ) => ( e . severity || e . type ) === 'error' ) : [ ]
388
+ if ( errs . length > 0 ) {
389
+ setCompileState ( 'idle' )
390
+ props . plugin . call ( 'notification' , 'toast' , 'Compilation failed (errors). See compiler output.' )
391
+ } else {
392
+ setCompileState ( 'compiled' )
393
+ }
394
+ }
395
+ settledSeqRef . current = mySeq
396
+ try { props . plugin . off ( compilerName , 'compilationFinished' ) } catch { }
397
+ }
398
+ props . plugin . on ( compilerName , 'compilationFinished' , onFinished )
399
+ }
400
+
310
401
const handleCompileClick = async ( ) => {
311
402
setCompileState ( 'compiling' )
312
403
_paq . push ( [ 'trackEvent' , 'editor' , 'clickRunFromEditor' , tabsState . currentExt ] )
313
404
314
405
try {
315
- const path = active ( ) . substr ( active ( ) . indexOf ( '/' ) + 1 , active ( ) . length )
406
+ const activePathRaw = active ( )
407
+ if ( ! activePathRaw || activePathRaw . indexOf ( '/' ) === - 1 ) {
408
+ setCompileState ( 'idle' )
409
+ props . plugin . call ( 'notification' , 'toast' , 'No file selected.' )
410
+ return
411
+ }
412
+ const path = activePathRaw . substr ( activePathRaw . indexOf ( '/' ) + 1 )
316
413
317
414
if ( tabsState . currentExt === 'js' || tabsState . currentExt === 'ts' ) {
318
- const content = await props . plugin . call ( 'fileManager' , 'readFile' , path )
319
- await props . plugin . call ( 'scriptRunnerBridge' , 'execute' , content , path )
320
- setCompileState ( 'compiled' )
415
+ try {
416
+ const content = await props . plugin . call ( 'fileManager' , 'readFile' , path )
417
+ await props . plugin . call ( 'scriptRunnerBridge' , 'execute' , content , path )
418
+ setCompileState ( 'compiled' )
419
+ } catch ( e ) {
420
+ console . error ( e )
421
+ props . plugin . call ( 'notification' , 'toast' , `Script error: ${ e . message } ` )
422
+ setCompileState ( 'idle' )
423
+ }
321
424
return
322
425
}
323
426
@@ -329,26 +432,45 @@ export const TabsUI = (props: TabsUIProps) => {
329
432
nr : 'noir-compiler'
330
433
} [ tabsState . currentExt ]
331
434
332
- if ( ! compilerName ) {
333
- setCompileState ( 'idle' )
334
- return
435
+ if ( ! compilerName ) {
436
+ setCompileState ( 'idle' )
437
+ return
335
438
}
336
439
337
- props . plugin . once ( compilerName , 'compilationFinished' , ( fileName , source , languageVersion , data ) => {
338
- const hasErrors = data . errors && data . errors . filter ( e => e . severity === 'error' ) . length > 0
339
-
340
- if ( hasErrors ) {
341
- setCompileState ( 'idle' )
342
- } else {
343
- setCompileState ( 'compiled' )
440
+ await props . plugin . call ( 'fileManager' , 'saveCurrentFile' )
441
+ await props . plugin . call ( 'manager' , 'activatePlugin' , compilerName )
442
+
443
+ const mySeq = ++ compileSeq . current
444
+ const startedAt = Date . now ( )
445
+
446
+ attachCompilationListener ( compilerName , mySeq , path , startedAt )
447
+
448
+ if ( compileWatchdog . current ) clearTimeout ( compileWatchdog . current )
449
+ compileWatchdog . current = window . setTimeout ( async ( ) => {
450
+ if ( mySeq !== compileSeq . current || settledSeqRef . current === mySeq ) return
451
+ const maybe = await props . plugin . call ( 'solidity' , 'getCompilationResult' ) . catch ( ( ) => null )
452
+ if ( maybe ) {
453
+ const fresh = await waitForFreshCompilationResult ( mySeq , path , startedAt , 400 , 120 )
454
+ if ( fresh ) {
455
+ const errs = Array . isArray ( fresh . errors ) ? fresh . errors . filter ( ( e : any ) => ( e . severity || e . type ) === 'error' ) : [ ]
456
+ setCompileState ( errs . length ? 'idle' : 'compiled' )
457
+ if ( errs . length ) props . plugin . call ( 'notification' , 'toast' , 'Compilation failed (errors). See compiler output.' )
458
+ settledSeqRef . current = mySeq
459
+ return
460
+ }
344
461
}
345
- } )
462
+ setCompileState ( 'idle' )
463
+ props . plugin . call ( 'notification' , 'toast' , 'Compilation failed (errors). See compiler output.' )
464
+ settledSeqRef . current = mySeq
465
+ try { props . plugin . off ( compilerName , 'compilationFinished' ) } catch { }
466
+ } , 3000 )
346
467
347
468
if ( tabsState . currentExt === 'vy' ) {
348
469
await props . plugin . call ( compilerName , 'vyperCompileCustomAction' )
349
470
} else {
350
471
await props . plugin . call ( compilerName , 'compile' , path )
351
472
}
473
+
352
474
} catch ( e ) {
353
475
console . error ( e )
354
476
setCompileState ( 'idle' )
0 commit comments