diff --git a/src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs b/src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs index b695530f678da..7ea407ef0afca 100644 --- a/src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs +++ b/src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Progress; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; @@ -218,7 +219,7 @@ private static async Task FixProjectsAsync( progressTracker.AddItems(projects.Sum(static p => p.DocumentIds.Count)); // Run in parallel across all projects. - return await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync( + var changedRoots = await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync( source: projects, produceItems: static async (project, callback, args, cancellationToken) => { @@ -245,17 +246,10 @@ await RoslynParallel.ForEachAsync( callback((document.Id, await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false))); }).ConfigureAwait(false); }, - consumeItems: static async (stream, args, cancellationToken) => - { - // Now consume the changed documents, applying their new roots to the solution. - var currentSolution = args.solution; - await foreach (var (documentId, newRoot) in stream) - currentSolution = currentSolution.WithDocumentSyntaxRoot(documentId, newRoot); - - return currentSolution; - }, args: (globalOptions, solution, enabledFixIds, progressTracker), cancellationToken).ConfigureAwait(false); + + return solution.WithDocumentSyntaxRoots(changedRoots.SelectAsArray(t => (t.documentId, t.newRoot, PreservationMode.PreserveValue))); } private static async Task FixDocumentAsync( diff --git a/src/Workspaces/Core/Portable/CodeActions/CodeAction_Cleanup.cs b/src/Workspaces/Core/Portable/CodeActions/CodeAction_Cleanup.cs index 18ca9283f4f0c..33e3ebc44bbcc 100644 --- a/src/Workspaces/Core/Portable/CodeActions/CodeAction_Cleanup.cs +++ b/src/Workspaces/Core/Portable/CodeActions/CodeAction_Cleanup.cs @@ -142,7 +142,7 @@ async Task RunParallelCleanupPassAsync( // on the same fork and do not cause the forked solution to be created and dropped repeatedly. using var _ = await RemoteKeepAliveSession.CreateAsync(solution, cancellationToken).ConfigureAwait(false); - return await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync( + var changedRoots = await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync( source: documentIdsAndOptions, produceItems: static async (documentIdAndOptions, callback, args, cancellationToken) => { @@ -164,17 +164,11 @@ async Task RunParallelCleanupPassAsync( var newRoot = await cleanedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); callback((documentId, newRoot)); }, - consumeItems: static async (stream, args, cancellationToken) => - { - // Grab all the cleaned roots and produce the new solution snapshot from that. - var currentSolution = args.solution; - await foreach (var (documentId, newRoot) in stream) - currentSolution = currentSolution.WithDocumentSyntaxRoot(documentId, newRoot); - - return currentSolution; - }, args: (solution, progress, cleanupDocumentAsync), cancellationToken).ConfigureAwait(false); + + // Grab all the cleaned roots and produce the new solution snapshot from that. + return solution.WithDocumentSyntaxRoots(changedRoots.SelectAsArray(t => (t.documentId, t.newRoot, PreservationMode.PreserveValue))); } } } diff --git a/src/Workspaces/Core/Portable/CodeFixesAndRefactorings/DocumentBasedFixAllProviderHelpers.cs b/src/Workspaces/Core/Portable/CodeFixesAndRefactorings/DocumentBasedFixAllProviderHelpers.cs index 2383629bbdc42..24589e3884540 100644 --- a/src/Workspaces/Core/Portable/CodeFixesAndRefactorings/DocumentBasedFixAllProviderHelpers.cs +++ b/src/Workspaces/Core/Portable/CodeFixesAndRefactorings/DocumentBasedFixAllProviderHelpers.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; @@ -71,7 +72,7 @@ async Task GetInitialUncleanedSolutionAsync(Solution originalSolution) // so we never resync or recompute anything. using var _ = await RemoteKeepAliveSession.CreateAsync(originalSolution, cancellationToken).ConfigureAwait(false); - return await ProducerConsumer<(DocumentId documentId, (SyntaxNode? node, SourceText? text))>.RunParallelAsync( + var changedRootsAndTexts = await ProducerConsumer<(DocumentId documentId, (SyntaxNode? node, SourceText? text))>.RunParallelAsync( source: fixAllContexts, produceItems: static async (fixAllContext, callback, args, cancellationToken) => { @@ -94,28 +95,22 @@ await args.getFixedDocumentsAsync( callback((newDocument.Id, (newRoot, newText))); }).ConfigureAwait(false); }, - consumeItems: static async (stream, args, cancellationToken) => - { - var currentSolution = args.originalSolution; - - // Next, go and insert those all into the solution so all the docs in this particular project point - // at the new trees (or text). At this point though, the trees have not been semantically cleaned - // up. We don't cleanup the documents as they are created, or one at a time as we add them, as that - // would cause us to run semantic cleanup on N different solution forks (which would be very - // expensive as we'd fork, produce semantics, fork, produce semantics, etc. etc.). Instead, by - // adding all the changed documents to one solution, and then cleaning *those* we only perform - // cleanup semantics on one forked solution. - await foreach (var (docId, (newRoot, newText)) in stream) - { - currentSolution = newRoot != null - ? currentSolution.WithDocumentSyntaxRoot(docId, newRoot) - : currentSolution.WithDocumentText(docId, newText!); - } - - return currentSolution; - }, args: (getFixedDocumentsAsync, progressTracker, originalSolution), cancellationToken).ConfigureAwait(false); + + // Next, go and insert those all into the solution so all the docs in this particular project point + // at the new trees (or text). At this point though, the trees have not been semantically cleaned + // up. We don't cleanup the documents as they are created, or one at a time as we add them, as that + // would cause us to run semantic cleanup on N different solution forks (which would be very + // expensive as we'd fork, produce semantics, fork, produce semantics, etc. etc.). Instead, by + // adding all the changed documents to one solution, and then cleaning *those* we only perform + // cleanup semantics on one forked solution. + var changedRoots = changedRootsAndTexts.SelectAsArray(t => t.Item2.node != null, t => (t.documentId, t.Item2.node!, PreservationMode.PreserveValue)); + var changedTexts = changedRootsAndTexts.SelectAsArray(t => t.Item2.text != null, t => (t.documentId, t.Item2.text!, PreservationMode.PreserveValue)); + + return originalSolution + .WithDocumentSyntaxRoots(changedRoots) + .WithDocumentTexts(changedTexts); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index 013c0f9aaa830..10b51a65ae47c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -185,28 +185,35 @@ private static async Task ComputeLatestDocumentVersionAsync(TextDo } private AsyncLazy CreateLazyLatestDocumentTopLevelChangeVersion( - TextDocumentState newDocument, + ImmutableArray newDocuments, TextDocumentStates newDocumentStates, TextDocumentStates newAdditionalDocumentStates) { if (_lazyLatestDocumentTopLevelChangeVersion.TryGetValue(out var oldVersion)) { - return AsyncLazy.Create(static (arg, c) => - ComputeTopLevelChangeTextVersionAsync(arg.oldVersion, arg.newDocument, c), - arg: (oldVersion, newDocument)); + return AsyncLazy.Create(static (arg, cancellationToken) => + ComputeTopLevelChangeTextVersionAsync(arg.oldVersion, arg.newDocuments, cancellationToken), + arg: (oldVersion, newDocuments)); } else { - return AsyncLazy.Create(static (arg, c) => - ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c), + return AsyncLazy.Create(static (arg, cancellationToken) => + ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, cancellationToken), arg: (newDocumentStates, newAdditionalDocumentStates)); } } - private static async Task ComputeTopLevelChangeTextVersionAsync(VersionStamp oldVersion, TextDocumentState newDocument, CancellationToken cancellationToken) + private static async Task ComputeTopLevelChangeTextVersionAsync( + VersionStamp oldVersion, ImmutableArray newDocuments, CancellationToken cancellationToken) { - var newVersion = await newDocument.GetTopLevelChangeTextVersionAsync(cancellationToken).ConfigureAwait(false); - return newVersion.GetNewerVersion(oldVersion); + var finalVersion = oldVersion; + foreach (var newDocument in newDocuments) + { + var newVersion = await newDocument.GetTopLevelChangeTextVersionAsync(cancellationToken).ConfigureAwait(false); + finalVersion = newVersion.GetNewerVersion(finalVersion); + } + + return finalVersion; } private static async Task ComputeLatestDocumentTopLevelChangeVersionAsync(TextDocumentStates documentStates, TextDocumentStates additionalDocumentStates, CancellationToken cancellationToken) @@ -834,16 +841,25 @@ public ProjectState RemoveAllNormalDocuments() } public ProjectState UpdateDocument(DocumentState newDocument, bool contentChanged) + => UpdateDocuments([newDocument], contentChanged); + + public ProjectState UpdateDocuments(ImmutableArray newDocuments, bool contentChanged) { - var oldDocument = DocumentStates.GetRequiredState(newDocument.Id); - if (oldDocument == newDocument) - { + var oldDocuments = newDocuments.SelectAsArray(d => DocumentStates.GetRequiredState(d.Id)); + if (oldDocuments.SequenceEqual(newDocuments)) return this; - } - var newDocumentStates = DocumentStates.SetState(newDocument.Id, newDocument); + // Must not be empty as we would have otherwise bailed out in the check above. + Contract.ThrowIfTrue(newDocuments.IsEmpty); + + var newDocumentStates = DocumentStates.SetStates(newDocuments); + + // When computing the latest dependent version, we just need to know how GetLatestDependentVersions( - newDocumentStates, AdditionalDocumentStates, oldDocument, newDocument, contentChanged, + newDocumentStates, AdditionalDocumentStates, + oldDocuments.CastArray(), + newDocuments.CastArray(), + contentChanged, out var dependentDocumentVersion, out var dependentSemanticVersion); return With( @@ -860,9 +876,9 @@ public ProjectState UpdateAdditionalDocument(AdditionalDocumentState newDocument return this; } - var newDocumentStates = AdditionalDocumentStates.SetState(newDocument.Id, newDocument); + var newDocumentStates = AdditionalDocumentStates.SetState(newDocument); GetLatestDependentVersions( - DocumentStates, newDocumentStates, oldDocument, newDocument, contentChanged, + DocumentStates, newDocumentStates, [oldDocument], [newDocument], contentChanged, out var dependentDocumentVersion, out var dependentSemanticVersion); return this.With( @@ -879,7 +895,7 @@ public ProjectState UpdateAnalyzerConfigDocument(AnalyzerConfigDocumentState new return this; } - var newDocumentStates = AnalyzerConfigDocumentStates.SetState(newDocument.Id, newDocument); + var newDocumentStates = AnalyzerConfigDocumentStates.SetState(newDocument); return CreateNewStateForChangedAnalyzerConfigDocuments(newDocumentStates); } @@ -899,46 +915,71 @@ public ProjectState UpdateDocumentsOrder(ImmutableList documentIds) private void GetLatestDependentVersions( TextDocumentStates newDocumentStates, TextDocumentStates newAdditionalDocumentStates, - TextDocumentState oldDocument, TextDocumentState newDocument, + ImmutableArray oldDocuments, + ImmutableArray newDocuments, bool contentChanged, - out AsyncLazy dependentDocumentVersion, out AsyncLazy dependentSemanticVersion) + out AsyncLazy dependentDocumentVersion, + out AsyncLazy dependentSemanticVersion) { var recalculateDocumentVersion = false; var recalculateSemanticVersion = false; if (contentChanged) { - if (oldDocument.TryGetTextVersion(out var oldVersion)) + foreach (var oldDocument in oldDocuments) { - if (!_lazyLatestDocumentVersion.TryGetValue(out var documentVersion) || documentVersion == oldVersion) + if (oldDocument.TryGetTextVersion(out var oldVersion)) { - recalculateDocumentVersion = true; - } + if (!_lazyLatestDocumentVersion.TryGetValue(out var documentVersion) || documentVersion == oldVersion) + recalculateDocumentVersion = true; - if (!_lazyLatestDocumentTopLevelChangeVersion.TryGetValue(out var semanticVersion) || semanticVersion == oldVersion) - { - recalculateSemanticVersion = true; + if (!_lazyLatestDocumentTopLevelChangeVersion.TryGetValue(out var semanticVersion) || semanticVersion == oldVersion) + recalculateSemanticVersion = true; } + + if (recalculateDocumentVersion && recalculateSemanticVersion) + break; } } - dependentDocumentVersion = recalculateDocumentVersion - ? AsyncLazy.Create(static (arg, c) => - ComputeLatestDocumentVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c), - arg: (newDocumentStates, newAdditionalDocumentStates)) - : contentChanged - ? AsyncLazy.Create(static (newDocument, c) => - newDocument.GetTextVersionAsync(c), - arg: newDocument) - : _lazyLatestDocumentVersion; - - dependentSemanticVersion = recalculateSemanticVersion - ? AsyncLazy.Create(static (arg, c) => - ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c), - arg: (newDocumentStates, newAdditionalDocumentStates)) - : contentChanged - ? CreateLazyLatestDocumentTopLevelChangeVersion(newDocument, newDocumentStates, newAdditionalDocumentStates) - : _lazyLatestDocumentTopLevelChangeVersion; + if (recalculateDocumentVersion) + { + dependentDocumentVersion = AsyncLazy.Create(static (arg, cancellationToken) => + ComputeLatestDocumentVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, cancellationToken), + arg: (newDocumentStates, newAdditionalDocumentStates)); + } + else if (contentChanged) + { + dependentDocumentVersion = AsyncLazy.Create( + static async (newDocuments, cancellationToken) => + { + var finalVersion = await newDocuments[0].GetTextVersionAsync(cancellationToken).ConfigureAwait(false); + for (var i = 1; i < newDocuments.Length; i++) + finalVersion = finalVersion.GetNewerVersion(await newDocuments[i].GetTextVersionAsync(cancellationToken).ConfigureAwait(false)); + + return finalVersion; + }, + arg: newDocuments); + } + else + { + dependentDocumentVersion = _lazyLatestDocumentVersion; + } + + if (recalculateSemanticVersion) + { + dependentSemanticVersion = AsyncLazy.Create(static (arg, cancellationToken) => + ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, cancellationToken), + arg: (newDocumentStates, newAdditionalDocumentStates)); + } + else if (contentChanged) + { + dependentSemanticVersion = CreateLazyLatestDocumentTopLevelChangeVersion(newDocuments, newDocumentStates, newAdditionalDocumentStates); + } + else + { + dependentSemanticVersion = _lazyLatestDocumentTopLevelChangeVersion; + } } public void AddDocumentIdsWithFilePath(ref TemporaryArray temporaryArray, string filePath) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index df13aee1dcd6a..5d07e98103010 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -1181,20 +1181,22 @@ public Solution WithDocumentFilePath(DocumentId documentId, string filePath) /// specified. /// public Solution WithDocumentText(DocumentId documentId, SourceText text, PreservationMode mode = PreservationMode.PreserveValue) - { - CheckContainsDocument(documentId); + => WithDocumentTexts([(documentId, text, mode)]); - if (text == null) + internal Solution WithDocumentTexts(ImmutableArray<(DocumentId documentId, SourceText text, PreservationMode mode)> texts) + { + foreach (var (documentId, text, mode) in texts) { - throw new ArgumentNullException(nameof(text)); - } + CheckContainsDocument(documentId); - if (!mode.IsValid()) - { - throw new ArgumentOutOfRangeException(nameof(mode)); + if (text == null) + throw new ArgumentNullException(nameof(text)); + + if (!mode.IsValid()) + throw new ArgumentOutOfRangeException(nameof(mode)); } - return WithCompilationState(_compilationState.WithDocumentText(documentId, text, mode)); + return WithCompilationState(_compilationState.WithDocumentTexts(texts)); } /// @@ -1307,20 +1309,23 @@ public Solution WithAnalyzerConfigDocumentText(DocumentId documentId, TextAndVer /// rooted by the specified syntax node. /// public Solution WithDocumentSyntaxRoot(DocumentId documentId, SyntaxNode root, PreservationMode mode = PreservationMode.PreserveValue) - { - CheckContainsDocument(documentId); + => WithDocumentSyntaxRoots([(documentId, root, mode)]); - if (root == null) + /// . + internal Solution WithDocumentSyntaxRoots(ImmutableArray<(DocumentId documentId, SyntaxNode root, PreservationMode mode)> syntaxRoots) + { + foreach (var (documentId, root, mode) in syntaxRoots) { - throw new ArgumentNullException(nameof(root)); - } + CheckContainsDocument(documentId); - if (!mode.IsValid()) - { - throw new ArgumentOutOfRangeException(nameof(mode)); + if (root == null) + throw new ArgumentNullException(nameof(root)); + + if (!mode.IsValid()) + throw new ArgumentOutOfRangeException(nameof(mode)); } - return WithCompilationState(_compilationState.WithDocumentSyntaxRoot(documentId, root, mode)); + return WithCompilationState(_compilationState.WithDocumentSyntaxRoots(syntaxRoots)); } internal Solution WithDocumentContentsFrom(DocumentId documentId, DocumentState documentState, bool forceEvenIfTreesWouldDiffer) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker_Generators.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker_Generators.cs index 4aea9e09b1162..3c9bd134dca24 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker_Generators.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker_Generators.cs @@ -144,7 +144,7 @@ private partial class CompilationTracker : ICompilationTracker foreach (var (documentIdentity, _, generationDateTime) in infos) { var documentId = documentIdentity.DocumentId; - oldGeneratedDocuments = oldGeneratedDocuments.SetState(documentId, oldGeneratedDocuments.GetRequiredState(documentId).WithGenerationDateTime(generationDateTime)); + oldGeneratedDocuments = oldGeneratedDocuments.SetState(oldGeneratedDocuments.GetRequiredState(documentId).WithGenerationDateTime(generationDateTime)); } // If there are no generated documents though, then just use the compilationWithoutGeneratedFiles so we diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs index d59b89fe1bcc9..da0ca13cc63d8 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs @@ -162,7 +162,7 @@ public async ValueTask> GetSour { // The generated file still exists in the underlying compilation, but the contents may not match the open file if the open file // is stale. Replace the syntax tree so we have a tree that matches the text. - newStates = newStates.SetState(id, replacementState); + newStates = newStates.SetState(replacementState); } else { 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 30961e91913ed..572aaa4cb1824 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; @@ -36,8 +34,10 @@ await _oldState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false), public DocumentId DocumentId => _newState.Attributes.Id; - // Replacing a single tree doesn't impact the generated trees in a compilation, so we can use this against - // compilations that have generated trees. + /// + /// Replacing a single tree doesn't impact the generated trees in a compilation, so we can use this against + /// compilations that have generated trees. + /// public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput => true; public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) @@ -57,6 +57,59 @@ public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generat } } + internal sealed class TouchDocumentsAction : TranslationAction + { + private readonly ImmutableArray _newStates; + + private TouchDocumentsAction( + ProjectState oldProjectState, + ProjectState newProjectState, + ImmutableArray newStates) + : base(oldProjectState, newProjectState) + { + _newStates = newStates; + } + + public static TranslationAction Create( + ProjectState oldProjectState, + ProjectState newProjectState, + ImmutableArray newStates) + { + // Special case when we're only updating a single document. This case can be optimized more, and + // corresponds to the common case of a single file being edited. + return newStates.Length == 1 + ? new TouchDocumentAction(oldProjectState, newProjectState, oldProjectState.DocumentStates.GetRequiredState(newStates[0].Id), newStates[0]) + : new TouchDocumentsAction(oldProjectState, newProjectState, newStates); + } + + public override async Task TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken) + { + var finalCompilation = oldCompilation; + for (int i = 0, n = _newStates.Length; i < n; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + var newState = _newStates[i]; + var oldState = this.OldProjectState.DocumentStates.GetRequiredState(newState.Id); + finalCompilation = finalCompilation.ReplaceSyntaxTree( + await oldState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false), + await newState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false)); + } + + return finalCompilation; + } + + /// + public override bool CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput + => true; + + /// + public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver generatorDriver) + => generatorDriver; + + public override TranslationAction? TryMergeWithPrior(TranslationAction priorAction) + => null; + } + internal sealed class TouchAdditionalDocumentAction( ProjectState oldProjectState, ProjectState newProjectState, diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs index 058055eb643fe..779dda124c3f3 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs @@ -709,11 +709,42 @@ public SolutionCompilationState WithDocumentFilePath( } /// - public SolutionCompilationState WithDocumentText( - DocumentId documentId, SourceText text, PreservationMode mode) - { - return UpdateDocumentState( - this.SolutionState.WithDocumentText(documentId, text, mode), documentId); + public SolutionCompilationState WithDocumentText(DocumentId documentId, SourceText text, PreservationMode mode) + => WithDocumentTexts([(documentId, text, mode)]); + + internal SolutionCompilationState WithDocumentTexts( + ImmutableArray<(DocumentId documentId, SourceText text, PreservationMode mode)> texts) + { + return UpdateDocumentsInMultipleProjects( + texts.GroupBy(d => d.documentId.ProjectId).Select(g => + { + var projectId = g.Key; + var projectState = this.SolutionState.GetRequiredProjectState(projectId); + + using var _ = ArrayBuilder.GetInstance(out var newDocumentStates); + foreach (var (documentId, text, mode) in g) + { + var documentState = projectState.DocumentStates.GetRequiredState(documentId); + if (IsUnchanged(documentState, text)) + continue; + + newDocumentStates.Add(documentState.UpdateText(text, mode)); + } + + return (projectId, newDocumentStates.ToImmutableAndClear()); + }), + static (projectState, newDocumentStates) => + { + return TranslationAction.TouchDocumentsAction.Create( + projectState, + projectState.UpdateDocuments(newDocumentStates, contentChanged: true), + newDocumentStates); + }); + + static bool IsUnchanged(DocumentState oldDocument, SourceText text) + { + return oldDocument.TryGetText(out var oldText) && text == oldText; + } } public SolutionCompilationState WithDocumentState( @@ -762,12 +793,41 @@ public SolutionCompilationState WithAnalyzerConfigDocumentText( this.SolutionState.WithAnalyzerConfigDocumentText(documentId, textAndVersion, mode)); } - /// - public SolutionCompilationState WithDocumentSyntaxRoot( - DocumentId documentId, SyntaxNode root, PreservationMode mode) - { - return UpdateDocumentState( - this.SolutionState.WithDocumentSyntaxRoot(documentId, root, mode), documentId); + /// + public SolutionCompilationState WithDocumentSyntaxRoots(ImmutableArray<(DocumentId documentId, SyntaxNode root, PreservationMode mode)> syntaxRoots) + { + return UpdateDocumentsInMultipleProjects( + syntaxRoots.GroupBy(d => d.documentId.ProjectId).Select(g => + { + var projectId = g.Key; + var projectState = this.SolutionState.GetRequiredProjectState(projectId); + + using var _ = ArrayBuilder.GetInstance(out var newDocumentStates); + foreach (var (documentId, root, mode) in g) + { + var documentState = projectState.DocumentStates.GetRequiredState(documentId); + if (IsUnchanged(documentState, root)) + continue; + + newDocumentStates.Add(documentState.UpdateTree(root, mode)); + } + + return (projectId, newDocumentStates.ToImmutableAndClear()); + }), + static (projectState, newDocumentStates) => + { + return TranslationAction.TouchDocumentsAction.Create( + projectState, + projectState.UpdateDocuments(newDocumentStates, contentChanged: true), + newDocumentStates); + }); + + static bool IsUnchanged(DocumentState oldDocument, SyntaxNode root) + { + return oldDocument.TryGetSyntaxTree(out var oldTree) && + oldTree.TryGetRoot(out var oldRoot) && + oldRoot == root; + } } public SolutionCompilationState WithDocumentContentsFrom( @@ -1497,13 +1557,21 @@ private SolutionCompilationState AddDocumentsToMultipleProjects( IEnumerable<(ProjectId projectId, ImmutableArray newDocumentStates)> projectIdAndNewDocuments, Func, TranslationAction> addDocumentsToProjectState) where TDocumentState : TextDocumentState + { + return UpdateDocumentsInMultipleProjects(projectIdAndNewDocuments, addDocumentsToProjectState); + } + + private SolutionCompilationState UpdateDocumentsInMultipleProjects( + IEnumerable<(ProjectId projectId, ImmutableArray updatedDocumentState)> projectIdAndUpdatedDocuments, + Func, TranslationAction> updatedDocumentsToProjectState) + where TDocumentState : TextDocumentState { var newCompilationState = this; - foreach (var (projectId, newDocumentStates) in projectIdAndNewDocuments) + foreach (var (projectId, newDocumentStates) in projectIdAndUpdatedDocuments) { var oldProjectState = newCompilationState.SolutionState.GetRequiredProjectState(projectId); - var compilationTranslationAction = addDocumentsToProjectState(oldProjectState, newDocumentStates); + var compilationTranslationAction = updatedDocumentsToProjectState(oldProjectState, newDocumentStates); var newProjectState = compilationTranslationAction.NewProjectState; var stateChange = newCompilationState.SolutionState.ForkProject( diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index 9600215792ec1..9e9663b46e49b 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -988,24 +988,6 @@ public StateChange WithAnalyzerConfigDocumentText(DocumentId documentId, TextAnd return UpdateAnalyzerConfigDocumentState(oldDocument.UpdateText(textAndVersion, mode)); } - /// - /// Creates a new solution instance with the document specified updated to have a syntax tree - /// rooted by the specified syntax node. - /// - public StateChange WithDocumentSyntaxRoot(DocumentId documentId, SyntaxNode root, PreservationMode mode = PreservationMode.PreserveValue) - { - var oldDocument = GetRequiredDocumentState(documentId); - if (oldDocument.TryGetSyntaxTree(out var oldTree) && - oldTree.TryGetRoot(out var oldRoot) && - oldRoot == root) - { - var oldProject = GetRequiredProjectState(documentId.ProjectId); - return new(this, oldProject, oldProject); - } - - return UpdateDocumentState(oldDocument.UpdateTree(root, mode), contentChanged: true); - } - /// Whether or not the specified document is forced to have the same text and /// green-tree-root from . If , then they will share /// these values. If , then they will only be shared when safe to do so (for example, diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs index d9a24fa7d891a..6a4fee023332c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs @@ -168,14 +168,27 @@ public TextDocumentStates RemoveRange(ImmutableArray ids) return new(_ids.RemoveRange(enumerableIds), _map.RemoveRange(enumerableIds), filePathToDocumentIds: null); } - internal TextDocumentStates SetState(DocumentId id, TState state) + internal TextDocumentStates SetState(TState state) + => SetStates([state]); + + internal TextDocumentStates SetStates(ImmutableArray states) { - var oldState = _map[id]; - var filePathToDocumentIds = oldState.FilePath != state.FilePath - ? null - : _filePathToDocumentIds; + var builder = _map.ToBuilder(); + var filePathToDocumentIds = _filePathToDocumentIds; + + foreach (var state in states) + { + var id = state.Id; + var oldState = _map[id]; + + // If any file paths have changed, don't preseve the computed map. We'll regenerate the new map on demand when needed. + if (filePathToDocumentIds != null && oldState.FilePath != state.FilePath) + filePathToDocumentIds = null; + + builder[id] = state; + } - return new(_ids, _map.SetItem(id, state), filePathToDocumentIds); + return new(_ids, builder.ToImmutable(), filePathToDocumentIds); } public TextDocumentStates UpdateStates(Func transformation, TArg arg)