@@ -343,6 +343,11 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
343343 return false ;
344344 }
345345
346+ if ( HasProjectLevelDifferences ( oldProject , newProject , differences ) && differences == null )
347+ {
348+ return true ;
349+ }
350+
346351 foreach ( var documentId in newProject . State . DocumentStates . GetChangedStateIds ( oldProject . State . DocumentStates , ignoreUnchangedContent : true ) )
347352 {
348353 var document = newProject . GetRequiredDocument ( documentId ) ;
@@ -361,7 +366,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
361366 return true ;
362367 }
363368
364- differences . Value . ChangedOrAddedDocuments . Add ( document ) ;
369+ differences . ChangedOrAddedDocuments . Add ( document ) ;
365370 }
366371
367372 foreach ( var documentId in newProject . State . DocumentStates . GetAddedStateIds ( oldProject . State . DocumentStates ) )
@@ -377,7 +382,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
377382 return true ;
378383 }
379384
380- differences . Value . ChangedOrAddedDocuments . Add ( document ) ;
385+ differences . ChangedOrAddedDocuments . Add ( document ) ;
381386 }
382387
383388 foreach ( var documentId in newProject . State . DocumentStates . GetRemovedStateIds ( oldProject . State . DocumentStates ) )
@@ -393,7 +398,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
393398 return true ;
394399 }
395400
396- differences . Value . DeletedDocuments . Add ( document ) ;
401+ differences . DeletedDocuments . Add ( document ) ;
397402 }
398403
399404 // The following will check for any changes in non-generated document content (editorconfig, additional docs).
@@ -436,10 +441,51 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
436441 return false ;
437442 }
438443
439- internal static async Task GetProjectDifferencesAsync ( TraceLog log , Project oldProject , Project newProject , ProjectDifferences documentDifferences , ArrayBuilder < Diagnostic > diagnostics , CancellationToken cancellationToken )
444+ /// <summary>
445+ /// Return true if projects might have differences in state other than document content that migth affect EnC.
446+ /// The checks need to be fast. May return true even if the changes don't actually affect the behavior.
447+ /// </summary>
448+ internal static bool HasProjectLevelDifferences ( Project oldProject , Project newProject , ProjectDifferences ? differences )
449+ {
450+ if ( oldProject . ParseOptions != newProject . ParseOptions ||
451+ oldProject . CompilationOptions != newProject . CompilationOptions ||
452+ oldProject . AssemblyName != newProject . AssemblyName )
453+ {
454+ if ( differences != null )
455+ {
456+ differences . HasSettingChange = true ;
457+ }
458+ else
459+ {
460+ return true ;
461+ }
462+ }
463+
464+ if ( ! oldProject . MetadataReferences . SequenceEqual ( newProject . MetadataReferences ) ||
465+ ! oldProject . ProjectReferences . SequenceEqual ( newProject . ProjectReferences ) )
466+ {
467+ if ( differences != null )
468+ {
469+ differences . HasReferenceChange = true ;
470+ }
471+ else
472+ {
473+ return true ;
474+ }
475+ }
476+
477+ return false ;
478+ }
479+
480+ internal static async Task GetProjectDifferencesAsync ( TraceLog log , Project ? oldProject , Project newProject , ProjectDifferences documentDifferences , ArrayBuilder < Diagnostic > diagnostics , CancellationToken cancellationToken )
440481 {
441482 documentDifferences . Clear ( ) ;
442483
484+ if ( oldProject == null )
485+ {
486+ return ;
487+ }
488+
443489 if ( ! await HasDifferencesAsync ( oldProject , newProject , documentDifferences , cancellationToken ) . ConfigureAwait ( false ) )
444490 {
445491 return ;
@@ -697,6 +743,16 @@ private static bool HasReferenceRudeEdits(ImmutableDictionary<string, OneOrMany<
697743 return hasRudeEdit ;
698744 }
699745
746+ private static bool HasAddedReference ( Compilation oldCompilation , Compilation newCompilation )
747+ {
748+ using var pooledOldNames = SharedPools . StringIgnoreCaseHashSet . GetPooledObject ( ) ;
749+ var oldNames = pooledOldNames . Object ;
750+ Debug . Assert ( oldNames . Comparer == AssemblyIdentityComparer . SimpleNameComparer ) ;
751+
752+ oldNames . AddRange ( oldCompilation . ReferencedAssemblyNames . Select ( static r => r . Name ) ) ;
753+ return newCompilation . ReferencedAssemblyNames . Any ( static ( newReference , oldNames ) => ! oldNames . Contains ( newReference . Name ) , oldNames ) ;
754+ }
755+
700756 internal static async ValueTask < ProjectChanges > GetProjectChangesAsync (
701757 ActiveStatementsMap baseActiveStatements ,
702758 Compilation oldCompilation ,
@@ -900,9 +956,11 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
900956 using var _1 = ArrayBuilder < ManagedHotReloadUpdate > . GetInstance ( out var deltas ) ;
901957 using var _2 = ArrayBuilder < ( Guid ModuleId , ImmutableArray < ( ManagedModuleMethodId Method , NonRemappableRegion Region ) > ) > . GetInstance ( out var nonRemappableRegions ) ;
902958 using var _3 = ArrayBuilder < ProjectBaseline > . GetInstance ( out var newProjectBaselines ) ;
903- using var _4 = ArrayBuilder < ( ProjectId id , Guid mvid ) > . GetInstance ( out var projectsToStale ) ;
904- using var _5 = ArrayBuilder < ProjectId > . GetInstance ( out var projectsToUnstale ) ;
959+ using var _4 = ArrayBuilder < ProjectId > . GetInstance ( out var addedUnbuiltProjects ) ;
960+ using var _5 = ArrayBuilder < ProjectId > . GetInstance ( out var projectsToRedeploy ) ;
905961 using var _6 = PooledDictionary < ProjectId , ArrayBuilder < Diagnostic > > . GetInstance ( out var diagnosticBuilders ) ;
962+
963+ // Project differences for currently analyzed project. Reused and cleared.
906964 using var projectDifferences = new ProjectDifferences ( ) ;
907965
908966 // After all projects have been analyzed "true" value indicates changed document that is only included in stale projects.
@@ -945,39 +1003,16 @@ void UpdateChangedDocumentsStaleness(bool isStale)
9451003 }
9461004
9471005 var oldProject = oldSolution . GetProject ( newProject . Id ) ;
948- if ( oldProject == null )
949- {
950- Log . Write ( $ "EnC state of { newProject . GetLogDisplay ( ) } queried: project not loaded") ;
951-
952- // TODO (https://github.com/dotnet/roslyn/issues/1204):
953- //
954- // When debugging session is started some projects might not have been loaded to the workspace yet (may be explicitly unloaded by the user).
955- // We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied
956- // and will result in source mismatch when the user steps into them.
957- //
958- // We can allow project to be added by including all its documents here.
959- // When we analyze these documents later on we'll check if they match the PDB.
960- // If so we can add them to the committed solution and detect further changes.
961- // It might be more efficient though to track added projects separately.
962-
963- continue ;
964- }
965-
966- Debug . Assert ( oldProject . SupportsEditAndContinue ( ) ) ;
967-
968- if ( ! oldProject . ProjectSettingsSupportEditAndContinue ( Log ) )
969- {
970- // reason alrady reported
971- continue ;
972- }
1006+ Debug . Assert ( oldProject == null || oldProject . SupportsEditAndContinue ( ) ) ;
9731007
9741008 projectDiagnostics = ArrayBuilder < Diagnostic > . GetInstance ( ) ;
9751009
9761010 await GetProjectDifferencesAsync ( Log , oldProject , newProject , projectDifferences , projectDiagnostics , cancellationToken ) . ConfigureAwait ( false ) ;
1011+ projectDifferences . Log ( Log , newProject ) ;
9771012
978- if ( projectDifferences . HasDocumentChanges )
1013+ if ( projectDifferences . IsEmpty )
9791014 {
980- Log . Write ( $ "Found { projectDifferences . ChangedOrAddedDocuments . Count } potentially changed, { projectDifferences . DeletedDocuments . Count } deleted document(s) in project { newProject . GetLogDisplay ( ) } " ) ;
1015+ continue ;
9811016 }
9821017
9831018 var ( mvid , mvidReadError ) = await DebuggingSession . GetProjectModuleIdAsync ( newProject , cancellationToken ) . ConfigureAwait ( false ) ;
@@ -989,8 +1024,9 @@ void UpdateChangedDocumentsStaleness(bool isStale)
9891024 if ( mvid == staleModuleId || mvidReadError != null )
9901025 {
9911026 Log . Write ( $ "EnC state of { newProject . GetLogDisplay ( ) } queried: project is stale") ;
992- UpdateChangedDocumentsStaleness ( isStale : true ) ;
9931027
1028+ // Track changed documents that are only included in stale or unbuilt projects:
1029+ UpdateChangedDocumentsStaleness ( isStale : true ) ;
9941030 continue ;
9951031 }
9961032
@@ -1003,17 +1039,32 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10031039 // The MVID is required for emit so we consider the error permanent and report it here.
10041040 // Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID.
10051041 projectDiagnostics . Add ( mvidReadError ) ;
1006- projectSummaryToReport = ProjectAnalysisSummary . ValidChanges ;
10071042 continue ;
10081043 }
10091044
10101045 if ( mvid == Guid . Empty )
10111046 {
1012- Log . Write ( $ "Changes not applied to { newProject . GetLogDisplay ( ) } : project not built") ;
1047+ // If the project has been added to the solution, ask the project system to build it.
1048+ if ( oldProject == null )
1049+ {
1050+ Log . Write ( $ "Project build requested for { newProject . GetLogDisplay ( ) } ") ;
1051+ addedUnbuiltProjects . Add ( newProject . Id ) ;
1052+ }
1053+ else
1054+ {
1055+ Log . Write ( $ "Changes not applied to { newProject . GetLogDisplay ( ) } : project not built") ;
1056+ }
1057+
1058+ // Track changed documents that are only included in stale or unbuilt projects:
10131059 UpdateChangedDocumentsStaleness ( isStale : true ) ;
10141060 continue ;
10151061 }
10161062
1063+ if ( oldProject == null )
1064+ {
1065+ continue ;
1066+ }
1067+
10171068 // Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
10181069 // Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by
10191070 // incoming events updating the content of out-of-sync documents.
@@ -1079,8 +1130,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10791130
10801131 // Unsupported changes in referenced assemblies will be reported below.
10811132 if ( projectSummary is ProjectAnalysisSummary . NoChanges or ProjectAnalysisSummary . ValidInsignificantChanges &&
1082- oldProject . MetadataReferences . SequenceEqual ( newProject . MetadataReferences ) &&
1083- oldProject . ProjectReferences . SequenceEqual ( newProject . ProjectReferences ) )
1133+ ! projectDifferences . HasReferenceChange )
10841134 {
10851135 continue ;
10861136 }
@@ -1140,6 +1190,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
11401190 continue ;
11411191 }
11421192
1193+ // If the project references new dependencies, the host needs to invoke ReferenceCopyLocalPathsOutputGroup target on this project
1194+ // to deploy these dependencies to the projects output directory. The deployment shouldn't overwrite existing files.
1195+ // It should only happen if the project has no rude edits (especially not rude edits related to references) -- we bailed above if so.
1196+ if ( HasAddedReference ( oldCompilation , newCompilation ) )
1197+ {
1198+ projectsToRedeploy . Add ( newProject . Id ) ;
1199+ }
1200+
11431201 if ( projectSummary is ProjectAnalysisSummary . NoChanges or ProjectAnalysisSummary . ValidInsignificantChanges )
11441202 {
11451203 continue ;
@@ -1338,6 +1396,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
13381396 solution ,
13391397 updates ,
13401398 diagnostics ,
1399+ addedUnbuiltProjects ,
13411400 runningProjects ,
13421401 out var projectsToRestart ,
13431402 out var projectsToRebuild ) ;
@@ -1352,7 +1411,8 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
13521411 diagnostics ,
13531412 syntaxError : null ,
13541413 projectsToRestart ,
1355- projectsToRebuild ) ;
1414+ projectsToRebuild ,
1415+ projectsToRedeploy . ToImmutable ( ) ) ;
13561416 }
13571417 catch ( Exception e ) when ( LogException ( e ) && FatalError . ReportAndPropagateUnlessCanceled ( e , cancellationToken ) )
13581418 {
0 commit comments