File: Workspace\VisualStudioWorkspaceStatusServiceFactory.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.ServiceHub.Framework;
using Microsoft.VisualStudio.OperationProgress;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using IAsyncServiceProvider2 = Microsoft.VisualStudio.Shell.IAsyncServiceProvider2;
using Task = System.Threading.Tasks.Task;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation;
 
[ExportWorkspaceServiceFactory(typeof(IWorkspaceStatusService), ServiceLayer.Host), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class VisualStudioWorkspaceStatusServiceFactory(
    SVsServiceProvider serviceProvider,
    IThreadingContext threadingContext,
    IAsynchronousOperationListenerProvider listenerProvider) : IWorkspaceServiceFactory
{
    private readonly IAsyncServiceProvider2 _serviceProvider = (IAsyncServiceProvider2)serviceProvider;
    private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.Workspace);
 
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
    {
        return workspaceServices.Workspace is VisualStudioWorkspace
            ? new Service(_serviceProvider, threadingContext, _listener)
            : new DefaultWorkspaceStatusService();
    }
 
    /// <summary>
    /// for prototype, we won't care about what solution is actually fully loaded. 
    /// we will just see whatever solution VS has at this point of time has actually fully loaded
    /// </summary>
    private sealed class Service : IWorkspaceStatusService
    {
        private readonly IAsyncServiceProvider2 _serviceProvider;
        private readonly IThreadingContext _threadingContext;
 
        /// <summary>
        /// A task indicating that the <see cref="Guids.GlobalHubClientPackageGuid"/> package has loaded. Calls to
        /// <see cref="IServiceBroker.GetProxyAsync"/> may have a main thread dependency if the proffering package
        /// is not loaded prior to the call.
        /// </summary>
        private readonly JoinableTask _loadHubClientPackage;
 
        /// <summary>
        /// A task providing the result of asynchronous computation of
        /// <see cref="IVsOperationProgressStatusService.GetStageStatusForSolutionLoad"/>. The result of this
        /// operation is accessed through <see cref="GetProgressStageStatusAsync"/>.
        /// </summary>
        private readonly JoinableTask<IVsOperationProgressStageStatusForSolutionLoad?> _progressStageStatus;
 
        public event EventHandler? StatusChanged;
 
        public Service(IAsyncServiceProvider2 serviceProvider, IThreadingContext threadingContext, IAsynchronousOperationListener listener)
        {
            _serviceProvider = serviceProvider;
            _threadingContext = threadingContext;
 
            _loadHubClientPackage = _threadingContext.JoinableTaskFactory.RunAsync(async () =>
            {
                // Use the disposal token, since the caller's cancellation token will apply instead to the
                // JoinAsync operation in GetProgressStageStatusAsync.
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _threadingContext.DisposalToken);
 
                // Make sure the HubClient package is loaded, since we rely on it for proffered OOP services
                var shell = await _serviceProvider.GetServiceAsync<SVsShell, IVsShell7>(_threadingContext.JoinableTaskFactory).ConfigureAwait(true);
                Assumes.Present(shell);
 
                await shell.LoadPackageAsync(Guids.GlobalHubClientPackageGuid);
            });
 
            _progressStageStatus = _threadingContext.JoinableTaskFactory.RunAsync(async () =>
            {
                // preemptively make sure event is subscribed. if APIs are called before it is done, calls will be
                // blocked until event subscription is done
                using var asyncToken = listener.BeginAsyncOperation("StatusChanged_EventSubscription");
 
                await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _threadingContext.DisposalToken);
                var service = await serviceProvider.GetServiceAsync<SVsOperationProgress, IVsOperationProgressStatusService>(_threadingContext.JoinableTaskFactory, throwOnFailure: false).ConfigureAwait(true);
                if (service is null)
                    return null;
 
                var status = service.GetStageStatusForSolutionLoad(CommonOperationProgressStageIds.Intellisense);
                status.PropertyChanged += (_, _) => StatusChanged?.Invoke(this, EventArgs.Empty);
 
                return status;
            });
        }
 
        // unfortunately, IVsOperationProgressStatusService requires UI thread to let project system to proceed to next stages.
        // this method should only be used with either await or JTF.Run, it should be never used with Task.Wait otherwise, it can
        // deadlock
        //
        // This method also ensures the GlobalHubClientPackage package is loaded, since brokered services in Visual
        // Studio require this package to provide proxy interfaces for invoking out-of-process services.
        public async Task WaitUntilFullyLoadedAsync(CancellationToken cancellationToken)
        {
            using (Logger.LogBlock(FunctionId.PartialLoad_FullyLoaded, KeyValueLogMessage.NoProperty, cancellationToken))
            {
                var status = await GetProgressStageStatusAsync(cancellationToken).ConfigureAwait(false);
                if (status == null)
                {
                    return;
                }
 
                var completionTask = status.WaitForCompletionAsync();
                Logger.Log(FunctionId.PartialLoad_FullyLoaded, KeyValueLogMessage.Create(LogType.Trace, m => m["AlreadyFullyLoaded"] = completionTask.IsCompleted, LogLevel.Debug));
 
                // TODO: WaitForCompletionAsync should accept cancellation directly.
                //       for now, use WithCancellation to indirectly add cancellation
                await completionTask.WithCancellation(cancellationToken).ConfigureAwait(false);
 
                await _loadHubClientPackage.JoinAsync(cancellationToken).ConfigureAwait(false);
            }
        }
 
        public async Task<bool> IsFullyLoadedAsync(CancellationToken cancellationToken)
        {
            var status = await GetProgressStageStatusAsync(cancellationToken).ConfigureAwait(false);
            return status != null && !status.IsInProgress;
        }
 
        private async ValueTask<IVsOperationProgressStageStatusForSolutionLoad?> GetProgressStageStatusAsync(CancellationToken cancellationToken)
            => await _progressStageStatus.JoinAsync(cancellationToken).ConfigureAwait(false);
    }
}