File: Completion\CompletionProviders\OperatorsAndIndexer\UnnamedSymbolCompletionProvider.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.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Recommendations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
 
/// <summary>
/// Provides completion for uncommon unnamed symbols, like conversions, indexer and operators.  These completion 
/// items will be brought up with <c>dot</c> like normal, but will end up inserting more than just a name into
/// the editor.  For example, committing a conversion will insert the conversion prior to the expression being
/// dotted off of.
/// </summary>
[ExportCompletionProvider(nameof(UnnamedSymbolCompletionProvider), LanguageNames.CSharp), Shared]
[ExtensionOrder(After = nameof(SymbolCompletionProvider))]
internal sealed partial class UnnamedSymbolCompletionProvider : LSPCompletionProvider
{
    /// <summary>
    /// CompletionItems for indexers/operators should be sorted below other suggestions like methods or properties
    /// of the type.  We accomplish this by placing a character known to be greater than all other normal identifier
    /// characters as the start of our item's name. This doesn't affect what we insert though as all derived
    /// providers have specialized logic for what they need to do.
    /// </summary> 
    private const string SortingPrefix = "\uFFFD";
 
    /// <summary>
    /// Used to store what sort of unnamed symbol a completion item represents.
    /// </summary>
    internal const string KindName = "Kind";
    internal const string IndexerKindName = "Indexer";
    internal const string OperatorKindName = "Operator";
    internal const string ConversionKindName = "Conversion";
 
    /// <summary>
    /// Used to store the doc comment for some operators/conversions.  This is because some of them will be
    /// synthesized, so there will be no symbol we can recover after the fact in <see cref="GetDescriptionAsync"/>.
    /// </summary>
    private const string DocumentationCommentXmlName = "DocumentationCommentXml";
 
    [ImportingConstructor]
    [System.Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public UnnamedSymbolCompletionProvider()
    {
    }
 
    internal override string Language => LanguageNames.CSharp;
 
    public override ImmutableHashSet<char> TriggerCharacters => ['.'];
 
    public override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options)
        => text[insertedCharacterPosition] == '.';
 
    /// <summary>
    /// We keep operators sorted in a specific order.  We don't want to sort them alphabetically, but instead want
    /// to keep things like <c>==</c> and <c>!=</c> together.
    /// </summary>
    private static string SortText(int sortingGroupIndex, string sortTextSymbolPart)
        => $"{SortingPrefix}{sortingGroupIndex:000}_{sortTextSymbolPart}";
 
    /// <summary>
    /// Gets the dot-like token we're after, and also the start of the expression we'd want to place any text before.
    /// </summary>
    private static (SyntaxToken dotLikeToken, int expressionStart) GetDotAndExpressionStart(SyntaxNode root, int position, CancellationToken cancellationToken)
    {
        if (CompletionUtilities.GetDotTokenLeftOfPosition(root.SyntaxTree, position, cancellationToken) is not SyntaxToken dotToken)
            return default;
 
        // if we have `.Name`, we want to get the parent member-access of that to find the starting position.
        // Otherwise, if we have .. then we want the left side of that to find the starting position.
        var expression = dotToken.Kind() == SyntaxKind.DotToken
            ? dotToken.Parent as ExpressionSyntax
            : (dotToken.Parent as RangeExpressionSyntax)?.LeftOperand;
 
        if (expression == null)
            return default;
 
        // If we're after a ?. find the root of that conditional to find the start position of the expression.
        expression = expression.GetRootConditionalAccessExpression() ?? expression;
        return (dotToken, expression.SpanStart);
    }
 
    public override async Task ProvideCompletionsAsync(CompletionContext context)
    {
        var cancellationToken = context.CancellationToken;
        var document = context.Document;
        var position = context.Position;
 
        // Escape hatch feature flag to let us disable this feature remotely if we run into any issues with it, 
        if (context.CompletionOptions.UnnamedSymbolCompletionDisabled)
            return;
 
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var dotAndExprStart = GetDotAndExpressionStart(root, position, cancellationToken);
        if (dotAndExprStart == default)
            return;
 
        var recommender = document.GetRequiredLanguageService<IRecommendationService>();
        var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false);
        var semanticModel = syntaxContext.SemanticModel;
 
        var options = context.CompletionOptions.ToRecommendationServiceOptions();
        var recommendedSymbols = recommender.GetRecommendedSymbolsInContext(syntaxContext, options, cancellationToken);
 
        AddUnnamedSymbols(context, position, semanticModel, recommendedSymbols.UnnamedSymbols, cancellationToken);
    }
 
    private void AddUnnamedSymbols(
        CompletionContext context, int position, SemanticModel semanticModel, ImmutableArray<ISymbol> unnamedSymbols, CancellationToken cancellationToken)
    {
        // Add one 'this[]' entry for all the indexers this type may have.
        AddIndexers(context, unnamedSymbols.WhereAsArray(s => s.IsIndexer()));
 
        // Group all the related operators and add a single completion entry per group.
        var operatorGroups = unnamedSymbols.WhereAsArray(s => s.IsUserDefinedOperator()).GroupBy(op => op.Name);
        foreach (var opGroup in operatorGroups)
            AddOperatorGroup(context, opGroup.Key, opGroup);
 
        foreach (var symbol in unnamedSymbols)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            if (symbol.IsConversion())
                AddConversion(context, semanticModel, position, (IMethodSymbol)symbol);
        }
    }
 
    public override Task<CompletionChange> GetChangeAsync(
        Document document,
        CompletionItem item,
        char? commitKey,
        CancellationToken cancellationToken)
    {
        var kind = item.GetProperty(KindName);
        return kind switch
        {
            IndexerKindName => GetIndexerChangeAsync(document, item, cancellationToken),
            OperatorKindName => GetOperatorChangeAsync(document, item, cancellationToken),
            ConversionKindName => GetConversionChangeAsync(document, item, cancellationToken),
            _ => throw ExceptionUtilities.UnexpectedValue(kind),
        };
    }
 
    internal override async Task<CompletionDescription?> GetDescriptionAsync(
        Document document,
        CompletionItem item,
        CompletionOptions options,
        SymbolDescriptionOptions displayOptions,
        CancellationToken cancellationToken)
    {
        var kind = item.GetProperty(KindName);
        return kind switch
        {
            IndexerKindName => await GetIndexerDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false),
            OperatorKindName => await GetOperatorDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false),
            ConversionKindName => await GetConversionDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false),
            _ => throw ExceptionUtilities.UnexpectedValue(kind),
        };
    }
 
    private static Task<CompletionChange> ReplaceTextAfterOperatorAsync(Document document, CompletionItem item, string text, CancellationToken cancellationToken)
        => ReplaceTextAfterOperatorAsync(document, item, text, keepQuestion: false, positionOffset: 0, cancellationToken);
 
    private static async Task<CompletionChange> ReplaceTextAfterOperatorAsync(
        Document document,
        CompletionItem item,
        string text,
        bool keepQuestion,
        int positionOffset,
        CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var position = SymbolCompletionItem.GetContextPosition(item);
 
        var (dotToken, _) = GetDotAndExpressionStart(root, position, cancellationToken);
        var questionToken = dotToken.GetPreviousToken().Kind() == SyntaxKind.QuestionToken
            ? dotToken.GetPreviousToken()
            : (SyntaxToken?)null;
 
        var replacementStart = !keepQuestion && questionToken != null
            ? questionToken.Value.SpanStart
            : dotToken.SpanStart;
        var newPosition = replacementStart + text.Length + positionOffset;
 
        var tokenOnLeft = root.FindTokenOnLeftOfPosition(position, includeSkipped: true);
        return CompletionChange.Create(
            new TextChange(TextSpan.FromBounds(replacementStart, tokenOnLeft.Span.End), text),
            newPosition);
    }
}