File: Completion\CompletionProviders\CompletionUtilities.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.
 
#nullable disable
 
using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
 
internal static class CompletionUtilities
{
    internal static TextSpan GetCompletionItemSpan(SourceText text, int position)
        => CommonCompletionUtilities.GetWordSpan(text, position, IsCompletionItemStartCharacter, IsWordCharacter);
 
    public static bool IsWordStartCharacter(char ch)
        => SyntaxFacts.IsIdentifierStartCharacter(ch);
 
    public static bool IsWordCharacter(char ch)
        => SyntaxFacts.IsIdentifierStartCharacter(ch) || SyntaxFacts.IsIdentifierPartCharacter(ch);
 
    public static bool IsCompletionItemStartCharacter(char ch)
        => ch == '@' || IsWordCharacter(ch);
 
    public static bool TreatAsDot(SyntaxToken token, int characterPosition)
    {
        if (token.Kind() == SyntaxKind.DotToken)
            return true;
 
        // if we're right after the first dot in .. then that's considered completion on dot.
        if (token.Kind() == SyntaxKind.DotDotToken && token.SpanStart == characterPosition)
            return true;
 
        return false;
    }
 
    public static SyntaxToken? GetDotTokenLeftOfPosition(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
    {
        var tokenOnLeft = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeSkipped: true);
        var dotToken = tokenOnLeft.GetPreviousTokenIfTouchingWord(position);
 
        // Has to be a . or a .. token
        if (!TreatAsDot(dotToken, position - 1))
            return null;
 
        // don't want to trigger after a number. All other cases after dot are ok.
        if (dotToken.GetPreviousToken().Kind() == SyntaxKind.NumericLiteralToken)
            return null;
 
        return dotToken;
    }
 
    internal static bool IsTriggerCharacter(SourceText text, int characterPosition, in CompletionOptions options)
    {
        var ch = text[characterPosition];
 
        // Trigger off of a normal `.`, but not off of `..`
        if (ch == '.' && !(characterPosition >= 1 && text[characterPosition - 1] == '.'))
        {
            return true;
        }
 
        // Trigger for directive
        if (ch == '#')
        {
            return true;
        }
 
        // Trigger on pointer member access
        if (ch == '>' && characterPosition >= 1 && text[characterPosition - 1] == '-')
        {
            return true;
        }
 
        // Trigger on alias name
        if (ch == ':' && characterPosition >= 1 && text[characterPosition - 1] == ':')
        {
            return true;
        }
 
        if (options.TriggerOnTypingLetters && IsStartingNewWord(text, characterPosition))
        {
            return true;
        }
 
        return false;
    }
 
    /// <summary>
    /// Tells if we are in positions like this: <c>#nullable $$</c> or <c>#pragma warning $$</c>
    /// </summary>
    internal static bool IsCompilerDirectiveTriggerCharacter(SourceText text, int characterPosition)
    {
        while (text[characterPosition] == ' ' ||
               char.IsLetter(text[characterPosition]))
        {
            characterPosition--;
 
            if (characterPosition < 0)
                return false;
        }
 
        return text[characterPosition] == '#';
    }
 
    internal static ImmutableHashSet<char> CommonTriggerCharacters { get; } = ['.', '#', '>', ':'];
 
    internal static ImmutableHashSet<char> CommonTriggerCharactersWithArgumentList { get; } = ['.', '#', '>', ':', '(', '[', ' '];
 
    internal static bool IsTriggerCharacterOrArgumentListCharacter(SourceText text, int characterPosition, in CompletionOptions options)
        => IsTriggerCharacter(text, characterPosition, options) || IsArgumentListCharacter(text, characterPosition);
 
    private static bool IsArgumentListCharacter(SourceText text, int characterPosition)
        => IsArgumentListCharacter(text[characterPosition]);
 
    internal static bool IsArgumentListCharacter(char ch)
        => ch is '(' or '[' or ' ';
 
    internal static bool IsTriggerAfterSpaceOrStartOfWordCharacter(SourceText text, int characterPosition, in CompletionOptions options)
    {
        // Bring up on space or at the start of a word.
        var ch = text[characterPosition];
        return SpaceTypedNotBeforeWord(ch, text, characterPosition) ||
            (IsStartingNewWord(text, characterPosition) && options.TriggerOnTypingLetters);
    }
 
    internal static ImmutableHashSet<char> SpaceTriggerCharacter => [' '];
 
    private static bool SpaceTypedNotBeforeWord(char ch, SourceText text, int characterPosition)
        => ch == ' ' && (characterPosition == text.Length - 1 || !IsWordStartCharacter(text[characterPosition + 1]));
 
    public static bool IsStartingNewWord(SourceText text, int characterPosition)
    {
        return CommonCompletionUtilities.IsStartingNewWord(
            text, characterPosition, IsWordStartCharacter, IsWordCharacter);
    }
 
    public static (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(
        ISymbol symbol, SyntaxContext context)
    {
        var insertionText = GetInsertionText(symbol, context);
        var suffix = symbol.GetArity() == 0 ? "" : "<>";
 
        return (insertionText, suffix, insertionText);
    }
 
    public static string GetInsertionText(ISymbol symbol, SyntaxContext context)
    {
        if (CommonCompletionUtilities.TryRemoveAttributeSuffix(symbol, context, out var name))
        {
            // Cannot escape Attribute name with the suffix removed. Only use the name with
            // the suffix removed if it does not need to be escaped.
            if (name.Equals(name.EscapeIdentifier()))
            {
                return name;
            }
        }
 
        if (symbol.Kind == SymbolKind.Label &&
            symbol.DeclaringSyntaxReferences[0].GetSyntax().Kind() == SyntaxKind.DefaultSwitchLabel)
        {
            return symbol.Name;
        }
 
        return symbol.Name.EscapeIdentifier(isQueryContext: context.IsInQuery);
    }
 
    public static int GetTargetCaretPositionForMethod(MethodDeclarationSyntax methodDeclaration)
    {
        if (methodDeclaration.Body == null)
        {
            return methodDeclaration.GetLocation().SourceSpan.End;
        }
        else
        {
            // move to the end of the last statement in the method
            var lastStatement = methodDeclaration.Body.Statements.Last();
            return lastStatement.GetLocation().SourceSpan.End;
        }
    }
}