File: FindReferences\FindReferencesCommandHandler.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.
 
#nullable disable
 
using System;
using System.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.FindReferences;
 
[Export(typeof(ICommandHandler))]
[ContentType(ContentTypeNames.RoslynContentType)]
[Name(PredefinedCommandHandlerNames.FindReferences)]
internal class FindReferencesCommandHandler : ICommandHandler<FindReferencesCommandArgs>
{
    private readonly IStreamingFindUsagesPresenter _streamingPresenter;
    private readonly IGlobalOptionService _globalOptions;
    private readonly IAsynchronousOperationListener _asyncListener;
 
    public string DisplayName => EditorFeaturesResources.Find_References;
 
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    public FindReferencesCommandHandler(
        IStreamingFindUsagesPresenter streamingPresenter,
        IGlobalOptionService globalOptions,
        IAsynchronousOperationListenerProvider listenerProvider)
    {
        Contract.ThrowIfNull(listenerProvider);
 
        _streamingPresenter = streamingPresenter;
        _globalOptions = globalOptions;
        _asyncListener = listenerProvider.GetListener(FeatureAttribute.FindReferences);
    }
 
    public CommandState GetCommandState(FindReferencesCommandArgs args)
    {
        var (_, service) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot);
        return service != null
            ? CommandState.Available
            : CommandState.Unspecified;
    }
 
    public bool ExecuteCommand(FindReferencesCommandArgs args, CommandExecutionContext context)
    {
        var subjectBuffer = args.SubjectBuffer;
 
        // Get the selection that user has in our buffer (this also works if there
        // is no selection and the caret is just at a single position).  If we 
        // can't get the selection, or there are multiple spans for it (i.e. a 
        // box selection), then don't do anything.
        var snapshotSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer);
        if (snapshotSpans.Count == 1)
        {
            var selectedSpan = snapshotSpans[0];
            var (document, service) = GetDocumentAndService(subjectBuffer.CurrentSnapshot);
            if (document != null)
            {
                // Do a find-refs at the *start* of the selection.  That way if the
                // user has selected a symbol that has another symbol touching it
                // on the right (i.e.  Goo++  ), then we'll do the find-refs on the
                // symbol selected, not the symbol following.
                if (TryExecuteCommand(selectedSpan.Start, document, service))
                {
                    return true;
                }
            }
        }
 
        return false;
    }
 
    private static (Document, IFindUsagesService) GetDocumentAndService(ITextSnapshot snapshot)
    {
        var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
        return (document, document?.GetLanguageService<IFindUsagesService>());
    }
 
    private bool TryExecuteCommand(int caretPosition, Document document, IFindUsagesService findUsagesService)
    {
        // See if we're running on a host that can provide streaming results.
        // We'll both need a FAR service that can stream results to us, and 
        // a presenter that can accept streamed results.
        if (findUsagesService != null && _streamingPresenter != null)
        {
            // kick this work off in a fire and forget fashion.  Importantly, this means we do
            // not pass in any ambient cancellation information as the execution of this command
            // will complete and will have no bearing on the computation of the references we compute.
            _ = StreamingFindReferencesAsync(document, caretPosition, findUsagesService, _streamingPresenter);
            return true;
        }
 
        return false;
    }
 
    private async Task StreamingFindReferencesAsync(
        Document document,
        int caretPosition,
        IFindUsagesService findUsagesService,
        IStreamingFindUsagesPresenter presenter)
    {
        try
        {
            using var token = _asyncListener.BeginAsyncOperation(nameof(StreamingFindReferencesAsync));
            var classificationOptions = _globalOptions.GetClassificationOptionsProvider();
 
            // Let the presented know we're starting a search.  It will give us back the context object that the FAR
            // service will push results into. This operation is not externally cancellable.  Instead, the find refs
            // window will cancel it if another request is made to use it.
            var (context, cancellationToken) = presenter.StartSearch(
                EditorFeaturesResources.Find_References,
                new StreamingFindUsagesPresenterOptions()
                {
                    SupportsReferences = true,
                    IncludeContainingTypeAndMemberColumns = document.Project.SupportsCompilation,
                    IncludeKindColumn = document.Project.Language != LanguageNames.FSharp
                });
 
            using (Logger.LogBlock(
                FunctionId.CommandHandler_FindAllReference,
                KeyValueLogMessage.Create(LogType.UserAction, m => m["type"] = "streaming"),
                cancellationToken))
            {
                try
                {
                    await findUsagesService.FindReferencesAsync(context, document, caretPosition, classificationOptions, cancellationToken).ConfigureAwait(false);
                }
                finally
                {
                    await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false);
                }
            }
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception e) when (FatalError.ReportAndCatch(e))
        {
        }
    }
}