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;
+ }
+ }
+}