diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index ae6aeb8e343f6..dd02f2e8289d0 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -129,6 +129,14 @@ public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo, _lazyContentHashToDocumentId = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeContentHashToDocumentIdAsync(cancellationToken), arg: this); } + public TextDocumentStates GetDocumentStates() + where TDocumentState : TextDocumentState + => (TextDocumentStates)(object)( + typeof(TDocumentState) == typeof(DocumentState) ? DocumentStates : + typeof(TDocumentState) == typeof(AdditionalDocumentState) ? AdditionalDocumentStates : + typeof(TDocumentState) == typeof(AnalyzerConfigDocumentState) ? AnalyzerConfigDocumentStates : + throw ExceptionUtilities.UnexpectedValue(typeof(TDocumentState))); + private async Task, DocumentId>> ComputeContentHashToDocumentIdAsync(CancellationToken cancellationToken) { var result = new Dictionary, DocumentId>(ImmutableArrayComparer.Instance); @@ -276,6 +284,14 @@ internal DocumentState CreateDocument(DocumentInfo documentInfo, ParseOptions? p return doc; } + internal TDocumentState CreateDocument(DocumentInfo documentInfo) + where TDocumentState : TextDocumentState + => (TDocumentState)(object)( + typeof(TDocumentState) == typeof(DocumentState) ? CreateDocument(documentInfo, ParseOptions, new LoadTextOptions(ChecksumAlgorithm)) : + typeof(TDocumentState) == typeof(AdditionalDocumentState) ? new AdditionalDocumentState(LanguageServices.SolutionServices, documentInfo, new LoadTextOptions(ChecksumAlgorithm)) : + typeof(TDocumentState) == typeof(AnalyzerConfigDocumentState) ? new AnalyzerConfigDocumentState(LanguageServices.SolutionServices, documentInfo, new LoadTextOptions(ChecksumAlgorithm)) : + throw ExceptionUtilities.UnexpectedValue(typeof(TDocumentState))); + public AnalyzerOptions AnalyzerOptions => _lazyAnalyzerOptions ??= new AnalyzerOptions( additionalFiles: AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText), @@ -828,26 +844,25 @@ public ProjectState RemoveAllNormalDocuments() } public ProjectState UpdateDocument(DocumentState newDocument, bool contentChanged) - => UpdateDocuments([newDocument], contentChanged); + => UpdateDocuments([DocumentStates.GetRequiredState(newDocument.Id)], [newDocument], contentChanged); - public ProjectState UpdateDocuments(ImmutableArray newDocuments, bool contentChanged) + public ProjectState UpdateDocuments(ImmutableArray oldDocuments, ImmutableArray newDocuments, bool contentChanged) { - var oldDocuments = newDocuments.SelectAsArray(d => DocumentStates.GetRequiredState(d.Id)); - if (oldDocuments.SequenceEqual(newDocuments)) - return this; - - // Must not be empty as we would have otherwise bailed out in the check above. - Contract.ThrowIfTrue(newDocuments.IsEmpty); + Contract.ThrowIfTrue(oldDocuments.IsEmpty); + Contract.ThrowIfFalse(oldDocuments.Length == newDocuments.Length); + Debug.Assert(!oldDocuments.SequenceEqual(newDocuments)); var newDocumentStates = DocumentStates.SetStates(newDocuments); // When computing the latest dependent version, we just need to know how GetLatestDependentVersions( - newDocumentStates, AdditionalDocumentStates, + newDocumentStates, + AdditionalDocumentStates, oldDocuments.CastArray(), newDocuments.CastArray(), contentChanged, - out var dependentDocumentVersion, out var dependentSemanticVersion); + out var dependentDocumentVersion, + out var dependentSemanticVersion); return With( documentStates: newDocumentStates, @@ -856,34 +871,38 @@ public ProjectState UpdateDocuments(ImmutableArray newDocuments, } public ProjectState UpdateAdditionalDocument(AdditionalDocumentState newDocument, bool contentChanged) + => UpdateAdditionalDocuments([AdditionalDocumentStates.GetRequiredState(newDocument.Id)], [newDocument], contentChanged); + + public ProjectState UpdateAdditionalDocuments(ImmutableArray oldDocuments, ImmutableArray newDocuments, bool contentChanged) { - var oldDocument = AdditionalDocumentStates.GetRequiredState(newDocument.Id); - if (oldDocument == newDocument) - { - return this; - } + Contract.ThrowIfTrue(oldDocuments.IsEmpty); + Contract.ThrowIfFalse(oldDocuments.Length == newDocuments.Length); + Debug.Assert(!oldDocuments.SequenceEqual(newDocuments)); + + var newDocumentStates = AdditionalDocumentStates.SetStates(newDocuments); - var newDocumentStates = AdditionalDocumentStates.SetState(newDocument); GetLatestDependentVersions( - DocumentStates, newDocumentStates, [oldDocument], [newDocument], contentChanged, - out var dependentDocumentVersion, out var dependentSemanticVersion); + DocumentStates, + newDocumentStates, + oldDocuments.CastArray(), + newDocuments.CastArray(), + contentChanged, + out var dependentDocumentVersion, + out var dependentSemanticVersion); - return this.With( + return With( additionalDocumentStates: newDocumentStates, latestDocumentVersion: dependentDocumentVersion, latestDocumentTopLevelChangeVersion: dependentSemanticVersion); } public ProjectState UpdateAnalyzerConfigDocument(AnalyzerConfigDocumentState newDocument) - { - var oldDocument = AnalyzerConfigDocumentStates.GetRequiredState(newDocument.Id); - if (oldDocument == newDocument) - { - return this; - } - - var newDocumentStates = AnalyzerConfigDocumentStates.SetState(newDocument); + => UpdateAnalyzerConfigDocuments([AnalyzerConfigDocumentStates.GetRequiredState(newDocument.Id)], [newDocument]); + public ProjectState UpdateAnalyzerConfigDocuments(ImmutableArray oldDocuments, ImmutableArray newDocuments) + { + Debug.Assert(!oldDocuments.SequenceEqual(newDocuments)); + var newDocumentStates = AnalyzerConfigDocumentStates.SetStates(newDocuments); return CreateNewStateForChangedAnalyzerConfig(newDocumentStates, _analyzerConfigOptionsCache.FallbackOptions); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index 281ab04903100..121df9c3fe2b6 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -983,7 +983,7 @@ public Solution AddDocument(DocumentInfo documentInfo) /// /// A new with the documents added. public Solution AddDocuments(ImmutableArray documentInfos) - => WithCompilationState(_compilationState.AddDocuments(documentInfos)); + => WithCompilationState(_compilationState.AddDocumentsToMultipleProjects(documentInfos)); /// /// Creates a new solution instance with the corresponding project updated to include a new @@ -1021,7 +1021,7 @@ public Solution AddAdditionalDocument(DocumentInfo documentInfo) => AddAdditionalDocuments([documentInfo]); public Solution AddAdditionalDocuments(ImmutableArray documentInfos) - => WithCompilationState(_compilationState.AddAdditionalDocuments(documentInfos)); + => WithCompilationState(_compilationState.AddDocumentsToMultipleProjects(documentInfos)); /// /// Creates a new solution instance with the corresponding project updated to include a new @@ -1073,7 +1073,7 @@ private ProjectState GetRequiredProjectState(ProjectId projectId) /// Creates a new Solution instance that contains a new compiler configuration document like a .editorconfig file. /// public Solution AddAnalyzerConfigDocuments(ImmutableArray documentInfos) - => WithCompilationState(_compilationState.AddAnalyzerConfigDocuments(documentInfos)); + => WithCompilationState(_compilationState.AddDocumentsToMultipleProjects(documentInfos)); /// /// Creates a new solution instance that no longer includes the specified document. @@ -1094,7 +1094,7 @@ public Solution RemoveDocuments(ImmutableArray documentIds) } private Solution RemoveDocumentsImpl(ImmutableArray documentIds) - => WithCompilationState(_compilationState.RemoveDocuments(documentIds)); + => WithCompilationState(_compilationState.RemoveDocumentsFromMultipleProjects(documentIds)); /// /// Creates a new solution instance that no longer includes the specified additional document. @@ -1115,7 +1115,7 @@ public Solution RemoveAdditionalDocuments(ImmutableArray documentIds } private Solution RemoveAdditionalDocumentsImpl(ImmutableArray documentIds) - => WithCompilationState(_compilationState.RemoveAdditionalDocuments(documentIds)); + => WithCompilationState(_compilationState.RemoveDocumentsFromMultipleProjects(documentIds)); /// /// Creates a new solution instance that no longer includes the specified . @@ -1136,7 +1136,7 @@ public Solution RemoveAnalyzerConfigDocuments(ImmutableArray documen } private Solution RemoveAnalyzerConfigDocumentsImpl(ImmutableArray documentIds) - => WithCompilationState(_compilationState.RemoveAnalyzerConfigDocuments(documentIds)); + => WithCompilationState(_compilationState.RemoveDocumentsFromMultipleProjects(documentIds)); /// /// Creates a new solution instance with the document specified updated to have the new name. diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs index 6d624d7c540f0..e45045e3c21c7 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs @@ -20,15 +20,16 @@ private abstract partial class TranslationAction internal sealed class TouchDocumentsAction( ProjectState oldProjectState, ProjectState newProjectState, + ImmutableArray oldStates, ImmutableArray newStates) : TranslationAction(oldProjectState, newProjectState) { - private readonly ImmutableArray _oldStates = newStates.SelectAsArray(s => oldProjectState.DocumentStates.GetRequiredState(s.Id)); + private readonly ImmutableArray _oldStates = oldStates; private readonly ImmutableArray _newStates = newStates; public override async Task TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken) { var finalCompilation = oldCompilation; - for (int i = 0, n = _newStates.Length; i < n; i++) + for (var i = 0; i < _newStates.Length; i++) { cancellationToken.ThrowIfCancellationRequested(); var newState = _newStates[i]; @@ -57,22 +58,22 @@ public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generat { // As we're merging ourselves with the prior touch action, we want to keep the old project state // that we are translating from. - return new TouchDocumentsAction(priorAction.OldProjectState, this.NewProjectState, _newStates); + return new TouchDocumentsAction(priorAction.OldProjectState, NewProjectState, priorTouchAction._oldStates, _newStates); } return null; } } - internal sealed class TouchAdditionalDocumentAction( + internal sealed class TouchAdditionalDocumentsAction( ProjectState oldProjectState, ProjectState newProjectState, - AdditionalDocumentState oldState, - AdditionalDocumentState newState) + ImmutableArray oldStates, + ImmutableArray newStates) : TranslationAction(oldProjectState, newProjectState) { - private readonly AdditionalDocumentState _oldState = oldState; - private readonly AdditionalDocumentState _newState = newState; + private readonly ImmutableArray _oldStates = oldStates; + private readonly ImmutableArray _newStates = newStates; // Changing an additional document doesn't change the compilation directly, so we can "apply" the // translation (which is a no-op). Since we use a 'false' here to mean that it's not worth keeping the @@ -84,12 +85,12 @@ public override Task TransformCompilationAsync(Compilation oldCompi public override TranslationAction? TryMergeWithPrior(TranslationAction priorAction) { - if (priorAction is TouchAdditionalDocumentAction priorTouchAction && - priorTouchAction._newState == _oldState) + if (priorAction is TouchAdditionalDocumentsAction priorTouchAction && + priorTouchAction._newStates.SequenceEqual(_oldStates)) { // As we're merging ourselves with the prior touch action, we want to keep the old project state // that we are translating from. - return new TouchAdditionalDocumentAction(priorAction.OldProjectState, this.NewProjectState, priorTouchAction._oldState, _newState); + return new TouchAdditionalDocumentsAction(priorAction.OldProjectState, NewProjectState, priorTouchAction._oldStates, _newStates); } return null; @@ -97,11 +98,35 @@ public override Task TransformCompilationAsync(Compilation oldCompi public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) { - var oldText = _oldState.AdditionalText; - var newText = _newState.AdditionalText; + for (var i = 0; i < _newStates.Length; i++) + { + generatorDriver = generatorDriver.ReplaceAdditionalText(_oldStates[i].AdditionalText, _newStates[i].AdditionalText); + } + + return generatorDriver; + } + } - return generatorDriver.ReplaceAdditionalText(oldText, newText); + internal sealed class TouchAnalyzerConfigDocumentsAction( + ProjectState oldProjectState, + ProjectState newProjectState) + : TranslationAction(oldProjectState, newProjectState) + { + /// + /// Updating editorconfig document updates . + /// + public override Task TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken) + { + RoslynDebug.AssertNotNull(this.NewProjectState.CompilationOptions); + return Task.FromResult(oldCompilation.WithOptions(this.NewProjectState.CompilationOptions)); } + + // Updating the analyzer config optons doesn't require us to reparse trees, so we can use this to update + // compilations with stale generated trees. + public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true; + + public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) + => generatorDriver.WithUpdatedAnalyzerConfigOptions(NewProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider); } internal sealed class RemoveDocumentsAction( @@ -210,8 +235,7 @@ public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generat internal sealed class ProjectCompilationOptionsAction( ProjectState oldProjectState, - ProjectState newProjectState, - bool isAnalyzerConfigChange) + ProjectState newProjectState) : TranslationAction(oldProjectState, newProjectState) { public override Task TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken) @@ -226,16 +250,9 @@ public override Task TransformCompilationAsync(Compilation oldCompi public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) { - if (isAnalyzerConfigChange) - { - return generatorDriver.WithUpdatedAnalyzerConfigOptions(this.NewProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider); - } - else - { - // Changing any other option is fine and the driver can be reused. The driver - // will get the new compilation once we pass it to it. - return generatorDriver; - } + // Changing any other option is fine and the driver can be reused. The driver + // will get the new compilation once we pass it to it. + return generatorDriver; } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs index 6e8de06b8085d..efd1574e6a203 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs @@ -18,7 +18,6 @@ using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer; @@ -492,8 +491,7 @@ public SolutionCompilationState WithProjectCompilationOptions( { return ForkProject( this.SolutionState.WithProjectCompilationOptions(projectId, options), - static stateChange => new TranslationAction.ProjectCompilationOptionsAction( - stateChange.OldProjectState, stateChange.NewProjectState, isAnalyzerConfigChange: false), + static stateChange => new TranslationAction.ProjectCompilationOptionsAction(stateChange.OldProjectState, stateChange.NewProjectState), forkTracker: true); } @@ -699,48 +697,128 @@ public SolutionCompilationState WithDocumentAttributes( } internal SolutionCompilationState WithDocumentTexts(ImmutableArray<(DocumentId documentId, SourceText text)> texts, PreservationMode mode) - => UpdateDocumentsInMultipleProjects( + => UpdateDocumentsInMultipleProjects( texts, arg: mode, updateDocument: static (oldDocumentState, text, mode) => - SourceTextIsUnchanged(oldDocumentState, text) ? oldDocumentState : oldDocumentState.UpdateText(text, mode)); + SourceTextIsUnchanged(oldDocumentState, text) ? oldDocumentState : oldDocumentState.UpdateText(text, mode), + contentChanged: true); private static bool SourceTextIsUnchanged(DocumentState oldDocument, SourceText text) => oldDocument.TryGetText(out var oldText) && text == oldText; - private SolutionCompilationState UpdateDocumentsInMultipleProjects( - ImmutableArray<(DocumentId documentId, TDocumentData documentData)> updates, + /// + /// Applies an update operation to specified . + /// Documents may be in different projects. + /// + /// + /// True if changes the content of the document + /// (i.e. document text, not just document attributes). + /// + private SolutionCompilationState UpdateDocumentsInMultipleProjects( + ImmutableArray<(DocumentId documentId, TDocumentData documentData)> documentsToUpdate, TArg arg, - Func updateDocument) + Func updateDocument, + bool contentChanged) + where TDocumentState : TextDocumentState + { + return WithDocumentStatesOfMultipleProjects( + documentsToUpdate + .GroupBy(static d => d.documentId.ProjectId) + .Select(g => + { + var projectId = g.Key; + var oldProjectState = SolutionState.GetRequiredProjectState(projectId); + var oldDocumentStates = oldProjectState.GetDocumentStates(); + + using var _ = ArrayBuilder.GetInstance(out var newDocumentStates); + foreach (var (documentId, documentData) in g) + { + var oldDocumentState = oldDocumentStates.GetRequiredState(documentId); + var newDocumentState = updateDocument(oldDocumentState, documentData, arg); + + if (ReferenceEquals(oldDocumentState, newDocumentState)) + continue; + + newDocumentStates.Add(newDocumentState); + } + + return (projectId, newDocumentStates.ToImmutableAndClear()); + }), + (oldProjectState, newDocumentStates) => GetUpdateDocumentsTranslationAction(oldProjectState, newDocumentStates, contentChanged)); + } + + /// + /// Returns with projects updated to new document states specified in . + /// + private SolutionCompilationState WithDocumentStatesOfMultipleProjects( + IEnumerable<(ProjectId projectId, ImmutableArray updatedDocumentState)> updatedDocumentStatesPerProject, + Func, TranslationAction> getTranslationAction) + where TDocumentState : TextDocumentState { - return UpdateDocumentsInMultipleProjects( - updates.GroupBy(static d => d.documentId.ProjectId).Select(g => + var newCompilationState = this; + + foreach (var (projectId, newDocumentStates) in updatedDocumentStatesPerProject) + { + if (newDocumentStates.IsEmpty) { - var projectId = g.Key; - var projectState = SolutionState.GetRequiredProjectState(projectId); + continue; + } - using var _ = ArrayBuilder.GetInstance(out var newDocumentStates); - foreach (var (documentId, documentData) in g) - { - var documentState = projectState.DocumentStates.GetRequiredState(documentId); - var newDocumentState = updateDocument(documentState, documentData, arg); + var oldProjectState = newCompilationState.SolutionState.GetRequiredProjectState(projectId); + var compilationTranslationAction = getTranslationAction(oldProjectState, newDocumentStates); + var newProjectState = compilationTranslationAction.NewProjectState; - if (ReferenceEquals(documentState, newDocumentState)) - continue; + var stateChange = newCompilationState.SolutionState.ForkProject( + oldProjectState, + newProjectState); - newDocumentStates.Add(newDocumentState); - } + newCompilationState = newCompilationState.ForkProject( + stateChange, + static (_, compilationTranslationAction) => compilationTranslationAction, + forkTracker: true, + arg: compilationTranslationAction); + } - return (projectId, newDocumentStates.ToImmutableAndClear()); - }), - GetUpdateDocumentsTranslationAction); + return newCompilationState; } - private static TranslationAction GetUpdateDocumentsTranslationAction(ProjectState projectState, ImmutableArray newDocumentStates) - => new TranslationAction.TouchDocumentsAction( - projectState, - projectState.UpdateDocuments(newDocumentStates, contentChanged: true), - newDocumentStates); + /// + /// Updates the to a new state with and returns a that + /// reflects these changes in the project compilation. + /// + private static TranslationAction GetUpdateDocumentsTranslationAction(ProjectState oldProjectState, ImmutableArray newDocumentStates, bool contentChanged) + where TDocumentState : TextDocumentState + { + return newDocumentStates switch + { + ImmutableArray ordinaryNewDocumentStates => GetUpdateOrdinaryDocumentsTranslationAction(oldProjectState, ordinaryNewDocumentStates, contentChanged), + ImmutableArray additionalNewDocumentStates => GetUpdateAdditionalDocumentsTranslationAction(oldProjectState, additionalNewDocumentStates, contentChanged), + ImmutableArray analyzerConfigNewDocumentStates => GetUpdateAnalyzerConfigDocumentsTranslationAction(oldProjectState, analyzerConfigNewDocumentStates), + _ => throw ExceptionUtilities.UnexpectedValue(typeof(TDocumentState)) + }; + + TranslationAction GetUpdateOrdinaryDocumentsTranslationAction(ProjectState oldProjectState, ImmutableArray newDocumentStates, bool contentChanged) + { + var oldDocumentStates = newDocumentStates.SelectAsArray(static (s, oldProjectState) => oldProjectState.DocumentStates.GetRequiredState(s.Id), oldProjectState); + var newProjectState = oldProjectState.UpdateDocuments(oldDocumentStates, newDocumentStates, contentChanged); + return new TranslationAction.TouchDocumentsAction(oldProjectState, newProjectState, oldDocumentStates, newDocumentStates); + } + + TranslationAction GetUpdateAdditionalDocumentsTranslationAction(ProjectState oldProjectState, ImmutableArray newDocumentStates, bool contentChanged) + { + var oldDocumentStates = newDocumentStates.SelectAsArray(static (s, oldProjectState) => oldProjectState.AdditionalDocumentStates.GetRequiredState(s.Id), oldProjectState); + var newProjectState = oldProjectState.UpdateAdditionalDocuments(oldDocumentStates, newDocumentStates, contentChanged); + return new TranslationAction.TouchAdditionalDocumentsAction(oldProjectState, newProjectState, oldDocumentStates, newDocumentStates); + } + + TranslationAction GetUpdateAnalyzerConfigDocumentsTranslationAction(ProjectState oldProjectState, ImmutableArray newDocumentStates) + { + var oldDocumentStates = newDocumentStates.SelectAsArray(static (s, oldProjectState) => oldProjectState.AnalyzerConfigDocumentStates.GetRequiredState(s.Id), oldProjectState); + var newProjectState = oldProjectState.UpdateAnalyzerConfigDocuments(oldDocumentStates, newDocumentStates); + return new TranslationAction.TouchAnalyzerConfigDocumentsAction(oldProjectState, newProjectState); + } + } public SolutionCompilationState WithDocumentState( DocumentState documentState) @@ -795,25 +873,27 @@ public SolutionCompilationState WithAnalyzerConfigDocumentText( /// public SolutionCompilationState WithDocumentSyntaxRoots(ImmutableArray<(DocumentId documentId, SyntaxNode root)> syntaxRoots, PreservationMode mode) { - return UpdateDocumentsInMultipleProjects( + return UpdateDocumentsInMultipleProjects( syntaxRoots, arg: mode, static (oldDocumentState, root, mode) => oldDocumentState.TryGetSyntaxTree(out var oldTree) && oldTree.TryGetRoot(out var oldRoot) && oldRoot == root ? oldDocumentState - : oldDocumentState.UpdateTree(root, mode)); + : oldDocumentState.UpdateTree(root, mode), + contentChanged: true); } public SolutionCompilationState WithDocumentContentsFrom( ImmutableArray<(DocumentId documentId, DocumentState documentState)> documentIdsAndStates, bool forceEvenIfTreesWouldDiffer) { - return UpdateDocumentsInMultipleProjects( + return UpdateDocumentsInMultipleProjects( documentIdsAndStates, arg: forceEvenIfTreesWouldDiffer, static (oldDocumentState, documentState, forceEvenIfTreesWouldDiffer) => oldDocumentState.TextAndVersionSource == documentState.TextAndVersionSource && oldDocumentState.TreeSource == documentState.TreeSource ? oldDocumentState - : oldDocumentState.UpdateTextAndTreeContents(documentState.TextAndVersionSource, documentState.TreeSource, forceEvenIfTreesWouldDiffer)); + : oldDocumentState.UpdateTextAndTreeContents(documentState.TextAndVersionSource, documentState.TreeSource, forceEvenIfTreesWouldDiffer), + contentChanged: true); } /// @@ -878,10 +958,11 @@ private SolutionCompilationState UpdateDocumentState(StateChange stateChange, Do // This function shouldn't have been called if the document has not changed Debug.Assert(stateChange.OldProjectState != stateChange.NewProjectState); + var oldDocument = stateChange.OldProjectState.DocumentStates.GetRequiredState(documentId); var newDocument = stateChange.NewProjectState.DocumentStates.GetRequiredState(documentId); return new TranslationAction.TouchDocumentsAction( - stateChange.OldProjectState, stateChange.NewProjectState, [newDocument]); + stateChange.OldProjectState, stateChange.NewProjectState, [oldDocument], [newDocument]); }, forkTracker: true, arg: documentId); @@ -899,7 +980,8 @@ private SolutionCompilationState UpdateAdditionalDocumentState(StateChange state var oldDocument = stateChange.OldProjectState.AdditionalDocumentStates.GetRequiredState(documentId); var newDocument = stateChange.NewProjectState.AdditionalDocumentStates.GetRequiredState(documentId); - return new TranslationAction.TouchAdditionalDocumentAction(stateChange.OldProjectState, stateChange.NewProjectState, oldDocument, newDocument); + return new TranslationAction.TouchAdditionalDocumentsAction( + stateChange.OldProjectState, stateChange.NewProjectState, [oldDocument], [newDocument]); }, forkTracker: true, arg: documentId); @@ -909,10 +991,7 @@ private SolutionCompilationState UpdateAnalyzerConfigDocumentState(StateChange s { return ForkProject( stateChange, - static stateChange => stateChange.NewProjectState.CompilationOptions != null - ? new TranslationAction.ProjectCompilationOptionsAction( - stateChange.OldProjectState, stateChange.NewProjectState, isAnalyzerConfigChange: true) - : null, + static stateChange => new TranslationAction.TouchAnalyzerConfigDocumentsAction(stateChange.OldProjectState, stateChange.NewProjectState), forkTracker: true); } @@ -1460,70 +1539,22 @@ static SolutionCompilationState ComputeFrozenPartialState( } // Now, add all missing documents per project. - currentState = currentState.UpdateDocumentsInMultipleProjects( + currentState = currentState.WithDocumentStatesOfMultipleProjects( // Do a SelectAsArray here to ensure that we realize the array once, and as such only call things like // ToImmutableAndFree once per ArrayBuilder. missingDocumentStates.SelectAsArray(kvp => (kvp.Key, kvp.Value.ToImmutableAndFree())), - static (oldProjectState, newDocumentStates) => - new TranslationAction.AddDocumentsAction(oldProjectState, oldProjectState.AddDocuments(newDocumentStates), newDocumentStates)); + GetAddDocumentsTranslationAction); return currentState; } } - public SolutionCompilationState AddDocuments(ImmutableArray documentInfos) - { - return AddDocumentsToMultipleProjects(documentInfos, - static (documentInfo, project) => project.CreateDocument(documentInfo, project.ParseOptions, new LoadTextOptions(project.ChecksumAlgorithm)), - static (oldProject, documents) => new TranslationAction.AddDocumentsAction(oldProject, oldProject.AddDocuments(documents), documents)); - } - - public SolutionCompilationState AddAdditionalDocuments(ImmutableArray documentInfos) - { - return AddDocumentsToMultipleProjects(documentInfos, - static (documentInfo, project) => new AdditionalDocumentState(project.LanguageServices.SolutionServices, documentInfo, new LoadTextOptions(project.ChecksumAlgorithm)), - static (oldProject, documents) => new TranslationAction.AddAdditionalDocumentsAction(oldProject, oldProject.AddAdditionalDocuments(documents), documents)); - } - - public SolutionCompilationState AddAnalyzerConfigDocuments(ImmutableArray documentInfos) - { - return AddDocumentsToMultipleProjects(documentInfos, - static (documentInfo, project) => new AnalyzerConfigDocumentState(project.LanguageServices.SolutionServices, documentInfo, new LoadTextOptions(project.ChecksumAlgorithm)), - static (oldProject, documents) => new TranslationAction.ProjectCompilationOptionsAction(oldProject, oldProject.AddAnalyzerConfigDocuments(documents), isAnalyzerConfigChange: true)); - } - - public SolutionCompilationState RemoveDocuments(ImmutableArray documentIds) - { - return RemoveDocumentsFromMultipleProjects(documentIds, - static (projectState, documentId) => projectState.DocumentStates.GetRequiredState(documentId), - static (oldProject, documentIds, documentStates) => new TranslationAction.RemoveDocumentsAction(oldProject, oldProject.RemoveDocuments(documentIds), documentStates)); - } - - public SolutionCompilationState RemoveAdditionalDocuments(ImmutableArray documentIds) - { - return RemoveDocumentsFromMultipleProjects(documentIds, - static (projectState, documentId) => projectState.AdditionalDocumentStates.GetRequiredState(documentId), - static (oldProject, documentIds, documentStates) => new TranslationAction.RemoveAdditionalDocumentsAction(oldProject, oldProject.RemoveAdditionalDocuments(documentIds), documentStates)); - } - - public SolutionCompilationState RemoveAnalyzerConfigDocuments(ImmutableArray documentIds) - { - return RemoveDocumentsFromMultipleProjects(documentIds, - static (projectState, documentId) => projectState.AnalyzerConfigDocumentStates.GetRequiredState(documentId), - static (oldProject, documentIds, _) => new TranslationAction.ProjectCompilationOptionsAction(oldProject, oldProject.RemoveAnalyzerConfigDocuments(documentIds), isAnalyzerConfigChange: true)); - } - /// /// Core helper that takes a set of s and does the application of the appropriate documents to each project. /// /// The set of documents to add. - /// Returns the new with the documents added, - /// and the needed as - /// well. - private SolutionCompilationState AddDocumentsToMultipleProjects( - ImmutableArray documentInfos, - Func createDocumentState, - Func, TranslationAction> addDocumentsToProjectState) + public SolutionCompilationState AddDocumentsToMultipleProjects( + ImmutableArray documentInfos) where TDocumentState : TextDocumentState { if (documentInfos.IsDefault) @@ -1534,48 +1565,18 @@ private SolutionCompilationState AddDocumentsToMultipleProjects( // The documents might be contributing to multiple different projects; split them by project and then we'll // process one project at a time. - return UpdateDocumentsInMultipleProjects( + return WithDocumentStatesOfMultipleProjects( documentInfos.GroupBy(d => d.Id.ProjectId).Select(g => { var projectId = g.Key; - this.SolutionState.CheckContainsProject(projectId); - var projectState = this.SolutionState.GetRequiredProjectState(projectId); - return (projectId, newDocumentStates: g.SelectAsArray(di => createDocumentState(di, projectState))); + SolutionState.CheckContainsProject(projectId); + var projectState = SolutionState.GetRequiredProjectState(projectId); + return (projectId, newDocumentStates: g.SelectAsArray(projectState.CreateDocument)); }), - addDocumentsToProjectState); + GetAddDocumentsTranslationAction); } - private SolutionCompilationState UpdateDocumentsInMultipleProjects( - IEnumerable<(ProjectId projectId, ImmutableArray updatedDocumentState)> projectIdAndUpdatedDocuments, - Func, TranslationAction> getTranslationAction) - where TDocumentState : TextDocumentState - { - var newCompilationState = this; - - foreach (var (projectId, newDocumentStates) in projectIdAndUpdatedDocuments) - { - var oldProjectState = newCompilationState.SolutionState.GetRequiredProjectState(projectId); - var compilationTranslationAction = getTranslationAction(oldProjectState, newDocumentStates); - var newProjectState = compilationTranslationAction.NewProjectState; - - var stateChange = newCompilationState.SolutionState.ForkProject( - oldProjectState, - newProjectState); - - newCompilationState = newCompilationState.ForkProject( - stateChange, - static (_, compilationTranslationAction) => compilationTranslationAction, - forkTracker: true, - arg: compilationTranslationAction); - } - - return newCompilationState; - } - - private SolutionCompilationState RemoveDocumentsFromMultipleProjects( - ImmutableArray documentIds, - Func getExistingTextDocumentState, - Func, ImmutableArray, TranslationAction> removeDocumentsFromProjectState) + public SolutionCompilationState RemoveDocumentsFromMultipleProjects(ImmutableArray documentIds) where T : TextDocumentState { if (documentIds.IsEmpty) @@ -1591,39 +1592,59 @@ private SolutionCompilationState RemoveDocumentsFromMultipleProjects( foreach (var documentIdsInProject in documentIdsByProjectId) { - var oldProjectState = this.SolutionState.GetProjectState(documentIdsInProject.Key); + newCompilationState = newCompilationState.RemoveDocumentsFromSingleProject(documentIdsInProject.Key, [.. documentIdsInProject]); + } - if (oldProjectState == null) - { - throw new InvalidOperationException(string.Format(WorkspacesResources._0_is_not_part_of_the_workspace, documentIdsInProject.Key)); - } + return newCompilationState; + } - using var _ = ArrayBuilder.GetInstance(out var removedDocumentStates); + private SolutionCompilationState RemoveDocumentsFromSingleProject(ProjectId projectId, ImmutableArray documentIds) + where T : TextDocumentState + { + using var _ = ArrayBuilder.GetInstance(out var removedDocumentStates); - foreach (var documentId in documentIdsInProject) - { - removedDocumentStates.Add(getExistingTextDocumentState(oldProjectState, documentId)); - } + var oldProjectState = SolutionState.GetRequiredProjectState(projectId); + var oldDocumentStates = oldProjectState.GetDocumentStates(); - var removedDocumentStatesForProject = removedDocumentStates.ToImmutable(); + foreach (var documentId in documentIds) + { + removedDocumentStates.Add(oldDocumentStates.GetRequiredState(documentId)); + } - var compilationTranslationAction = removeDocumentsFromProjectState(oldProjectState, [.. documentIdsInProject], removedDocumentStatesForProject); - var newProjectState = compilationTranslationAction.NewProjectState; + var removedDocumentStatesForProject = removedDocumentStates.ToImmutable(); - var stateChange = newCompilationState.SolutionState.ForkProject( - oldProjectState, - newProjectState); + var compilationTranslationAction = GetRemoveDocumentsTranslationAction(oldProjectState, documentIds, removedDocumentStatesForProject); + var newProjectState = compilationTranslationAction.NewProjectState; - newCompilationState = newCompilationState.ForkProject( - stateChange, - static (_, compilationTranslationAction) => compilationTranslationAction, - forkTracker: true, - arg: compilationTranslationAction); - } + var stateChange = SolutionState.ForkProject( + oldProjectState, + newProjectState); - return newCompilationState; + return ForkProject( + stateChange, + static (_, compilationTranslationAction) => compilationTranslationAction, + forkTracker: true, + arg: compilationTranslationAction); } + private static TranslationAction GetRemoveDocumentsTranslationAction(ProjectState oldProject, ImmutableArray documentIds, ImmutableArray states) + => states switch + { + ImmutableArray documentStates => new TranslationAction.RemoveDocumentsAction(oldProject, oldProject.RemoveDocuments(documentIds), documentStates), + ImmutableArray additionalDocumentStates => new TranslationAction.RemoveAdditionalDocumentsAction(oldProject, oldProject.RemoveAdditionalDocuments(documentIds), additionalDocumentStates), + ImmutableArray _ => new TranslationAction.TouchAnalyzerConfigDocumentsAction(oldProject, oldProject.RemoveAnalyzerConfigDocuments(documentIds)), + _ => throw ExceptionUtilities.UnexpectedValue(states) + }; + + private static TranslationAction GetAddDocumentsTranslationAction(ProjectState oldProject, ImmutableArray states) + => states switch + { + ImmutableArray documentStates => new TranslationAction.AddDocumentsAction(oldProject, oldProject.AddDocuments(documentStates), documentStates), + ImmutableArray additionalDocumentStates => new TranslationAction.AddAdditionalDocumentsAction(oldProject, oldProject.AddAdditionalDocuments(additionalDocumentStates), additionalDocumentStates), + ImmutableArray analyzerConfigDocumentStates => new TranslationAction.TouchAnalyzerConfigDocumentsAction(oldProject, oldProject.AddAnalyzerConfigDocuments(analyzerConfigDocumentStates)), + _ => throw ExceptionUtilities.UnexpectedValue(states) + }; + /// public SolutionCompilationState WithCachedSourceGeneratorState(ProjectId projectToUpdate, Project projectWithCachedGeneratorState) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index e87df89f8e4f1..f081d9dba0ead 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -1137,7 +1137,7 @@ private StateChange UpdateDocumentState(DocumentState newDocument, bool contentC private StateChange UpdateAdditionalDocumentState(AdditionalDocumentState newDocument, bool contentChanged) { - var oldProject = GetProjectState(newDocument.Id.ProjectId)!; + var oldProject = GetRequiredProjectState(newDocument.Id.ProjectId); var newProject = oldProject.UpdateAdditionalDocument(newDocument, contentChanged); // This method shouldn't have been called if the document has not changed. @@ -1148,7 +1148,7 @@ private StateChange UpdateAdditionalDocumentState(AdditionalDocumentState newDoc private StateChange UpdateAnalyzerConfigDocumentState(AnalyzerConfigDocumentState newDocument) { - var oldProject = GetProjectState(newDocument.Id.ProjectId)!; + var oldProject = GetRequiredProjectState(newDocument.Id.ProjectId); var newProject = oldProject.UpdateAnalyzerConfigDocument(newDocument); // This method shouldn't have been called if the document has not changed.