diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/LanguageServiceHostEnvironment.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/LanguageServiceHostEnvironment.cs index b719b3655ea..4906d062427 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/LanguageServiceHostEnvironment.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/LanguageServiceHostEnvironment.cs @@ -19,8 +19,6 @@ public LanguageServiceHostEnvironment(IVsShellServices vsShell, JoinableTaskCont _isEnabled = new( async () => { - await joinableTaskContext.Factory.SwitchToMainThreadAsync(); - // If VS is running in command line mode (e.g. "devenv.exe /build my.sln"), // the language service host is not enabled. The one exception to this is // when we're populating a solution cache via "/populateSolutionCache". diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 2968f2a91af..1f7d12c4bf7 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.ProjectSystem.LanguageServices; [Export(typeof(IWorkspaceWriter))] [Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))] [AppliesTo(ProjectCapability.DotNetLanguageService)] -internal sealed class LanguageServiceHost : OnceInitializedOnceDisposedUnderLockAsync, IProjectDynamicLoadComponent, IWorkspaceWriter +internal sealed class LanguageServiceHost : OnceInitializedOnceDisposedAsync, IProjectDynamicLoadComponent, IWorkspaceWriter { private readonly TaskCompletionSource _firstPrimaryWorkspaceSet = new(); @@ -106,6 +106,9 @@ protected override async Task InitializeCoreAsync(CancellationToken cancellation return; } + // Ensure we also cancel on project unload + cancellationToken = _tasksService.LinkUnload(cancellationToken); + // We have one "workspace" per "slice". // // - A "workspace" models the project state that Roslyn needs for a specific configuration. @@ -130,12 +133,15 @@ protected override async Task InitializeCoreAsync(CancellationToken cancellation // We track per-slice data via this source. _activeConfigurationGroupSubscriptionService.SourceBlock.SyncLinkOptions(), target: DataflowBlockFactory.CreateActionBlock>( - async update => await ExecuteUnderLockAsync(cancellationToken => OnSlicesChanged(update, cancellationToken)), + update => OnSlicesChanged(update, cancellationToken), _unconfiguredProject, - ProjectFaultSeverity.LimitedFunctionality), + ProjectFaultSeverity.LimitedFunctionality, + "LanguageServiceHostSlices {0}"), linkOptions: DataflowOption.PropagateCompletion, cancellationToken: cancellationToken), + ProjectDataSources.JoinUpstreamDataSources(_joinableTaskFactory, _projectFaultHandler, _activeConfiguredProjectProvider, _activeConfigurationGroupSubscriptionService), + new DisposableDelegate(() => { // Dispose all workspaces. Note that this happens within a lock, so we will not race with project updates. @@ -265,10 +271,10 @@ public Task IsEnabledAsync(CancellationToken cancellationToken) public async Task WhenInitialized(CancellationToken token) { - await ValidateEnabledAsync(token); - using (_joinableTaskCollection.Join()) { + await ValidateEnabledAsync(token); + await _firstPrimaryWorkspaceSet.Task.WithCancellation(token); } } @@ -277,8 +283,6 @@ public async Task WriteAsync(Func action, CancellationToken to { token = _tasksService.LinkUnload(token); - await ValidateEnabledAsync(token); - Workspace workspace = await GetPrimaryWorkspaceAsync(token); await workspace.WriteAsync(action, token); @@ -288,8 +292,6 @@ public async Task WriteAsync(Func> action, Cancellatio { token = _tasksService.LinkUnload(token); - await ValidateEnabledAsync(token); - Workspace workspace = await GetPrimaryWorkspaceAsync(token); return await workspace.WriteAsync(action, token); @@ -297,23 +299,26 @@ public async Task WriteAsync(Func> action, Cancellatio private async Task GetPrimaryWorkspaceAsync(CancellationToken cancellationToken) { - await ValidateEnabledAsync(cancellationToken); + using (_joinableTaskCollection.Join()) + { + await ValidateEnabledAsync(cancellationToken); - await WhenProjectLoaded(cancellationToken); + await WhenProjectLoaded(); + } return _primaryWorkspace ?? throw Assumes.Fail("Primary workspace unknown."); - } - private async Task WhenProjectLoaded(CancellationToken cancellationToken) - { - // The active configuration can change multiple times during initialization in cases where we've incorrectly - // guessed the configuration via our IProjectConfigurationDimensionsProvider3 implementation. - // Wait until that has been determined before we publish the wrong configuration. - await _tasksService.PrioritizedProjectLoadedInHost.WithCancellation(cancellationToken); - - // We block project load on initialization of the primary workspace. - // Therefore by this point we must have set the primary workspace. - System.Diagnostics.Debug.Assert(_firstPrimaryWorkspaceSet.Task is { IsCompleted: true, IsFaulted: false }); + async Task WhenProjectLoaded() + { + // The active configuration can change multiple times during initialization in cases where we've incorrectly + // guessed the configuration via our IProjectConfigurationDimensionsProvider3 implementation. + // Wait until that has been determined before we publish the wrong configuration. + await _tasksService.PrioritizedProjectLoadedInHost.WithCancellation(cancellationToken); + + // We block project load on initialization of the primary workspace. + // Therefore by this point we must have set the primary workspace. + System.Diagnostics.Debug.Assert(_firstPrimaryWorkspaceSet.Task is { IsCompleted: true, IsFaulted: false }); + } } #endregion @@ -333,10 +338,7 @@ public async Task AfterLoadInitialConfigurationAsync() // Ensure the project is not considered loaded until our first publication. Task result = _tasksService.PrioritizedProjectLoadedInHostAsync(async () => { - using (_joinableTaskCollection.Join()) - { - await WhenInitialized(_tasksService.UnloadCancellationToken); - } + await WhenInitialized(_tasksService.UnloadCancellationToken); }); // While we want make sure it's loaded before PrioritizedProjectLoadedInHost, @@ -344,7 +346,7 @@ public async Task AfterLoadInitialConfigurationAsync() _projectFaultHandler.Forget(result, _unconfiguredProject, ProjectFaultSeverity.LimitedFunctionality); } - protected override Task DisposeCoreUnderLockAsync(bool initialized) + protected override Task DisposeCoreAsync(bool initialized) { _firstPrimaryWorkspaceSet.TrySetCanceled(); diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/Workspace.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/Workspace.cs index a6082dcc8b6..967019186c0 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/Workspace.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/Workspace.cs @@ -134,6 +134,10 @@ protected override Task DisposeCoreUnderLockAsync(bool initialized) return Task.CompletedTask; } + /// + /// Adds an object that will be disposed along with this instance. + /// + /// The object to dispose when this object is disposed. public void ChainDisposal(IDisposable disposable) { Verify.NotDisposed(this); @@ -141,6 +145,23 @@ public void ChainDisposal(IDisposable disposable) _disposableBag.Add(disposable); } + /// + /// Integrates project updates into the workspace. + /// + /// + /// + /// This method must always receive an evaluation update first. After that point, + /// both evaluation and build updates may arrive in any order, so long as values + /// of each type are ordered correctly. + /// + /// + /// Calls must not overlap. This method is not thread-safe. This method is designed + /// to be called from a dataflow ActionBlock, which will serialize calls, so we + /// needn't perform any locking or protection here. + /// + /// + /// The project update to integrate. + /// A task that completes when the update has been integrated. internal async Task OnWorkspaceUpdateAsync(IProjectVersionedValue update) { Verify.NotDisposed(this);