From 4a475f6e4d5679eb48241bcb59ff2291fabebca2 Mon Sep 17 00:00:00 2001 From: Brett Forsgren Date: Tue, 3 Mar 2015 10:50:10 -0800 Subject: [PATCH] show conditional element access sig-help after arbitrary expressions the old model would only appear after an identifier name --- ...ntAccessExpressionSignatureHelpProvider.cs | 91 ++++++++++++++++--- ...essExpressionSignatureHelpProviderTests.cs | 14 +++ ...ionExpressionSignatureHelpProviderTests.vb | 41 +++++++++ 3 files changed, 134 insertions(+), 12 deletions(-) diff --git a/src/EditorFeatures/CSharp/SignatureHelp/ElementAccessExpressionSignatureHelpProvider.cs b/src/EditorFeatures/CSharp/SignatureHelp/ElementAccessExpressionSignatureHelpProvider.cs index 13051f4d885..434ef491e76 100644 --- a/src/EditorFeatures/CSharp/SignatureHelp/ElementAccessExpressionSignatureHelpProvider.cs +++ b/src/EditorFeatures/CSharp/SignatureHelp/ElementAccessExpressionSignatureHelpProvider.cs @@ -39,7 +39,8 @@ public override bool IsRetriggerCharacter(char ch) private static bool TryGetElementAccessExpression(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, out ExpressionSyntax identifier, out SyntaxToken openBrace) { return CompleteElementAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace) || - IncompleteElementAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace); + IncompleteElementAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace) || + ConditionalAccessExpression.TryGetSyntax(root, position, syntaxFacts, triggerReason, cancellationToken, out identifier, out openBrace); } protected override async Task GetItemsWorkerAsync(Document document, int position, SignatureHelpTriggerInfo triggerInfo, CancellationToken cancellationToken) @@ -115,7 +116,15 @@ private TextSpan GetTextSpan(ExpressionSyntax expression, SyntaxToken openBracke { if (openBracket.Parent is BracketedArgumentListSyntax) { - return CompleteElementAccessExpression.GetTextSpan(expression, openBracket); + var conditional = expression.Parent as ConditionalAccessExpressionSyntax; + if (conditional != null) + { + return TextSpan.FromBounds(conditional.Span.Start, openBracket.FullSpan.End); + } + else + { + return CompleteElementAccessExpression.GetTextSpan(expression, openBracket); + } } else if (openBracket.Parent is ArrayRankSpecifierSyntax) { @@ -142,16 +151,35 @@ public override SignatureHelpState GetCurrentArgumentState(SyntaxNode root, int return null; } - ElementAccessExpressionSyntax elementAccessExpression = SyntaxFactory.ElementAccessExpression( - expression, - SyntaxFactory.ParseBracketedArgumentList(openBracket.Parent.ToString())); + // If the user is actively typing, it's likely that we're in a broken state and the + // syntax tree will be incorrect. Because of this we need to synthesize a new + // bracketed argument list so we can correctly map the cursor to the current argument + // and then we need to account for this and offset the position check accordingly. + int offset; + BracketedArgumentListSyntax argumentList; + var newBracketedArgumentList = SyntaxFactory.ParseBracketedArgumentList(openBracket.Parent.ToString()); + if (expression.Parent is ConditionalAccessExpressionSyntax) + { + // The typed code looks like: ?[ + var conditional = (ConditionalAccessExpressionSyntax)expression.Parent; + var elementBinding = SyntaxFactory.ElementBindingExpression(newBracketedArgumentList); + var conditionalAccessExpression = SyntaxFactory.ConditionalAccessExpression(expression, elementBinding); + offset = expression.SpanStart - conditionalAccessExpression.SpanStart; + argumentList = ((ElementBindingExpressionSyntax)conditionalAccessExpression.WhenNotNull).ArgumentList; + } + else + { + // The typed code looks like: + // [ + // or + // ?[ + ElementAccessExpressionSyntax elementAccessExpression = SyntaxFactory.ElementAccessExpression(expression, newBracketedArgumentList); + offset = expression.SpanStart - elementAccessExpression.SpanStart; + argumentList = elementAccessExpression.ArgumentList; + } - // Because we synthesized the elementAccessExpression, it will have an index starting at 0 - // instead of at the actual position it's at in the text. Because of this, we need to - // offset the position we are checking accordingly. - var offset = expression.SpanStart - elementAccessExpression.SpanStart; position -= offset; - return SignatureHelpUtilities.GetSignatureHelpState(elementAccessExpression.ArgumentList, position); + return SignatureHelpUtilities.GetSignatureHelpState(argumentList, position); } private bool TryGetComIndexers(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken, out IEnumerable indexers, out ITypeSymbol expressionType) @@ -254,7 +282,8 @@ internal static bool IsArgumentListToken(ElementAccessExpressionSyntax expressio internal static TextSpan GetTextSpan(SyntaxNode expression, SyntaxToken openBracket) { - Contract.ThrowIfFalse(openBracket.Parent is BracketedArgumentListSyntax && openBracket.Parent.Parent is ElementAccessExpressionSyntax); + Contract.ThrowIfFalse(openBracket.Parent is BracketedArgumentListSyntax && + (openBracket.Parent.Parent is ElementAccessExpressionSyntax || openBracket.Parent.Parent is ElementBindingExpressionSyntax)); return SignatureHelpUtilities.GetSignatureHelpSpan((BracketedArgumentListSyntax)openBracket.Parent); } @@ -275,7 +304,7 @@ internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsSer } /// Error tolerance case for - /// "foo[]" + /// "foo[$$]" or "foo?[$$]" /// which is parsed as an ArrayTypeSyntax variable declaration instead of an ElementAccessExpression private static class IncompleteElementAccessExpression { @@ -314,5 +343,43 @@ internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsSer return false; } } + + /// Error tolerance case for + /// "new String()?[$$]" + /// which is parsed as a BracketedArgumentListSyntax parented by an ElementBindingExpressionSyntax parented by a ConditionalAccessExpressionSyntax + private static class ConditionalAccessExpression + { + internal static bool IsTriggerToken(SyntaxToken token) + { + return !token.IsKind(SyntaxKind.None) && + token.ValueText.Length == 1 && + IsTriggerCharacterInternal(token.ValueText[0]) && + token.Parent is BracketedArgumentListSyntax && + token.Parent.Parent is ElementBindingExpressionSyntax && + token.Parent.Parent.Parent is ConditionalAccessExpressionSyntax; + } + + internal static bool IsArgumentListToken(ElementBindingExpressionSyntax expression, SyntaxToken token) + { + return expression.ArgumentList.Span.Contains(token.SpanStart) && + token != expression.ArgumentList.CloseBracketToken; + } + + internal static bool TryGetSyntax(SyntaxNode root, int position, ISyntaxFactsService syntaxFacts, SignatureHelpTriggerReason triggerReason, CancellationToken cancellationToken, out ExpressionSyntax identifier, out SyntaxToken openBrace) + { + ElementBindingExpressionSyntax elementBindingExpression; + if (CommonSignatureHelpUtilities.TryGetSyntax(root, position, syntaxFacts, triggerReason, IsTriggerToken, IsArgumentListToken, cancellationToken, out elementBindingExpression)) + { + identifier = ((ConditionalAccessExpressionSyntax)elementBindingExpression.Parent).Expression; + openBrace = elementBindingExpression.ArgumentList.OpenBracketToken; + + return true; + } + + identifier = null; + openBrace = default(SyntaxToken); + return false; + } + } } } diff --git a/src/EditorFeatures/CSharpTest/SignatureHelp/ElementAccessExpressionSignatureHelpProviderTests.cs b/src/EditorFeatures/CSharpTest/SignatureHelp/ElementAccessExpressionSignatureHelpProviderTests.cs index e711b563b2c..2519e9d416a 100644 --- a/src/EditorFeatures/CSharpTest/SignatureHelp/ElementAccessExpressionSignatureHelpProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/SignatureHelp/ElementAccessExpressionSignatureHelpProviderTests.cs @@ -810,6 +810,20 @@ public void foo() Test(markup, expectedOrderedItems); } + [WorkItem(32, "https://github.com/dotnet/roslyn/issues/32")] + [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] + public void NonIdentifierConditionalIndexer() + { + var expected = new[] { new SignatureHelpTestItem("char string[int index]") }; + Test(@"class C { void M() { """"?[$$ } }", expected); // inline with a string literal + Test(@"class C { void M() { """"?[/**/$$ } }", expected); // inline with a string literal and multiline comment + Test(@"class C { void M() { ("""")?[$$ } }", expected); // parenthesized expression + Test(@"class C { void M() { new System.String(' ', 1)?[$$ } }", expected); // new object expression + + // more complicated parenthesized expression + Test(@"class C { void M() { (null as System.Collections.Generic.List)?[$$ } }", new[] { new SignatureHelpTestItem("int System.Collections.Generic.List[int index]") }); + } + [WorkItem(1067933)] [Fact, Trait(Traits.Feature, Traits.Features.SignatureHelp)] public void InvokedWithNoToken() diff --git a/src/EditorFeatures/VisualBasicTest/SignatureHelp/InvocationExpressionSignatureHelpProviderTests.vb b/src/EditorFeatures/VisualBasicTest/SignatureHelp/InvocationExpressionSignatureHelpProviderTests.vb index 192d772d118..f7ff7671faa 100644 --- a/src/EditorFeatures/VisualBasicTest/SignatureHelp/InvocationExpressionSignatureHelpProviderTests.vb +++ b/src/EditorFeatures/VisualBasicTest/SignatureHelp/InvocationExpressionSignatureHelpProviderTests.vb @@ -932,6 +932,47 @@ End Class Test(markup, expected, experimental:=True) End Sub + + Public Sub NonIdentifierConditionalIndexer() + Dim expected = {New SignatureHelpTestItem("String(index As Integer) As Char")} + + ' inline with a string literal + Test(" +Class C + Sub M() + Dim c = """"?($$ + End Sub +End Class +", expected) + + ' parenthesized expression + Test(" +Class C + Sub M() + Dim c = ("""")?($$ + End Sub +End Class +", expected) + + ' new object expression + Test(" +Class C + Sub M() + Dim c = (New System.String("" ""c, 1))?($$ + End Sub +End Class +", expected) + + ' more complicated parenthesized expression + Test(" +Class C + Sub M() + Dim c = (CType(Nothing, System.Collections.Generic.List(Of Integer)))?($$ + End Sub +End Class +", {New SignatureHelpTestItem("System.Collections.Generic.List(Of Integer)(index As Integer) As Integer")}) + End Sub + Public Sub TestTriggerCharacters() Dim expectedTriggerCharacters() As Char = {","c, "("c} -- GitLab