diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProviderTests.cs index 89ba73cc831e..ab90c35c3b91 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProviderTests.cs @@ -30,6 +30,9 @@ interface IGoo void Goo(); void Goo(int x); int Prop { get; } + int Generic(K key, V value); + string this[int i] { get; } + void With_Underscore(); } class Bar : IGoo @@ -37,9 +40,12 @@ class Bar : IGoo void IGoo.$$ }"; - await VerifyItemExistsAsync(markup, "Goo()"); - await VerifyItemExistsAsync(markup, "Goo(int x)"); + await VerifyItemExistsAsync(markup, "Goo", displayTextSuffix: "()"); + await VerifyItemExistsAsync(markup, "Goo", displayTextSuffix: "(int x)"); await VerifyItemExistsAsync(markup, "Prop"); + await VerifyItemExistsAsync(markup, "Generic", displayTextSuffix: "(K key, V value)"); + await VerifyItemExistsAsync(markup, "this", displayTextSuffix: "[int i]"); + await VerifyItemExistsAsync(markup, "With_Underscore", displayTextSuffix: "()"); } [Fact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -58,8 +64,8 @@ interface IBar : IGoo void IGoo.$$ }"; - await VerifyItemExistsAsync(markup, "Goo()"); - await VerifyItemExistsAsync(markup, "Goo(int x)"); + await VerifyItemExistsAsync(markup, "Goo", displayTextSuffix: "()"); + await VerifyItemExistsAsync(markup, "Goo", displayTextSuffix: "(int x)"); await VerifyItemExistsAsync(markup, "Prop"); } @@ -79,8 +85,8 @@ class Bar : IGoo void IGoo.$$ }"; - await VerifyItemExistsAsync(markup, "Goo()"); - await VerifyItemExistsAsync(markup, "Goo(int x)"); + await VerifyItemExistsAsync(markup, "Goo", displayTextSuffix: "()"); + await VerifyItemExistsAsync(markup, "Goo", displayTextSuffix: "(int x)"); await VerifyItemExistsAsync(markup, "Prop"); } @@ -100,8 +106,8 @@ interface IBar : IGoo void IGoo.$$ }"; - await VerifyItemExistsAsync(markup, "Goo()"); - await VerifyItemExistsAsync(markup, "Goo(int x)"); + await VerifyItemExistsAsync(markup, "Goo", displayTextSuffix: "()"); + await VerifyItemExistsAsync(markup, "Goo", displayTextSuffix: "(int x)"); await VerifyItemExistsAsync(markup, "Prop"); } @@ -190,7 +196,7 @@ void I2.$$ await VerifyItemIsAbsentAsync(markup, "GetType()"); await VerifyItemIsAbsentAsync(markup, "ToString()"); - await VerifyItemExistsAsync(markup, "Goo2()"); + await VerifyItemExistsAsync(markup, "Goo2", displayTextSuffix: "()"); await VerifyItemExistsAsync(markup, "Prop"); } @@ -216,7 +222,7 @@ void I1.$$ await VerifyItemIsAbsentAsync(markup, "TestEvent.add"); await VerifyItemIsAbsentAsync(markup, "TestEvent.remove"); - await VerifyItemExistsAsync(markup, "Foo()"); + await VerifyItemExistsAsync(markup, "Foo", displayTextSuffix: "()"); await VerifyItemExistsAsync(markup, "Prop"); await VerifyItemExistsAsync(markup, "TestEvent"); } @@ -365,9 +371,9 @@ protected void Goo2() {} "; - await VerifyItemIsAbsentAsync(markup, "Goo1()"); + await VerifyItemIsAbsentAsync(markup, "Goo1", displayTextSuffix: "()"); await VerifyItemIsAbsentAsync(markup, "Prop1"); - await VerifyItemExistsAsync(markup, "Goo2()"); + await VerifyItemExistsAsync(markup, "Goo2", displayTextSuffix: "()"); await VerifyItemExistsAsync(markup, "Prop2"); } @@ -401,10 +407,178 @@ protected void Goo2() {} "; - await VerifyItemIsAbsentAsync(markup, "Goo1()"); + await VerifyItemIsAbsentAsync(markup, "Goo1", displayTextSuffix: "()"); await VerifyItemIsAbsentAsync(markup, "Prop1"); - await VerifyItemExistsAsync(markup, "Goo2()"); + await VerifyItemExistsAsync(markup, "Goo2", displayTextSuffix: "()"); await VerifyItemExistsAsync(markup, "Prop2"); } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task VerifySignatureCommit_Generic_Tab() + { + var markup = @" +interface IGoo +{ + int Generic(K key, V value); +} + +class Bar : IGoo +{ + void IGoo.$$ +}"; + + var expected = @" +interface IGoo +{ + int Generic(K key, V value); +} + +class Bar : IGoo +{ + void IGoo.Generic(K key, V value) +}"; + + await VerifyProviderCommitAsync(markup, "Generic(K key, V value)", expected, '\t', ""); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task VerifySignatureCommit_Generic_OpenBrace() + { + var markup = @" +interface IGoo +{ + int Generic(K key, V value); +} + +class Bar : IGoo +{ + void IGoo.$$ +}"; + + var expected = @" +interface IGoo +{ + int Generic(K key, V value); +} + +class Bar : IGoo +{ + void IGoo.Generic< +}"; + + await VerifyProviderCommitAsync(markup, "Generic(K key, V value)", expected, '<', ""); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task VerifySignatureCommit_Method_Tab() + { + var markup = @" +interface IGoo +{ + int Generic(K key, V value); +} + +class Bar : IGoo +{ + void IGoo.$$ +}"; + + var expected = @" +interface IGoo +{ + int Generic(K key, V value); +} + +class Bar : IGoo +{ + void IGoo.Generic(K key, V value) +}"; + + await VerifyProviderCommitAsync(markup, "Generic(K key, V value)", expected, '\t', ""); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task VerifySignatureCommit_Method_OpenBrace() + { + var markup = @" +interface IGoo +{ + int Generic(K key, V value); +} + +class Bar : IGoo +{ + void IGoo.$$ +}"; + + var expected = @" +interface IGoo +{ + int Generic(K key, V value); +} + +class Bar : IGoo +{ + void IGoo.Generic( +}"; + + await VerifyProviderCommitAsync(markup, "Generic(K key, V value)", expected, '(', ""); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task VerifySignatureCommit_Indexer_Tab() + { + var markup = @" +interface IGoo +{ + int this[K key, V value] { get; } +} + +class Bar : IGoo +{ + void IGoo.$$ +}"; + + var expected = @" +interface IGoo +{ + int this[K key, V value] { get; } +} + +class Bar : IGoo +{ + void IGoo.this[K key, V value] +}"; + + await VerifyProviderCommitAsync(markup, "this[K key, V value]", expected, '\t', ""); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task VerifySignatureCommit_Indexer_OpenBrace() + { + var markup = @" +interface IGoo +{ + int this[K key, V value] { get; } +} + +class Bar : IGoo +{ + void IGoo.$$ +}"; + + var expected = @" +interface IGoo +{ + int this[K key, V value] { get; } +} + +class Bar : IGoo +{ + void IGoo.this[ +}"; + + await VerifyProviderCommitAsync(markup, "this[K key, V value]", expected, '[', ""); + } } } diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/SpellCheck/SpellCheckTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/SpellCheck/SpellCheckTests.cs index a478e0367af8..b8701332ee21 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/SpellCheck/SpellCheckTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/SpellCheck/SpellCheckTests.cs @@ -592,5 +592,91 @@ await TestInRegularAndScriptAsync( public SomeClass() { } }"); } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsSpellcheck)] + public async Task TestInExplicitInterfaceImplementation1() + { + var text = @" +using System; + +class Program : IDisposable +{ + void IDisposable.[|Dspose|] +}"; + + var expected = @" +using System; + +class Program : IDisposable +{ + void IDisposable.Dispose +}"; + + await TestInRegularAndScriptAsync(text, expected); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsSpellcheck)] + public async Task TestInExplicitInterfaceImplementation2() + { + var text = @" +using System; + +interface IInterface +{ + void Generic(); +} + +class Program : IInterface +{ + void IInterface.[|Generi|] +}"; + + var expected = @" +using System; + +interface IInterface +{ + void Generic(); +} + +class Program : IInterface +{ + void IInterface.Generic +}"; + + await TestInRegularAndScriptAsync(text, expected); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsSpellcheck)] + public async Task TestInExplicitInterfaceImplementation3() + { + var text = @" +using System; + +interface IInterface +{ + int this[int i] { get; } +} + +class Program : IInterface +{ + void IInterface.[|thi|] +}"; + + var expected = @" +using System; + +interface IInterface +{ + int this[int i] { get; } +} + +class Program : IInterface +{ + void IInterface.this +}"; + + await TestInRegularAndScriptAsync(text, expected); + } } } diff --git a/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs b/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs index 5d2d6a1d8971..1b3eb2c6df4f 100644 --- a/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs +++ b/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs @@ -260,8 +260,13 @@ protected async Task VerifyCustomCommitProviderAsync(string markupBeforeCommit, } } - protected async Task VerifyProviderCommitAsync(string markupBeforeCommit, string itemToCommit, string expectedCodeAfterCommit, - char? commitChar, string textTypedSoFar, SourceCodeKind? sourceCodeKind = null) + protected async Task VerifyProviderCommitAsync( + string markupBeforeCommit, + string itemToCommit, + string expectedCodeAfterCommit, + char? commitChar, + string textTypedSoFar, + SourceCodeKind? sourceCodeKind = null) { WorkspaceFixture.GetWorkspace(markupBeforeCommit, ExportProvider); diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.cs index 52a865e8cf1a..a4000cda41d0 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ExplicitInterfaceMemberCompletionProvider.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + using System; using System.Collections.Immutable; using System.Composition; @@ -20,13 +22,10 @@ namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers { - [ExportCompletionProvider(nameof(ExplicitInterfaceMemberCompletionProvider), LanguageNames.CSharp)] + [ExportCompletionProvider(nameof(ExplicitInterfaceMemberCompletionProvider), LanguageNames.CSharp), Shared] [ExtensionOrder(After = nameof(SymbolCompletionProvider))] - [Shared] internal partial class ExplicitInterfaceMemberCompletionProvider : LSPCompletionProvider { - private const string InsertionTextOnOpenParen = nameof(InsertionTextOnOpenParen); - private static readonly SymbolDisplayFormat s_signatureDisplayFormat = new SymbolDisplayFormat( genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, @@ -57,15 +56,13 @@ public override async Task ProvideCompletionsAsync(CompletionContext context) { var document = context.Document; var position = context.Position; - var options = context.Options; var cancellationToken = context.CancellationToken; - var span = new TextSpan(position, length: 0); - var semanticModel = await document.GetSemanticModelForSpanAsync(span, cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetSemanticModelForSpanAsync(new TextSpan(position, length: 0), cancellationToken).ConfigureAwait(false); var syntaxTree = semanticModel.SyntaxTree; - var syntaxFacts = document.GetLanguageService(); - var semanticFacts = document.GetLanguageService(); + var syntaxFacts = document.GetRequiredLanguageService(); + var semanticFacts = document.GetRequiredLanguageService(); if (syntaxFacts.IsInNonUserCode(syntaxTree, position, cancellationToken) || semanticFacts.IsPreProcessorDirectiveContext(semanticModel, position, cancellationToken)) @@ -77,55 +74,46 @@ public override async Task ProvideCompletionsAsync(CompletionContext context) .GetPreviousTokenIfTouchingWord(position); if (!syntaxTree.IsRightOfDotOrArrowOrColonColon(position, targetToken, cancellationToken)) - { return; - } var node = targetToken.Parent; - - if (node.Kind() != SyntaxKind.ExplicitInterfaceSpecifier) - { + if (!node.IsKind(SyntaxKind.ExplicitInterfaceSpecifier, out ExplicitInterfaceSpecifierSyntax? specifierNode)) return; - } // Bind the interface name which is to the left of the dot - var name = ((ExplicitInterfaceSpecifierSyntax)node).Name; + var name = specifierNode.Name; var symbol = semanticModel.GetSymbolInfo(name, cancellationToken).Symbol as ITypeSymbol; if (symbol?.TypeKind != TypeKind.Interface) - { return; - } - - var members = symbol.GetMembers(); // We're going to create a entry for each one, including the signature var namePosition = name.SpanStart; - - var text = await syntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); - - foreach (var member in members) + foreach (var member in symbol.GetMembers()) { - if (member.IsAccessor() || member.Kind == SymbolKind.NamedType || !(member.IsAbstract || member.IsVirtual) || + if (!member.IsAbstract && !member.IsVirtual) + continue; + + if (member.IsAccessor() || + member.Kind == SymbolKind.NamedType || !semanticModel.IsAccessible(node.SpanStart, member)) { continue; } - var displayText = member.ToMinimalDisplayString( - semanticModel, namePosition, s_signatureDisplayFormat); - var insertionText = displayText; + var memberString = member.ToMinimalDisplayString(semanticModel, namePosition, s_signatureDisplayFormat); - var item = SymbolCompletionItem.CreateWithSymbolId( + // Split the member string into two parts (generally the name, and the signature portion). We want + // the split so that other features (like spell-checking), only look at the name portion. + var (displayText, displayTextSuffix) = SplitMemberName(memberString); + + context.AddItem(SymbolCompletionItem.CreateWithSymbolId( displayText, - displayTextSuffix: "", - insertionText: insertionText, - symbols: ImmutableArray.Create(member), + displayTextSuffix, + insertionText: memberString, + symbols: ImmutableArray.Create(member), contextPosition: position, - rules: CompletionItemRules.Default); - item = item.AddProperty(InsertionTextOnOpenParen, member.Name); - - context.AddItem(item); + rules: CompletionItemRules.Default)); } } catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e)) @@ -134,21 +122,31 @@ public override async Task ProvideCompletionsAsync(CompletionContext context) } } + private static (string text, string suffix) SplitMemberName(string memberString) + { + for (var i = 0; i < memberString.Length; i++) + { + if (!SyntaxFacts.IsIdentifierPartCharacter(memberString[i])) + return (memberString[0..i], memberString[i..]); + } + + return (memberString, ""); + } + protected override Task GetDescriptionWorkerAsync(Document document, CompletionItem item, CancellationToken cancellationToken) => SymbolCompletionItem.GetDescriptionAsync(item, document, cancellationToken); public override Task GetTextChangeAsync( Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) { - if (ch == '(') - { - if (selectedItem.Properties.TryGetValue(InsertionTextOnOpenParen, out var insertionText)) - { - return Task.FromResult(new TextChange(selectedItem.Span, insertionText)); - } - } - - return Task.FromResult(new TextChange(selectedItem.Span, selectedItem.DisplayText)); + // If the user is typing a punctuation portion of the signature, then just emit the name. i.e. if the + // member is `Contains(string key)`, then typing `<` should just emit `Contains` and not + // `Contains(string key)<` + return Task.FromResult(new TextChange( + selectedItem.Span, + ch == '(' || ch == '[' || ch == '<' + ? selectedItem.DisplayText + : SymbolCompletionItem.GetInsertionText(selectedItem))); } } } diff --git a/src/Features/Core/Portable/SpellCheck/AbstractSpellCheckCodeFixProvider.cs b/src/Features/Core/Portable/SpellCheck/AbstractSpellCheckCodeFixProvider.cs index 4abd2f3107d8..56f0b7d75fb7 100644 --- a/src/Features/Core/Portable/SpellCheck/AbstractSpellCheckCodeFixProvider.cs +++ b/src/Features/Core/Portable/SpellCheck/AbstractSpellCheckCodeFixProvider.cs @@ -185,12 +185,17 @@ private async Task CheckItemsAsync( } } + private static readonly char[] s_punctuation = new[] { '(', '[', '<' }; + private static async Task GetInsertionTextAsync(Document document, CompletionItem item, TextSpan completionListSpan, CancellationToken cancellationToken) { var service = CompletionService.GetService(document); var change = await service.GetChangeAsync(document, item, completionListSpan, commitCharacter: null, cancellationToken).ConfigureAwait(false); - - return change.TextChange.NewText; + var text = change.TextChange.NewText; + var nonCharIndex = text.IndexOfAny(s_punctuation); + return nonCharIndex > 0 + ? text[0..nonCharIndex] + : text; } private SpellCheckCodeAction CreateCodeAction(SyntaxToken nameToken, string oldName, string newName, Document document)