File: Completion\KeywordRecommenders\WhereKeywordRecommender.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// 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.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders;
 
internal class WhereKeywordRecommender : AbstractSyntacticSingleKeywordRecommender
{
    public WhereKeywordRecommender()
        : base(SyntaxKind.WhereKeyword)
    {
    }
 
    protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken)
    {
        return
            IsQueryContext(context) ||
            IsTypeParameterConstraintContext(context);
    }
 
    private static bool IsTypeParameterConstraintContext(CSharpSyntaxContext context)
    {
        // cases:
        //   class C<T> |
        //   class C<T> : IGoo |
        //   class C<T> where T : IGoo |
        //   delegate void D<T> |
        //   delegate void D<T> where T : IGoo |
        //   void Goo<T>() |
        //   void Goo<T>() where T : IGoo |
 
        var token = context.TargetToken;
 
        // class C<T> |
 
        if (token.Kind() == SyntaxKind.GreaterThanToken)
        {
            var typeParameters = token.GetAncestor<TypeParameterListSyntax>();
            if (typeParameters != null && token == typeParameters.GetLastToken(includeSkipped: true))
            {
                var decl = typeParameters.GetAncestorOrThis<TypeDeclarationSyntax>();
                if (decl != null && decl.TypeParameterList == typeParameters)
                {
                    return true;
                }
            }
        }
 
        // delegate void D<T>() |
        if (token.Kind() == SyntaxKind.CloseParenToken &&
            token.Parent.IsKind(SyntaxKind.ParameterList) &&
            token.Parent.IsParentKind(SyntaxKind.DelegateDeclaration))
        {
            var decl = token.GetAncestor<DelegateDeclarationSyntax>();
            if (decl != null && decl.TypeParameterList != null)
            {
                return true;
            }
        }
 
        // void Goo<T>() |
 
        if (token.Kind() == SyntaxKind.CloseParenToken &&
            token.Parent.IsKind(SyntaxKind.ParameterList))
        {
            var tokenParent = token.Parent;
            if (tokenParent.IsParentKind<MethodDeclarationSyntax>(SyntaxKind.MethodDeclaration, out var methodDeclaration))
            {
                if (methodDeclaration.Arity > 0)
                {
                    return true;
                }
            }
            else if (tokenParent.Parent is LocalFunctionStatementSyntax { TypeParameterList.Parameters.Count: > 0 })
            {
                return true;
            }
        }
 
        // class C<T> : IGoo |
        var baseList = token.GetAncestor<BaseListSyntax>();
        if (baseList?.Parent is TypeDeclarationSyntax typeDecl)
        {
            if (typeDecl.TypeParameterList != null &&
                baseList.Types.Any(t => token == t.GetLastToken(includeSkipped: true)))
            {
                // token is IdentifierName "where"
                // only suggest "where" if token's previous token is also "where"
                if (token.Parent is IdentifierNameSyntax && token.HasMatchingText(SyntaxKind.WhereKeyword))
                {
                    // Check for zero-width tokens in case there is a missing comma in the base list.
                    // For example: class C<T> : Goo where where |
                    return token
                        .GetPreviousToken(includeZeroWidth: true)
                        .IsKindOrHasMatchingText(SyntaxKind.WhereKeyword);
                }
 
                // System.|
                // Not done typing the qualified name
                if (token.IsKind(SyntaxKind.DotToken))
                {
                    return false;
                }
 
                return true;
            }
        }
 
        // class C<T> where T : IGoo |
        // delegate void D<T> where T : IGoo |
        if (token.IsLastTokenOfNode<TypeParameterConstraintSyntax>())
        {
            return true;
        }
 
        return false;
    }
 
    private static bool IsQueryContext(CSharpSyntaxContext context)
    {
        var token = context.TargetToken;
 
        // var q = from x in y
        //         |
        if (!token.IntersectsWith(context.Position) &&
            token.IsLastTokenOfQueryClause())
        {
            return true;
        }
 
        return false;
    }
}