File: SignatureHelp\AbstractGenericNameSignatureHelpProvider.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;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp;
 
internal abstract partial class AbstractGenericNameSignatureHelpProvider : AbstractCSharpSignatureHelpProvider
{
    public override ImmutableArray<char> TriggerCharacters => ['<', ','];
 
    public override ImmutableArray<char> RetriggerCharacters => ['>'];
 
    protected abstract TextSpan GetTextSpan(SyntaxToken genericIdentifier, SyntaxToken lessThanToken);
 
    protected abstract bool TryGetGenericIdentifier(
        SyntaxNode root, int position,
        ISyntaxFactsService syntaxFacts,
        SignatureHelpTriggerReason triggerReason,
        CancellationToken cancellationToken,
        out SyntaxToken genericIdentifier,
        out SyntaxToken lessThanToken);
 
    protected override async Task<SignatureHelpItems?> GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, MemberDisplayOptions options, CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        if (!TryGetGenericIdentifier(root, position, document.GetRequiredLanguageService<ISyntaxFactsService>(), triggerInfo.TriggerReason, cancellationToken,
                out var genericIdentifier, out var lessThanToken))
        {
            return null;
        }
 
        if (genericIdentifier.Parent is not SimpleNameSyntax simpleName)
        {
            return null;
        }
 
        var beforeDotExpression = simpleName.IsRightSideOfDot() ? simpleName.GetLeftSideOfDot() : null;
 
        var semanticModel = await document.ReuseExistingSpeculativeModelAsync(simpleName, cancellationToken).ConfigureAwait(false);
 
        var leftSymbol = beforeDotExpression == null
            ? null
            : semanticModel.GetSymbolInfo(beforeDotExpression, cancellationToken).GetAnySymbol() as INamespaceOrTypeSymbol;
        var leftType = beforeDotExpression == null
            ? null
            : semanticModel.GetTypeInfo(beforeDotExpression, cancellationToken).Type as INamespaceOrTypeSymbol;
 
        var leftContainer = leftSymbol ?? leftType;
 
        var isBaseAccess = beforeDotExpression is BaseExpressionSyntax;
        var namespacesOrTypesOnly = SyntaxFacts.IsInNamespaceOrTypeContext(simpleName);
        var includeExtensions = leftSymbol == null && leftType != null;
        var name = genericIdentifier.ValueText;
        var symbols = isBaseAccess
            ? semanticModel.LookupBaseMembers(position, name)
            : namespacesOrTypesOnly
                ? semanticModel.LookupNamespacesAndTypes(position, leftContainer, name)
                : semanticModel.LookupSymbols(position, leftContainer, name, includeExtensions);
 
        var within = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken);
        if (within == null)
        {
            return null;
        }
 
        var accessibleSymbols = symbols
            .WhereAsArray(s => s.GetArity() > 0)
            .FilterToVisibleAndBrowsableSymbols(options.HideAdvancedMembers, semanticModel.Compilation, inclusionFilter: static s => true)
            .Sort(semanticModel, genericIdentifier.SpanStart);
 
        if (!accessibleSymbols.Any())
            return null;
 
        var structuralTypeDisplayService = document.GetRequiredLanguageService<IStructuralTypeDisplayService>();
        var documentationCommentFormattingService = document.GetRequiredLanguageService<IDocumentationCommentFormattingService>();
        var textSpan = GetTextSpan(genericIdentifier, lessThanToken);
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        return CreateSignatureHelpItems([.. accessibleSymbols.Select(s =>
            Convert(s, lessThanToken, semanticModel, structuralTypeDisplayService, documentationCommentFormattingService))],
            textSpan, GetCurrentArgumentState(root, position, syntaxFacts, cancellationToken), selectedItemIndex: null, parameterIndexOverride: -1);
    }
 
    private SignatureHelpState? GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken)
    {
        if (!TryGetGenericIdentifier(root, position, syntaxFacts, SignatureHelpTriggerReason.InvokeSignatureHelpCommand, cancellationToken,
                out var genericIdentifier, out _))
        {
            return null;
        }
 
        if (genericIdentifier.TryParseGenericName(cancellationToken, out var genericName))
        {
            // Because we synthesized the generic name, it will have an index starting at 0
            // instead of at the actual position it's at in the text.  Because of this, we need to
            // offset the position we are checking accordingly.
            var offset = genericIdentifier.SpanStart - genericName.SpanStart;
            position -= offset;
            return SignatureHelpUtilities.GetSignatureHelpState(genericName.TypeArgumentList, position);
        }
 
        return null;
    }
 
    private static SignatureHelpItem Convert(
        ISymbol symbol,
        SyntaxToken lessThanToken,
        SemanticModel semanticModel,
        IStructuralTypeDisplayService structuralTypeDisplayService,
        IDocumentationCommentFormattingService documentationCommentFormattingService)
    {
        var position = lessThanToken.SpanStart;
 
        if (symbol is INamedTypeSymbol namedType)
        {
            return CreateItem(
                symbol, semanticModel, position,
                structuralTypeDisplayService,
                isVariadic: false,
                symbol.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService),
                GetPreambleParts(namedType, semanticModel, position),
                GetSeparatorParts(),
                GetPostambleParts(),
                [.. namedType.TypeParameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService))]);
        }
        else if (symbol is IMethodSymbol method)
        {
            return CreateItem(
                symbol, semanticModel, position,
                structuralTypeDisplayService,
                isVariadic: false,
                symbol.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService),
                GetPreambleParts(method, semanticModel, position),
                GetSeparatorParts(),
                GetPostambleParts(method, semanticModel, position),
                GetTypeArguments(method));
        }
        else
        {
            throw ExceptionUtilities.UnexpectedValue(symbol);
        }
 
        ImmutableArray<SignatureHelpSymbolParameter> GetTypeArguments(IMethodSymbol method)
        {
            using var _ = ArrayBuilder<SignatureHelpSymbolParameter>.GetInstance(out var result);
 
            // Signature help for generic modern extensions must include the generic type *arguments* for the containing
            // extension as well.  These are fixed given the receiver, and need to be repeated in the method type argument
            // list.
            if (method.ContainingType.IsExtension)
            {
                result.AddRange(method.ContainingType.TypeArguments.Select(t => new SignatureHelpSymbolParameter(
                    name: null, isOptional: false,
                    t.GetDocumentationPartsFactory(semanticModel, position, documentationCommentFormattingService),
                    t.ToMinimalDisplayParts(semanticModel, position))));
            }
 
            result.AddRange(method.TypeParameters.Select(p => Convert(p, semanticModel, position, documentationCommentFormattingService)));
 
            return result.ToImmutableAndClear();
        }
    }
 
    private static readonly SymbolDisplayFormat s_minimallyQualifiedFormat =
        SymbolDisplayFormat.MinimallyQualifiedFormat.WithGenericsOptions(
            SymbolDisplayFormat.MinimallyQualifiedFormat.GenericsOptions | SymbolDisplayGenericsOptions.IncludeVariance);
 
    private static SignatureHelpSymbolParameter Convert(
        ITypeParameterSymbol parameter,
        SemanticModel semanticModel,
        int position,
        IDocumentationCommentFormattingService formatter)
    {
        return new SignatureHelpSymbolParameter(
            parameter.Name,
            isOptional: false,
            documentationFactory: parameter.GetDocumentationPartsFactory(semanticModel, position, formatter),
            displayParts: parameter.ToMinimalDisplayParts(semanticModel, position, s_minimallyQualifiedFormat),
            selectedDisplayParts: GetSelectedDisplayParts(parameter, semanticModel, position));
    }
 
    private static ImmutableArray<SymbolDisplayPart> GetSelectedDisplayParts(
        ITypeParameterSymbol typeParam,
        SemanticModel semanticModel,
        int position)
    {
        using var _ = ArrayBuilder<SymbolDisplayPart>.GetInstance(out var parts);
 
        if (TypeParameterHasConstraints(typeParam))
        {
            parts.Add(Space());
            parts.Add(Keyword(SyntaxKind.WhereKeyword));
            parts.Add(Space());
 
            parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.TypeParameterName, typeParam, typeParam.Name));
 
            parts.Add(Space());
            parts.Add(Punctuation(SyntaxKind.ColonToken));
            parts.Add(Space());
 
            var needComma = false;
 
            // class/struct constraint must be first
            if (typeParam.HasReferenceTypeConstraint)
            {
                parts.Add(Keyword(SyntaxKind.ClassKeyword));
                needComma = true;
            }
            else if (typeParam.HasUnmanagedTypeConstraint)
            {
                parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Keyword, null, "unmanaged"));
                needComma = true;
            }
            else if (typeParam.HasValueTypeConstraint)
            {
                parts.Add(Keyword(SyntaxKind.StructKeyword));
                needComma = true;
            }
 
            foreach (var baseType in typeParam.ConstraintTypes)
            {
                if (needComma)
                {
                    parts.Add(Punctuation(SyntaxKind.CommaToken));
                    parts.Add(Space());
                }
 
                parts.AddRange(baseType.ToMinimalDisplayParts(semanticModel, position));
                needComma = true;
            }
 
            // ctor constraint must be last
            if (typeParam.HasConstructorConstraint)
            {
                if (needComma)
                {
                    parts.Add(Punctuation(SyntaxKind.CommaToken));
                    parts.Add(Space());
                }
 
                parts.Add(Keyword(SyntaxKind.NewKeyword));
                parts.Add(Punctuation(SyntaxKind.OpenParenToken));
                parts.Add(Punctuation(SyntaxKind.CloseParenToken));
                needComma = true;
            }
 
            if (typeParam.AllowsRefLikeType)
            {
                if (needComma)
                {
                    parts.Add(Punctuation(SyntaxKind.CommaToken));
                    parts.Add(Space());
                }
 
                parts.Add(Keyword(SyntaxKind.AllowsKeyword));
                parts.Add(Space());
                parts.Add(Keyword(SyntaxKind.RefKeyword));
                parts.Add(Space());
                parts.Add(Keyword(SyntaxKind.StructKeyword));
            }
        }
 
        return parts.ToImmutableAndClear();
    }
 
    private static bool TypeParameterHasConstraints(ITypeParameterSymbol typeParam)
    {
        return !typeParam.ConstraintTypes.IsDefaultOrEmpty || typeParam.HasConstructorConstraint ||
            typeParam.HasReferenceTypeConstraint || typeParam.HasValueTypeConstraint ||
            typeParam.AllowsRefLikeType;
    }
}