@@ -15,7 +15,7 @@ internal sealed class CompilationHandler : IDisposable
1515 {
1616 public readonly IncrementalMSBuildWorkspace Workspace ;
1717 public readonly EnvironmentOptions EnvironmentOptions ;
18-
18+ private readonly GlobalOptions _options ;
1919 private readonly IReporter _reporter ;
2020 private readonly WatchHotReloadService _hotReloadService ;
2121
@@ -41,10 +41,11 @@ internal sealed class CompilationHandler : IDisposable
4141
4242 private bool _isDisposed ;
4343
44- public CompilationHandler ( IReporter reporter , EnvironmentOptions environmentOptions , CancellationToken shutdownCancellationToken )
44+ public CompilationHandler ( IReporter reporter , EnvironmentOptions environmentOptions , GlobalOptions options , CancellationToken shutdownCancellationToken )
4545 {
4646 _reporter = reporter ;
4747 EnvironmentOptions = environmentOptions ;
48+ _options = options ;
4849 Workspace = new IncrementalMSBuildWorkspace ( reporter ) ;
4950 _hotReloadService = new WatchHotReloadService ( Workspace . CurrentSolution . Services , ( ) => ValueTask . FromResult ( GetAggregateCapabilities ( ) ) ) ;
5051 _shutdownCancellationToken = shutdownCancellationToken ;
@@ -256,41 +257,25 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
256257 var currentSolution = Workspace . CurrentSolution ;
257258 var runningProjects = _runningProjects ;
258259
259- var runningProjectIds = currentSolution . Projects
260- . Where ( project => project . FilePath != null && runningProjects . ContainsKey ( project . FilePath ) )
261- . Select ( project => project . Id )
262- . ToImmutableHashSet ( ) ;
260+ var runningProjectInfos =
261+ ( from project in currentSolution . Projects
262+ let runningProject = GetCorrespondingRunningProject ( project , runningProjects )
263+ where runningProject != null
264+ let autoRestart = _options . NonInteractive || runningProject . ProjectNode . IsAutoRestartEnabled ( )
265+ select ( project . Id , info : new WatchHotReloadService . RunningProjectInfo ( ) { RestartWhenChangesHaveNoEffect = autoRestart } ) )
266+ . ToImmutableDictionary ( e => e . Id , e => e . info ) ;
263267
264- var updates = await _hotReloadService . GetUpdatesAsync ( currentSolution , runningProjectIds , cancellationToken ) ;
265- var anyProcessNeedsRestart = ! updates . ProjectIdsToRestart . IsEmpty ;
268+ var updates = await _hotReloadService . GetUpdatesAsync ( currentSolution , runningProjectInfos , cancellationToken ) ;
266269
267- await DisplayResultsAsync ( updates , cancellationToken ) ;
270+ await DisplayResultsAsync ( updates , runningProjectInfos , cancellationToken ) ;
268271
269- if ( updates . Status is ModuleUpdateStatus . None or ModuleUpdateStatus . Blocked )
272+ if ( updates . Status is WatchHotReloadService . Status . NoChangesToApply or WatchHotReloadService . Status . Blocked )
270273 {
271274 // If Hot Reload is blocked (due to compilation error) we ignore the current
272275 // changes and await the next file change.
273276 return ( ImmutableDictionary < ProjectId , string > . Empty , [ ] ) ;
274277 }
275278
276- if ( updates . Status == ModuleUpdateStatus . RestartRequired )
277- {
278- if ( ! anyProcessNeedsRestart )
279- {
280- return ( ImmutableDictionary < ProjectId , string > . Empty , [ ] ) ;
281- }
282-
283- await restartPrompt . Invoke ( updates . ProjectIdsToRestart . Select ( id => currentSolution . GetProject ( id ) ! . Name ) , cancellationToken ) ;
284-
285- // Terminate all tracked processes that need to be restarted,
286- // except for the root process, which will terminate later on.
287- var terminatedProjects = await TerminateNonRootProcessesAsync ( updates . ProjectIdsToRestart . Select ( id => currentSolution . GetProject ( id ) ! . FilePath ! ) , cancellationToken ) ;
288-
289- return ( updates . ProjectIdsToRebuild . ToImmutableDictionary ( keySelector : id => id , elementSelector : id => currentSolution . GetProject ( id ) ! . FilePath ! ) , terminatedProjects ) ;
290- }
291-
292- Debug . Assert ( updates . Status == ModuleUpdateStatus . Ready ) ;
293-
294279 ImmutableDictionary < string , ImmutableArray < RunningProject > > projectsToUpdate ;
295280 lock ( _runningProjectsAndUpdatesGuard )
296281 {
@@ -326,115 +311,151 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT
326311 }
327312 } , cancellationToken ) ;
328313
329- return ( ImmutableDictionary < ProjectId , string > . Empty , [ ] ) ;
314+
315+ if ( updates . ProjectsToRestart . IsEmpty )
316+ {
317+ return ( ImmutableDictionary < ProjectId , string > . Empty , [ ] ) ;
318+ }
319+
320+ // Terminate projects that need restarting.
321+
322+ var projectsToPromptForRestart =
323+ ( from projectId in updates . ProjectsToRestart . Keys
324+ where ! runningProjectInfos [ projectId ] . RestartWhenChangesHaveNoEffect // equivallent to auto-restart
325+ select currentSolution . GetProject ( projectId ) ! . Name ) . ToList ( ) ;
326+
327+ if ( projectsToPromptForRestart is not [ ] )
328+ {
329+ await restartPrompt . Invoke ( projectsToPromptForRestart , cancellationToken ) ;
330+ }
331+
332+ // Terminate all tracked processes that need to be restarted,
333+ // except for the root process, which will terminate later on.
334+ var terminatedProjects = await TerminateNonRootProcessesAsync ( updates . ProjectsToRestart . Select ( e => currentSolution . GetProject ( e . Key ) ! . FilePath ! ) , cancellationToken ) ;
335+ var projectsToRebuild = updates . ProjectsToRebuild . ToImmutableDictionary ( keySelector : id => id , elementSelector : id => currentSolution . GetProject ( id ) ! . FilePath ! ) ;
336+
337+ return ( projectsToRebuild , terminatedProjects ) ;
330338 }
331339
332- private async ValueTask DisplayResultsAsync ( WatchHotReloadService . Updates updates , CancellationToken cancellationToken )
340+ private static RunningProject ? GetCorrespondingRunningProject ( Project project , ImmutableDictionary < string , ImmutableArray < RunningProject > > runningProjects )
333341 {
334- var anyProcessNeedsRestart = ! updates . ProjectIdsToRestart . IsEmpty ;
342+ if ( project . FilePath == null || ! runningProjects . TryGetValue ( project . FilePath , out var projectsWithPath ) )
343+ {
344+ return null ;
345+ }
335346
336- switch ( updates . Status )
347+ // msbuild workspace doesn't set TFM if the project is not multi-targeted
348+ var tfm = WatchHotReloadService . GetTargetFramework ( project ) ;
349+ if ( tfm == null )
337350 {
338- case ModuleUpdateStatus . None :
339- _reporter . Report ( MessageDescriptor . NoCSharpChangesToApply ) ;
340- break ;
351+ return projectsWithPath [ 0 ] ;
352+ }
341353
342- case ModuleUpdateStatus . Ready :
343- break ;
354+ return projectsWithPath . SingleOrDefault ( p => string . Equals ( p . ProjectNode . GetTargetFramework ( ) , tfm , StringComparison . OrdinalIgnoreCase ) ) ;
355+ }
344356
345- case ModuleUpdateStatus . RestartRequired :
346- if ( anyProcessNeedsRestart )
347- {
348- _reporter . Output ( "Unable to apply hot reload, restart is needed to apply the changes." ) ;
349- }
350- else
351- {
352- _reporter . Verbose ( "Rude edits detected but do not affect any running process" ) ;
353- }
357+ private async ValueTask DisplayResultsAsync ( WatchHotReloadService . Updates2 updates , ImmutableDictionary < ProjectId , WatchHotReloadService . RunningProjectInfo > runningProjectInfos , CancellationToken cancellationToken )
358+ {
359+ switch ( updates . Status )
360+ {
361+ case WatchHotReloadService . Status . ReadyToApply :
362+ break ;
354363
364+ case WatchHotReloadService . Status . NoChangesToApply :
365+ _reporter . Report ( MessageDescriptor . NoCSharpChangesToApply ) ;
355366 break ;
356367
357- case ModuleUpdateStatus . Blocked :
368+ case WatchHotReloadService . Status . Blocked :
358369 _reporter . Output ( "Unable to apply hot reload due to compilation errors." ) ;
359370 break ;
360371
361372 default :
362373 throw new InvalidOperationException ( ) ;
363374 }
364375
365- // Diagnostics include syntactic errors, semantic warnings/errors and rude edit warnigns/errors for members being updated.
376+ if ( ! updates . ProjectsToRestart . IsEmpty )
377+ {
378+ _reporter . Output ( "Restart is needed to apply the changes." ) ;
379+ }
366380
367- var diagnosticsToDisplay = new List < string > ( ) ;
381+ var diagnosticsToDisplayInApp = new List < string > ( ) ;
368382
369383 // Display errors first, then warnings:
370- Display ( MessageSeverity . Error ) ;
371- Display ( MessageSeverity . Warning ) ;
384+ ReportCompilationDiagnostics ( DiagnosticSeverity . Error ) ;
385+ ReportCompilationDiagnostics ( DiagnosticSeverity . Warning ) ;
386+ ReportRudeEdits ( ) ;
387+
388+ // report or clear diagnostics in the browser UI
389+ await ForEachProjectAsync (
390+ _runningProjects ,
391+ ( project , cancellationToken ) => project . BrowserRefreshServer ? . ReportCompilationErrorsInBrowserAsync ( [ .. diagnosticsToDisplayInApp ] , cancellationToken ) . AsTask ( ) ?? Task . CompletedTask ,
392+ cancellationToken ) ;
372393
373- void Display ( MessageSeverity severity )
394+ void ReportCompilationDiagnostics ( DiagnosticSeverity severity )
374395 {
375- foreach ( var diagnostic in updates . Diagnostics )
396+ foreach ( var diagnostic in updates . CompilationDiagnostics )
376397 {
377- MessageDescriptor descriptor ;
378-
379- if ( diagnostic . Id == "ENC0118" )
380- {
381- // Changing '<entry-point>' might not have any effect until the application is restarted.
382- descriptor = MessageDescriptor . ApplyUpdate_ChangingEntryPoint ;
383- }
384- else if ( diagnostic . Id == "ENC1005" )
385- {
386- // TODO: This warning is overreported in cases when the solution contains projects that are not rebuilt (up-to-date)
387- // and a document is updated that is linked to such a project and another "active" project.
388- // E.g. multi-tfm projects where only one TFM is currently built/running.
389-
390- // Warning: The current content of source file 'D:\Temp\App\Program.cs' does not match the built source.
391- // Any changes made to this file while debugging won't be applied until its content matches the built source.
392- descriptor = MessageDescriptor . ApplyUpdate_FileContentDoesNotMatchBuiltSource ;
393- }
394- else if ( diagnostic . Id == "CS8002" )
398+ if ( diagnostic . Id == "CS8002" )
395399 {
396400 // TODO: This is not a useful warning. Compiler shouldn't be reporting this on .NET/
397401 // Referenced assembly '...' does not have a strong name"
398402 continue ;
399403 }
400- else
401- {
402- // Use the default severity of the diagnostic as it conveys impact on Hot Reload
403- // (ignore warnings as errors and other severity configuration).
404- descriptor = diagnostic . DefaultSeverity switch
405- {
406- DiagnosticSeverity . Error => MessageDescriptor . ApplyUpdate_Error ,
407- DiagnosticSeverity . Warning => MessageDescriptor . ApplyUpdate_Warning ,
408- _ => MessageDescriptor . ApplyUpdate_Verbose ,
409- } ;
410- }
411404
412- if ( descriptor . Severity != severity )
405+ if ( diagnostic . DefaultSeverity != severity )
413406 {
414407 continue ;
415408 }
416409
417- // Do not report rude edits as errors/warnings if no running process is affected.
418- if ( ! anyProcessNeedsRestart && diagnostic . Id is [ 'E' , 'N' , 'C' , >= '0' and <= '9' , ..] )
410+ ReportDiagnostic ( diagnostic , GetMessageDescritor ( diagnostic ) ) ;
411+ }
412+ }
413+
414+ void ReportRudeEdits ( )
415+ {
416+ // Rude edits in projects that caused restart of a project that can be restarted automatically
417+ // will be reported only as verbose output.
418+ var projectsWithVerboseRudeEdits = updates . ProjectsToRestart
419+ . Where ( e => runningProjectInfos . TryGetValue ( e . Key , out var info ) && info . RestartWhenChangesHaveNoEffect )
420+ . SelectMany ( e => e . Value )
421+ . ToImmutableHashSet ( ) ;
422+
423+ foreach ( var ( projectId , diagnostics ) in updates . RudeEdits )
424+ {
425+ foreach ( var diagnostic in diagnostics )
419426 {
420- descriptor = descriptor with { Severity = MessageSeverity . Verbose } ;
421- }
427+ var descriptor = GetMessageDescritor ( diagnostic ) ;
422428
423- var display = CSharpDiagnosticFormatter . Instance . Format ( diagnostic ) ;
424- _reporter . Report ( descriptor , display ) ;
429+ if ( projectsWithVerboseRudeEdits . Contains ( projectId ) )
430+ {
431+ descriptor = descriptor with { Severity = MessageSeverity . Verbose } ;
432+ }
425433
426- if ( descriptor . TryGetMessage ( prefix : null , [ display ] , out var message ) )
427- {
428- diagnosticsToDisplay . Add ( message ) ;
434+ ReportDiagnostic ( diagnostic , descriptor ) ;
429435 }
430436 }
431437 }
432438
433- // report or clear diagnostics in the browser UI
434- await ForEachProjectAsync (
435- _runningProjects ,
436- ( project , cancellationToken ) => project . BrowserRefreshServer ? . ReportCompilationErrorsInBrowserAsync ( diagnosticsToDisplay . ToImmutableArray ( ) , cancellationToken ) . AsTask ( ) ?? Task . CompletedTask ,
437- cancellationToken ) ;
439+ void ReportDiagnostic ( Diagnostic diagnostic , MessageDescriptor descriptor )
440+ {
441+ var display = CSharpDiagnosticFormatter . Instance . Format ( diagnostic ) ;
442+ _reporter . Report ( descriptor , display ) ;
443+
444+ if ( descriptor . TryGetMessage ( prefix : null , [ display ] , out var message ) )
445+ {
446+ diagnosticsToDisplayInApp . Add ( message ) ;
447+ }
448+ }
449+
450+ // Use the default severity of the diagnostic as it conveys impact on Hot Reload
451+ // (ignore warnings as errors and other severity configuration).
452+ static MessageDescriptor GetMessageDescritor ( Diagnostic diagnostic )
453+ => diagnostic . DefaultSeverity switch
454+ {
455+ DiagnosticSeverity . Error => MessageDescriptor . ApplyUpdate_Error ,
456+ DiagnosticSeverity . Warning => MessageDescriptor . ApplyUpdate_Warning ,
457+ _ => MessageDescriptor . ApplyUpdate_Verbose ,
458+ } ;
438459 }
439460
440461 public async ValueTask < bool > HandleStaticAssetChangesAsync ( IReadOnlyList < ChangedFile > files , ProjectNodeMap projectMap , CancellationToken cancellationToken )
0 commit comments