diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index 19b809f0b094..d80d459bc567 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -33,7 +33,7 @@ public partial class Solution /// /// Result of calling . /// - private AsyncLazy CachedFrozenSolution { get; init; } + private readonly AsyncLazy _cachedFrozenSolution; /// /// Mapping of DocumentId to the frozen solution we produced for it the last time we were queried. This @@ -41,12 +41,16 @@ public partial class Solution /// private readonly Dictionary> _documentIdToFrozenSolution = []; - private Solution(SolutionCompilationState compilationState) + private Solution( + SolutionCompilationState compilationState, + AsyncLazy? cachedFrozenSolution = null) { _projectIdToProjectMap = []; _compilationState = compilationState; - this.CachedFrozenSolution = AsyncLazy.Create(ComputeFrozenSolution); + _cachedFrozenSolution = cachedFrozenSolution ?? new AsyncLazy( + c => Task.FromResult(ComputeFrozenSolution(c)), + c => ComputeFrozenSolution(c)); } internal Solution( @@ -1458,8 +1462,12 @@ public Solution WithAnalyzerConfigDocumentTextLoader(DocumentId documentId, Text /// Returns a solution instance where every project is frozen at whatever current state it is in /// /// + internal Solution WithFrozenPartialCompilations(CancellationToken cancellationToken) + => _cachedFrozenSolution.GetValue(cancellationToken); + + /// internal Task WithFrozenPartialCompilationsAsync(CancellationToken cancellationToken) - => this.CachedFrozenSolution.GetValueAsync(cancellationToken); + => _cachedFrozenSolution.GetValueAsync(cancellationToken); private Solution ComputeFrozenSolution(CancellationToken cancellationToken) { @@ -1468,11 +1476,11 @@ private Solution ComputeFrozenSolution(CancellationToken cancellationToken) return this; var newCompilationState = this.CompilationState.WithFrozenPartialCompilations(cancellationToken); - var frozenSolution = new Solution(newCompilationState) - { + + var frozenSolution = new Solution( + newCompilationState, // Set the frozen solution to be its own frozen solution. Freezing multiple times is a no-op. - CachedFrozenSolution = this.CachedFrozenSolution, - }; + cachedFrozenSolution: _cachedFrozenSolution); return frozenSolution; } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs index f0675df8fd4d..fa7f2d25227c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs @@ -186,153 +186,6 @@ ImmutableList UpdatePendingTranslationActions( } } - public ICompilationTracker FreezePartialState(CancellationToken cancellationToken) - { - var state = this.ReadState(); - - // If we're already finalized then just return what we have, and with the frozen bit flipped so that - // any future forks keep things frozen. - if (state is FinalCompilationTrackerState finalState) - { - // If we're finalized and already frozen, we can just use ourselves. - return finalState.IsFrozen - ? this - : new CompilationTracker(this.ProjectState, finalState.WithIsFrozen(), this.SkeletonReferenceCache.Clone()); - } - - Contract.ThrowIfFalse(state is null or InProgressState); - - GetPartialCompilationState( - state, - out var inProgressProject, - out var compilationWithoutGeneratedDocuments, - out var compilationWithGeneratedDocuments, - out var generatorInfo, - cancellationToken); - - // Transition us to a frozen in progress state. With the best compilations up to this point, but no - // more pending actions, and with the frozen bit flipped so that any future forks keep things - // frozen. - var inProgressState = InProgressState.Create( - isFrozen: true, - compilationWithoutGeneratedDocuments, - generatorInfo, - compilationWithGeneratedDocuments, - pendingTranslationActions: []); - - return new CompilationTracker(inProgressProject, inProgressState, this.SkeletonReferenceCache.Clone()); - } - - public ICompilationTracker FreezePartialStateWithDocument( - DocumentState docState, - CancellationToken cancellationToken) - { - var state = this.ReadState(); - - // If we're already finalized, and no change has been made to this document. Then just return what we - // have (just with the frozen bit flipped so that any future forks keep things frozen). - if (state is FinalCompilationTrackerState finalState && - this.ProjectState.DocumentStates.TryGetState(docState.Id, out var oldState) && - oldState == docState) - { - // If we're finalized, already frozen and have the document being asked for, we can just use ourselves. - return finalState.IsFrozen - ? this - : new CompilationTracker(this.ProjectState, finalState.WithIsFrozen(), this.SkeletonReferenceCache.Clone()); - } - - // Otherwise, we're not finalized, or the document has changed. We'll create an in-progress state - // to represent the new state of the project, with all the necessary translation steps to - // incorporate the new document. - - GetPartialCompilationState( - state, - out var oldProjectState, - out var compilationWithoutGeneratedDocuments, - out var compilationWithGeneratedDocuments, - out var generatorInfo, - cancellationToken); - - TranslationAction pendingAction = oldProjectState.DocumentStates.TryGetState(docState.Id, out oldState) - // The document had been previously parsed and it's there, so we can update it with our current - // state. Note if no compilation existed GetPartialCompilationState would have produced an empty - // one, and removed any documents, so inProgressProject.DocumentStates would have been empty - // originally. - ? new TranslationAction.TouchDocumentAction(oldProjectState, oldProjectState.UpdateDocument(docState, contentChanged: true), oldState, docState) - // The document wasn't present in the original snapshot at all, and we just need to add the - // document. - : new TranslationAction.AddDocumentsAction(oldProjectState, oldProjectState.AddDocuments([docState]), [docState]); - - // Transition us to a frozen in progress state. With the best compilations up to this point, and the - // pending actions we'll need to perform on it to get to the final state. - var inProgressState = InProgressState.Create( - isFrozen: true, - compilationWithoutGeneratedDocuments, - generatorInfo, - compilationWithGeneratedDocuments, - [pendingAction]); - - return new CompilationTracker(pendingAction.NewProjectState, inProgressState, this.SkeletonReferenceCache.Clone()); - } - - /// - /// Tries to get the latest snapshot of the compilation without waiting for it to be fully built. This - /// method takes advantage of the progress side-effect produced during . It will either return the already built compilation, any in-progress - /// compilation or any known old compilation in that order of preference. The compilation state that is - /// returned will have a compilation that is retained so that it cannot disappear. - /// - private void GetPartialCompilationState( - CompilationTrackerState? state, - out ProjectState inProgressProject, - out Compilation compilationWithoutGeneratedDocuments, - out Compilation compilationWithGeneratedDocuments, - out CompilationTrackerGeneratorInfo generatorInfo, - CancellationToken cancellationToken) - { - if (state is null) - { - inProgressProject = this.ProjectState.RemoveAllDocuments(); - generatorInfo = CompilationTrackerGeneratorInfo.Empty; - - compilationWithoutGeneratedDocuments = this.CreateEmptyCompilation(); - compilationWithGeneratedDocuments = compilationWithoutGeneratedDocuments; - } - else if (state is FinalCompilationTrackerState finalState) - { - inProgressProject = this.ProjectState; - generatorInfo = finalState.GeneratorInfo; - - compilationWithoutGeneratedDocuments = finalState.CompilationWithoutGeneratedDocuments; - compilationWithGeneratedDocuments = finalState.FinalCompilationWithGeneratedDocuments; - - } - else if (state is InProgressState inProgressState) - { - generatorInfo = inProgressState.GeneratorInfo; - inProgressProject = inProgressState is { PendingTranslationActions: [var translationAction, ..] } - ? translationAction.OldProjectState - : this.ProjectState; - - compilationWithoutGeneratedDocuments = inProgressState.CompilationWithoutGeneratedDocuments; - - // Parse the generated documents in parallel. - Parallel.ForEach( - generatorInfo.Documents.States.Values, - new ParallelOptions { CancellationToken = cancellationToken }, - state => state.GetSyntaxTree(cancellationToken)); - cancellationToken.ThrowIfCancellationRequested(); - - // Retrieving the syntax trees will be free now that we computed them above. - compilationWithGeneratedDocuments = compilationWithoutGeneratedDocuments.AddSyntaxTrees( - generatorInfo.Documents.States.Values.Select(state => state.GetSyntaxTree(cancellationToken))); - } - else - { - throw ExceptionUtilities.UnexpectedValue(state.GetType()); - } - } - /// /// Gets the final compilation if it is available. /// @@ -780,6 +633,86 @@ private async Task HasSuccessfullyLoadedSlowAsync( return finalState.HasSuccessfullyLoaded; } + public ICompilationTracker FreezePartialState(CancellationToken cancellationToken) + { + var state = this.ReadState(); + + // If we're finalized and already frozen, we can just use ourselves. + if (state is FinalCompilationTrackerState { IsFrozen: true } finalState) + return this; + + var projectState = state switch + { + // If we don't have an existing state, then transition to a project state without any data. + null => this.ProjectState.RemoveAllDocuments(), + + FinalCompilationTrackerState => this.ProjectState, + + // If we have an in progress state with no steps, then we're just at the current project state. + InProgressState { PendingTranslationActions: [] } => this.ProjectState, + + // Otherwise, reset us to whatever state the InProgressState had currently transitioned to. + InProgressState inProgressState => inProgressState.PendingTranslationActions.First().OldProjectState, + + _ => throw ExceptionUtilities.UnexpectedValue(state.GetType()), + }; + + var frozenState = GetFrozenCompilationState(); + Contract.ThrowIfFalse(frozenState.IsFrozen); + return new CompilationTracker(projectState, frozenState, this.SkeletonReferenceCache.Clone()); + + CompilationTrackerState GetFrozenCompilationState() + { + if (state is FinalCompilationTrackerState finalState) + { + // Checked by caller. + Contract.ThrowIfTrue(finalState.IsFrozen); + // If we're already finalized then just return what we have, and with the frozen bit flipped so + // that any future forks keep things frozen. + return finalState.WithIsFrozen(); + } + + // Non-final state currently. Produce an in-progress-state containing the forked change. Note: we + // transition to in-progress-state here (and not final-state) as we still want to leverage all the + // final-state-transition logic contained in FinalizeCompilationAsync (for example, properly setting + // up all references). + if (state is null) + { + // We have no data at all. Create a frozen empty project/compilation to represent this state. + var compilationWithoutGeneratedDocuments = this.CreateEmptyCompilation(); + var compilationWithGeneratedDocuments = compilationWithoutGeneratedDocuments; + + return InProgressState.Create( + isFrozen: true, + compilationWithoutGeneratedDocuments, + CompilationTrackerGeneratorInfo.Empty, + compilationWithGeneratedDocuments, + pendingTranslationActions: []); + } + else if (state is InProgressState inProgressState) + { + // Grab whatever is in the in-progress-state so far, add any generated docs, and snap + // us to a frozen state with that information. + var generatorInfo = inProgressState.GeneratorInfo; + + var compilationWithoutGeneratedDocuments = inProgressState.CompilationWithoutGeneratedDocuments; + var compilationWithGeneratedDocuments = compilationWithoutGeneratedDocuments.AddSyntaxTrees( + generatorInfo.Documents.States.Values.Select(state => state.GetSyntaxTree(cancellationToken))); + + return InProgressState.Create( + isFrozen: true, + compilationWithoutGeneratedDocuments, + generatorInfo, + compilationWithGeneratedDocuments, + pendingTranslationActions: []); + } + else + { + throw ExceptionUtilities.UnexpectedValue(state.GetType()); + } + } + } + public async ValueTask> GetSourceGeneratedDocumentStatesAsync( SolutionCompilationState compilationState, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs index 08e107d83005..42b9fe9a0528 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs @@ -63,12 +63,6 @@ public ICompilationTracker FreezePartialState(CancellationToken cancellationToke return new GeneratedFileReplacingCompilationTracker(UnderlyingTracker.FreezePartialState(cancellationToken), replacementDocumentStates); } - public ICompilationTracker FreezePartialStateWithDocument(DocumentState docState, CancellationToken cancellationToken) - { - // Because we override SourceGeneratedDocument.WithFrozenPartialSemantics directly, we shouldn't be able to get here. - throw ExceptionUtilities.Unreachable(); - } - public async Task GetCompilationAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken) { // Fast path if we've definitely already done this before diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs index e0cfcee0b914..8fd3d0630c2f 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs @@ -39,7 +39,6 @@ private interface ICompilationTracker Task GetCompilationAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); ICompilationTracker FreezePartialState(CancellationToken cancellationToken); - ICompilationTracker FreezePartialStateWithDocument(DocumentState docState, CancellationToken cancellationToken); Task GetDependentVersionAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); Task GetDependentSemanticVersionAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs index 22cf97a1b090..d23d7f642a02 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs @@ -53,11 +53,14 @@ internal sealed partial class SolutionCompilationState private ConditionalWeakTable? _unrootedSymbolToProjectId; private static readonly Func> s_createTable = () => new ConditionalWeakTable(); + private readonly AsyncLazy _cachedFrozenSnapshot; + private SolutionCompilationState( SolutionState solution, bool partialSemanticsEnabled, ImmutableDictionary projectIdToTrackerMap, - TextDocumentStates? frozenSourceGeneratedDocumentStates) + TextDocumentStates? frozenSourceGeneratedDocumentStates, + AsyncLazy? cachedFrozenSnapshot = null) { SolutionState = solution; PartialSemanticsEnabled = partialSemanticsEnabled; @@ -66,6 +69,7 @@ private SolutionCompilationState( // when solution state is changed, we recalculate its checksum _lazyChecksums = AsyncLazy.Create(c => ComputeChecksumsAsync(projectId: null, c)); + _cachedFrozenSnapshot = cachedFrozenSnapshot ?? AsyncLazy.Create(ComputeFrozenSnapshot); CheckInvariants(); } @@ -94,7 +98,8 @@ private void CheckInvariants() private SolutionCompilationState Branch( SolutionState newSolutionState, ImmutableDictionary? projectIdToTrackerMap = null, - Optional?> frozenSourceGeneratedDocumentStates = default) + Optional?> frozenSourceGeneratedDocumentStates = default, + AsyncLazy? cachedFrozenSnapshot = null) { projectIdToTrackerMap ??= _projectIdToTrackerMap; var newFrozenSourceGeneratedDocumentStates = frozenSourceGeneratedDocumentStates.HasValue ? frozenSourceGeneratedDocumentStates.Value : FrozenSourceGeneratedDocumentStates; @@ -110,7 +115,8 @@ private SolutionCompilationState Branch( newSolutionState, PartialSemanticsEnabled, projectIdToTrackerMap, - newFrozenSourceGeneratedDocumentStates); + newFrozenSourceGeneratedDocumentStates, + cachedFrozenSnapshot); } /// @@ -572,6 +578,13 @@ public SolutionCompilationState WithDocumentText( this.SolutionState.WithDocumentText(documentId, text, mode), documentId); } + public SolutionCompilationState WithDocumentState( + DocumentState documentState) + { + return UpdateDocumentState( + this.SolutionState.WithDocumentState(documentState), documentState.Id); + } + /// public SolutionCompilationState WithAdditionalDocumentText( DocumentId documentId, SourceText text, PreservationMode mode) @@ -1044,32 +1057,101 @@ public SolutionCompilationState WithOptions(SolutionOptionSet options) } public SolutionCompilationState WithFrozenPartialCompilations(CancellationToken cancellationToken) + => _cachedFrozenSnapshot.GetValue(cancellationToken); + + private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancellationToken) { var newIdToProjectStateMapBuilder = this.SolutionState.ProjectStates.ToBuilder(); var newIdToTrackerMapBuilder = _projectIdToTrackerMap.ToBuilder(); + using var _1 = ArrayBuilder.GetInstance(out var documentsToRemove); + using var _2 = ArrayBuilder.GetInstance(out var documentsToAdd); + foreach (var projectId in this.SolutionState.ProjectIds) { + cancellationToken.ThrowIfCancellationRequested(); + // if we don't have one or it is stale, create a new partial solution - var tracker = GetCompilationTracker(projectId); - var newTracker = tracker.FreezePartialState(cancellationToken); + var oldTracker = GetCompilationTracker(projectId); + var newTracker = oldTracker.FreezePartialState(cancellationToken); + if (oldTracker == newTracker) + continue; Contract.ThrowIfFalse(newIdToProjectStateMapBuilder.ContainsKey(projectId)); - newIdToProjectStateMapBuilder[projectId] = newTracker.ProjectState; + + var oldProjectState = this.SolutionState.GetRequiredProjectState(projectId); + var newProjectState = newTracker.ProjectState; + + newIdToProjectStateMapBuilder[projectId] = newProjectState; newIdToTrackerMapBuilder[projectId] = newTracker; + + // Freezing projects can cause them to have an entirely different set of documents (since it effectively + // rewinds the project back to the last time it produced a compilation). Ensure we keep track of the docs + // added or removed from the project states to keep the final filepath-to-documentid map accurate. + // + // Note: we only have to do this if the actual project-state changed. If we were able to use the same + // instance (common if we already got the compilation for a project), then nothing changes with the set + // of documents. + // + // Examples of where the documents may absolutely change though are when we haven't even gotten a + // compilation yet. In that case, the project transitions to an empty state, which means we should remove + // all its documents from the filePathToDocumentIdsMap. Similarly, if we were at some in-progress-state we + // might reset the project back to a prior state from when the last compilation was requested, losing + // information about documents recently added or removed. + + if (oldProjectState != newProjectState) + { + CheckDocumentStates(oldProjectState.DocumentStates, newProjectState.DocumentStates); + CheckDocumentStates(oldProjectState.AdditionalDocumentStates, newProjectState.AdditionalDocumentStates); + CheckDocumentStates(oldProjectState.AnalyzerConfigDocumentStates, newProjectState.AnalyzerConfigDocumentStates); + } } var newIdToProjectStateMap = newIdToProjectStateMapBuilder.ToImmutable(); var newIdToTrackerMap = newIdToTrackerMapBuilder.ToImmutable(); + var filePathToDocumentIdsMap = this.SolutionState.CreateFilePathToDocumentIdsMapWithAddedAndRemovedDocuments( + documentsToAdd: documentsToAdd, + documentsToRemove: documentsToRemove); + var dependencyGraph = SolutionState.CreateDependencyGraph(this.SolutionState.ProjectIds, newIdToProjectStateMap); + var newState = this.SolutionState.Branch( idToProjectStateMap: newIdToProjectStateMap, - dependencyGraph: SolutionState.CreateDependencyGraph(this.SolutionState.ProjectIds, newIdToProjectStateMap)); + filePathToDocumentIdsMap: filePathToDocumentIdsMap, + dependencyGraph: dependencyGraph); + var newCompilationState = this.Branch( newState, - newIdToTrackerMap); + newIdToTrackerMap, + // Set the frozen solution to be its own frozen solution. Freezing multiple times is a no-op. + cachedFrozenSnapshot: _cachedFrozenSnapshot); return newCompilationState; + + void CheckDocumentStates( + TextDocumentStates oldStates, + TextDocumentStates newStates) where TDocumentState : TextDocumentState + { + // Get the trivial sets of documents that are present in one set but not the other. + foreach (var documentId in newStates.GetAddedStateIds(oldStates)) + documentsToAdd.Add(newStates.GetRequiredState(documentId)); + + foreach (var documentId in newStates.GetRemovedStateIds(oldStates)) + documentsToRemove.Add(oldStates.GetRequiredState(documentId)); + + // Now go through the states that are in both sets. We have to check these all as it is possible for + // document to change its file path without its id changing. + foreach (var (documentId, oldDocumentState) in oldStates.States) + { + if (newStates.States.TryGetValue(documentId, out var newDocumentState) && + oldDocumentState != newDocumentState && + oldDocumentState.FilePath != newDocumentState.FilePath) + { + documentsToRemove.Remove(oldDocumentState); + documentsToAdd.Add(newDocumentState); + } + } + } } /// @@ -1147,7 +1229,10 @@ static SolutionCompilationState WithFrozenPartialCompilationIncludingSpecificDoc documentStates.Add(documentState); } - return ComputeFrozenPartialState(@this, documentStates, cancellationToken); + // now freeze the solution state, capturing whatever compilations are in progress. + var frozenCompilationState = @this.WithFrozenPartialCompilations(cancellationToken); + + return ComputeFrozenPartialState(frozenCompilationState, documentStates, cancellationToken); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical)) { @@ -1156,32 +1241,33 @@ static SolutionCompilationState WithFrozenPartialCompilationIncludingSpecificDoc } static SolutionCompilationState ComputeFrozenPartialState( - SolutionCompilationState @this, + SolutionCompilationState frozenCompilationState, ArrayBuilder documentStates, CancellationToken cancellationToken) { - var newIdToProjectStateMap = @this.SolutionState.ProjectStates; - var newIdToTrackerMap = @this._projectIdToTrackerMap; - - foreach (var docState in documentStates) + var currentState = frozenCompilationState; + foreach (var newDocumentState in documentStates) { - // if we don't have one or it is stale, create a new partial solution - var tracker = @this.GetCompilationTracker(docState.Id.ProjectId); - var newTracker = tracker.FreezePartialStateWithDocument(docState, cancellationToken); + var documentId = newDocumentState.Id; + var oldProjectState = currentState.SolutionState.GetRequiredProjectState(documentId.ProjectId); + var oldDocumentState = oldProjectState.DocumentStates.GetState(documentId); - Contract.ThrowIfFalse(newIdToProjectStateMap.ContainsKey(docState.Id.ProjectId)); - newIdToProjectStateMap = newIdToProjectStateMap.SetItem(docState.Id.ProjectId, newTracker.ProjectState); - newIdToTrackerMap = newIdToTrackerMap.SetItem(docState.Id.ProjectId, newTracker); + if (oldDocumentState is null) + { + // Project doesn't have this document, attempt to fork it with the document added. + currentState = currentState.AddDocumentsToMultipleProjects( + [(oldProjectState, [newDocumentState])], + static (oldProjectState, newDocumentStates) => + new TranslationAction.AddDocumentsAction(oldProjectState, oldProjectState.AddDocuments(newDocumentStates), newDocumentStates)); + } + else + { + // Project has this document, attempt to fork it with the new contents. + currentState = currentState.WithDocumentState(newDocumentState); + } } - var newState = @this.SolutionState.Branch( - idToProjectStateMap: newIdToProjectStateMap, - dependencyGraph: SolutionState.CreateDependencyGraph(@this.SolutionState.ProjectIds, newIdToProjectStateMap)); - var newCompilationState = @this.Branch( - newState, - newIdToTrackerMap); - - return newCompilationState; + return currentState; } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index 83c54037a6d8..8a6fb868502e 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -404,6 +404,25 @@ public SolutionState RemoveProject(ProjectId projectId) dependencyGraph: newDependencyGraph); } + public ImmutableDictionary> CreateFilePathToDocumentIdsMapWithAddedAndRemovedDocuments( + ArrayBuilder documentsToAdd, + ArrayBuilder documentsToRemove) + { + if (documentsToRemove.Count == 0 && documentsToAdd.Count == 0) + return _filePathToDocumentIdsMap; + + var builder = _filePathToDocumentIdsMap.ToBuilder(); + + // Add first, then remove. This helps avoid the case where a filepath now sees no documents, so we remove + // the entry entirely for it in the dictionary, only to add it back in. Adding then removing will at least + // keep the entry, but increase the docs for it, then lower it back down. + + AddDocumentFilePaths(documentsToAdd, builder); + RemoveDocumentFilePaths(documentsToRemove, builder); + + return builder.ToImmutable(); + } + public ImmutableDictionary> CreateFilePathToDocumentIdsMapWithAddedDocuments(IEnumerable documentStates) { var builder = _filePathToDocumentIdsMap.ToBuilder(); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs index 2f0a0f9c7f2b..2df39ead6024 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs @@ -203,7 +203,7 @@ public IEnumerable GetAddedStateIds(TextDocumentStates oldSt public IEnumerable GetRemovedStateIds(TextDocumentStates oldStates) => (_ids == oldStates._ids) ? SpecializedCollections.EmptyEnumerable() : Except(oldStates._ids, _map); - private static IEnumerable Except(IEnumerable ids, ImmutableSortedDictionary map) + private static IEnumerable Except(ImmutableList ids, ImmutableSortedDictionary map) { foreach (var id in ids) { diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index 647588b16453..f99467d9ab0b 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -4527,7 +4527,7 @@ public async Task TestFrozenPartialSolution1() // Because we froze before ever even looking at anything semantics related, we should have no documents in // this project. - var frozenSolution = await project.Solution.WithFrozenPartialCompilationsAsync(CancellationToken.None); + var frozenSolution = project.Solution.WithFrozenPartialCompilations(CancellationToken.None); var frozenProject = frozenSolution.Projects.Single(); Assert.Empty(frozenProject.Documents); @@ -4546,7 +4546,7 @@ public async Task TestFrozenPartialSolution2() // Because we froze after looking at anything semantics related, we should have the documents in this // project. - var frozenSolution = await project.Solution.WithFrozenPartialCompilationsAsync(CancellationToken.None); + var frozenSolution = project.Solution.WithFrozenPartialCompilations(CancellationToken.None); var frozenProject = frozenSolution.Projects.Single(); Assert.Single(frozenProject.Documents); @@ -4568,7 +4568,7 @@ public async Task TestFrozenPartialSolution3() // Getting compilation from one should not affect frozen-ness of other project. await project1.GetCompilationAsync(); - var frozenSolution = await project1.Solution.WithFrozenPartialCompilationsAsync(CancellationToken.None); + var frozenSolution = project1.Solution.WithFrozenPartialCompilations(CancellationToken.None); var frozenProject1 = frozenSolution.GetProject(project1.Id); Assert.Single(frozenProject1.Documents); @@ -4598,7 +4598,7 @@ public async Task TestFrozenPartialSolution4() // Getting compilation from project1 should not affect project 2. await project1.GetCompilationAsync(); - var frozenSolution = await project1.Solution.WithFrozenPartialCompilationsAsync(CancellationToken.None); + var frozenSolution = project1.Solution.WithFrozenPartialCompilations(CancellationToken.None); var frozenProject1 = frozenSolution.GetProject(project1.Id); Assert.Single(frozenProject1.Documents); @@ -4628,7 +4628,7 @@ public async Task TestFrozenPartialSolution5() // Getting compilation from project2 should affect project 1 as there's a ptp relationship with it. await project2.GetCompilationAsync(); - var frozenSolution = await project1.Solution.WithFrozenPartialCompilationsAsync(CancellationToken.None); + var frozenSolution = project1.Solution.WithFrozenPartialCompilations(CancellationToken.None); var frozenProject1 = frozenSolution.GetProject(project1.Id); Assert.Single(frozenProject1.Documents); @@ -4658,7 +4658,7 @@ public async Task TestFrozenPartialSolution6() var compilation1 = await project1.GetCompilationAsync(); var syntaxTree1 = await project1.Documents.Single().GetSyntaxTreeAsync(); - var frozenSolution = await project1.Solution.WithFrozenPartialCompilationsAsync(CancellationToken.None); + var frozenSolution = project1.Solution.WithFrozenPartialCompilations(CancellationToken.None); var forkedProject1 = frozenSolution.WithDocumentText(project1.Documents.Single().Id, SourceText.From("class Doc2 { }")).GetProject(project1.Id); var forkedDocument1 = forkedProject1.Documents.Single(); @@ -4693,7 +4693,7 @@ public async Task TestFrozenPartialSolution7(bool freeze) Assert.Equal("// source1", generatedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString()); var frozenSolution = freeze - ? await project1.Solution.WithFrozenPartialCompilationsAsync(CancellationToken.None) + ? project1.Solution.WithFrozenPartialCompilations(CancellationToken.None) : project1.Solution; var forkedProject1 = frozenSolution.WithDocumentText(project1.Documents.Single().Id, SourceText.From("class Doc2 { }")).GetProject(project1.Id); diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs index 4334f1fa84ee..1a5d986601cf 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs @@ -771,6 +771,8 @@ public async Task LinkedDocumentOfFrozenShouldNotRunSourceGenerator(TestHost tes foreach (var documentIdToTest in documentIdsToTest) { var document = frozenSolution.GetRequiredDocument(documentIdToTest); + Assert.Single(document.GetLinkedDocumentIds()); + Assert.Equal(document.GetLinkedDocumentIds().Single(), documentIdsToTest.Except([documentIdToTest]).Single()); document = document.WithText(SourceText.From("// Something else"));