File: Host\IStreamingFindReferencesPresenter.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.PooledObjects;
 
namespace Microsoft.CodeAnalysis.Editor.Host;
 
/// <summary>
/// API for hosts to provide if they can present FindUsages results in a streaming manner.
/// i.e. if they support showing results as they are found instead of after all of the results
/// are found.
/// </summary>
internal interface IStreamingFindUsagesPresenter
{
    /// <summary>
    /// Tells the presenter that a search is starting.  The returned <see cref="FindUsagesContext"/>
    /// is used to push information about the search into.  i.e. when a reference is found
    /// <see cref="FindUsagesContext.OnReferencesFoundAsync"/> should be called.  When the
    /// search completes <see cref="FindUsagesContext.OnCompletedAsync"/> should be called. 
    /// etc. etc.
    /// </summary>
    /// <param name="title">A title to display to the user in the presentation of the results.</param>
    /// <param name="options">Options</param>
    /// <returns>A cancellation token that will be triggered if the presenter thinks the search
    /// should stop.  This can normally happen if the presenter view is closed, or recycled to
    /// start a new search in it.  Callers should only use this if they intend to report results
    /// asynchronously and thus relinquish their own control over cancellation from their own
    /// surrounding context.  If the caller intends to populate the presenter synchronously,
    /// then this cancellation token can be ignored.</returns>
    (FindUsagesContext context, CancellationToken cancellationToken) StartSearch(string title, StreamingFindUsagesPresenterOptions options);
 
    /// <summary>
    /// Clears all the items from the presenter.
    /// </summary>
    void ClearAll();
}
 
/// <summary>
/// <see cref="IStreamingFindUsagesPresenter.StartSearch(string, StreamingFindUsagesPresenterOptions)"/> options.
/// </summary>
/// <param name="SupportsReferences">
/// Whether or not showing references is supported.
/// If true, then the presenter can group by definition, showing references underneath.
/// It can also show messages about no references being found at the end of the search.
/// If false, the presenter will not group by definitions, and will show the definition
/// items in isolation.</param>
/// <param name="IncludeContainingTypeAndMemberColumns"></param>
/// <param name="IncludeKindColumn"></param>
internal readonly record struct StreamingFindUsagesPresenterOptions(
    bool SupportsReferences = false,
    bool IncludeContainingTypeAndMemberColumns = false,
    bool IncludeKindColumn = false)
{
    public static readonly StreamingFindUsagesPresenterOptions Default = new();
}
 
internal static class IStreamingFindUsagesPresenterExtensions
{
    public static async Task<bool> TryPresentLocationOrNavigateIfOneAsync(
        this IStreamingFindUsagesPresenter presenter,
        IThreadingContext threadingContext,
        Workspace workspace,
        string title,
        ImmutableArray<DefinitionItem> items,
        CancellationToken cancellationToken)
    {
        var location = await presenter.GetStreamingLocationAsync(
            threadingContext, workspace, title, items, cancellationToken).ConfigureAwait(false);
        return await location.TryNavigateToAsync(
            threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Returns a navigable location that will take the user to the location there's only destination, or which will
    /// present all the locations if there are many.
    /// </summary>
    public static async Task<INavigableLocation?> GetStreamingLocationAsync(
        this IStreamingFindUsagesPresenter presenter,
        IThreadingContext threadingContext,
        Workspace workspace,
        string title,
        ImmutableArray<DefinitionItem> items,
        CancellationToken cancellationToken)
    {
        if (items.IsDefaultOrEmpty)
            return null;
 
        using var _ = ArrayBuilder<(DefinitionItem item, INavigableLocation location)>.GetInstance(out var builder);
        foreach (var item in items)
        {
            // Ignore any definitions that we can't navigate to.
            var navigableItem = await item.GetNavigableLocationAsync(workspace, cancellationToken).ConfigureAwait(false);
            if (navigableItem != null)
            {
                // If there's a third party external item we can navigate to.  Defer to that item and finish.
                if (item.IsExternal)
                    return navigableItem;
 
                builder.Add((item, navigableItem));
            }
        }
 
        if (builder.Count == 0)
            return null;
 
        if (builder is [{ item.SourceSpans.Length: <= 1, location: var location }])
        {
            // There was only one location to navigate to.  Just directly go to that location. If we're directly
            // going to a location we need to activate the preview so that focus follows to the new cursor position.
            return location;
        }
 
        if (presenter == null)
            return null;
 
        var navigableItems = builder.SelectAsArray(t => t.item);
        return new NavigableLocation(async (options, cancellationToken) =>
        {
            // Can only navigate or present items on UI thread.
            await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            // We have multiple definitions, or we have definitions with multiple locations. Present this to the
            // user so they can decide where they want to go to.
            //
            // We ignore the cancellation token returned by StartSearch as we're in a context where
            // we've computed all the results and we're synchronously populating the UI with it.
            var (context, _) = presenter.StartSearch(title, StreamingFindUsagesPresenterOptions.Default);
            try
            {
                foreach (var item in navigableItems)
                    await context.OnDefinitionFoundAsync(item, cancellationToken).ConfigureAwait(false);
            }
            finally
            {
                await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false);
            }
 
            return true;
        });
    }
}