|
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
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.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
[ExportCompletionProvider(nameof(CrefCompletionProvider), LanguageNames.CSharp), Shared]
[ExtensionOrder(After = nameof(EnumAndCompletionListTagCompletionProvider))]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class CrefCompletionProvider() : AbstractCrefCompletionProvider
{
private static readonly SymbolDisplayFormat QualifiedCrefFormat =
new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
propertyStyle: SymbolDisplayPropertyStyle.NameOnly,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
parameterOptions: SymbolDisplayParameterOptions.None,
miscellaneousOptions:
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.ExpandValueTuple);
private static readonly SymbolDisplayFormat CrefFormat =
QualifiedCrefFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
private static readonly SymbolDisplayFormat MinimalParameterTypeFormat =
SymbolDisplayFormat.MinimallyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.ExpandValueTuple);
private Action<SyntaxNode?>? _testSpeculativeNodeCallback;
internal override string Language => LanguageNames.CSharp;
public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options)
=> CompletionUtilities.IsTriggerCharacter(text, characterPosition, options);
public override ImmutableHashSet<char> TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters;
public override async Task ProvideCompletionsAsync(CompletionContext context)
{
try
{
var document = context.Document;
var position = context.Position;
var options = context.CompletionOptions;
var cancellationToken = context.CancellationToken;
var (token, semanticModel, symbols) = await GetSymbolsAsync(document, position, options, cancellationToken).ConfigureAwait(false);
if (symbols.IsDefaultOrEmpty)
return;
Contract.ThrowIfNull(semanticModel);
context.IsExclusive = true;
var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
var span = GetCompletionItemSpan(text, position);
var serializedOptions = ImmutableArray.Create(KeyValuePairUtil.Create(HideAdvancedMembers, options.MemberDisplayOptions.HideAdvancedMembers.ToString()));
var items = CreateCompletionItems(semanticModel, symbols, token, position, serializedOptions);
context.AddItems(items);
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General))
{
}
}
protected override async Task<(SyntaxToken, SemanticModel?, ImmutableArray<ISymbol>)> GetSymbolsAsync(
Document document, int position, CompletionOptions options, CancellationToken cancellationToken)
{
var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
if (!tree.IsEntirelyWithinCrefSyntax(position, cancellationToken))
return default;
var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDocumentationComments: true)
.GetPreviousTokenIfTouchingWord(position);
// To get a Speculative SemanticModel (which is much faster), we need to
// walk up to the node the DocumentationTrivia is attached to.
var parentNode = token.Parent?.FirstAncestorOrSelf<DocumentationCommentTriviaSyntax>()?.ParentTrivia.Token.Parent;
_testSpeculativeNodeCallback?.Invoke(parentNode);
if (parentNode == null)
return default;
var semanticModel = await document.ReuseExistingSpeculativeModelAsync(
parentNode, cancellationToken).ConfigureAwait(false);
var symbols = GetSymbols(token, semanticModel, cancellationToken)
.FilterToVisibleAndBrowsableSymbols(options.MemberDisplayOptions.HideAdvancedMembers, semanticModel.Compilation);
return (token, semanticModel, symbols);
}
private static bool IsCrefStartContext(SyntaxToken token)
{
// cases:
// <see cref="|
// <see cref='|
return token.Kind() is SyntaxKind.DoubleQuoteToken or SyntaxKind.SingleQuoteToken &&
token.Parent.IsKind(SyntaxKind.XmlCrefAttribute);
}
private static bool IsCrefParameterListContext(SyntaxToken token)
{
// cases:
// <see cref="M(|
// <see cref="M(x, |
// <see cref="M(x, ref |
// <see cref="M(x, out |
// <see cref="M[|
// <see cref="M[x, |
// <see cref="M[x, ref |
// <see cref="M[x, out |
if (token.Parent?.Kind() is not (SyntaxKind.CrefParameterList or SyntaxKind.CrefBracketedParameterList))
return false;
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.Parent.IsKind(SyntaxKind.CrefParameterList))
{
return true;
}
if (token.IsKind(SyntaxKind.OpenBracketToken) &&
token.Parent.IsKind(SyntaxKind.CrefBracketedParameterList))
{
return true;
}
return token is (kind: SyntaxKind.CommaToken or SyntaxKind.RefKeyword or SyntaxKind.OutKeyword);
}
private static bool IsCrefQualifiedNameContext(SyntaxToken token)
{
// cases:
// <see cref="x.|
return token.IsKind(SyntaxKind.DotToken)
&& token.Parent.IsKind(SyntaxKind.QualifiedCref);
}
private static ImmutableArray<ISymbol> GetSymbols(
SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken)
{
if (IsCrefStartContext(token))
return GetUnqualifiedSymbols(token, semanticModel, cancellationToken);
if (IsCrefParameterListContext(token))
return semanticModel.LookupNamespacesAndTypes(token.SpanStart);
if (IsCrefQualifiedNameContext(token))
return GetQualifiedSymbols((QualifiedCrefSyntax)token.Parent!, token, semanticModel, cancellationToken);
return [];
}
private static ImmutableArray<ISymbol> GetUnqualifiedSymbols(
SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken)
{
using var _ = ArrayBuilder<ISymbol>.GetInstance(out var result);
result.AddRange(semanticModel.LookupSymbols(token.SpanStart));
// LookupSymbols doesn't return indexers or operators because they can't be referred to by name.
// So, try to find the innermost type declaration and return its operators and indexers
var typeDeclaration = token.Parent?.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration != null)
{
var type = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken);
if (type != null)
{
foreach (var baseType in type.GetBaseTypesAndThis())
{
foreach (var member in baseType.GetMembers())
{
if ((member.IsIndexer() || member.IsUserDefinedOperator()) &&
member.IsAccessibleWithin(type))
{
result.Add(member);
}
}
}
}
}
return result.ToImmutableAndClear();
}
private static ImmutableArray<ISymbol> GetQualifiedSymbols(
QualifiedCrefSyntax parent, SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken)
{
var leftType = semanticModel.GetTypeInfo(parent.Container, cancellationToken).Type;
var leftSymbol = semanticModel.GetSymbolInfo(parent.Container, cancellationToken).Symbol;
var container = (leftSymbol ?? leftType) as INamespaceOrTypeSymbol;
using var _ = ArrayBuilder<ISymbol>.GetInstance(out var result);
result.AddRange(semanticModel.LookupSymbols(token.SpanStart, container));
if (container is INamedTypeSymbol namedTypeContainer)
result.AddRange(namedTypeContainer.InstanceConstructors);
return result.ToImmutableAndClear();
}
private static TextSpan GetCompletionItemSpan(SourceText text, int position)
{
return CommonCompletionUtilities.GetWordSpan(
text,
position,
ch => CompletionUtilities.IsCompletionItemStartCharacter(ch) || ch == '{',
ch => CompletionUtilities.IsWordCharacter(ch) || ch is '{' or '}');
}
private static IEnumerable<CompletionItem> CreateCompletionItems(
SemanticModel semanticModel, ImmutableArray<ISymbol> symbols, SyntaxToken token, int position, ImmutableArray<KeyValuePair<string, string>> options)
{
using var _ = SharedPools.Default<StringBuilder>().GetPooledObject(out var builder);
foreach (var group in symbols.GroupBy(s => s.Name))
{
var groupCount = group.Count();
foreach (var symbol in group.OrderBy(s => s.GetArity()))
{
// Include the arity in the sort text so that we show types/methods from least arity to most arity.
var sortText = $"{symbol.Name}`{symbol.GetArity():000}";
// For every symbol, we create an item that uses the regular CrefFormat,
// which uses intrinsic type keywords
yield return CreateItem(semanticModel, symbol, groupCount, token, position, builder, sortText, options, CrefFormat);
if (TryCreateSpecialTypeItem(semanticModel, symbol, token, position, builder, options, out var item))
yield return item;
}
}
}
private static bool TryCreateSpecialTypeItem(
SemanticModel semanticModel, ISymbol symbol, SyntaxToken token, int position, StringBuilder builder,
ImmutableArray<KeyValuePair<string, string>> options, [NotNullWhen(true)] out CompletionItem? item)
{
// If the type is a SpecialType, create an additional item using
// its actual name (as opposed to intrinsic type keyword)
var typeSymbol = symbol as ITypeSymbol;
if (typeSymbol.IsSpecialType())
{
item = CreateItem(semanticModel, symbol, groupCount: 1, token, position, builder, builder.ToString(), options, QualifiedCrefFormat);
return true;
}
item = null;
return false;
}
private static CompletionItem CreateItem(
SemanticModel semanticModel,
ISymbol symbol,
int groupCount,
SyntaxToken token,
int position,
StringBuilder builder,
string sortText,
ImmutableArray<KeyValuePair<string, string>> options,
SymbolDisplayFormat unqualifiedCrefFormat)
{
builder.Clear();
if (symbol is INamespaceOrTypeSymbol && token.IsKind(SyntaxKind.DotToken))
{
// Handle qualified namespace and type names.
builder.Append(symbol.ToDisplayString(QualifiedCrefFormat));
}
else
{
// Handle unqualified namespace and type names, or member names.
builder.Append(symbol.ToMinimalDisplayString(semanticModel, token.SpanStart, unqualifiedCrefFormat));
var parameters = symbol.GetParameters();
// if this has parameters, then add them here. Otherwise, if this is a method without parameters, but
// there are overloads of it, then also add the parameters to disambiguate.
if (parameters.Length > 0 ||
(symbol is IMethodSymbol && groupCount >= 2))
{
// Note: we intentionally don't add the "params" modifier for any parameters.
builder.Append(symbol.IsIndexer() ? '[' : '(');
builder.AppendJoinedValues(", ", parameters,
(p, builder) =>
{
builder.Append(p.RefKind switch
{
RefKind.Ref => "ref ",
RefKind.Out => "out ",
RefKind.In => "in ",
RefKind.RefReadOnlyParameter => "ref readonly ",
_ => "",
});
builder.Append(p.Type.ToMinimalDisplayString(semanticModel, position, MinimalParameterTypeFormat));
});
builder.Append(symbol.IsIndexer() ? ']' : ')');
}
}
return CreateItemFromBuilder(symbol, position, builder, sortText, options);
}
private static CompletionItem CreateItemFromBuilder(
ISymbol symbol, int position, StringBuilder builder, string sortText, ImmutableArray<KeyValuePair<string, string>> options)
{
var insertionText = builder
.Replace('<', '{')
.Replace('>', '}')
.ToString();
return SymbolCompletionItem.CreateWithNameAndKind(
displayText: insertionText,
displayTextSuffix: "",
insertionText: insertionText,
symbols: ImmutableArray.Create(symbol),
contextPosition: position,
sortText: sortText,
filterText: insertionText,
properties: options,
rules: GetRules(insertionText));
}
private static readonly CharacterSetModificationRule s_WithoutOpenBrace = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '{');
private static readonly CharacterSetModificationRule s_WithoutOpenParen = CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '(');
private static CompletionItemRules GetRules(string displayText)
{
var commitRules = ImmutableArray<CharacterSetModificationRule>.Empty;
if (displayText.Contains("{"))
{
commitRules = commitRules.Add(s_WithoutOpenBrace);
}
if (displayText.Contains("("))
{
commitRules = commitRules.Add(s_WithoutOpenParen);
}
if (commitRules.IsEmpty)
{
return CompletionItemRules.Default;
}
else
{
return CompletionItemRules.Default.WithCommitCharacterRules(commitRules);
}
}
protected override Task<TextChange?> GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken)
{
if (!SymbolCompletionItem.TryGetInsertionText(selectedItem, out var insertionText))
{
insertionText = selectedItem.DisplayText;
}
return Task.FromResult<TextChange?>(new TextChange(selectedItem.Span, insertionText));
}
internal TestAccessor GetTestAccessor()
=> new(this);
internal readonly struct TestAccessor(CrefCompletionProvider crefCompletionProvider)
{
private readonly CrefCompletionProvider _crefCompletionProvider = crefCompletionProvider;
public void SetSpeculativeNodeCallback(Action<SyntaxNode?> value)
=> _crefCompletionProvider._testSpeculativeNodeCallback = value;
}
}
|