File: ReferenceHighlighting\NavigateToHighlightReferenceCommandHandler.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;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Outlining;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.ReferenceHighlighting;
 
[Export(typeof(ICommandHandler))]
[ContentType(ContentTypeNames.RoslynContentType)]
[Name(PredefinedCommandHandlerNames.NavigateToHighlightedReference)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal partial class NavigateToHighlightReferenceCommandHandler(
    IOutliningManagerService outliningManagerService,
    IViewTagAggregatorFactoryService tagAggregatorFactory) :
    ICommandHandler<NavigateToNextHighlightedReferenceCommandArgs>,
    ICommandHandler<NavigateToPreviousHighlightedReferenceCommandArgs>
{
    private readonly IOutliningManagerService _outliningManagerService = outliningManagerService ?? throw new ArgumentNullException(nameof(outliningManagerService));
    private readonly IViewTagAggregatorFactoryService _tagAggregatorFactory = tagAggregatorFactory ?? throw new ArgumentNullException(nameof(tagAggregatorFactory));
 
    public string DisplayName => EditorFeaturesResources.Navigate_To_Highlight_Reference;
 
    // We always want to support these commands.  If we don't, then they will be processed by the next higher item in
    // the command stack, which can cause VS to cycle focus to some other control other than the actual editor.  Once
    // the user is in a roslyn editing session, we always want to be processing this, even if the user is not actually
    // on a symbol (or symbol information hasn't been computed yet).
    //
    // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1875365
 
    public CommandState GetCommandState(NavigateToNextHighlightedReferenceCommandArgs args)
        => CommandState.Available;
 
    public CommandState GetCommandState(NavigateToPreviousHighlightedReferenceCommandArgs args)
        => CommandState.Available;
 
    public bool ExecuteCommand(NavigateToNextHighlightedReferenceCommandArgs args, CommandExecutionContext context)
        => ExecuteCommandImpl(args, navigateToNext: true);
 
    public bool ExecuteCommand(NavigateToPreviousHighlightedReferenceCommandArgs args, CommandExecutionContext context)
        => ExecuteCommandImpl(args, navigateToNext: false);
 
    private bool ExecuteCommandImpl(EditorCommandArgs args, bool navigateToNext)
    {
        using var tagAggregator = _tagAggregatorFactory.CreateTagAggregator<NavigableHighlightTag>(args.TextView);
        var tagUnderCursor = FindTagUnderCaret(tagAggregator, args.TextView);
 
        if (tagUnderCursor == null)
            return false;
 
        var spans = GetTags(tagAggregator, args.TextView.TextSnapshot.GetFullSpan());
 
        Contract.ThrowIfFalse(spans.Any(), "We should have at least found the tag under the cursor!");
 
        var destTag = GetDestinationTag(tagUnderCursor.Value, spans, navigateToNext);
 
        if (args.TextView.TryMoveCaretToAndEnsureVisible(destTag.Start, _outliningManagerService))
            args.TextView.SetSelection(destTag);
 
        return true;
    }
 
    private static ImmutableArray<SnapshotSpan> GetTags(
        ITagAggregator<NavigableHighlightTag> tagAggregator,
        SnapshotSpan span)
    {
        using var _ = PooledObjects.ArrayBuilder<SnapshotSpan>.GetInstance(out var tags);
 
        foreach (var tag in tagAggregator.GetTags(span))
            tags.AddRange(tag.Span.GetSpans(span.Snapshot.TextBuffer));
 
        tags.Sort(static (ss1, ss2) => ss1.Start - ss2.Start);
        return tags.ToImmutableAndClear();
    }
 
    private static SnapshotSpan GetDestinationTag(
        SnapshotSpan tagUnderCursor,
        ImmutableArray<SnapshotSpan> orderedTagSpans,
        bool navigateToNext)
    {
        var destIndex = orderedTagSpans.BinarySearch(tagUnderCursor, StartComparer.Instance);
 
        Contract.ThrowIfFalse(destIndex >= 0, "Expected to find start tag in the collection");
 
        destIndex += navigateToNext ? 1 : -1;
 
        // Handle wraparound
        var length = orderedTagSpans.Length;
        destIndex = ((destIndex % length) + length) % length;
 
        return orderedTagSpans[destIndex];
    }
 
    private static SnapshotSpan? FindTagUnderCaret(
        ITagAggregator<NavigableHighlightTag> tagAggregator,
        ITextView textView)
    {
        // We always want to be working with the surface buffer here, so this line is correct
        var caretPosition = textView.Caret.Position.BufferPosition.Position;
 
        var tags = GetTags(tagAggregator, new SnapshotSpan(textView.TextSnapshot, new Span(caretPosition, 0)));
        return tags.FirstOrNull();
    }
}