File: Completion\CompletionProviders\SymbolCompletionProvider.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.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Log;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
 
[ExportCompletionProvider(nameof(SymbolCompletionProvider), LanguageNames.CSharp)]
[ExtensionOrder(After = nameof(SpeculativeTCompletionProvider))]
[Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class SymbolCompletionProvider() : AbstractRecommendationServiceBasedCompletionProvider<CSharpSyntaxContext>
{
    private static readonly Dictionary<(bool importDirective, bool preselect, bool tupleLiteral), CompletionItemRules> s_cachedRules = [];
 
    static SymbolCompletionProvider()
    {
        for (var importDirective = 0; importDirective < 2; importDirective++)
        {
            for (var preselect = 0; preselect < 2; preselect++)
            {
                for (var tupleLiteral = 0; tupleLiteral < 2; tupleLiteral++)
                {
                    var context = (importDirective: importDirective == 1, preselect: preselect == 1, tupleLiteral: tupleLiteral == 1);
                    s_cachedRules[context] = MakeRule(context);
                }
            }
        }
 
        return;
 
        static CompletionItemRules MakeRule((bool importDirective, bool preselect, bool tupleLiteral) context)
        {
            // '<' should not filter the completion list, even though it's in generic items like IList<>
            var generalBaseline = CompletionItemRules.Default.
                WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '<'));
 
            var importDirectiveBaseline = CompletionItemRules.Create(commitCharacterRules:
                [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '.', ';')]);
 
            var rule = context.importDirective ? importDirectiveBaseline : generalBaseline;
 
            if (context.preselect)
                rule = rule.WithSelectionBehavior(CompletionItemSelectionBehavior.HardSelection);
 
            if (context.tupleLiteral)
                rule = rule.WithCommitCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, ':'));
 
            return rule;
        }
    }
 
    public override ImmutableHashSet<char> TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharactersWithArgumentList;
 
    internal override string Language => LanguageNames.CSharp;
 
    protected override CompletionItemSelectionBehavior PreselectedItemSelectionBehavior => CompletionItemSelectionBehavior.HardSelection;
 
    protected override string GetFilterText(ISymbol symbol, string displayText, CSharpSyntaxContext context)
        => GetFilterTextDefault(symbol, displayText, context);
 
    protected override async Task<bool> ShouldPreselectInferredTypesAsync(
        CompletionContext? context,
        int position,
        CompletionOptions options,
        CancellationToken cancellationToken)
    {
        if (context is null)
            return true;
 
        // Avoid preselection & hard selection when triggered via insertion in an argument list.
        // If an item is hard selected, then a user trying to type MethodCall() will get
        // MethodCall(someVariable) instead. We need only soft selected items to prevent this.
        return !await IsTriggeredInArgumentListAsync(context, position, options, cancellationToken).ConfigureAwait(false);
    }
 
    protected override async Task<bool> ShouldProvideAvailableSymbolsInCurrentContextAsync(
        CompletionContext? completionContext,
        CSharpSyntaxContext context,
        int position,
        CompletionOptions options,
        CancellationToken cancellationToken)
    {
        if (completionContext is null)
            return true;
 
        // If we are triggered in argument list, provide symbols only when the invoked method accept any arguments.
        if (await IsTriggeredInArgumentListAsync(completionContext, position, options, cancellationToken).ConfigureAwait(false) is false)
            return true;
 
        return !context.InferredTypes.IsEmpty || IsTopNodeInPrimaryConstructorArgumentList();
 
        // Special case for argument of base record primary constructor as a workaround
        // for bug https://github.com/dotnet/roslyn/issues/70803
        bool IsTopNodeInPrimaryConstructorArgumentList()
            => context.TargetToken.Parent?.Parent?.IsKind(SyntaxKind.PrimaryConstructorBaseType) is true;
    }
 
    private static async Task<bool> IsTriggeredInArgumentListAsync(
        CompletionContext completionContext,
        int position,
        CompletionOptions options,
        CancellationToken cancellationToken)
    {
        if (options.TriggerInArgumentLists)
        {
            if (completionContext.Trigger.Kind == CompletionTriggerKind.Insertion &&
                position > 0 &&
                await IsTriggerInArgumentListAsync(completionContext.Document, position - 1, cancellationToken).ConfigureAwait(false) == true)
            {
                return true;
            }
        }
 
        return false;
    }
 
    protected override bool IsInstrinsic(ISymbol s)
        => s is ITypeSymbol ts && ts.IsIntrinsicType();
 
    public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options)
    {
        return options.TriggerInArgumentLists
            ? CompletionUtilities.IsTriggerCharacterOrArgumentListCharacter(text, characterPosition, options)
            : CompletionUtilities.IsTriggerCharacter(text, characterPosition, options);
    }
 
    internal override async Task<bool> IsSyntacticTriggerCharacterAsync(Document document, int caretPosition, CompletionTrigger trigger, CompletionOptions options, CancellationToken cancellationToken)
    {
        if (trigger.Kind == CompletionTriggerKind.Insertion && caretPosition > 0)
        {
            var result = await IsTriggerOnDotAsync(document, caretPosition - 1, cancellationToken).ConfigureAwait(false);
            if (result.HasValue)
                return result.Value;
 
            if (options.TriggerInArgumentLists)
            {
                result = await IsTriggerInArgumentListAsync(document, caretPosition - 1, cancellationToken).ConfigureAwait(false);
                if (result.HasValue)
                    return result.Value;
            }
        }
 
        // By default we want to proceed with triggering completion if we have items.
        return true;
    }
 
    protected override bool IsTriggerOnDot(SyntaxToken token, int characterPosition)
    {
        if (!CompletionUtilities.TreatAsDot(token, characterPosition))
            return false;
 
        // don't want to trigger after a number.  All other cases after dot are ok.
        return token.GetPreviousToken().Kind() != SyntaxKind.NumericLiteralToken;
    }
 
    /// <returns><see langword="null"/> if not an argument list character, otherwise whether the trigger is in an argument list.</returns>
    private static async Task<bool?> IsTriggerInArgumentListAsync(Document document, int characterPosition, CancellationToken cancellationToken)
    {
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        if (!CompletionUtilities.IsArgumentListCharacter(text[characterPosition]))
        {
            return null;
        }
 
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var token = root.FindToken(characterPosition);
 
        if (token.Parent?.Kind() is not (SyntaxKind.ArgumentList or SyntaxKind.BracketedArgumentList or SyntaxKind.AttributeArgumentList or SyntaxKind.ArrayRankSpecifier))
        {
            return false;
        }
 
        // Be careful, e.g. if we're in a comment before the token
        if (token.Span.End > characterPosition + 1)
        {
            return false;
        }
 
        // Only allow spaces between the end of the token and the trigger character
        for (var i = token.Span.End; i < characterPosition; i++)
        {
            if (text[i] != ' ')
            {
                return false;
            }
        }
 
        return true;
    }
 
    protected override (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(ISymbol symbol, CSharpSyntaxContext context)
        => CompletionUtilities.GetDisplayAndSuffixAndInsertionText(symbol, context);
 
    protected override CompletionItemRules GetCompletionItemRules(ImmutableArray<SymbolAndSelectionInfo> symbols, CSharpSyntaxContext context)
    {
        var preselect = symbols.Any(static t => t.Preselect);
        s_cachedRules.TryGetValue(ValueTuple.Create(context.IsLeftSideOfImportAliasDirective, preselect, context.IsPossibleTupleContext), out var rule);
 
        return rule ?? CompletionItemRules.Default;
    }
 
    protected override CompletionItem CreateItem(
        CompletionContext completionContext,
        string displayText,
        string displayTextSuffix,
        string insertionText,
        ImmutableArray<SymbolAndSelectionInfo> symbols,
        CSharpSyntaxContext context,
        SupportedPlatformData? supportedPlatformData)
    {
        var item = base.CreateItem(
            completionContext,
            displayText,
            displayTextSuffix,
            insertionText,
            symbols,
            context,
            supportedPlatformData);
 
        var symbol = symbols[0].Symbol;
        // If it is a method symbol, also consider appending parenthesis when later, it is committed by using special characters.
        // 2 cases are excluded.
        // 1. If it is invoked under Nameof Context.
        // For example: var a = nameof(Bar$$)
        // In this case, if later committed by semicolon, we should have
        // var a = nameof(Bar);
        // 2. If the inferred type is delegate or function pointer.
        // e.g. Action c = Bar$$
        // In this case, if later committed by semicolon, we should have
        // e.g. Action c = = Bar;
        if (symbol.IsKind(SymbolKind.Method) && !context.IsNameOfContext)
        {
            var isInferredTypeDelegateOrFunctionPointer = context.InferredTypes.Any(static type => type.IsDelegateType() || type.IsFunctionPointerType());
            if (!isInferredTypeDelegateOrFunctionPointer)
            {
                item = SymbolCompletionItem.AddShouldProvideParenthesisCompletion(item);
            }
        }
        else if (symbol.IsKind(SymbolKind.NamedType) || symbol is IAliasSymbol aliasSymbol && aliasSymbol.Target.IsType)
        {
            // If this is a type symbol/alias symbol, also consider appending parenthesis when later, it is committed by using special characters,
            // and the type is used as constructor
            if (context.IsObjectCreationTypeContext)
                item = SymbolCompletionItem.AddShouldProvideParenthesisCompletion(item);
        }
 
        return item;
    }
 
    protected override string GetInsertionText(CompletionItem item, char ch)
    {
        if (ch is ';' or '.' && SymbolCompletionItem.GetShouldProvideParenthesisCompletion(item))
        {
            CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(ch);
            return SymbolCompletionItem.GetInsertionText(item) + "()";
        }
 
        return base.GetInsertionText(item, ch);
    }
}