// 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.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; } |