From ec057e2b61cf1fcbd67b29afe7246f191de918fc Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Tue, 24 Mar 2020 09:03:25 -0700 Subject: [PATCH 1/5] Wrap arguments --- .../CompleteStatement/CompleteStatementCommandHandler.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs index 35e106948b2f0..68f9daffacc72 100644 --- a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs +++ b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs @@ -143,7 +143,12 @@ private static void MoveCaretToSemicolonPosition( return; } - if (currentNode.IsKind(SyntaxKind.ArgumentList, SyntaxKind.ArrayRankSpecifier, SyntaxKind.BracketedArgumentList, SyntaxKind.ParenthesizedExpression, SyntaxKind.ParameterList)) + if (currentNode.IsKind( + SyntaxKind.ArgumentList, + SyntaxKind.ArrayRankSpecifier, + SyntaxKind.BracketedArgumentList, + SyntaxKind.ParenthesizedExpression, + SyntaxKind.ParameterList)) { // make sure the closing delimiter exists if (RequiredDelimiterIsMissing(currentNode)) From ada49f46bcc84b3256bef095880beed0bf3da228 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Tue, 24 Mar 2020 09:05:45 -0700 Subject: [PATCH 2/5] Extract helper method GetDelimiters --- .../CompleteStatementCommandHandler.cs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs index 68f9daffacc72..8a8d34ae80109 100644 --- a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs +++ b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs @@ -117,8 +117,9 @@ private static bool TryGetStartingNode(SyntaxNode root, SnapshotPoint caret, // Examples, // `obj.ToString$()` where `token` references `(` but the caret isn't actually inside the argument list. // `obj.ToString()$` or `obj.method()$ .method()` where `token` references `)` but the caret isn't inside the argument list. - if (token.IsKind(SyntaxKind.OpenBraceToken, SyntaxKind.OpenBracketToken, SyntaxKind.OpenParenToken) && token.Span.Start >= caretPosition - || token.IsKind(SyntaxKind.CloseBraceToken, SyntaxKind.CloseBracketToken, SyntaxKind.CloseParenToken) && token.Span.End <= caretPosition) + var (openingDelimeter, closingDelimiter) = GetDelimiters(startingNode); + if (!openingDelimeter.IsKind(SyntaxKind.None) && openingDelimeter.Span.Start >= caretPosition + || !closingDelimiter.IsKind(SyntaxKind.None) && closingDelimiter.Span.End <= caretPosition) { startingNode = startingNode.Parent; } @@ -439,32 +440,41 @@ private static bool StatementClosingDelimiterIsMissing(SyntaxNode currentNode) /// /// private static bool RequiredDelimiterIsMissing(SyntaxNode currentNode) + { + return GetDelimiters(currentNode).closingDelimiter.IsMissing; + } + + private static (SyntaxToken openingDelimeter, SyntaxToken closingDelimiter) GetDelimiters(SyntaxNode currentNode) { switch (currentNode.Kind()) { case SyntaxKind.ArgumentList: var argumentList = (ArgumentListSyntax)currentNode; - return argumentList.CloseParenToken.IsMissing; + return (argumentList.OpenParenToken, argumentList.CloseParenToken); case SyntaxKind.ParenthesizedExpression: var parenthesizedExpression = (ParenthesizedExpressionSyntax)currentNode; - return parenthesizedExpression.CloseParenToken.IsMissing; + return (parenthesizedExpression.OpenParenToken, parenthesizedExpression.CloseParenToken); case SyntaxKind.BracketedArgumentList: var bracketedArgumentList = (BracketedArgumentListSyntax)currentNode; - return bracketedArgumentList.CloseBracketToken.IsMissing; + return (bracketedArgumentList.OpenBracketToken, bracketedArgumentList.CloseBracketToken); case SyntaxKind.ObjectInitializerExpression: var initializerExpressionSyntax = (InitializerExpressionSyntax)currentNode; - return initializerExpressionSyntax.CloseBraceToken.IsMissing; + return (initializerExpressionSyntax.OpenBraceToken, initializerExpressionSyntax.CloseBraceToken); case SyntaxKind.ArrayRankSpecifier: var arrayRankSpecifierSyntax = (ArrayRankSpecifierSyntax)currentNode; - return arrayRankSpecifierSyntax.CloseBracketToken.IsMissing; + return (arrayRankSpecifierSyntax.OpenBracketToken, arrayRankSpecifierSyntax.CloseBracketToken); + + case SyntaxKind.ParameterList: + var parameterList = (ParameterListSyntax)currentNode; + return (parameterList.OpenParenToken, parameterList.CloseParenToken); default: - // Type of node does not require a closing delimiter - return false; + // Type of node does not have delimiters used by this feature + return default; } } } From d005cd502e66d6c1fb710e13de8b9c03a35111aa Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Tue, 24 Mar 2020 09:28:56 -0700 Subject: [PATCH 3/5] Support default, checked, and unchecked expressions --- .../CompleteStatementCommandHandler.cs | 18 +++- ...arpCompleteStatementCommandHandlerTests.cs | 96 +++++++++++++++++++ .../CSharp/Extensions/SyntaxNodeExtensions.cs | 11 +++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs index 8a8d34ae80109..730b1bca23c62 100644 --- a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs +++ b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs @@ -112,11 +112,13 @@ private static bool TryGetStartingNode(SyntaxNode root, SnapshotPoint caret, startingNode = token.Parent; - // If the caret is right before an opening delimiter or right after a closing delimeter, + // If the caret is before an opening delimiter or after a closing delimeter, // start analysis with node outside of delimiters. + // // Examples, // `obj.ToString$()` where `token` references `(` but the caret isn't actually inside the argument list. // `obj.ToString()$` or `obj.method()$ .method()` where `token` references `)` but the caret isn't inside the argument list. + // `defa$$ult(object)` where `token` references `default` but the caret isn't inside the parentheses. var (openingDelimeter, closingDelimiter) = GetDelimiters(startingNode); if (!openingDelimeter.IsKind(SyntaxKind.None) && openingDelimeter.Span.Start >= caretPosition || !closingDelimiter.IsKind(SyntaxKind.None) && closingDelimiter.Span.End <= caretPosition) @@ -149,7 +151,10 @@ private static void MoveCaretToSemicolonPosition( SyntaxKind.ArrayRankSpecifier, SyntaxKind.BracketedArgumentList, SyntaxKind.ParenthesizedExpression, - SyntaxKind.ParameterList)) + SyntaxKind.ParameterList, + SyntaxKind.DefaultExpression, + SyntaxKind.CheckedExpression, + SyntaxKind.UncheckedExpression)) { // make sure the closing delimiter exists if (RequiredDelimiterIsMissing(currentNode)) @@ -472,6 +477,15 @@ private static (SyntaxToken openingDelimeter, SyntaxToken closingDelimiter) GetD var parameterList = (ParameterListSyntax)currentNode; return (parameterList.OpenParenToken, parameterList.CloseParenToken); + case SyntaxKind.DefaultExpression: + var defaultExpressionSyntax = (DefaultExpressionSyntax)currentNode; + return (defaultExpressionSyntax.OpenParenToken, defaultExpressionSyntax.CloseParenToken); + + case SyntaxKind.CheckedExpression: + case SyntaxKind.UncheckedExpression: + var checkedExpressionSyntax = (CheckedExpressionSyntax)currentNode; + return (checkedExpressionSyntax.OpenParenToken, checkedExpressionSyntax.CloseParenToken); + default: // Type of node does not have delimiters used by this feature return default; diff --git a/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs b/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs index 38c330d0d08ac..b1ecdb412e423 100644 --- a/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs +++ b/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs @@ -2316,6 +2316,102 @@ static void Main() #endregion + [WpfTheory, Trait(Traits.Feature, Traits.Features.CompleteStatement)] + [InlineData("default(object$$)", "default(object)")] + [InlineData("default($$object)", "default(object)")] + public void DefaultExpression_Handled(string expression, string expectedExpression) + { + var code = $@" +public class Class1 +{{ + void M() + {{ + int i = {expression} + }} +}}"; + + var expected = $@" +public class Class1 +{{ + void M() + {{ + int i = {expectedExpression};$$ + }} +}}"; + + VerifyTypingSemicolon(code, expected); + } + + [WpfTheory, Trait(Traits.Feature, Traits.Features.CompleteStatement)] + [InlineData("default$$(object)")] + [InlineData("def$$ault(object)")] + [InlineData("default(object$$")] + [InlineData("default($$object")] + public void DefaultExpression_NotHandled(string expression) + { + var code = $@" +public class Class1 +{{ + void M() + {{ + int i = {expression} + }} +}}"; + + VerifyNoSpecialSemicolonHandling(code); + } + + [WpfTheory, Trait(Traits.Feature, Traits.Features.CompleteStatement)] + [InlineData("checked(3 + 3$$)", "checked(3 + 3)")] + [InlineData("checked($$3 + 3)", "checked(3 + 3)")] + [InlineData("unchecked(3 + 3$$)", "unchecked(3 + 3)")] + [InlineData("unchecked($$3 + 3)", "unchecked(3 + 3)")] + public void CheckedExpression_Handled(string expression, string expectedExpression) + { + var code = $@" +public class Class1 +{{ + void M() + {{ + int i = {expression} + }} +}}"; + + var expected = $@" +public class Class1 +{{ + void M() + {{ + int i = {expectedExpression};$$ + }} +}}"; + + VerifyTypingSemicolon(code, expected); + } + + [WpfTheory, Trait(Traits.Feature, Traits.Features.CompleteStatement)] + [InlineData("checked$$(3 + 3)")] + [InlineData("che$$cked(3 + 3)")] + [InlineData("checked(3 + 3$$")] + [InlineData("checked($$3 + 3")] + [InlineData("unchecked$$(3 + 3)")] + [InlineData("unche$$cked(3 + 3)")] + [InlineData("unchecked(3 + 3$$")] + [InlineData("unchecked($$3 + 3")] + public void CheckedExpression_NotHandled(string expression) + { + var code = $@" +public class Class1 +{{ + void M() + {{ + int i = {expression} + }} +}}"; + + VerifyNoSpecialSemicolonHandling(code); + } + [WpfFact, Trait(Traits.Feature, Traits.Features.CompleteStatement)] public void ThrowStatement_MissingBoth() { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs index 6e16ef8460b0c..f2db08cf7aef7 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs @@ -131,6 +131,17 @@ public static bool IsKind([NotNullWhen(returnValue: true)] this SyntaxNode? node return csharpKind == kind1 || csharpKind == kind2 || csharpKind == kind3 || csharpKind == kind4 || csharpKind == kind5 || csharpKind == kind6 || csharpKind == kind7; } + public static bool IsKind([NotNullWhen(returnValue: true)] this SyntaxNode? node, SyntaxKind kind1, SyntaxKind kind2, SyntaxKind kind3, SyntaxKind kind4, SyntaxKind kind5, SyntaxKind kind6, SyntaxKind kind7, SyntaxKind kind8) + { + if (node == null) + { + return false; + } + + var csharpKind = node.Kind(); + return csharpKind == kind1 || csharpKind == kind2 || csharpKind == kind3 || csharpKind == kind4 || csharpKind == kind5 || csharpKind == kind6 || csharpKind == kind7 || csharpKind == kind8; + } + public static bool IsKind([NotNullWhen(returnValue: true)] this SyntaxNode? node, SyntaxKind kind1, SyntaxKind kind2, SyntaxKind kind3, SyntaxKind kind4, SyntaxKind kind5, SyntaxKind kind6, SyntaxKind kind7, SyntaxKind kind8, SyntaxKind kind9, SyntaxKind kind10, SyntaxKind kind11) { if (node == null) From 456712da018005896023a89b7e8fe4405c7c4641 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Tue, 24 Mar 2020 10:59:18 -0700 Subject: [PATCH 4/5] Fix semicolon handling in expression-bodied members --- .../CompleteStatementCommandHandler.cs | 4 +++- .../CSharpCompleteStatementCommandHandlerTests.cs | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs index 730b1bca23c62..3983954f86743 100644 --- a/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs +++ b/src/EditorFeatures/CSharp/CompleteStatement/CompleteStatementCommandHandler.cs @@ -187,7 +187,8 @@ private static void MoveCaretToSemicolonPosition( } return; } - else if (syntaxFacts.IsStatement(currentNode) || currentNode.IsKind(SyntaxKind.FieldDeclaration, SyntaxKind.DelegateDeclaration)) + else if (syntaxFacts.IsStatement(currentNode) + || currentNode.IsKind(SyntaxKind.FieldDeclaration, SyntaxKind.DelegateDeclaration, SyntaxKind.ArrowExpressionClause)) { MoveCaretToFinalPositionInStatement(currentNode, args, caret, isInsideDelimiters); return; @@ -255,6 +256,7 @@ private static bool TryGetCaretPositionToMove(SyntaxNode statementNode, Snapshot case SyntaxKind.ThrowStatement: case SyntaxKind.FieldDeclaration: case SyntaxKind.DelegateDeclaration: + case SyntaxKind.ArrowExpressionClause: // These statement types end in a semicolon. // if the original caret was inside any delimiters, `caret` will be after the outermost delimiter targetPosition = caret; diff --git a/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs b/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs index b1ecdb412e423..3aa4bfcaa7e08 100644 --- a/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs +++ b/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs @@ -1894,8 +1894,18 @@ public string Name set => name = value; } }"; + var expected = @" +public class SaleItem +{ + string name; + public string Name + { + get => name.ToUpper();$$ + set => name = value; + } +}"; - VerifyNoSpecialSemicolonHandling(code); + VerifyTypingSemicolon(code, expected); } [WpfFact, Trait(Traits.Feature, Traits.Features.CompleteStatement)] From 35d3e092fcf1b0e55e4fddd256a5d4472b594af1 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Tue, 7 Apr 2020 08:40:15 -0700 Subject: [PATCH 5/5] Add tests for parameter lists --- ...arpCompleteStatementCommandHandlerTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs b/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs index 3aa4bfcaa7e08..3b1489d26ee07 100644 --- a/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs +++ b/src/EditorFeatures/CSharpTest/CompleteStatement/CSharpCompleteStatementCommandHandlerTests.cs @@ -39,6 +39,51 @@ internal static int MethodM(int a, int b) }"; } + #region ParameterList + + [WpfTheory, Trait(Traits.Feature, Traits.Features.CompleteStatement)] + [InlineData("abstract void M(object o$$)", "abstract void M(object o)")] + [InlineData("abstract void M($$object o)", "abstract void M(object o)")] + [InlineData("abstract void M(object o = default(object$$))", "abstract void M(object o = default(object))")] + [InlineData("abstract void M(object o = default($$object))", "abstract void M(object o = default(object))")] + [InlineData("abstract void M(object o = $$default(object))", "abstract void M(object o = default(object))")] + public void ParameterList_CouldBeHandled(string signature, string expectedSignature) + { + var code = $@" +public class Class1 +{{ + {signature} +}}"; + + var expected = $@" +public class Class1 +{{ + {expectedSignature};$$ +}}"; + + // These cases are not currently handled. If support is added in the future, 'expected' should be correct. + _ = expected; + VerifyNoSpecialSemicolonHandling(code); + } + + [WpfTheory, Trait(Traits.Feature, Traits.Features.CompleteStatement)] + [InlineData("void M$$(object o)")] + [InlineData("void Me$$thod(object o)")] + [InlineData("void Method(object o$$")] + [InlineData("void Method($$object o")] + public void ParameterList_NotHandled(string signature) + { + var code = $@" +public class Class1 +{{ + {signature} +}}"; + + VerifyNoSpecialSemicolonHandling(code); + } + + #endregion + #region ArgumentListOfMethodInvocation [WpfFact, Trait(Traits.Feature, Traits.Features.CompleteStatement)]