@@ -103,9 +103,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
103103 compilationHandler = new CompilationHandler ( Context . Reporter , Context . ProcessRunner ) ;
104104 var scopedCssFileHandler = new ScopedCssFileHandler ( Context . Reporter , projectMap , browserConnector ) ;
105105 var projectLauncher = new ProjectLauncher ( Context , projectMap , browserConnector , compilationHandler , iteration ) ;
106- var outputDirectories = GetProjectOutputDirectories ( evaluationResult . ProjectGraph ) ;
107- ReportOutputDirectories ( outputDirectories ) ;
108- var changeFilter = new Predicate < ChangedPath > ( change => AcceptChange ( change , evaluationResult , outputDirectories ) ) ;
106+ evaluationResult . ItemExclusions . Report ( Context . Reporter ) ;
109107
110108 var rootProjectNode = evaluationResult . ProjectGraph . GraphRoots . Single ( ) ;
111109
@@ -180,13 +178,13 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
180178 return ;
181179 }
182180
183- fileWatcher . WatchContainingDirectories ( evaluationResult . Files . Keys , includeSubdirectories : true ) ;
181+ evaluationResult . WatchFiles ( fileWatcher ) ;
184182
185183 var changedFilesAccumulator = ImmutableList < ChangedPath > . Empty ;
186184
187185 void FileChangedCallback ( ChangedPath change )
188186 {
189- if ( changeFilter ( change ) )
187+ if ( AcceptChange ( change , evaluationResult ) )
190188 {
191189 Context . Reporter . Verbose ( $ "File change: { change . Kind } '{ change . Path } '.") ;
192190 ImmutableInterlocked . Update ( ref changedFilesAccumulator , changedPaths => changedPaths . Add ( change ) ) ;
@@ -350,7 +348,7 @@ void FileChangedCallback(ChangedPath change)
350348 iterationCancellationToken . ThrowIfCancellationRequested ( ) ;
351349
352350 _ = await fileWatcher . WaitForFileChangeAsync (
353- changeFilter ,
351+ change => AcceptChange ( change , evaluationResult ) ,
354352 startedWatching : ( ) => Context . Reporter . Report ( MessageDescriptor . FixBuildError ) ,
355353 shutdownCancellationToken ) ;
356354 }
@@ -424,19 +422,25 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict
424422 } )
425423 . ToImmutableList ( ) ;
426424
425+ ReportFileChanges ( changedFiles ) ;
426+
427427 // When a new file is added we need to run design-time build to find out
428428 // what kind of the file it is and which project(s) does it belong to (can be linked, web asset, etc.).
429+ // We also need to re-evaluate the project if any project files have been modified.
429430 // We don't need to rebuild and restart the application though.
430- var hasAddedFile = changedFiles . Any ( f => f . Kind is ChangeKind . Add ) ;
431+ var fileAdded = changedFiles . Any ( f => f . Kind is ChangeKind . Add ) ;
432+ var projectChanged = ! fileAdded && changedFiles . Any ( f => evaluationResult . BuildFiles . Contains ( f . Item . FilePath ) ) ;
433+ var evaluationRequired = fileAdded || projectChanged ;
431434
432- if ( hasAddedFile )
435+ if ( evaluationRequired )
433436 {
434- Context . Reporter . Report ( MessageDescriptor . FileAdditionTriggeredReEvaluation ) ;
437+ Context . Reporter . Report ( fileAdded ? MessageDescriptor . FileAdditionTriggeredReEvaluation : MessageDescriptor . ProjectChangeTriggeredReEvaluation ) ;
435438
439+ // TODO: consider re-evaluating only affected projects instead of the whole graph.
436440 evaluationResult = await EvaluateRootProjectAsync ( iterationCancellationToken ) ;
437441
438442 // additional directories may have been added:
439- fileWatcher . WatchContainingDirectories ( evaluationResult . Files . Keys , includeSubdirectories : true ) ;
443+ evaluationResult . WatchFiles ( fileWatcher ) ;
440444
441445 await compilationHandler . Workspace . UpdateProjectConeAsync ( RootFileSetFactory . RootProjectFile , iterationCancellationToken ) ;
442446
@@ -447,9 +451,8 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict
447451 }
448452
449453 // Update files in the change set with new evaluation info.
450- changedFiles = changedFiles
451- . Select ( f => evaluationResult . Files . TryGetValue ( f . Item . FilePath , out var evaluatedFile ) ? f with { Item = evaluatedFile } : f )
452- . ToImmutableList ( ) ;
454+ changedFiles = [ .. changedFiles
455+ . Select ( f => evaluationResult . Files . TryGetValue ( f . Item . FilePath , out var evaluatedFile ) ? f with { Item = evaluatedFile } : f ) ] ;
453456
454457 Context . Reporter . Report ( MessageDescriptor . ReEvaluationCompleted ) ;
455458 }
@@ -477,12 +480,11 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict
477480 }
478481
479482 changedFiles = newChangedFiles ;
483+
480484 ImmutableInterlocked . Update ( ref changedFilesAccumulator , accumulator => accumulator . AddRange ( newAccumulator ) ) ;
481485 }
482486
483- ReportFileChanges ( changedFiles ) ;
484-
485- if ( ! hasAddedFile )
487+ if ( ! evaluationRequired )
486488 {
487489 // update the workspace to reflect changes in the file content:
488490 await compilationHandler . Workspace . UpdateFileContentAsync ( changedFiles , iterationCancellationToken ) ;
@@ -551,7 +553,7 @@ private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatche
551553 {
552554 if ( ! fileWatcher . WatchingDirectories )
553555 {
554- fileWatcher . WatchContainingDirectories ( evaluationResult . Files . Keys , includeSubdirectories : true ) ;
556+ evaluationResult . WatchFiles ( fileWatcher ) ;
555557 }
556558
557559 _ = await fileWatcher . WaitForFileChangeAsync (
@@ -571,31 +573,42 @@ private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatche
571573 }
572574 }
573575
574- private bool AcceptChange ( ChangedPath change , EvaluationResult evaluationResult , IReadOnlySet < string > outputDirectories )
576+ private Predicate < ChangedPath > CreateChangeFilter ( EvaluationResult evaluationResult )
577+ => new ( change => AcceptChange ( change , evaluationResult ) ) ;
578+
579+ private bool AcceptChange ( ChangedPath change , EvaluationResult evaluationResult )
575580 {
576581 var ( path , kind ) = change ;
577582
578583 // Handle changes to files that are known to be project build inputs from its evaluation.
584+ // Compile items might be explicitly added by targets to directories that are excluded by default
585+ // (e.g. global usings in obj directory). Changes to these files should not be ignored.
579586 if ( evaluationResult . Files . ContainsKey ( path ) )
580587 {
581588 return true ;
582589 }
583590
584- // Ignore other changes to output and intermediate output directories.
591+ if ( ! AcceptChange ( change ) )
592+ {
593+ return false ;
594+ }
595+
596+ // changes in *.*proj, *.props, *.targets:
597+ if ( evaluationResult . BuildFiles . Contains ( path ) )
598+ {
599+ return true ;
600+ }
601+
602+ // Ignore other changes that match DefaultItemExcludes glob if EnableDefaultItems is true,
603+ // otherwise changes under output and intermediate output directories.
585604 //
586605 // Unsupported scenario:
587606 // - msbuild target adds source files to intermediate output directory and Compile items
588607 // based on the content of non-source file.
589608 //
590609 // On the other hand, changes to source files produced by source generators will be registered
591610 // since the changes to additional file will trigger workspace update, which will trigger the source generator.
592- if ( PathUtilities . ContainsPath ( outputDirectories , path ) )
593- {
594- Context . Reporter . Report ( MessageDescriptor . IgnoringChangeInOutputDirectory , kind , path ) ;
595- return false ;
596- }
597-
598- return AcceptChange ( change ) ;
611+ return ! evaluationResult . ItemExclusions . IsExcluded ( path , kind , Context . Reporter ) ;
599612 }
600613
601614 private bool AcceptChange ( ChangedPath change )
@@ -618,54 +631,6 @@ private bool AcceptChange(ChangedPath change)
618631 private static bool IsHiddenDirectory ( string dir )
619632 => Path . GetFileName ( dir ) . StartsWith ( '.' ) ;
620633
621- private static IReadOnlySet < string > GetProjectOutputDirectories ( ProjectGraph projectGraph )
622- {
623- // TODO: https://github.com/dotnet/sdk/issues/45539
624- // Consider evaluating DefaultItemExcludes and DefaultExcludesInProjectFolder msbuild properties using
625- // https://github.com/dotnet/msbuild/blob/37eb419ad2c986ac5530292e6ee08e962390249e/src/Build/Globbing/MSBuildGlob.cs
626- // to determine which directories should be excluded.
627-
628- var projectOutputDirectories = new HashSet < string > ( PathUtilities . OSSpecificPathComparer ) ;
629-
630- foreach ( var projectNode in projectGraph . ProjectNodes )
631- {
632- TryAdd ( projectNode . GetOutputDirectory ( ) ) ;
633- TryAdd ( projectNode . GetIntermediateOutputDirectory ( ) ) ;
634- }
635-
636- return projectOutputDirectories ;
637-
638- void TryAdd ( string ? dir )
639- {
640- try
641- {
642- if ( dir != null )
643- {
644- // msbuild properties may use '\' as a directory separator even on Unix.
645- // GetFullPath does not normalize '\' to '/' on Unix.
646- if ( Path . DirectorySeparatorChar == '/' )
647- {
648- dir = dir . Replace ( '\\ ' , '/' ) ;
649- }
650-
651- projectOutputDirectories . Add ( Path . TrimEndingDirectorySeparator ( Path . GetFullPath ( dir ) ) ) ;
652- }
653- }
654- catch
655- {
656- // ignore
657- }
658- }
659- }
660-
661- private void ReportOutputDirectories ( IReadOnlySet < string > directories )
662- {
663- foreach ( var dir in directories )
664- {
665- Context . Reporter . Verbose ( $ "Output directory: '{ dir } '") ;
666- }
667- }
668-
669634 internal static IEnumerable < ChangedPath > NormalizePathChanges ( IEnumerable < ChangedPath > changes )
670635 => changes
671636 . GroupBy ( keySelector : change => change . Path )
0 commit comments