diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs index 411e8ed26fd8f..54171becad401 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs @@ -38,9 +38,10 @@ internal sealed partial class ProjectSystemProject /// /// A semaphore taken for all mutation of any mutable field in this type. /// - /// This is, for now, intentionally pessimistic. There are no doubt ways that we could allow more to run in parallel, - /// but the current tradeoff is for simplicity of code and "obvious correctness" than something that is subtle, fast, and wrong. - private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1); + /// This is, for now, intentionally pessimistic. There are no doubt ways that we could allow more to run in + /// parallel, but the current tradeoff is for simplicity of code and "obvious correctness" than something that is + /// subtle, fast, and wrong. + private readonly SemaphoreSlim _gate = new(initialCount: 1); /// /// The number of active batch scopes. If this is zero, we are not batching, non-zero means we are batching. @@ -52,13 +53,13 @@ internal sealed partial class ProjectSystemProject private readonly List _projectReferencesAddedInBatch = []; private readonly List _projectReferencesRemovedInBatch = []; - private readonly Dictionary _analyzerPathsToAnalyzers = []; - private readonly List _analyzersAddedInBatch = []; + private readonly Dictionary _analyzerPathsToAnalyzers = []; + private readonly List _analyzersAddedInBatch = []; /// /// The list of s that will be removed in this batch. /// - private readonly List _analyzersRemovedInBatch = []; + private readonly List _analyzersRemovedInBatch = []; private readonly List> _projectPropertyModificationsInBatch = []; @@ -653,12 +654,23 @@ await _projectSystemProjectFactory.ApplyBatchChangeToWorkspaceMaybeAsync(useAsyn newSolution: solutionChanges.Solution.RemoveProjectReference(Id, projectReference)); } + // Analyzer reference removing... + if (_analyzersRemovedInBatch.Count > 0) + { + projectUpdateState = projectUpdateState.WithIncrementalAnalyzerReferencesRemoved(_analyzersRemovedInBatch); + + foreach (var analyzerReference in _analyzersRemovedInBatch) + solutionChanges.UpdateSolutionForProjectAction(Id, solutionChanges.Solution.RemoveAnalyzerReference(Id, analyzerReference)); + } + // Analyzer reference adding... - solutionChanges.UpdateSolutionForProjectAction(Id, solutionChanges.Solution.AddAnalyzerReferences(Id, _analyzersAddedInBatch)); + if (_analyzersAddedInBatch.Count > 0) + { + projectUpdateState = projectUpdateState.WithIncrementalAnalyzerReferencesAdded(_analyzersAddedInBatch); - // Analyzer reference removing... - foreach (var analyzerReference in _analyzersRemovedInBatch) - solutionChanges.UpdateSolutionForProjectAction(Id, solutionChanges.Solution.RemoveAnalyzerReference(Id, analyzerReference)); + solutionChanges.UpdateSolutionForProjectAction( + Id, solutionChanges.Solution.AddAnalyzerReferences(Id, _analyzersAddedInBatch)); + } // Other property modifications... foreach (var propertyModification in _projectPropertyModificationsInBatch) @@ -918,47 +930,54 @@ public void AddAnalyzerReference(string fullPath) foreach (var mappedFullPath in mappedPaths) { if (_analyzerPathsToAnalyzers.ContainsKey(mappedFullPath)) - { throw new ArgumentException($"'{fullPath}' has already been added to this project.", nameof(fullPath)); - } } - foreach (var mappedFullPath in mappedPaths) + if (_activeBatchScopes > 0) { - // Are we adding one we just recently removed? If so, we can just keep using that one, and avoid removing - // it once we apply the batch - var analyzerPendingRemoval = _analyzersRemovedInBatch.FirstOrDefault(a => a.FullPath == mappedFullPath); - if (analyzerPendingRemoval != null) + foreach (var mappedFullPath in mappedPaths) { - _analyzersRemovedInBatch.Remove(analyzerPendingRemoval); - _analyzerPathsToAnalyzers.Add(mappedFullPath, analyzerPendingRemoval); - } - else - { - // Nope, we actually need to make a new one. - var analyzerReference = new AnalyzerFileReference(mappedFullPath, _analyzerAssemblyLoader); - - _analyzerPathsToAnalyzers.Add(mappedFullPath, analyzerReference); - - if (_activeBatchScopes > 0) + // Are we adding one we just recently removed? If so, we can just keep using that one, and avoid removing + // it once we apply the batch + var analyzerPendingRemoval = _analyzersRemovedInBatch.FirstOrDefault(a => a.FullPath == mappedFullPath); + if (analyzerPendingRemoval != null) { - _analyzersAddedInBatch.Add(analyzerReference); + _analyzersRemovedInBatch.Remove(analyzerPendingRemoval); + _analyzerPathsToAnalyzers.Add(mappedFullPath, analyzerPendingRemoval); } else { - _projectSystemProjectFactory.ApplyChangeToWorkspace(w => w.OnAnalyzerReferenceAdded(Id, analyzerReference)); + // Nope, we actually need to make a new one. + var analyzerReference = new AnalyzerFileReference(mappedFullPath, _analyzerAssemblyLoader); + + _analyzersAddedInBatch.Add(analyzerReference); + _analyzerPathsToAnalyzers.Add(mappedFullPath, analyzerReference); } } } + else + { + _projectSystemProjectFactory.ApplyChangeToWorkspaceWithProjectUpdateState((w, projectUpdateState) => + { + foreach (var mappedFullPath in mappedPaths) + { + var analyzerReference = new AnalyzerFileReference(mappedFullPath, _analyzerAssemblyLoader); + _analyzerPathsToAnalyzers.Add(mappedFullPath, analyzerReference); + w.OnAnalyzerReferenceAdded(Id, analyzerReference); + + projectUpdateState = projectUpdateState.WithIncrementalAnalyzerReferenceAdded(analyzerReference); + } + + return projectUpdateState; + }); + } } } public void RemoveAnalyzerReference(string fullPath) { if (string.IsNullOrEmpty(fullPath)) - { throw new ArgumentException("message", nameof(fullPath)); - } var mappedPaths = GetMappedAnalyzerPaths(fullPath); @@ -968,28 +987,38 @@ public void RemoveAnalyzerReference(string fullPath) foreach (var mappedFullPath in mappedPaths) { if (!_analyzerPathsToAnalyzers.ContainsKey(mappedFullPath)) - { throw new ArgumentException($"'{fullPath}' is not an analyzer of this project.", nameof(fullPath)); - } } - foreach (var mappedFullPath in mappedPaths) + if (_activeBatchScopes > 0) { - var analyzerReference = _analyzerPathsToAnalyzers[mappedFullPath]; + foreach (var mappedFullPath in mappedPaths) + { + var analyzerReference = _analyzerPathsToAnalyzers[mappedFullPath]; - _analyzerPathsToAnalyzers.Remove(mappedFullPath); + _analyzerPathsToAnalyzers.Remove(mappedFullPath); - if (_activeBatchScopes > 0) - { // This analyzer may be one we've just added in the same batch; in that case, just don't add it in // the first place. if (!_analyzersAddedInBatch.Remove(analyzerReference)) _analyzersRemovedInBatch.Add(analyzerReference); } - else + } + else + { + _projectSystemProjectFactory.ApplyChangeToWorkspaceWithProjectUpdateState((w, projectUpdateState) => { - _projectSystemProjectFactory.ApplyChangeToWorkspace(w => w.OnAnalyzerReferenceRemoved(Id, analyzerReference)); - } + foreach (var mappedFullPath in mappedPaths) + { + var analyzerReference = _analyzerPathsToAnalyzers[mappedFullPath]; + _analyzerPathsToAnalyzers.Remove(mappedFullPath); + + w.OnAnalyzerReferenceRemoved(Id, analyzerReference); + projectUpdateState = projectUpdateState.WithIncrementalAnalyzerReferenceRemoved(analyzerReference); + } + + return projectUpdateState; + }); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.ProjectUpdateState.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.ProjectUpdateState.cs index f5f72dad55794..f39c09b89c689 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.ProjectUpdateState.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.ProjectUpdateState.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis.Diagnostics; @@ -123,9 +124,15 @@ public ProjectUpdateState WithIncrementalMetadataReferenceAdded(PortableExecutab public ProjectUpdateState WithIncrementalAnalyzerReferenceRemoved(AnalyzerFileReference reference) => this with { RemovedAnalyzerReferences = RemovedAnalyzerReferences.Add(reference) }; + public ProjectUpdateState WithIncrementalAnalyzerReferencesRemoved(List references) + => this with { RemovedAnalyzerReferences = RemovedAnalyzerReferences.AddRange(references) }; + public ProjectUpdateState WithIncrementalAnalyzerReferenceAdded(AnalyzerFileReference reference) => this with { AddedAnalyzerReferences = AddedAnalyzerReferences.Add(reference) }; + public ProjectUpdateState WithIncrementalAnalyzerReferencesAdded(List references) + => this with { AddedAnalyzerReferences = AddedAnalyzerReferences.AddRange(references) }; + /// /// Returns a new instance with any incremental state that should not be saved between updates cleared. ///