diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/SolutionChangeAccumulator.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/SolutionChangeAccumulator.cs new file mode 100644 index 0000000000000000000000000000000000000000..f29cf30f980ae6dd4419faf773e0768c363062e3 --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/SolutionChangeAccumulator.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem +{ + /// + /// A little helper type to hold onto the being updated in a batch, which also + /// keeps track of the right to raise when we are done. + /// + internal class SolutionChangeAccumulator + { + /// + /// The kind that encompasses all the changes we've made. It's null if no changes have been made, + /// and or + /// if we can't give a more precise type. + /// + private WorkspaceChangeKind? _workspaceChangeKind; + + public SolutionChangeAccumulator(Solution startingSolution) + { + Solution = startingSolution; + } + + public Solution Solution { get; private set; } + + public bool HasChange => _workspaceChangeKind.HasValue; + public WorkspaceChangeKind WorkspaceChangeKind => _workspaceChangeKind.Value; + + public ProjectId WorkspaceChangeProjectId { get; private set; } + public DocumentId WorkspaceChangeDocumentId { get; private set; } + + public void UpdateSolutionForDocumentAction(Solution newSolution, WorkspaceChangeKind changeKind, IEnumerable documentIds) + { + // If the newSolution is the same as the current solution, there's nothing to actually do + if (Solution == newSolution) + { + return; + } + + Solution = newSolution; + + foreach (var documentId in documentIds) + { + // If we don't previously have change, this is our new change + if (!_workspaceChangeKind.HasValue) + { + _workspaceChangeKind = changeKind; + WorkspaceChangeProjectId = documentId.ProjectId; + WorkspaceChangeDocumentId = documentId; + } + else + { + // We do have a new change. At this point, the change is spanning multiple documents or projects we + // will coalesce accordingly + if (documentId.ProjectId == WorkspaceChangeProjectId) + { + // It's the same project, at least, so project change it is + _workspaceChangeKind = WorkspaceChangeKind.ProjectChanged; + WorkspaceChangeDocumentId = null; + } + else + { + // Multiple projects have changed, so it's a generic solution change. At this point + // we can bail from the loop, because this is already our most general case. + _workspaceChangeKind = WorkspaceChangeKind.SolutionChanged; + WorkspaceChangeProjectId = null; + WorkspaceChangeDocumentId = null; + break; + } + } + } + } + + /// + /// Should be called to update the solution if there isn't a specific document change kind that should be + /// given to + /// + public void UpdateSolutionForProjectAction(ProjectId projectId, Solution newSolution) + { + // If the newSolution is the same as the current solution, there's nothing to actually do + if (Solution == newSolution) + { + return; + } + + Solution = newSolution; + + // Since we're changing a project, we definitely have no DocumentId anymore + WorkspaceChangeDocumentId = null; + + if (!_workspaceChangeKind.HasValue || WorkspaceChangeProjectId == projectId) + { + // We can count this as a generic project change + _workspaceChangeKind = WorkspaceChangeKind.ProjectChanged; + WorkspaceChangeProjectId = projectId; + } + else + { + _workspaceChangeKind = WorkspaceChangeKind.SolutionChanged; + WorkspaceChangeProjectId = null; + } + } + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs index fe0d428b5f3f27cc9a4803d2dd27738cef9496a3..59ba547759838c5dff23e883df1a559bf41f3aec 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs @@ -350,23 +350,27 @@ private void OnBatchScopeDisposed() var documentsToOpen = new List<(DocumentId documentId, SourceTextContainer textContainer)>(); var additionalDocumentsToOpen = new List<(DocumentId documentId, SourceTextContainer textContainer)>(); - _workspace.ApplyBatchChangeToProject(Id, solution => + _workspace.ApplyBatchChangeToWorkspace(solution => { - solution = _sourceFiles.UpdateSolutionForBatch( - solution, + var solutionChanges = new SolutionChangeAccumulator(startingSolution: solution); + + _sourceFiles.UpdateSolutionForBatch( + solutionChanges, documentFileNamesAdded, documentsToOpen, (s, documents) => solution.AddDocuments(documents), + WorkspaceChangeKind.DocumentAdded, (s, id) => { // Clear any document-specific data now (like open file trackers, etc.). If we called OnRemoveDocument directly this is // called, but since we're doing this in one large batch we need to do it now. _workspace.ClearDocumentData(id); return s.RemoveDocument(id); - }); + }, + WorkspaceChangeKind.DocumentRemoved); - solution = _additionalFiles.UpdateSolutionForBatch( - solution, + _additionalFiles.UpdateSolutionForBatch( + solutionChanges, documentFileNamesAdded, additionalDocumentsToOpen, (s, documents) => @@ -378,13 +382,15 @@ private void OnBatchScopeDisposed() return s; }, + WorkspaceChangeKind.AdditionalDocumentAdded, (s, id) => { // Clear any document-specific data now (like open file trackers, etc.). If we called OnRemoveDocument directly this is // called, but since we're doing this in one large batch we need to do it now. _workspace.ClearDocumentData(id); return s.RemoveAdditionalDocument(id); - }); + }, + WorkspaceChangeKind.AdditionalDocumentRemoved); // Metadata reference adding... if (_metadataReferencesAddedInBatch.Count > 0) @@ -407,8 +413,10 @@ private void OnBatchScopeDisposed() } } - solution = solution.AddProjectReferences(Id, projectReferencesCreated) - .AddMetadataReferences(Id, metadataReferencesCreated); + solutionChanges.UpdateSolutionForProjectAction( + Id, + solutionChanges.Solution.AddProjectReferences(Id, projectReferencesCreated) + .AddMetadataReferences(Id, metadataReferencesCreated)); ClearAndZeroCapacity(_metadataReferencesAddedInBatch); } @@ -420,7 +428,9 @@ private void OnBatchScopeDisposed() if (projectReference != null) { - solution = solution.RemoveProjectReference(Id, projectReference); + solutionChanges.UpdateSolutionForProjectAction( + Id, + solutionChanges.Solution.RemoveProjectReference(Id, projectReference)); } else { @@ -430,32 +440,42 @@ private void OnBatchScopeDisposed() _workspace.FileWatchedReferenceFactory.StopWatchingReference(metadataReference); - solution = solution.RemoveMetadataReference(Id, metadataReference); + solutionChanges.UpdateSolutionForProjectAction( + Id, + newSolution: solutionChanges.Solution.RemoveMetadataReference(Id, metadataReference)); } } ClearAndZeroCapacity(_metadataReferencesRemovedInBatch); // Project reference adding... - solution = solution.AddProjectReferences(Id, _projectReferencesAddedInBatch); + solutionChanges.UpdateSolutionForProjectAction( + Id, + newSolution: solutionChanges.Solution.AddProjectReferences(Id, _projectReferencesAddedInBatch)); ClearAndZeroCapacity(_projectReferencesAddedInBatch); // Project reference removing... foreach (var projectReference in _projectReferencesRemovedInBatch) { - solution = solution.RemoveProjectReference(Id, projectReference); + solutionChanges.UpdateSolutionForProjectAction( + Id, + newSolution: solutionChanges.Solution.RemoveProjectReference(Id, projectReference)); } ClearAndZeroCapacity(_projectReferencesRemovedInBatch); // Analyzer reference adding... - solution = solution.AddAnalyzerReferences(Id, _analyzersAddedInBatch.Select(a => a.GetReference())); + solutionChanges.UpdateSolutionForProjectAction( + Id, + newSolution: solutionChanges.Solution.AddAnalyzerReferences(Id, _analyzersAddedInBatch.Select(a => a.GetReference()))); ClearAndZeroCapacity(_analyzersAddedInBatch); // Analyzer reference removing... foreach (var analyzerReference in _analyzersRemovedInBatch) { - solution = solution.RemoveAnalyzerReference(Id, analyzerReference.GetReference()); + solutionChanges.UpdateSolutionForProjectAction( + Id, + newSolution: solutionChanges.Solution.RemoveAnalyzerReference(Id, analyzerReference.GetReference())); } ClearAndZeroCapacity(_analyzersRemovedInBatch); @@ -463,12 +483,14 @@ private void OnBatchScopeDisposed() // Other property modifications... foreach (var propertyModification in _projectPropertyModificationsInBatch) { - solution = propertyModification(solution); + solutionChanges.UpdateSolutionForProjectAction( + Id, + propertyModification(solutionChanges.Solution)); } ClearAndZeroCapacity(_projectPropertyModificationsInBatch); - return solution; + return solutionChanges; }); foreach (var (documentId, textContainer) in documentsToOpen) @@ -1430,20 +1452,32 @@ public void ReorderFiles(ImmutableArray filePaths) } else { - _project._workspace.ApplyBatchChangeToProject(_project.Id, solution => solution.WithProjectDocumentsOrder(_project.Id, documentIds.ToImmutable())); + _project._workspace.ApplyBatchChangeToWorkspace(solution => + { + var solutionChanges = new SolutionChangeAccumulator(solution); + solutionChanges.UpdateSolutionForProjectAction( + _project.Id, + solutionChanges.Solution.WithProjectDocumentsOrder(_project.Id, documentIds.ToImmutable())); + return solutionChanges; + }); } } } - internal Solution UpdateSolutionForBatch( - Solution solution, + internal void UpdateSolutionForBatch( + SolutionChangeAccumulator solutionChanges, ImmutableArray.Builder documentFileNamesAdded, List<(DocumentId documentId, SourceTextContainer textContainer)> documentsToOpen, Func, Solution> addDocuments, - Func removeDocument) + WorkspaceChangeKind addDocumentChangeKind, + Func removeDocument, + WorkspaceChangeKind removeDocumentChangeKind) { // Document adding... - solution = addDocuments(solution, _documentsAddedInBatch.ToImmutable()); + solutionChanges.UpdateSolutionForDocumentAction( + newSolution: addDocuments(solutionChanges.Solution, _documentsAddedInBatch.ToImmutable()), + changeKind: addDocumentChangeKind, + documentIds: _documentsAddedInBatch.Select(d => d.Id)); foreach (var documentInfo in _documentsAddedInBatch) { @@ -1460,7 +1494,9 @@ public void ReorderFiles(ImmutableArray filePaths) // Document removing... foreach (var documentId in _documentsRemovedInBatch) { - solution = removeDocument(solution, documentId); + solutionChanges.UpdateSolutionForDocumentAction(removeDocument(solutionChanges.Solution, documentId), + removeDocumentChangeKind, + SpecializedCollections.SingletonEnumerable(documentId)); } ClearAndZeroCapacity(_documentsRemovedInBatch); @@ -1468,11 +1504,11 @@ public void ReorderFiles(ImmutableArray filePaths) // Update project's order of documents. if (_orderedDocumentsInBatch != null) { - solution = solution.WithProjectDocumentsOrder(_project.Id, _orderedDocumentsInBatch); + solutionChanges.UpdateSolutionForProjectAction( + _project.Id, + solutionChanges.Solution.WithProjectDocumentsOrder(_project.Id, _orderedDocumentsInBatch)); _orderedDocumentsInBatch = null; } - - return solution; } private DocumentInfo CreateDocumentInfoFromFileInfo(DynamicFileInfo fileInfo, IEnumerable folders) diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs index 292db861d8cf7810a6d5ce91bd9e3c6276314f01..5a61ea5f5bb801fb2c3513ac9cb78159882c2e32 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs @@ -1436,22 +1436,25 @@ public void ApplyChangeToWorkspace(Action action) /// /// This is needed to synchronize with to avoid any races. This /// method could be moved down to the core Workspace layer and then could use the synchronization lock there. - /// The to change. - /// A function that, given the old will produce a new one. - public void ApplyBatchChangeToProject(ProjectId projectId, Func mutation) + public void ApplyBatchChangeToWorkspace(Func mutation) { lock (_gate) { var oldSolution = this.CurrentSolution; - var newSolution = mutation(oldSolution); + var solutionChangeAccumulator = mutation(oldSolution); - if (oldSolution == newSolution) + if (!solutionChangeAccumulator.HasChange) { return; } - SetCurrentSolution(newSolution); - RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); + SetCurrentSolution(solutionChangeAccumulator.Solution); + RaiseWorkspaceChangedEventAsync( + solutionChangeAccumulator.WorkspaceChangeKind, + oldSolution, + solutionChangeAccumulator.Solution, + solutionChangeAccumulator.WorkspaceChangeProjectId, + solutionChangeAccumulator.WorkspaceChangeDocumentId); } } diff --git a/src/VisualStudio/Core/Test/Microsoft.VisualStudio.LanguageServices.UnitTests.vbproj b/src/VisualStudio/Core/Test/Microsoft.VisualStudio.LanguageServices.UnitTests.vbproj index ee2ee0a3b5c0c42ad4a914e2afbfd832ab4e91ff..abbcd6e79bfef13c23a43f47acbc9ba6a5c0473b 100644 --- a/src/VisualStudio/Core/Test/Microsoft.VisualStudio.LanguageServices.UnitTests.vbproj +++ b/src/VisualStudio/Core/Test/Microsoft.VisualStudio.LanguageServices.UnitTests.vbproj @@ -74,6 +74,7 @@ + diff --git a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/WorkspaceChangedEventTests.vb b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/WorkspaceChangedEventTests.vb new file mode 100644 index 0000000000000000000000000000000000000000..1febb06bd4db9eda498f41a4d469cfd964f20596 --- /dev/null +++ b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/WorkspaceChangedEventTests.vb @@ -0,0 +1,102 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Test.Utilities +Imports Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Framework +Imports Roslyn.Test.Utilities + +Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim + <[UseExportProvider]> + Public Class WorkspaceChangedEventTests + + + Public Async Sub AddingASingleSourceFileRaisesDocumentAdded(addInBatch As Boolean) + Using environment = New TestEnvironment() + Dim project = environment.ProjectFactory.CreateAndAddToWorkspace("Project", LanguageNames.CSharp) + Dim workspaceChangeEvents = New WorkspaceChangeWatcher(environment) + + Using If(addInBatch, project.CreateBatchScope(), Nothing) + project.AddSourceFile("Z:\Test.vb") + End Using + + Dim change = Assert.Single(Await workspaceChangeEvents.GetNewChangeEventsAsync()) + + Assert.Equal(WorkspaceChangeKind.DocumentAdded, change.Kind) + Assert.Equal(project.Id, change.ProjectId) + Assert.Equal(environment.Workspace.CurrentSolution.Projects.Single().DocumentIds.Single(), change.DocumentId) + End Using + End Sub + + + Public Async Sub AddingTwoDocumentsInBatchRaisesProjectChanged() + Using environment = New TestEnvironment() + Dim project = environment.ProjectFactory.CreateAndAddToWorkspace("Project", LanguageNames.CSharp) + Dim workspaceChangeEvents = New WorkspaceChangeWatcher(environment) + + Using project.CreateBatchScope() + project.AddSourceFile("Z:\Test1.vb") + project.AddSourceFile("Z:\Test2.vb") + End Using + + Dim change = Assert.Single(Await workspaceChangeEvents.GetNewChangeEventsAsync()) + + Assert.Equal(WorkspaceChangeKind.ProjectChanged, change.Kind) + Assert.Equal(project.Id, change.ProjectId) + Assert.Null(change.DocumentId) + End Using + End Sub + + + + Public Async Sub AddingASingleAdditionalFileInABatchRaisesDocumentAdded(addInBatch As Boolean) + Using environment = New TestEnvironment() + Dim project = environment.ProjectFactory.CreateAndAddToWorkspace("Project", LanguageNames.CSharp) + Dim workspaceChangeEvents = New WorkspaceChangeWatcher(environment) + + Using If(addInBatch, project.CreateBatchScope(), Nothing) + project.AddAdditionalFile("Z:\Test.vb") + End Using + + Dim change = Assert.Single(Await workspaceChangeEvents.GetNewChangeEventsAsync()) + + Assert.Equal(WorkspaceChangeKind.AdditionalDocumentAdded, change.Kind) + Assert.Equal(project.Id, change.ProjectId) + Assert.Equal(environment.Workspace.CurrentSolution.Projects.Single().AdditionalDocumentIds.Single(), change.DocumentId) + End Using + End Sub + + + + Public Async Sub AddingASingleMetadataReferenceRaisesProjectChanged(addInBatch As Boolean) + Using environment = New TestEnvironment() + Dim project = environment.ProjectFactory.CreateAndAddToWorkspace("Project", LanguageNames.CSharp) + Dim workspaceChangeEvents = New WorkspaceChangeWatcher(environment) + + Using If(addInBatch, project.CreateBatchScope(), Nothing) + project.AddMetadataReference("Z:\Test.dll", MetadataReferenceProperties.Assembly) + End Using + + Dim change = Assert.Single(Await workspaceChangeEvents.GetNewChangeEventsAsync()) + + Assert.Equal(WorkspaceChangeKind.ProjectChanged, change.Kind) + Assert.Equal(project.Id, change.ProjectId) + Assert.Null(change.DocumentId) + End Using + End Sub + + + + Public Async Sub StartingAndEndingBatchWithNoChangesDoesNothing() + Using environment = New TestEnvironment() + Dim project = environment.ProjectFactory.CreateAndAddToWorkspace("Project", LanguageNames.CSharp) + Dim workspaceChangeEvents = New WorkspaceChangeWatcher(environment) + Dim startingSolution = environment.Workspace.CurrentSolution + + project.CreateBatchScope().Dispose() + + Assert.Empty(Await workspaceChangeEvents.GetNewChangeEventsAsync()) + Assert.Same(startingSolution, environment.Workspace.CurrentSolution) + End Using + End Sub + End Class +End Namespace diff --git a/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/WorkspaceChangeWatcher.vb b/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/WorkspaceChangeWatcher.vb new file mode 100644 index 0000000000000000000000000000000000000000..9e72380abe7f7c56ab55da4c2b5a677ceeea5023 --- /dev/null +++ b/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/WorkspaceChangeWatcher.vb @@ -0,0 +1,39 @@ +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Shared.TestHooks +Imports Roslyn.Test.Utilities + +Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Framework + Friend Class WorkspaceChangeWatcher + Implements IDisposable + + Private ReadOnly _environment As TestEnvironment + Private ReadOnly _asynchronousOperationWaiter As IAsynchronousOperationWaiter + Private _changeEvents As New List(Of WorkspaceChangeEventArgs) + + Public Sub New(environment As TestEnvironment) + _environment = environment + + Dim listenerProvider = environment.ExportProvider.GetExportedValue(Of AsynchronousOperationListenerProvider)() + _asynchronousOperationWaiter = listenerProvider.GetWaiter(FeatureAttribute.Workspace) + + AddHandler environment.Workspace.WorkspaceChanged, AddressOf OnWorkspaceChanged + End Sub + + Private Sub OnWorkspaceChanged(sender As Object, e As WorkspaceChangeEventArgs) + _changeEvents.Add(e) + End Sub + + Friend Async Function GetNewChangeEventsAsync() As Task(Of IEnumerable(Of WorkspaceChangeEventArgs)) + Await _asynchronousOperationWaiter.CreateExpeditedWaitTask() + + ' Return the events so far, clearing the list if somebody wants to ask for further events + Dim changeEvents = _changeEvents + _changeEvents = New List(Of WorkspaceChangeEventArgs)() + Return changeEvents + End Function + + Public Sub Dispose() Implements IDisposable.Dispose + RemoveHandler _environment.Workspace.WorkspaceChanged, AddressOf OnWorkspaceChanged + End Sub + End Class +End Namespace diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index 6581b158c2b05828b5381a033563ae7a49e9efcc..835e3d8e988fe7ccd82705b014437231efdfa273 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -852,6 +852,11 @@ public SolutionState AddProjectReferences(ProjectId projectId, IEnumerable