diff --git a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj index 8b9e9e34b8533160473471ab893a24236e9bb069..623ccc818147222c46acebf5e559bd3883807196 100644 --- a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj +++ b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj @@ -317,6 +317,7 @@ + diff --git a/src/EditorFeatures/CSharpTest/SignatureHelp/TupleConstructionSignatureHelpProviderTests.cs b/src/EditorFeatures/CSharpTest/SignatureHelp/TupleConstructionSignatureHelpProviderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..822edd4abddc69a33378f18cee030d228d546a70 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/SignatureHelp/TupleConstructionSignatureHelpProviderTests.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.SignatureHelp; +using Microsoft.CodeAnalysis.CSharp.SignatureHelp; +using Xunit; +using Microsoft.CodeAnalysis.Editor.UnitTests.SignatureHelp; +using Roslyn.Test.Utilities; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.SignatureHelp +{ + public class TupleConstructionSignatureHelpProviderTests : AbstractCSharpSignatureHelpProviderTests + { + public TupleConstructionSignatureHelpProviderTests(CSharpTestWorkspaceFixture workspaceFixture) : base(workspaceFixture) + { + } + + internal override ISignatureHelpProvider CreateSignatureHelpProvider() + { + return new TupleConstructionSignatureHelpProvider(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task InvocationAfterOpenParen() + { + var markup = @" +class C +{ + (int, int) y = [|($$ +|]}"; + + var expectedOrderedItems = new List(); + expectedOrderedItems.Add(new SignatureHelpTestItem("(int, int)", currentParameterIndex: 0)); + + await TestAsync(markup, expectedOrderedItems, usePreviousCharAsTrigger: true); + } + + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task InvocationAfterOpenParen2() + { + var markup = @" +class C +{ + (int, int) y = [|($$) +|]}"; + + var expectedOrderedItems = new List(); + expectedOrderedItems.Add(new SignatureHelpTestItem("(int, int)", currentParameterIndex: 0)); + + await TestAsync(markup, expectedOrderedItems, usePreviousCharAsTrigger: true); + } + + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task InvocationAfterComma1() + { + var markup = @" +class C +{ + (int, int) y = [|(1, $$ +|]}"; + + var expectedOrderedItems = new List(); + expectedOrderedItems.Add(new SignatureHelpTestItem("(int, int)", currentParameterIndex: 1)); + + await TestAsync(markup, expectedOrderedItems, usePreviousCharAsTrigger: true); + } + + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task InvocationAfterComma2() + { + var markup = @" +class C +{ + (int, int) y = [|(1, $$) +|]}"; + + var expectedOrderedItems = new List(); + expectedOrderedItems.Add(new SignatureHelpTestItem("(int, int)", currentParameterIndex: 1)); + + await TestAsync(markup, expectedOrderedItems, usePreviousCharAsTrigger: true); + } + + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task ParameterIndexWithNameTyped() + { + var markup = @" +class C +{ + (int a, int b) y = [|(b: $$ +|]}"; + + var expectedOrderedItems = new List(); + + // currentParameterIndex only considers the position in the argument list + // and not names, hence passing 0 even though the controller will highlight + // "int b" in the actual display + expectedOrderedItems.Add(new SignatureHelpTestItem("(int a, int b)", currentParameterIndex: 0)); + + await TestAsync(markup, expectedOrderedItems); + } + + [Fact(Skip = "https://github.com/dotnet/roslyn/issues/14277"), Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task NestedTuple() + { + var markup = @" +class C +{ + (int a, (int b, int c)) y = [|(1, ($$ +|]}"; + + var expectedOrderedItems = new List(); + expectedOrderedItems.Add(new SignatureHelpTestItem("(int b, int c)", currentParameterIndex: 0)); + + await TestAsync(markup, expectedOrderedItems, usePreviousCharAsTrigger: true); + } + + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task NestedTupleWhenNotInferred() + { + var markup = @" +class C +{ + (int, object) y = [|(1, ($$ +|]}"; + + var expectedOrderedItems = new List(); + expectedOrderedItems.Add(new SignatureHelpTestItem("(int, object)", currentParameterIndex: 1)); + + await TestAsync(markup, expectedOrderedItems, usePreviousCharAsTrigger: true); + } + + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task NestedTupleWhenNotInferred2() + { + var markup = @" +class C +{ + (int, object) y = [|(1, (2, $$ +|]}"; + + var expectedOrderedItems = new List(); + expectedOrderedItems.Add(new SignatureHelpTestItem("(int, object)", currentParameterIndex: 1)); + + await TestAsync(markup, expectedOrderedItems, usePreviousCharAsTrigger: true); + } + + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public async Task MultipleOverloads() + { + var markup = @" +class Program +{ + static void Main(string[] args) + { + Do1([|($$)|]) + } + + static void Do1((int, int) i) { } + static void Do1((string, string) s) { } +}"; + + var expectedOrderedItems = new List(); + expectedOrderedItems.Add(new SignatureHelpTestItem("(int, int)", currentParameterIndex: 0)); + expectedOrderedItems.Add(new SignatureHelpTestItem("(string, string)", currentParameterIndex: 0)); + + await TestAsync(markup, expectedOrderedItems, usePreviousCharAsTrigger: true); + } + } +} diff --git a/src/EditorFeatures/Test2/IntelliSense/CSharpSignatureHelpCommandHandlerTests.vb b/src/EditorFeatures/Test2/IntelliSense/CSharpSignatureHelpCommandHandlerTests.vb index 7716ed7c746145da7cede569b01b7bd7484ffc47..776d0fb0739ac77b0f0702cb2be8649cd96a72bc 100644 --- a/src/EditorFeatures/Test2/IntelliSense/CSharpSignatureHelpCommandHandlerTests.vb +++ b/src/EditorFeatures/Test2/IntelliSense/CSharpSignatureHelpCommandHandlerTests.vb @@ -477,5 +477,42 @@ class C Await state.AssertSignatureHelpSession() End Using End Function + + + Public Async Function MixedTupleNaming() As Task + Using state = TestState.CreateCSharpTestState( + +class C +{ + void Foo() + { + (int, int x) t = (5$$ + } +} + ) + + state.SendTypeChars(",") + Await state.AssertSelectedSignatureHelpItem(displayText:="(int, int x)", selectedParameter:="int x") + End Using + End Function + + + Public Async Function ParameterSelectionWhileParsedAsParenthesizedExpression() As Task + Using state = TestState.CreateCSharpTestState( + +class C +{ + void Foo() + { + (int a, string b) x = (b$$ + } +} + ) + + state.SendInvokeSignatureHelp() + Await state.AssertSelectedSignatureHelpItem(displayText:="(int a, string b)", selectedParameter:="string b") + End Using + End Function + End Class End Namespace diff --git a/src/Features/CSharp/Portable/CSharpFeatures.csproj b/src/Features/CSharp/Portable/CSharpFeatures.csproj index 90cd2404424afd174073f23c54bfb0a8772c6781..2a45fce393eb5c058c3255220e45605cbb3b3a47 100644 --- a/src/Features/CSharp/Portable/CSharpFeatures.csproj +++ b/src/Features/CSharp/Portable/CSharpFeatures.csproj @@ -380,6 +380,7 @@ + diff --git a/src/Features/CSharp/Portable/SignatureHelp/TupleConstructionSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/TupleConstructionSignatureHelpProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..d4850c407f8885e3ce68c849f7f5ebc6eefea16c --- /dev/null +++ b/src/Features/CSharp/Portable/SignatureHelp/TupleConstructionSignatureHelpProvider.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.SignatureHelp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslyn.Utilities; +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp +{ + [ExportSignatureHelpProvider("TupleSignatureHelpProvider", LanguageNames.CSharp), Shared] + internal class TupleConstructionSignatureHelpProvider : AbstractCSharpSignatureHelpProvider + { + private static readonly Func s_getOpenToken = e => e.OpenParenToken; + private static readonly Func s_getCloseToken = e => e.CloseParenToken; + private static readonly Func> s_getArgumentsWithSeparators = e => e.Arguments.GetWithSeparators(); + private static readonly Func> s_getArgumentNames = e => e.Arguments.Select(a => a.NameColon?.Name.Identifier.ValueText ?? string.Empty); + + public override SignatureHelpState GetCurrentArgumentState(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, TextSpan currentSpan, CancellationToken cancellationToken) + { + TupleExpressionSyntax expression; + if (TryGetTupleExpression(SignatureHelpTriggerReason.InvokeSignatureHelpCommand, + root, position, syntaxFacts, cancellationToken, out expression) && + currentSpan.Start == expression.SpanStart) + { + return CommonSignatureHelpUtilities.GetSignatureHelpState(expression, position, + getOpenToken: s_getOpenToken, + getCloseToken: s_getCloseToken, + getArgumentsWithSeparators: s_getArgumentsWithSeparators, + getArgumentNames: s_getArgumentNames); + } + + ParenthesizedExpressionSyntax parenthesizedExpression; + if (TryGetParenthesizedExpression(SignatureHelpTriggerReason.InvokeSignatureHelpCommand, + root, position, syntaxFacts, cancellationToken, out parenthesizedExpression)) + { + // This could only have parsed as a parenthesized expression in these two cases: + // ($$) + // (name$$) + string name = 0.ToString(); // This causes the controller to match against the 0th tuple member + if (parenthesizedExpression.Expression is IdentifierNameSyntax) + { + name = ((IdentifierNameSyntax)parenthesizedExpression.Expression).Identifier.ValueText; + } + + return new SignatureHelpState( + argumentIndex: 0, + argumentCount: 0, + argumentName: name, + argumentNames: null); + } + + return null; + } + + public override Boolean IsRetriggerCharacter(Char ch) + { + return ch == ')'; + } + + public override Boolean IsTriggerCharacter(Char ch) + { + return ch == '(' || ch == ','; + } + + protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.Project.LanguageServices.GetService(); + TupleExpressionSyntax tupleExpression; + ParenthesizedExpressionSyntax parenthesizedExpression = null; + if (!TryGetTupleExpression(triggerInfo.TriggerReason, root, position, syntaxFacts, cancellationToken, out tupleExpression) && + !TryGetParenthesizedExpression(triggerInfo.TriggerReason, root, position, syntaxFacts, cancellationToken, out parenthesizedExpression)) + { + return null; + } + + var targetExpression = (SyntaxNode)tupleExpression ?? parenthesizedExpression; + + var typeInferrer = document.Project.LanguageServices.GetService(); + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var inferredTypes = typeInferrer.InferTypes(semanticModel, targetExpression.SpanStart, cancellationToken); + + var tupleTypes = inferredTypes.Where(t => t.IsTupleType).OfType(); + return CreateItems(position, root, syntaxFacts, targetExpression, semanticModel, tupleTypes, cancellationToken); + } + + private SignatureHelpItems CreateItems(int position, SyntaxNode root, ISyntaxFactsService syntaxFacts, SyntaxNode targetExpression, SemanticModel semanticModel, IEnumerable tupleTypes, CancellationToken cancellationToken) + { + var prefixParts = SpecializedCollections.SingletonEnumerable(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, "(")); + var suffixParts = SpecializedCollections.SingletonEnumerable(new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, ")")); + var separatorParts = GetSeparatorParts(); + + var items = tupleTypes.Select(t => + new SignatureHelpItem(isVariadic: false, + documentationFactory: c => null, + prefixParts: prefixParts, + separatorParts: separatorParts, + suffixParts: suffixParts, + parameters: ConvertTupleMembers(t, semanticModel, position), + descriptionParts: null)).ToList(); + + var state = GetCurrentArgumentState(root, position, syntaxFacts, targetExpression.FullSpan, cancellationToken); + return CreateSignatureHelpItems(items, targetExpression.FullSpan, state); + } + + private IEnumerable ConvertTupleMembers(INamedTypeSymbol tupleType, SemanticModel semanticModel, int position) + { + var spacePart = Space(); + var result = new List(); + for (int i = 0; i < tupleType.TupleElementTypes.Length; i++) + { + var type = tupleType.TupleElementTypes[i]; + var parameterItemName = GetParameterName(tupleType.TupleElementNames, i); + var elementName = GetElementName(tupleType.TupleElementNames, i); + + var typeParts = type.ToMinimalDisplayParts(semanticModel, position).ToList(); + if (!string.IsNullOrEmpty(elementName)) + { + typeParts.Add(spacePart); + typeParts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.PropertyName, null, elementName)); + } + + result.Add(new SignatureHelpParameter(parameterItemName, false, c => null, typeParts)); + } + + return result; + } + + // The name used by the controller when selecting parameters + // Each element needs a unique name to make selection work property + private string GetParameterName(ImmutableArray tupleElementNames, int i) + { + if (tupleElementNames == default(ImmutableArray)) + { + return i.ToString(); + } + + return tupleElementNames[i] ?? i.ToString(); + } + + // The display name for each parameter. Empty strings are allowed for + // parameters without names. + private string GetElementName(ImmutableArray tupleElementNames, int i) + { + if (tupleElementNames == default(ImmutableArray)) + { + return string.Empty; + } + + return tupleElementNames[i] ?? string.Empty; + } + + private bool TryGetTupleExpression(SignatureHelpTriggerReason triggerReason, SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, out TupleExpressionSyntax tupleExpression) + { + return CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, IsTupleExpressionTriggerToken, IsTupleArgumentListToken, cancellationToken, out tupleExpression); + } + + private bool IsTupleExpressionTriggerToken(SyntaxToken token) + { + return SignatureHelpUtilities.IsTriggerParenOrComma(token, IsTriggerCharacter); + } + + private static bool IsTupleArgumentListToken(TupleExpressionSyntax tupleExpression, SyntaxToken token) + { + return tupleExpression.Arguments.FullSpan.Contains(token.SpanStart) && + token != tupleExpression.CloseParenToken; + } + + private bool TryGetParenthesizedExpression(SignatureHelpTriggerReason triggerReason, SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, out ParenthesizedExpressionSyntax parenthesizedExpression) + { + return CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, IsParenthesizedExpressionTriggerToken, IsParenthesizedExpressionToken, cancellationToken, out parenthesizedExpression); + } + + private bool IsParenthesizedExpressionTriggerToken(SyntaxToken token) + { + return token.IsKind(SyntaxKind.OpenParenToken) && token.Parent is ParenthesizedExpressionSyntax; + } + + private static bool IsParenthesizedExpressionToken(ParenthesizedExpressionSyntax expr, SyntaxToken token) + { + return expr.FullSpan.Contains(token.SpanStart) && + token != expr.CloseParenToken; + } + } +}