File: HostWorkspace\ProjectInitializationHandler.cs
Web Access
Project: src\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj (Microsoft.CodeAnalysis.LanguageServer)
// 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.Composition;
using Microsoft.CodeAnalysis.BrokeredServices;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services;
using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.Definitions;
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Telemetry;
using Microsoft.Extensions.Logging;
using Microsoft.ServiceHub.Framework;
using Roslyn.Utilities;
using StreamJsonRpc;
 
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
 
[Export, Shared]
internal sealed class ProjectInitializationHandler : IDisposable
{
    private const string ProjectInitializationCompleteName = "workspace/projectInitializationComplete";
 
    private readonly IServiceBroker _serviceBroker;
    private readonly ServiceBrokerClient _serviceBrokerClient;
    private readonly ILogger _logger;
 
    private readonly TaskCompletionSource _serviceAvailable = new();
    private readonly ProjectInitializationCompleteObserver _projectInitializationCompleteObserver;
 
    private IDisposable? _subscription;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public ProjectInitializationHandler(IServiceBrokerProvider serviceBrokerProvider, ILoggerFactory loggerFactory)
    {
        _serviceBroker = serviceBrokerProvider.ServiceBroker;
        _serviceBroker.AvailabilityChanged += AvailabilityChanged;
        _serviceBrokerClient = new ServiceBrokerClient(_serviceBroker, joinableTaskFactory: null);
 
        _logger = loggerFactory.CreateLogger<ProjectInitializationHandler>();
        _projectInitializationCompleteObserver = new ProjectInitializationCompleteObserver(_logger);
    }
 
    public static async Task SendProjectInitializationCompleteNotificationAsync()
    {
        Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through.");
        var languageServerManager = LanguageServerHost.Instance.GetRequiredLspService<IClientLanguageServerManager>();
        await languageServerManager.SendNotificationAsync(ProjectInitializationCompleteName, CancellationToken.None);
    }
 
    public async Task SubscribeToInitializationCompleteAsync(CancellationToken cancellationToken)
    {
        // Use the ServiceBrokerClient so that we actually hold onto the instance of the service to prevent it from being disposed of until we're shutting down.
        var didSubscribe = await TrySubscribeAsync(cancellationToken);
        if (!didSubscribe)
        {
            // Service might be null the first time we try to access it - wait for it to become available on the remote side.
            await _serviceAvailable.Task;
            didSubscribe = await TrySubscribeAsync(cancellationToken);
            Contract.ThrowIfFalse(didSubscribe, $"Unable to subscribe to {Descriptors.RemoteProjectInitializationStatusService.Moniker}");
        }
    }
 
    private async Task<bool> TrySubscribeAsync(CancellationToken cancellationToken)
    {
        using var rental = await _serviceBrokerClient.GetProxyAsync<IProjectInitializationStatusService>(Descriptors.RemoteProjectInitializationStatusService, cancellationToken);
        if (rental.Proxy is not null)
        {
            _subscription = await rental.Proxy.SubscribeInitializationCompletionAsync(_projectInitializationCompleteObserver, cancellationToken);
            return true;
        }
 
        return false;
    }
 
    private void AvailabilityChanged(object? sender, BrokeredServicesChangedEventArgs e)
    {
        if (e.ImpactedServices.Contains(Descriptors.RemoteProjectInitializationStatusService.Moniker))
            _serviceAvailable.SetResult();
    }
 
    public void Dispose()
    {
        _serviceBroker.AvailabilityChanged -= AvailabilityChanged;
        _subscription?.Dispose();
        _serviceBrokerClient.Dispose();
    }
 
    internal class ProjectInitializationCompleteObserver : IObserver<ProjectInitializationCompletionState>
    {
        private readonly ILogger _logger;
 
        public ProjectInitializationCompleteObserver(ILogger logger)
        {
            _logger = logger;
        }
 
        [JsonRpcMethod("onCompleted")]
        public void OnCompleted()
        {
            // NoOp - OnNext is the only method that will be called upon completion of initial project load.
        }
 
        [JsonRpcMethod("onError", UseSingleObjectParameterDeserialization = true)]
        public void OnError(Exception error)
        {
            _logger.LogError(error, "Devkit project initialization observer failed");
        }
 
        [JsonRpcMethod("onNext", UseSingleObjectParameterDeserialization = true)]
        public void OnNext(ProjectInitializationCompletionState value)
        {
            _logger.LogDebug("Devkit project initialization completed");
            VSCodeRequestTelemetryLogger.ReportProjectInitializationComplete();
            _ = SendProjectInitializationCompleteNotificationAsync().ReportNonFatalErrorAsync();
        }
    }
}
#pragma warning restore RS0030 // Do not used banned APIs