|
// 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.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
[ExportCompletionProvider(nameof(OverrideCompletionProvider), LanguageNames.CSharp), Shared]
[ExtensionOrder(After = nameof(PreprocessorCompletionProvider))]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed partial class OverrideCompletionProvider() : AbstractOverrideCompletionProvider
{
internal override string Language => LanguageNames.CSharp;
protected override SyntaxNode GetSyntax(SyntaxToken token)
{
return token.GetAncestor<EventFieldDeclarationSyntax>()
?? token.GetAncestor<EventDeclarationSyntax>()
?? token.GetAncestor<PropertyDeclarationSyntax>()
?? token.GetAncestor<IndexerDeclarationSyntax>()
?? (SyntaxNode?)token.GetAncestor<MethodDeclarationSyntax>()
?? throw ExceptionUtilities.UnexpectedValue(token);
}
public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options)
=> CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options);
public override ImmutableHashSet<char> TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter;
protected override SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken)
{
var tokenSpanEnd = MemberInsertionCompletionItem.GetTokenSpanEnd(completionItem);
return tree.FindTokenOnLeftOfPosition(tokenSpanEnd, cancellationToken);
}
public override bool TryDetermineReturnType(SyntaxToken startToken, SemanticModel semanticModel, CancellationToken cancellationToken, out ITypeSymbol? returnType, out SyntaxToken nextToken)
{
nextToken = startToken;
returnType = null;
if (startToken.Parent is TypeSyntax typeSyntax)
{
// 'partial' is actually an identifier. If we see it just bail. This does mean
// we won't handle overrides that actually return a type called 'partial'. And
// not a single tear was shed.
if (typeSyntax is IdentifierNameSyntax identifierName &&
identifierName.Identifier.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword))
{
return false;
}
returnType = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type;
nextToken = typeSyntax.GetFirstToken().GetPreviousToken();
}
return true;
}
public override bool TryDetermineModifiers(
SyntaxToken startToken,
SourceText text,
int startLine,
out Accessibility seenAccessibility,
out DeclarationModifiers modifiers)
{
var token = startToken;
var parentMember = token.Parent;
modifiers = default;
seenAccessibility = Accessibility.NotApplicable;
if (parentMember is null)
return false;
// Keep walking backwards as long as we're still within our parent member.
while (token != default)
{
if (token.SpanStart < parentMember.SpanStart)
{
// moved before the start of the member we're in. If previous member's token is on the same line,
// we bail out as our replacement will delete the entire line we're on.
if (IsOnStartLine(token.SpanStart, text, startLine))
return false;
break;
}
// Ok to hit a `]` if it's the end of attributes on this member.
if (token.Kind() == SyntaxKind.CloseBracketToken)
{
if (token.Parent is not AttributeListSyntax)
return false;
break;
}
// We only accept tokens that precede us on the same line. Splitting across multiple lines is too niche
// to want to support, and more likely indicates a case of broken code that the user is in the middle of
// fixing up.
if (!IsOnStartLine(token.SpanStart, text, startLine))
return false;
switch (token.Kind())
{
// Standard modifier cases we accept.
case SyntaxKind.AbstractKeyword:
modifiers = modifiers.WithIsAbstract(true);
break;
case SyntaxKind.ExternKeyword:
modifiers = modifiers.WithIsExtern(true);
break;
case SyntaxKind.OverrideKeyword:
modifiers = modifiers.WithIsOverride(true);
break;
case SyntaxKind.RequiredKeyword:
modifiers = modifiers.WithIsRequired(true);
break;
case SyntaxKind.SealedKeyword:
modifiers = modifiers.WithIsSealed(true);
break;
case SyntaxKind.UnsafeKeyword:
modifiers = modifiers.WithIsUnsafe(true);
break;
// Accessibility modifiers we accept.
case SyntaxKind.PublicKeyword:
seenAccessibility = seenAccessibility == Accessibility.NotApplicable
? Accessibility.Public
: seenAccessibility;
break;
case SyntaxKind.PrivateKeyword:
seenAccessibility = seenAccessibility switch
{
Accessibility.NotApplicable => Accessibility.Private,
// If we see private AND protected, filter for private protected
Accessibility.Protected => Accessibility.ProtectedAndInternal,
_ => seenAccessibility,
};
break;
case SyntaxKind.InternalKeyword:
// If we see internal AND protected, filter for protected internal
seenAccessibility = seenAccessibility switch
{
Accessibility.NotApplicable => Accessibility.Internal,
Accessibility.Protected => Accessibility.ProtectedOrInternal,
_ => seenAccessibility,
};
break;
case SyntaxKind.ProtectedKeyword:
seenAccessibility = seenAccessibility switch
{
Accessibility.NotApplicable => Accessibility.Protected,
// If we see protected AND internal, filter for protected internal.
Accessibility.Internal => Accessibility.ProtectedOrInternal,
// Or if we see private AND protected, filter for private protected
Accessibility.Private => Accessibility.ProtectedAndInternal,
_ => seenAccessibility,
};
break;
default:
// If we hit anything else, then this token is not valid for override completions, and we can just bail here.
return false;
}
token = token.GetPreviousToken();
}
// Have to at least found the override token for us to offer override-completion.
return modifiers.IsOverride;
}
public override SyntaxToken FindStartingToken(SyntaxTree tree, int position, CancellationToken cancellationToken)
{
var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken);
return token.GetPreviousTokenIfTouchingWord(position);
}
public override ImmutableArray<ISymbol> FilterOverrides(ImmutableArray<ISymbol> members, ITypeSymbol? returnType)
{
if (returnType == null)
{
return members;
}
var filteredMembers = members.WhereAsArray(m =>
SymbolEquivalenceComparer.Instance.Equals(GetReturnType(m), returnType));
// Don't filter by return type if we would then have nothing to show.
// This way, the user gets completion even if they speculatively typed the wrong return type
return filteredMembers.Length > 0 ? filteredMembers : members;
}
protected override int GetTargetCaretPosition(SyntaxNode caretTarget)
{
return CompletionUtilities.GetTargetCaretNodeForInsertedMember(caretTarget).GetLocation().SourceSpan.End;
}
}
|