Skip to content

Commit

Permalink
Merge pull request dotnet#42733 from sharwell/polish-semicolon-comple…
Browse files Browse the repository at this point in the history
…tion

Polish Complete Statement (semicolon insertion)
  • Loading branch information
sharwell authored Apr 10, 2020
2 parents 6c454f9 + 35d3e09 commit 2c2b9c9
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,16 @@ 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.
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)
// `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)
{
startingNode = startingNode.Parent;
}
Expand All @@ -143,7 +146,15 @@ 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,
SyntaxKind.DefaultExpression,
SyntaxKind.CheckedExpression,
SyntaxKind.UncheckedExpression))
{
// make sure the closing delimiter exists
if (RequiredDelimiterIsMissing(currentNode))
Expand Down Expand Up @@ -176,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;
Expand Down Expand Up @@ -244,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;
Expand Down Expand Up @@ -434,32 +447,50 @@ private static bool StatementClosingDelimiterIsMissing(SyntaxNode currentNode)
/// </list>
/// </returns>
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);

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 require a closing delimiter
return false;
// Type of node does not have delimiters used by this feature
return default;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -1894,8 +1939,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)]
Expand Down Expand Up @@ -2316,6 +2371,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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,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)
Expand Down

0 comments on commit 2c2b9c9

Please sign in to comment.