diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProject.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProject.cs
index a0490ce5f43ea..310bd18639769 100644
--- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProject.cs
+++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProject.cs
@@ -1231,7 +1231,17 @@ public void RemoveFromWorkspace()
// references being converted to metadata references (or vice versa) and we might either
// miss stopping a file watcher or might end up double-stopping a file watcher.
remainingMetadataReferences = w.CurrentSolution.GetRequiredProject(Id).MetadataReferences;
- w.OnProjectRemoved(Id);
+ _workspace.RemoveProjectFromTrackingMaps_NoLock(Id);
+
+ // If this is our last project, clear the entire solution.
+ if (w.CurrentSolution.ProjectIds.Count == 1)
+ {
+ _workspace.RemoveSolution_NoLock();
+ }
+ else
+ {
+ _workspace.OnProjectRemoved(Id);
+ }
});
Contract.ThrowIfNull(remainingMetadataReferences);
diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
index cfb053d1c31cc..1a0a8c2e8c398 100644
--- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
+++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
@@ -11,6 +11,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
+using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.LanguageServices.ExternalAccess.VSTypeScript.Api;
@@ -115,6 +116,7 @@ await _visualStudioWorkspaceImpl.ApplyChangeToWorkspaceAsync(w =>
.WithTelemetryId(creationInfo.ProjectGuid);
// If we don't have any projects and this is our first project being added, then we'll create a new SolutionId
+ // and count this as the solution being added so that event is raised.
if (w.CurrentSolution.ProjectIds.Count == 0)
{
var solutionSessionId = GetSolutionSessionId();
diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
index 4c90ae323c70a..21afd95186c41 100644
--- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
+++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
@@ -1667,7 +1667,11 @@ private ProjectReferenceInformation GetReferenceInfo_NoLock(ProjectId projectId)
return _projectReferenceInfoMap.GetOrAdd(projectId, _ => new ProjectReferenceInformation());
}
- protected internal override void OnProjectRemoved(ProjectId projectId)
+ ///
+ /// Removes the project from the various maps this type maintains; it's still up to the caller to actually remove
+ /// the project in one way or another.
+ ///
+ internal void RemoveProjectFromTrackingMaps_NoLock(ProjectId projectId)
{
string? languageName;
@@ -1711,8 +1715,6 @@ protected internal override void OnProjectRemoved(ProjectId projectId)
}
}
- base.OnProjectRemoved(projectId);
-
// Try to update the UI context info. But cancel that work if we're shutting down.
_threadingContext.RunWithShutdownBlockAsync(async cancellationToken =>
{
@@ -1721,6 +1723,31 @@ protected internal override void OnProjectRemoved(ProjectId projectId)
});
}
+ internal void RemoveSolution_NoLock()
+ {
+ Contract.ThrowIfFalse(_gate.CurrentCount == 0);
+
+ // At this point, we should have had RemoveProjectFromTrackingMaps_NoLock called for everything else, so it's just the solution itself
+ // to clean up
+ Contract.ThrowIfFalse(_projectReferenceInfoMap.Count == 0);
+ Contract.ThrowIfFalse(_projectToHierarchyMap.Count == 0);
+ Contract.ThrowIfFalse(_projectToGuidMap.Count == 0);
+ Contract.ThrowIfFalse(_projectToMaxSupportedLangVersionMap.Count == 0);
+ Contract.ThrowIfFalse(_projectToDependencyNodeTargetIdentifier.Count == 0);
+ Contract.ThrowIfFalse(_projectToRuleSetFilePath.Count == 0);
+ Contract.ThrowIfFalse(_projectSystemNameToProjectsMap.Count == 0);
+
+ // Create a new empty solution and set this; we will reuse the same SolutionId and path since components still may have persistence information they still need
+ // to look up by that location; we also keep the existing analyzer references around since those are host-level analyzers that were loaded asynchronously.
+ SetCurrentSolution(
+ solution => CreateSolution(
+ SolutionInfo.Create(
+ SolutionId.CreateNewId(),
+ VersionStamp.Create(),
+ analyzerReferences: solution.AnalyzerReferences)),
+ WorkspaceChangeKind.SolutionRemoved);
+ }
+
private sealed class ProjectReferenceInformation
{
public readonly List OutputPaths = new();
diff --git a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/WorkspaceChangedEventTests.vb b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/WorkspaceChangedEventTests.vb
index 2da9431ccc652..2d70ca2932ac6 100644
--- a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/WorkspaceChangedEventTests.vb
+++ b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/WorkspaceChangedEventTests.vb
@@ -106,5 +106,21 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim
Assert.Same(startingSolution, environment.Workspace.CurrentSolution)
End Using
End Function
+
+
+
+ Public Async Function AddingAndRemovingOnlyProjectTriggersSolutionAddedAndSolutionRemoved() As Task
+ Using environment = New TestEnvironment()
+ Dim workspaceChangeEvents = New WorkspaceChangeWatcher(environment)
+ Dim project = Await environment.ProjectFactory.CreateAndAddToWorkspaceAsync(
+ "Project", LanguageNames.CSharp, CancellationToken.None)
+
+ Assert.Equal(WorkspaceChangeKind.SolutionAdded, Assert.Single(Await workspaceChangeEvents.GetNewChangeEventsAsync()).Kind)
+
+ project.RemoveFromWorkspace()
+
+ Assert.Equal(WorkspaceChangeKind.SolutionRemoved, Assert.Single(Await workspaceChangeEvents.GetNewChangeEventsAsync()).Kind)
+ End Using
+ End Function
End Class
End Namespace
diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/CSharp/CSharpFindReferences.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/CSharp/CSharpFindReferences.cs
index ea715a51c316d..80a75db577e65 100644
--- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/CSharp/CSharpFindReferences.cs
+++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/CSharp/CSharpFindReferences.cs
@@ -164,9 +164,9 @@ public async Task VerifyWorkingFolder()
await TestServices.SolutionExplorer.CloseSolutionAsync(HangMitigatingCancellationToken);
- // because the solution cache directory is stored in the user temp folder,
- // closing the solution has no effect on what is returned.
- Assert.NotNull(persistentStorageConfiguration.TryGetStorageLocation(SolutionKey.ToSolutionKey(visualStudioWorkspace.CurrentSolution)));
+ // Since we no longer have an open solution, we don't have a storage location for it, since that
+ // depends on the open solution.
+ Assert.Null(persistentStorageConfiguration.TryGetStorageLocation(SolutionKey.ToSolutionKey(visualStudioWorkspace.CurrentSolution)));
}
private async Task WaitForNavigateAsync(CancellationToken cancellationToken)