File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Services\SelectedMembers\AbstractSelectedMembers.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.LanguageService;
 
internal abstract class AbstractSelectedMembers<
    TMemberDeclarationSyntax,
    TFieldDeclarationSyntax,
    TPropertyDeclarationSyntax,
    TTypeDeclarationSyntax,
    TVariableSyntax>
    where TMemberDeclarationSyntax : SyntaxNode
    where TFieldDeclarationSyntax : TMemberDeclarationSyntax
    where TPropertyDeclarationSyntax : TMemberDeclarationSyntax
    where TTypeDeclarationSyntax : TMemberDeclarationSyntax
    where TVariableSyntax : SyntaxNode
{
    protected abstract SyntaxList<TMemberDeclarationSyntax> GetMembers(TTypeDeclarationSyntax containingType);
    protected abstract ImmutableArray<(SyntaxNode declarator, SyntaxToken identifier)> GetDeclaratorsAndIdentifiers(TMemberDeclarationSyntax member);
 
    public Task<ImmutableArray<SyntaxNode>> GetSelectedFieldsAndPropertiesAsync(
        SyntaxTree tree, TextSpan textSpan, bool allowPartialSelection, CancellationToken cancellationToken)
            => GetSelectedMembersAsync(tree, textSpan, allowPartialSelection, IsFieldOrProperty, cancellationToken);
 
    public Task<ImmutableArray<SyntaxNode>> GetSelectedMembersAsync(
        SyntaxTree tree, TextSpan textSpan, bool allowPartialSelection, CancellationToken cancellationToken)
            => GetSelectedMembersAsync(tree, textSpan, allowPartialSelection, static _ => true, cancellationToken);
 
    private async Task<ImmutableArray<SyntaxNode>> GetSelectedMembersAsync(
        SyntaxTree tree, TextSpan textSpan, bool allowPartialSelection,
        Func<TMemberDeclarationSyntax, bool> membersToKeep, CancellationToken cancellationToken)
    {
        var text = await tree.GetTextAsync(cancellationToken).ConfigureAwait(false);
        var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false);
 
        // If there is a selection, look for the token to the right of the selection That helps
        // the user select like so:
        //
        //          int i;[|
        //          int j;|]
        //
        // In this case (which is common with a mouse), we want to consider 'j' selected, and
        // 'i' not involved in all.
        //
        // However, if there is no selection and the user has:
        //
        //          int i;$$
        //          int j;
        //
        // Then we want to consider 'i' selected instead.  So we do a normal FindToken.
 
        var token = textSpan.IsEmpty
            ? root.FindToken(textSpan.Start)
            : root.FindTokenOnRightOfPosition(textSpan.Start);
        var firstMember = token.GetAncestors<TMemberDeclarationSyntax>()
                               .Where(m => m.Parent is TTypeDeclarationSyntax)
                               .FirstOrDefault();
        if (firstMember == null)
            return [];
 
        return GetMembersInSpan(root, text, textSpan, firstMember, allowPartialSelection, membersToKeep);
    }
 
    private ImmutableArray<SyntaxNode> GetMembersInSpan(
        SyntaxNode root, SourceText text, TextSpan textSpan,
        TMemberDeclarationSyntax firstMember, bool allowPartialSelection,
        Func<TMemberDeclarationSyntax, bool> membersToKeep)
    {
        var containingType = (TTypeDeclarationSyntax)firstMember.Parent;
        var members = GetMembers(containingType);
        var fieldIndex = members.IndexOf(firstMember);
        if (fieldIndex < 0)
            return [];
 
        using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var selectedMembers);
        for (var i = fieldIndex; i < members.Count; i++)
        {
            var member = members[i];
            AddSelectedMemberDeclarations(member, membersToKeep);
        }
 
        return selectedMembers.ToImmutableAndClear();
 
        void AddAllMembers(TMemberDeclarationSyntax member)
        {
            selectedMembers.AddRange(GetDeclaratorsAndIdentifiers(member).Select(pair => pair.declarator));
        }
 
        // local functions
        void AddSelectedMemberDeclarations(TMemberDeclarationSyntax member, Func<TMemberDeclarationSyntax, bool> membersToKeep)
        {
            if (!membersToKeep(member))
            {
                return;
            }
 
            // first, check if entire member is selected.  If so, we definitely include this member.
            if (textSpan.Contains(member.Span))
            {
                AddAllMembers(member);
                return;
            }
 
            if (textSpan.IsEmpty)
            {
                // No selection.  We consider this member selected if a few cases are true:
                //
                //  1. Position precedes the first token of the member (on the same line).
                //  2. Position touches the name of the member.
                //  3. Position touches an immediate child token of the member (on the same line)
                //  4. Position is after the last token of the member (on the same line).
 
                var position = textSpan.Start;
                if (IsBeforeOrAfterNodeOnSameLine(text, root, member, position))
                {
                    AddAllMembers(member);
                    return;
                }
                else
                {
                    foreach (var (decl, id) in GetDeclaratorsAndIdentifiers(member))
                    {
                        if (id.FullSpan.IntersectsWith(position))
                        {
                            selectedMembers.Add(decl);
                            return;
                        }
                    }
                }
            }
            else
            {
                // if the user has an actual selection, get the fields/props if the selection
                // surrounds the names of in the case of allowPartialSelection. Selecting other keywords
                // should not be considered member selection if the name is not also selected
 
                if (!allowPartialSelection)
                    return;
 
                foreach (var (decl, id) in GetDeclaratorsAndIdentifiers(member))
                {
                    if (textSpan.OverlapsWith(id.Span))
                    {
                        selectedMembers.Add(decl);
                    }
                }
            }
        }
    }
 
    private static bool IsBeforeOrAfterNodeOnSameLine(
        SourceText text, SyntaxNode root, SyntaxNode member, int position)
    {
        var token = root.FindToken(position);
        if (token == member.GetFirstToken() &&
            position <= token.SpanStart &&
            text.AreOnSameLine(position, token.SpanStart))
        {
            return true;
        }
 
        if (token == member.GetLastToken() &&
            position >= token.Span.End &&
            text.AreOnSameLine(position, token.Span.End))
        {
            return true;
        }
 
        return false;
    }
 
    private static bool IsFieldOrProperty(TMemberDeclarationSyntax member)
        => member is TFieldDeclarationSyntax or TPropertyDeclarationSyntax;
}