File: GoToDefinition\GoToDefinitionCommandHandler.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.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Notification;
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.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.GoToDefinition;
 
[Export(typeof(ICommandHandler))]
[ContentType(ContentTypeNames.RoslynContentType)]
[Name(PredefinedCommandHandlerNames.GoToDefinition)]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal class GoToDefinitionCommandHandler(
    IGlobalOptionService globalOptionService,
    IThreadingContext threadingContext,
    IUIThreadOperationExecutor executor,
    IAsynchronousOperationListenerProvider listenerProvider) :
    ICommandHandler<GoToDefinitionCommandArgs>
{
    private readonly IGlobalOptionService _globalOptionService = globalOptionService;
    private readonly IThreadingContext _threadingContext = threadingContext;
    private readonly IUIThreadOperationExecutor _executor = executor;
    private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.GoToDefinition);
 
    public string DisplayName => EditorFeaturesResources.Go_to_Definition;
 
    private static (Document?, IDefinitionLocationService?) GetDocumentAndService(ITextSnapshot snapshot)
    {
        var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
        return (document, document?.GetLanguageService<IDefinitionLocationService>());
    }
 
    public CommandState GetCommandState(GoToDefinitionCommandArgs args)
    {
        var (_, service) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot);
        return service != null
            ? CommandState.Available
            : CommandState.Unspecified;
    }
 
    public bool ExecuteCommand(GoToDefinitionCommandArgs args, CommandExecutionContext context)
    {
        var subjectBuffer = args.SubjectBuffer;
        var (document, service) = GetDocumentAndService(subjectBuffer.CurrentSnapshot);
 
        if (service == null)
            return false;
 
        Contract.ThrowIfNull(document);
 
        // In Live Share, typescript exports a gotodefinition service that returns no results and prevents the LSP
        // client from handling the request.  So prevent the local service from handling goto def commands in the
        // remote workspace. This can be removed once typescript implements LSP support for goto def.
        if (subjectBuffer.IsInLspEditorContext())
            return false;
 
        // If the file is empty, there's nothing to be on that we can goto-def on.  This also ensures that we can
        // create an appropriate non-empty tracking span later on.
        var currentSnapshot = subjectBuffer.CurrentSnapshot;
        if (currentSnapshot.Length == 0)
            return false;
 
        // If there's a selection, use the starting point of the selection as the invocation point. Otherwise, just
        // pick wherever the caret is exactly at.
        var caretPos =
            args.TextView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer).FirstOrNull()?.Start ??
            args.TextView.GetCaretPoint(subjectBuffer);
 
        if (!caretPos.HasValue)
            return false;
 
        // We're showing our own UI, ensure the editor doesn't show anything itself.
        context.OperationContext.TakeOwnership();
        var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand));
        ExecuteAsynchronouslyAsync(args, document, service, caretPos.Value)
            .ReportNonFatalErrorAsync()
            .CompletesAsyncOperation(token);
 
        return true;
    }
 
    private async Task ExecuteAsynchronouslyAsync(
        GoToDefinitionCommandArgs args, Document document, IDefinitionLocationService service, SnapshotPoint position)
    {
        bool succeeded;
 
        var indicatorFactory = document.Project.Solution.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
 
        // TODO: prior logic was to get a tracking span of length 1 here.  Preserving that, though it's unclear if
        // that is necessary for the BWI to work properly.
        Contract.ThrowIfTrue(position.Snapshot.Length == 0);
        var applicableToSpan = position < position.Snapshot.Length
            ? new SnapshotSpan(position, position + 1)
            : new SnapshotSpan(position - 1, position);
 
        using (var backgroundIndicator = indicatorFactory.Create(
            args.TextView, applicableToSpan,
            EditorFeaturesResources.Navigating_to_definition))
        {
            var cancellationToken = backgroundIndicator.UserCancellationToken;
 
            // Switch to a background thread before continuing. We don't want synchronous work before the first yield to
            // prevent the background work indicator from functioning properly.
            await TaskScheduler.Default;
 
            // determine the location first.
            var definitionLocation = await service.GetDefinitionLocationAsync(
                document, position, cancellationToken).ConfigureAwait(false);
 
            // make sure that if our background indicator got canceled, that we do not still perform the navigation.
            if (backgroundIndicator.UserCancellationToken.IsCancellationRequested)
                return;
 
            // we're about to navigate.  so disable cancellation on focus-lost in our indicator so we don't end up
            // causing ourselves to self-cancel.
            backgroundIndicator.CancelOnFocusLost = false;
            succeeded = definitionLocation != null && await definitionLocation.Location.TryNavigateToAsync(
                _threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false);
        }
 
        if (!succeeded)
        {
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None);
 
            var notificationService = document.Project.Solution.Services.GetRequiredService<INotificationService>();
            notificationService.SendNotification(
                FeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret, EditorFeaturesResources.Go_to_Definition, NotificationSeverity.Information);
        }
    }
}