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"));