@@ -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,64 @@ 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+         Debug . Assert ( oldProject . CompilationOptions  !=  null ) ; 
451+         Debug . Assert ( newProject . CompilationOptions  !=  null ) ; 
452+ 
453+         if  ( oldProject . ParseOptions  !=  newProject . ParseOptions  || 
454+             HasDifferences ( oldProject . CompilationOptions ,  newProject . CompilationOptions )  || 
455+             oldProject . AssemblyName  !=  newProject . AssemblyName ) 
456+         { 
457+             if  ( differences  !=  null ) 
458+             { 
459+                 differences . HasSettingChange  =  true ; 
460+             } 
461+             else 
462+             { 
463+                 return  true ; 
464+             } 
465+         } 
466+ 
467+         if  ( ! oldProject . MetadataReferences . SequenceEqual ( newProject . MetadataReferences )  || 
468+             ! oldProject . ProjectReferences . SequenceEqual ( newProject . ProjectReferences ) ) 
469+         { 
470+             if  ( differences  !=  null ) 
471+             { 
472+                 differences . HasReferenceChange  =  true ; 
473+             } 
474+             else 
475+             { 
476+                 return  true ; 
477+             } 
478+         } 
479+ 
480+         return  false ; 
481+     } 
482+ 
483+     /// <summary> 
484+     /// True if given compilation options differ in a way that might affect EnC. 
485+     /// </summary> 
486+     internal  static   bool  HasDifferences ( CompilationOptions  oldOptions ,  CompilationOptions  newOptions ) 
487+         =>  ! oldOptions 
488+             . WithSyntaxTreeOptionsProvider ( newOptions . SyntaxTreeOptionsProvider ) 
489+             . WithStrongNameProvider ( newOptions . StrongNameProvider ) 
490+             . WithXmlReferenceResolver ( newOptions . XmlReferenceResolver ) 
491+             . Equals ( newOptions ) ; 
492+ 
493+     internal  static   async  Task  GetProjectDifferencesAsync ( TraceLog  log ,  Project ?  oldProject ,  Project  newProject ,  ProjectDifferences  documentDifferences ,  ArrayBuilder < Diagnostic >  diagnostics ,  CancellationToken  cancellationToken ) 
440494    { 
441495        documentDifferences . Clear ( ) ; 
442496
497+         if  ( oldProject  ==  null ) 
498+         { 
499+             return ; 
500+         } 
501+ 
443502        if  ( ! await  HasDifferencesAsync ( oldProject ,  newProject ,  documentDifferences ,  cancellationToken ) . ConfigureAwait ( false ) ) 
444503        { 
445504            return ; 
@@ -697,6 +756,16 @@ private static bool HasReferenceRudeEdits(ImmutableDictionary<string, OneOrMany<
697756        return  hasRudeEdit ; 
698757    } 
699758
759+     private  static   bool  HasAddedReference ( Compilation  oldCompilation ,  Compilation  newCompilation ) 
760+     { 
761+         using  var  pooledOldNames  =  SharedPools . StringIgnoreCaseHashSet . GetPooledObject ( ) ; 
762+         var  oldNames  =  pooledOldNames . Object ; 
763+         Debug . Assert ( oldNames . Comparer  ==  AssemblyIdentityComparer . SimpleNameComparer ) ; 
764+ 
765+         oldNames . AddRange ( oldCompilation . ReferencedAssemblyNames . Select ( static  r =>  r . Name ) ) ; 
766+         return  newCompilation . ReferencedAssemblyNames . Any ( static  ( newReference ,  oldNames )  =>  ! oldNames . Contains ( newReference . Name ) ,  oldNames ) ; 
767+     } 
768+ 
700769    internal  static   async  ValueTask < ProjectChanges >  GetProjectChangesAsync ( 
701770        ActiveStatementsMap  baseActiveStatements , 
702771        Compilation  oldCompilation , 
@@ -900,9 +969,11 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
900969            using  var  _1  =  ArrayBuilder < ManagedHotReloadUpdate > . GetInstance ( out  var  deltas ) ; 
901970            using  var  _2  =  ArrayBuilder < ( Guid  ModuleId ,  ImmutableArray < ( ManagedModuleMethodId  Method ,  NonRemappableRegion  Region ) > ) > . GetInstance ( out  var  nonRemappableRegions ) ; 
902971            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 ) ; 
972+             using  var  _4  =  ArrayBuilder < ProjectId > . GetInstance ( out  var  addedUnbuiltProjects ) ; 
973+             using  var  _5  =  ArrayBuilder < ProjectId > . GetInstance ( out  var  projectsToRedeploy ) ; 
905974            using  var  _6  =  PooledDictionary < ProjectId ,  ArrayBuilder < Diagnostic > > . GetInstance ( out  var  diagnosticBuilders ) ; 
975+ 
976+             // Project differences for currently analyzed project. Reused and cleared. 
906977            using  var  projectDifferences  =  new  ProjectDifferences ( ) ; 
907978
908979            // After all projects have been analyzed "true" value indicates changed document that is only included in stale projects. 
@@ -945,39 +1016,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
9451016                    } 
9461017
9471018                    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-                     } 
973- 
974-                     projectDiagnostics  =  ArrayBuilder < Diagnostic > . GetInstance ( ) ; 
1019+                     Debug . Assert ( oldProject  ==  null  ||  oldProject . SupportsEditAndContinue ( ) ) ; 
9751020
9761021                    await  GetProjectDifferencesAsync ( Log ,  oldProject ,  newProject ,  projectDifferences ,  projectDiagnostics ,  cancellationToken ) . ConfigureAwait ( false ) ; 
1022+                     projectDifferences . Log ( Log ,  newProject ) ; 
9771023
978-                     if  ( projectDifferences . HasDocumentChanges ) 
1024+                     if  ( projectDifferences . IsEmpty ) 
9791025                    { 
980-                         Log . Write ( $ "Found  { projectDifferences . ChangedOrAddedDocuments . Count }  potentially changed,  { projectDifferences . DeletedDocuments . Count }  deleted document(s) in project  { newProject . GetLogDisplay ( ) } " ) ; 
1026+                         continue ; 
9811027                    } 
9821028
9831029                    var  ( mvid ,  mvidReadError )  =  await  DebuggingSession . GetProjectModuleIdAsync ( newProject ,  cancellationToken ) . ConfigureAwait ( false ) ; 
@@ -989,8 +1035,9 @@ void UpdateChangedDocumentsStaleness(bool isStale)
9891035                        if  ( mvid  ==  staleModuleId  ||  mvidReadError  !=  null ) 
9901036                        { 
9911037                            Log . Write ( $ "EnC state of { newProject . GetLogDisplay ( ) }  queried: project is stale") ; 
992-                             UpdateChangedDocumentsStaleness ( isStale :  true ) ; 
9931038
1039+                             // Track changed documents that are only included in stale or unbuilt projects: 
1040+                             UpdateChangedDocumentsStaleness ( isStale :  true ) ; 
9941041                            continue ; 
9951042                        } 
9961043
@@ -1003,17 +1050,32 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10031050                        // The MVID is required for emit so we consider the error permanent and report it here. 
10041051                        // Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID. 
10051052                        projectDiagnostics . Add ( mvidReadError ) ; 
1006-                         projectSummaryToReport  =  ProjectAnalysisSummary . ValidChanges ; 
10071053                        continue ; 
10081054                    } 
10091055
10101056                    if  ( mvid  ==  Guid . Empty ) 
10111057                    { 
1012-                         Log . Write ( $ "Changes not applied to { newProject . GetLogDisplay ( ) } : project not built") ; 
1058+                         // If the project has been added to the solution, ask the project system to build it. 
1059+                         if  ( oldProject  ==  null ) 
1060+                         { 
1061+                             Log . Write ( $ "Project build requested for { newProject . GetLogDisplay ( ) } ") ; 
1062+                             addedUnbuiltProjects . Add ( newProject . Id ) ; 
1063+                         } 
1064+                         else 
1065+                         { 
1066+                             Log . Write ( $ "Changes not applied to { newProject . GetLogDisplay ( ) } : project not built") ; 
1067+                         } 
1068+ 
1069+                         // Track changed documents that are only included in stale or unbuilt projects: 
10131070                        UpdateChangedDocumentsStaleness ( isStale :  true ) ; 
10141071                        continue ; 
10151072                    } 
10161073
1074+                     if  ( oldProject  ==  null ) 
1075+                     { 
1076+                         continue ; 
1077+                     } 
1078+ 
10171079                    // Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync. 
10181080                    // Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by 
10191081                    // incoming events updating the content of out-of-sync documents. 
@@ -1079,8 +1141,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10791141
10801142                    // Unsupported changes in referenced assemblies will be reported below. 
10811143                    if  ( projectSummary  is  ProjectAnalysisSummary . NoChanges  or ProjectAnalysisSummary . ValidInsignificantChanges  && 
1082-                         oldProject . MetadataReferences . SequenceEqual ( newProject . MetadataReferences )  && 
1083-                         oldProject . ProjectReferences . SequenceEqual ( newProject . ProjectReferences ) ) 
1144+                         ! projectDifferences . HasReferenceChange ) 
10841145                    { 
10851146                        continue ; 
10861147                    } 
@@ -1140,6 +1201,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
11401201                        continue ; 
11411202                    } 
11421203
1204+                     // If the project references new dependencies, the host needs to invoke ReferenceCopyLocalPathsOutputGroup target on this project 
1205+                     // to deploy these dependencies to the projects output directory. The deployment shouldn't overwrite existing files. 
1206+                     // It should only happen if the project has no rude edits (especially not rude edits related to references) -- we bailed above if so. 
1207+                     if  ( HasAddedReference ( oldCompilation ,  newCompilation ) ) 
1208+                     { 
1209+                         projectsToRedeploy . Add ( newProject . Id ) ; 
1210+                     } 
1211+ 
11431212                    if  ( projectSummary  is  ProjectAnalysisSummary . NoChanges  or ProjectAnalysisSummary . ValidInsignificantChanges ) 
11441213                    { 
11451214                        continue ; 
@@ -1286,9 +1355,9 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
12861355                } 
12871356                finally 
12881357                { 
1289-                     if  ( projectSummaryToReport . HasValue ) 
1358+                     if  ( projectSummaryToReport . HasValue   ||   ! projectDiagnostics . IsEmpty ) 
12901359                    { 
1291-                         Telemetry . LogProjectAnalysisSummary ( projectSummaryToReport . Value ,  newProject . State . ProjectInfo . Attributes . TelemetryId ,  projectDiagnostics ) ; 
1360+                         Telemetry . LogProjectAnalysisSummary ( projectSummaryToReport ,  newProject . State . ProjectInfo . Attributes . TelemetryId ,  projectDiagnostics ) ; 
12921361                    } 
12931362
12941363                    if  ( ! projectDiagnostics . IsEmpty ) 
@@ -1338,6 +1407,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
13381407                solution , 
13391408                updates , 
13401409                diagnostics , 
1410+                 addedUnbuiltProjects , 
13411411                runningProjects , 
13421412                out  var  projectsToRestart , 
13431413                out  var  projectsToRebuild ) ; 
@@ -1352,7 +1422,8 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
13521422                diagnostics , 
13531423                syntaxError :  null , 
13541424                projectsToRestart , 
1355-                 projectsToRebuild ) ; 
1425+                 projectsToRebuild , 
1426+                 projectsToRedeploy . ToImmutable ( ) ) ; 
13561427        } 
13571428        catch  ( Exception  e )  when  ( LogException ( e )  &&  FatalError . ReportAndPropagateUnlessCanceled ( e ,  cancellationToken ) ) 
13581429        { 
0 commit comments