File: Editor\GoToAdjacentMemberCommandHandler.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.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor.Commanding;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Outlining;
 
using ContentTypeAttribute = Microsoft.VisualStudio.Utilities.ContentTypeAttribute;
using NameAttribute = Microsoft.VisualStudio.Utilities.NameAttribute;
 
namespace Microsoft.CodeAnalysis.Editor;
 
[Export(typeof(ICommandHandler))]
[ContentType(ContentTypeNames.RoslynContentType)]
[Name(PredefinedCommandHandlerNames.GoToAdjacentMember)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class GoToAdjacentMemberCommandHandler(IOutliningManagerService outliningManagerService) :
    ICommandHandler<GoToNextMemberCommandArgs>,
    ICommandHandler<GoToPreviousMemberCommandArgs>
{
    private readonly IOutliningManagerService _outliningManagerService = outliningManagerService;
 
    public string DisplayName => EditorFeaturesResources.Go_To_Adjacent_Member;
 
    public CommandState GetCommandState(GoToNextMemberCommandArgs args)
        => GetCommandStateImpl(args);
 
    public bool ExecuteCommand(GoToNextMemberCommandArgs args, CommandExecutionContext context)
        => ExecuteCommandImpl(args, gotoNextMember: true, context);
 
    public CommandState GetCommandState(GoToPreviousMemberCommandArgs args)
        => GetCommandStateImpl(args);
 
    public bool ExecuteCommand(GoToPreviousMemberCommandArgs args, CommandExecutionContext context)
        => ExecuteCommandImpl(args, gotoNextMember: false, context);
 
    private static CommandState GetCommandStateImpl(EditorCommandArgs args)
    {
        var subjectBuffer = args.SubjectBuffer;
        var caretPoint = args.TextView.GetCaretPoint(subjectBuffer);
        if (!caretPoint.HasValue || !subjectBuffer.SupportsNavigationToAnyPosition())
        {
            return CommandState.Unspecified;
        }
 
        var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        if (document?.SupportsSyntaxTree != true)
        {
            return CommandState.Unspecified;
        }
 
        return CommandState.Available;
    }
 
    private bool ExecuteCommandImpl(EditorCommandArgs args, bool gotoNextMember, CommandExecutionContext context)
    {
        var subjectBuffer = args.SubjectBuffer;
        var caretPoint = args.TextView.GetCaretPoint(subjectBuffer);
        if (!caretPoint.HasValue || !subjectBuffer.SupportsNavigationToAnyPosition())
        {
            return false;
        }
 
        var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        var syntaxFactsService = document?.GetLanguageService<ISyntaxFactsService>();
        if (syntaxFactsService == null)
        {
            return false;
        }
 
        int? targetPosition = null;
        using (context.OperationContext.AddScope(allowCancellation: true, description: EditorFeaturesResources.Navigating))
        {
            var root = document.GetSyntaxRootSynchronously(context.OperationContext.UserCancellationToken);
            targetPosition = GetTargetPosition(syntaxFactsService, root, caretPoint.Value.Position, gotoNextMember);
        }
 
        if (targetPosition != null)
        {
            args.TextView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, targetPosition.Value), _outliningManagerService);
        }
 
        return true;
    }
 
    /// <summary>
    /// Internal for testing purposes.
    /// </summary>
    internal static int? GetTargetPosition(ISyntaxFactsService service, SyntaxNode root, int caretPosition, bool next)
    {
        using var pooledMembers = service.GetMethodLevelMembers(root);
        var members = pooledMembers.Object;
        if (members.Count == 0)
        {
            return null;
        }
 
        var starts = members.Select(m => MemberStart(m)).ToArray();
        var index = Array.BinarySearch(starts, caretPosition);
        if (index >= 0)
        {
            // We're actually contained in a member, go to the next or previous.
            index = next ? index + 1 : index - 1;
        }
        else
        {
            // We're in between to members, ~index gives us the member we're before, so we'll just
            // advance to the start of it
            index = next ? ~index : ~index - 1;
        }
 
        // Wrap if necessary
        if (index >= members.Count)
        {
            index = 0;
        }
        else if (index < 0)
        {
            index = members.Count - 1;
        }
 
        return MemberStart(members[index]);
    }
 
    private static int MemberStart(SyntaxNode node)
    {
        // TODO: Better position within the node (e.g. attributes?)
        return node.SpanStart;
    }
}