File: SignatureHelp\AbstractSignatureHelpProvider.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.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.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.SignatureHelp;
 
internal abstract partial class AbstractSignatureHelpProvider : ISignatureHelpProvider
{
    protected static readonly SymbolDisplayFormat MinimallyQualifiedWithoutParametersFormat =
        SymbolDisplayFormat.MinimallyQualifiedFormat.WithMemberOptions(
            SymbolDisplayFormat.MinimallyQualifiedFormat.MemberOptions & ~SymbolDisplayMemberOptions.IncludeParameters);
 
    protected static readonly SymbolDisplayFormat MinimallyQualifiedWithoutTypeParametersFormat =
        SymbolDisplayFormat.MinimallyQualifiedFormat.WithGenericsOptions(
            SymbolDisplayFormat.MinimallyQualifiedFormat.GenericsOptions & ~SymbolDisplayGenericsOptions.IncludeTypeParameters);
 
    protected AbstractSignatureHelpProvider()
    {
    }
 
    public abstract bool IsTriggerCharacter(char ch);
    public abstract bool IsRetriggerCharacter(char ch);
 
    protected abstract Task<SignatureHelpItems?> GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, MemberDisplayOptions options, CancellationToken cancellationToken);
 
    protected static SignatureHelpItems? CreateSignatureHelpItems(
        IList<SignatureHelpItem> items, TextSpan applicableSpan, SignatureHelpState? state, int? selectedItemIndex, int parameterIndexOverride)
    {
        if (items is null || items.Count == 0 || state == null)
            return null;
 
        if (selectedItemIndex < 0)
            selectedItemIndex = null;
 
        (items, selectedItemIndex) = Filter(items, state.Value.ArgumentNames, selectedItemIndex);
 
        // If the caller provided a preferred parameter for us to be on then override whatever we found syntactically.
        var preferredParameterIndex = state.Value.SemanticParameterIndex;
        if (parameterIndexOverride >= 0)
        {
            // However, in the case where the overridden index is to a variadic member, and the syntactic index goes
            // beyond the length of hte normal parameters, do not do this.  The syntactic index is valid for the
            // variadic member, and we still want to remember where we are syntactically so that if the user picks
            // another member that we correctly pick the right parameter for it.
            var keepSyntacticIndex =
                preferredParameterIndex > parameterIndexOverride &&
                selectedItemIndex != null &&
                items[selectedItemIndex.Value].IsVariadic &&
                preferredParameterIndex >= items[selectedItemIndex.Value].Parameters.Length;
 
            if (!keepSyntacticIndex)
                preferredParameterIndex = parameterIndexOverride;
        }
 
        return new SignatureHelpItems(items, applicableSpan, preferredParameterIndex, state.Value.SyntacticArgumentCount, state.Value.ArgumentName, selectedItemIndex);
    }
 
    protected static SignatureHelpItems? CreateCollectionInitializerSignatureHelpItems(
        IList<SignatureHelpItem> items, TextSpan applicableSpan, SignatureHelpState? state)
    {
        // We will have added all the accessible '.Add' methods that take at least one
        // arguments. However, in general the one-arg Add method is the least likely for the
        // user to invoke. For example, say there is:
        //
        //      new JObject { { $$ } }
        //
        // Technically, the user could be calling the `.Add(object)` overload in this case.
        // However, normally in that case, they would just supply the value directly like so:
        //
        //      new JObject { new JProperty(...), new JProperty(...) }
        //
        // So, it's a strong signal when they're inside another `{ $$ }` that they want to call
        // the .Add methods that take multiple args, like so:
        //
        //      new JObject { { propName, propValue }, { propName, propValue } }
        // 
        // So, we include all the .Add methods, but we prefer selecting the first that has
        // at least two parameters.
        return CreateSignatureHelpItems(
            items, applicableSpan, state, items.IndexOf(i => i.Parameters.Length >= 2), parameterIndexOverride: -1);
    }
 
    private static (IList<SignatureHelpItem> items, int? selectedItem) Filter(IList<SignatureHelpItem> items, ImmutableArray<string> parameterNames, int? selectedItem)
    {
        if (parameterNames.IsDefault)
            return (items.ToList(), selectedItem);
 
        var filteredList = items.Where(i => Include(i, parameterNames)).ToList();
        var isEmpty = filteredList.Count == 0;
        if (!selectedItem.HasValue || isEmpty)
            return (isEmpty ? [.. items] : filteredList, selectedItem);
 
        // adjust the selected item
        var selection = items[selectedItem.Value];
        selectedItem = filteredList.IndexOf(selection);
 
        return (filteredList, selectedItem);
    }
 
    private static bool Include(SignatureHelpItem item, ImmutableArray<string> parameterNames)
    {
        using var _ = PooledHashSet<string>.GetInstance(out var itemParameterNames);
        foreach (var parameter in item.Parameters)
            itemParameterNames.Add(parameter.Name);
 
        foreach (var name in parameterNames)
        {
            if (!itemParameterNames.Contains(name))
                return false;
        }
 
        return true;
    }
 
    // TODO: remove once Pythia moves to ExternalAccess APIs
    [Obsolete("Use overload without ISymbolDisplayService")]
#pragma warning disable CA1822 // Mark members as static - see obsolete comment above.
    protected SignatureHelpItem CreateItem(
#pragma warning restore CA1822 // Mark members as static
        ISymbol orderSymbol,
        SemanticModel semanticModel,
        int position,
        ISymbolDisplayService symbolDisplayService,
        IStructuralTypeDisplayService structuralTypeDisplayService,
        bool isVariadic,
        Func<CancellationToken, IEnumerable<TaggedText>> documentationFactory,
        IList<SymbolDisplayPart> prefixParts,
        IList<SymbolDisplayPart> separatorParts,
        IList<SymbolDisplayPart> suffixParts,
        IList<SignatureHelpSymbolParameter> parameters,
        IList<SymbolDisplayPart>? descriptionParts = null)
    {
        return CreateItem(orderSymbol, semanticModel, position, structuralTypeDisplayService,
            isVariadic, documentationFactory, prefixParts, separatorParts, suffixParts, parameters, descriptionParts);
    }
 
    protected static SignatureHelpItem CreateItem(
        ISymbol orderSymbol,
        SemanticModel semanticModel,
        int position,
        IStructuralTypeDisplayService structuralTypeDisplayService,
        bool isVariadic,
        Func<CancellationToken, IEnumerable<TaggedText>>? documentationFactory,
        IList<SymbolDisplayPart> prefixParts,
        IList<SymbolDisplayPart> separatorParts,
        IList<SymbolDisplayPart> suffixParts,
        IList<SignatureHelpSymbolParameter> parameters,
        IList<SymbolDisplayPart>? descriptionParts = null)
    {
        return CreateItemImpl(orderSymbol, semanticModel, position, structuralTypeDisplayService,
            isVariadic, documentationFactory, prefixParts, separatorParts, suffixParts, parameters, descriptionParts);
    }
 
    protected static SignatureHelpItem CreateItemImpl(
        ISymbol orderSymbol,
        SemanticModel semanticModel,
        int position,
        IStructuralTypeDisplayService structuralTypeDisplayService,
        bool isVariadic,
        Func<CancellationToken, IEnumerable<TaggedText>>? documentationFactory,
        IList<SymbolDisplayPart> prefixParts,
        IList<SymbolDisplayPart> separatorParts,
        IList<SymbolDisplayPart> suffixParts,
        IList<SignatureHelpSymbolParameter> parameters,
        IList<SymbolDisplayPart>? descriptionParts)
    {
        descriptionParts = descriptionParts == null
            ? SpecializedCollections.EmptyList<SymbolDisplayPart>()
            : descriptionParts;
 
        var allParts = prefixParts.Concat(separatorParts)
                                  .Concat(suffixParts)
                                  .Concat(parameters.SelectMany(p => p.GetAllParts()))
                                  .Concat(descriptionParts);
 
        var structuralTypes =
            from part in allParts
            where part.Symbol.IsAnonymousType() || part.Symbol.IsTupleType()
            select (INamedTypeSymbol)part.Symbol!;
 
        var info = structuralTypeDisplayService.GetTypeDisplayInfo(
            orderSymbol, structuralTypes.ToImmutableArray(), semanticModel, position);
 
        if (info.TypesParts.Count > 0)
        {
            var structuralTypeParts = new List<SymbolDisplayPart>
            {
                new SymbolDisplayPart(SymbolDisplayPartKind.Space, null, "\r\n\r\n")
            };
 
            structuralTypeParts.AddRange(info.TypesParts);
 
            return new SymbolKeySignatureHelpItem(
                orderSymbol,
                isVariadic,
                documentationFactory,
                info.ReplaceStructuralTypes(prefixParts, semanticModel, position).ToTaggedText(),
                info.ReplaceStructuralTypes(separatorParts, semanticModel, position).ToTaggedText(),
                info.ReplaceStructuralTypes(suffixParts, semanticModel, position).ToTaggedText(),
                parameters.Select(p => ReplaceStructuralTypes(p, info, semanticModel, position)).Select(p => (SignatureHelpParameter)p),
                structuralTypeParts.ToTaggedText());
        }
 
        return new SymbolKeySignatureHelpItem(
            orderSymbol,
            isVariadic,
            documentationFactory,
            prefixParts.ToTaggedText(),
            separatorParts.ToTaggedText(),
            suffixParts.ToTaggedText(),
            parameters.Select(p => (SignatureHelpParameter)p),
            descriptionParts.ToTaggedText());
    }
 
    private static SignatureHelpSymbolParameter ReplaceStructuralTypes(
        SignatureHelpSymbolParameter parameter,
        StructuralTypeDisplayInfo info,
        SemanticModel semanticModel,
        int position)
    {
        return new SignatureHelpSymbolParameter(
            parameter.Name,
            parameter.IsOptional,
            parameter.DocumentationFactory,
            info.ReplaceStructuralTypes(parameter.DisplayParts, semanticModel, position),
            info.ReplaceStructuralTypes(parameter.SelectedDisplayParts, semanticModel, position));
    }
 
    public async Task<SignatureHelpItems?> GetItemsAsync(
        Document document, int position, SignatureHelpTriggerInfo triggerInfo, MemberDisplayOptions options, CancellationToken cancellationToken)
    {
        var itemsForCurrentDocument = await GetItemsWorkerAsync(document, position, triggerInfo, options, cancellationToken).ConfigureAwait(false);
        if (itemsForCurrentDocument == null)
        {
            return itemsForCurrentDocument;
        }
 
        var relatedDocuments = await FindActiveRelatedDocumentsAsync(position, document, cancellationToken).ConfigureAwait(false);
        if (relatedDocuments.IsEmpty)
        {
            return itemsForCurrentDocument;
        }
 
        var totalProjects = relatedDocuments.Select(d => d.Project.Id).Concat(document.Project.Id);
 
        var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false);
        var compilation = semanticModel.Compilation;
 
        var finalItems = new List<SignatureHelpItem>();
        foreach (var item in itemsForCurrentDocument.Items)
        {
            if (item is not SymbolKeySignatureHelpItem symbolKeyItem ||
                symbolKeyItem.SymbolKey is not SymbolKey symbolKey ||
                symbolKey.Resolve(compilation, ignoreAssemblyKey: true, cancellationToken).Symbol is not ISymbol symbol)
            {
                finalItems.Add(item);
                continue;
            }
 
            // If the symbol is an instantiated generic method, ensure we use its original
            // definition for symbol key resolution in related compilations.
            if (symbol is IMethodSymbol methodSymbol && methodSymbol.IsGenericMethod && methodSymbol != methodSymbol.OriginalDefinition)
            {
                symbolKey = SymbolKey.Create(methodSymbol.OriginalDefinition, cancellationToken);
            }
 
            var invalidProjectsForCurrentSymbol = new List<ProjectId>();
            foreach (var relatedDocument in relatedDocuments)
            {
                // Try to resolve symbolKey in each related compilation,
                // unresolvable key means the symbol is unavailable in the corresponding project.
                var relatedSemanticModel = await relatedDocument.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false);
                if (symbolKey.Resolve(relatedSemanticModel.Compilation, ignoreAssemblyKey: true, cancellationToken).Symbol == null)
                {
                    invalidProjectsForCurrentSymbol.Add(relatedDocument.Project.Id);
                }
            }
 
            var platformData = new SupportedPlatformData(document.Project.Solution, invalidProjectsForCurrentSymbol, totalProjects);
            finalItems.Add(UpdateItem(item, platformData));
        }
 
        return new SignatureHelpItems(
            finalItems,
            itemsForCurrentDocument.ApplicableSpan,
            itemsForCurrentDocument.SemanticParameterIndex,
            itemsForCurrentDocument.SyntacticArgumentCount,
            itemsForCurrentDocument.ArgumentName,
            itemsForCurrentDocument.SelectedItemIndex);
    }
 
    private static async Task<ImmutableArray<Document>> FindActiveRelatedDocumentsAsync(int position, Document document, CancellationToken cancellationToken)
    {
        using var _ = ArrayBuilder<Document>.GetInstance(out var builder);
        foreach (var relatedDocument in document.GetLinkedDocuments())
        {
            var syntaxTree = await relatedDocument.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
            if (!relatedDocument.GetRequiredLanguageService<ISyntaxFactsService>().IsInInactiveRegion(syntaxTree, position, cancellationToken))
            {
                builder.Add(relatedDocument);
            }
        }
 
        return builder.ToImmutableAndClear();
    }
 
    private static SignatureHelpItem UpdateItem(SignatureHelpItem item, SupportedPlatformData platformData)
    {
        var platformParts = platformData.ToDisplayParts().ToTaggedText();
        if (platformParts.Length == 0)
        {
            return item;
        }
 
        var startingNewLine = new List<TaggedText>();
        startingNewLine.AddLineBreak();
 
        var concatted = startingNewLine.Concat(platformParts);
        var updatedDescription = item.DescriptionParts.IsDefault
            ? concatted
            : item.DescriptionParts.Concat(concatted);
 
        item.DescriptionParts = updatedDescription.ToImmutableArrayOrEmpty();
        return item;
    }
 
    protected static int? TryGetSelectedIndex<TSymbol>(ImmutableArray<TSymbol> candidates, ISymbol? currentSymbol) where TSymbol : class, ISymbol
    {
        if (currentSymbol is TSymbol matched)
        {
            var found = candidates.IndexOf(matched);
            if (found >= 0)
                return found;
        }
 
        return null;
    }
}