File: NavigateTo\INavigateToSearcherHost.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
 
namespace Microsoft.CodeAnalysis.NavigateTo;
 
/// <summary>
/// Host interface abstracting over all the external functionality the <see cref="NavigateToSearcher"/> needs. This
/// provide an easy entry point for swapping out functionality of the host, including for testing purposes.
/// </summary>
internal interface INavigateToSearcherHost
{
    INavigateToSearchService? GetNavigateToSearchService(Project project);
 
    /// <summary>
    /// Returns the fully loaded state for both the project system and the remote host.
    /// </summary>
    ValueTask<bool> IsFullyLoadedAsync(CancellationToken cancellationToken);
}
 
internal interface IWorkspaceNavigateToSearcherHostService : IWorkspaceService
{
    ValueTask<bool> IsFullyLoadedAsync(CancellationToken cancellationToken);
}
 
internal sealed class DefaultNavigateToSearchHost(
    Solution solution,
    IAsynchronousOperationListener asyncListener,
    CancellationToken disposalToken) : INavigateToSearcherHost
{
    private readonly Solution _solution = solution;
    private readonly IAsynchronousOperationListener _asyncListener = asyncListener;
    private readonly CancellationToken _disposalToken = disposalToken;
 
    /// <summary>
    /// Single task used to both hydrate the remote host with the initial workspace solution,
    /// and track if that work completed.  Prior to it completing, we will try to get all
    /// navigate-to requests from our caches.  Once it is populated though, we can attempt to
    /// use the latest data instead.
    /// </summary>
    private static readonly object s_gate = new();
    private static Task? s_remoteHostHydrateTask = null;
 
    public INavigateToSearchService? GetNavigateToSearchService(Project project)
        => project.GetLanguageService<INavigateToSearchService>();
 
    public async ValueTask<bool> IsFullyLoadedAsync(CancellationToken cancellationToken)
    {
        var workspaceService = _solution.Workspace.Services.GetService<IWorkspaceNavigateToSearcherHostService>();
        if (workspaceService != null)
            return await workspaceService.IsFullyLoadedAsync(cancellationToken).ConfigureAwait(false);
 
        var service = _solution.Services.GetRequiredService<IWorkspaceStatusService>();
 
        // We consider ourselves fully loaded when both the project system has completed loaded
        // us, and we've totally hydrated the oop side.  Until that happens, we'll attempt to
        // return cached data from languages that support that.
        var isProjectSystemFullyLoaded = await service.IsFullyLoadedAsync(cancellationToken).ConfigureAwait(false);
        if (!isProjectSystemFullyLoaded)
            return false;
 
        var isRemoteHostFullyLoaded = GetRemoteHostHydrateTask().IsCompleted;
        return isRemoteHostFullyLoaded;
    }
 
    /// <summary>
    /// If we're in a solution that is using OOP, this kicks off a task to get the oop side in
    /// sync with us.  Until that happens, we'll continue to use the cached results from prior
    /// sessions so that we can get results very quickly right after launch without forcing the
    /// user to wait for OOP to hydrate the entire solution over.  This strikes a good balance
    /// of speed and accuracy as most of the time cached results will be fast and good enough,
    /// and eventually (usually within dozens of seconds, even for large projects) we will
    /// switch over to full and accurate results which can also come back quickly.
    /// </summary>
    /// <remarks>
    /// If we do report cached data, we inform the user of this so they know the results may be
    /// incomplete or inaccurate and that they can try again later if necessary.
    /// </remarks>
#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods
    private Task GetRemoteHostHydrateTask()
#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods
    {
        lock (s_gate)
        {
            // Only need to do this once.
            if (s_remoteHostHydrateTask == null)
            {
                // If there are no projects in this solution that use OOP, then there's nothing we need to do.
                if (_solution.Projects.All(p => !RemoteSupportedLanguages.IsSupported(p.Language)))
                {
                    s_remoteHostHydrateTask = Task.CompletedTask;
                }
                else
                {
                    var asyncToken = _asyncListener.BeginAsyncOperation(nameof(GetRemoteHostHydrateTask));
 
                    s_remoteHostHydrateTask = Task.Run(async () =>
                    {
                        var client = await RemoteHostClient.TryGetClientAsync(_solution.Services, _disposalToken).ConfigureAwait(false);
                        if (client != null)
                        {
                            await client.TryInvokeAsync<IRemoteNavigateToSearchService>(
                                _solution,
                                (service, solutionInfo, cancellationToken) =>
                                service.HydrateAsync(solutionInfo, cancellationToken),
                                _disposalToken).ConfigureAwait(false);
                        }
                    }, _disposalToken);
                    s_remoteHostHydrateTask.CompletesAsyncOperation(asyncToken);
                }
            }
 
            return s_remoteHostHydrateTask;
        }
    }
}