|
// 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;
}
}
}
|