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 0000000000000..f29cf30f980ae
--- /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 fe0d428b5f3f2..59ba547759838 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 @@ internal Solution UpdateSolutionForBatch(
// 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 @@ internal Solution UpdateSolutionForBatch(
// 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 292db861d8cf7..5a61ea5f5bb80 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 ee2ee0a3b5c0c..abbcd6e79bfef 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 0000000000000..1febb06bd4db9
--- /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 0000000000000..9e72380abe7f7
--- /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 6581b158c2b05..835e3d8e988fe 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