|
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
internal sealed partial class UnnamedSymbolCompletionProvider
{
[Flags]
private enum OperatorPosition
{
None = 0,
Prefix = 1,
Infix = 2,
Postfix = 4,
}
// Place operators after conversions.
private readonly int OperatorSortingGroupIndex = 2;
private readonly string OperatorName = nameof(OperatorName);
private readonly ImmutableArray<KeyValuePair<string, string>> OperatorProperties =
[KeyValuePairUtil.Create(KindName, OperatorKindName)];
/// <summary>
/// Ordered in the order we want to display operators in the completion list.
/// </summary>
private static readonly ImmutableArray<(string name, OperatorPosition position)> s_operatorInfo =
[
(WellKnownMemberNames.EqualityOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.InequalityOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.GreaterThanOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.GreaterThanOrEqualOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.LessThanOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.LessThanOrEqualOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.LogicalNotOperatorName, OperatorPosition.Prefix),
(WellKnownMemberNames.AdditionOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.SubtractionOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.MultiplyOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.DivisionOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.ModulusOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.IncrementOperatorName, OperatorPosition.Prefix | OperatorPosition.Postfix),
(WellKnownMemberNames.DecrementOperatorName, OperatorPosition.Prefix | OperatorPosition.Postfix),
(WellKnownMemberNames.UnaryPlusOperatorName, OperatorPosition.Prefix),
(WellKnownMemberNames.UnaryNegationOperatorName, OperatorPosition.Prefix),
(WellKnownMemberNames.BitwiseAndOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.BitwiseOrOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.ExclusiveOrOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.LeftShiftOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.RightShiftOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.UnsignedRightShiftOperatorName, OperatorPosition.Infix),
(WellKnownMemberNames.OnesComplementOperatorName, OperatorPosition.Prefix),
];
/// <summary>
/// Mapping from operator name to info about it.
/// </summary>
private static readonly Dictionary<string, (int sortOrder, OperatorPosition position)> s_operatorNameToInfo = [];
private static readonly CompletionItemRules s_operatorRules;
static UnnamedSymbolCompletionProvider()
{
// Collect all the characters used in C# operators and make them filter characters and not commit
// characters. We want people to be able to write `x.=` and have that filter down to operators like `==` and
// `!=` so they can select and commit them.
using var _ = PooledHashSet<char>.GetInstance(out var filterCharacters);
for (var i = 0; i < s_operatorInfo.Length; i++)
{
var (opName, position) = s_operatorInfo[i];
var opText = GetOperatorText(opName);
s_operatorNameToInfo[opName] = (sortOrder: i, position);
foreach (var ch in opText)
{
if (!char.IsLetterOrDigit(ch))
filterCharacters.Add(ch);
}
}
var opCharacters = ImmutableArray.CreateRange(filterCharacters);
s_operatorRules = CompletionItemRules.Default
.WithFilterCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Add, opCharacters))
.WithCommitCharacterRule(CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, opCharacters));
}
private void AddOperatorGroup(CompletionContext context, string opName, IEnumerable<ISymbol> operators)
{
if (!s_operatorNameToInfo.TryGetValue(opName, out var sortOrderAndPosition))
return;
var displayText = GetOperatorText(opName);
context.AddItem(SymbolCompletionItem.CreateWithSymbolId(
displayText: displayText,
displayTextSuffix: null,
inlineDescription: GetOperatorInlineDescription(opName),
filterText: displayText,
sortText: SortText(OperatorSortingGroupIndex, $"{sortOrderAndPosition.sortOrder:000}"),
symbols: operators.ToImmutableArray(),
rules: s_operatorRules,
contextPosition: context.Position,
properties: [.. OperatorProperties, KeyValuePairUtil.Create(OperatorName, opName)],
isComplexTextEdit: true));
}
private static string GetOperatorText(string opName)
=> SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(opName));
private async Task<CompletionChange> GetOperatorChangeAsync(
Document document, CompletionItem item, CancellationToken cancellationToken)
{
var opName = item.GetProperty(OperatorName);
var opPosition = GetOperatorPosition(opName);
if (opPosition.HasFlag(OperatorPosition.Infix))
return await ReplaceTextAfterOperatorAsync(document, item, text: $" {item.DisplayText} ", cancellationToken).ConfigureAwait(false);
if (opPosition.HasFlag(OperatorPosition.Postfix))
return await ReplaceTextAfterOperatorAsync(document, item, text: $"{item.DisplayText} ", cancellationToken).ConfigureAwait(false);
if (opPosition.HasFlag(OperatorPosition.Prefix))
{
var position = SymbolCompletionItem.GetContextPosition(item);
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var (dotLikeToken, expressionStart) = GetDotAndExpressionStart(root, position, cancellationToken);
// Place the new operator before the expression, and delete the dot.
var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
var replacement = item.DisplayText + text.ToString(TextSpan.FromBounds(expressionStart, dotLikeToken.SpanStart));
var fullTextChange = new TextChange(
TextSpan.FromBounds(
expressionStart,
dotLikeToken.Kind() == SyntaxKind.DotDotToken ? dotLikeToken.Span.Start + 1 : dotLikeToken.Span.End),
replacement);
var newPosition = expressionStart + replacement.Length;
return CompletionChange.Create(fullTextChange, newPosition);
}
throw ExceptionUtilities.UnexpectedValue(opPosition);
}
private static OperatorPosition GetOperatorPosition(string operatorName)
=> s_operatorNameToInfo[operatorName].position;
private static Task<CompletionDescription> GetOperatorDescriptionAsync(Document document, CompletionItem item, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
=> SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken);
private static string GetOperatorInlineDescription(string opName)
{
var opText = GetOperatorText(opName);
var opPosition = GetOperatorPosition(opName);
if (opPosition.HasFlag(OperatorPosition.Postfix))
return $"x{opText}";
if (opPosition.HasFlag(OperatorPosition.Infix))
return $"x {opText} y";
if (opPosition.HasFlag(OperatorPosition.Prefix))
return $"{opText}x";
return opText;
}
}
|