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)