diff --git a/src/EditorFeatures/CSharpTest/InvertIf/InvertIfTests.cs b/src/EditorFeatures/CSharpTest/InvertIf/InvertIfTests.cs index 1248844667782ffe6f9897dc866c9bf8f422f79c..367c0c0a9b61e3fb9d2b9c9e14ad1e77fe18fa7d 100644 --- a/src/EditorFeatures/CSharpTest/InvertIf/InvertIfTests.cs +++ b/src/EditorFeatures/CSharpTest/InvertIf/InvertIfTests.cs @@ -479,6 +479,123 @@ void Goo() }"); } + [WorkItem(35525, "https://github.com/dotnet/roslyn/issues/35525")] + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInvertIf)] + public async Task TestMultiline_IfElseIfElseSelection1() + { + await TestInRegularAndScriptAsync( +@"class A +{ + void Goo() + { + [|if (a) + { + a(); + } + else if (b) + { + b(); + } + else + { + c(); + }|] + } +}", +@"class A +{ + void Goo() + { + if (!a) + { + if (b) + { + b(); + } + else + { + c(); + } + } + else + { + a(); + } + } +}"); + } + + [WorkItem(35525, "https://github.com/dotnet/roslyn/issues/35525")] + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInvertIf)] + public async Task TestMultiline_IfElseIfElseSelection2() + { + await TestInRegularAndScriptAsync( +@"class A +{ + void Goo() + { + [|if (a) + { + a(); + }|] + else if (b) + { + b(); + } + else + { + c(); + } + } +}", +@"class A +{ + void Goo() + { + if (!a) + { + if (b) + { + b(); + } + else + { + c(); + } + } + else + { + a(); + } + } +}"); + } + + [WorkItem(35525, "https://github.com/dotnet/roslyn/issues/35525")] + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInvertIf)] + public async Task TestMultilineMissing_IfElseIfElseSubSelection() + { + await TestMissingInRegularAndScriptAsync( +@"class A +{ + void Goo() + { + if (a) + { + a(); + } + [|else if (b) + { + b(); + } + else + { + c(); + }|] + } +}"); + } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInvertIf)] public async Task TestMultiline_IfElse() { diff --git a/src/EditorFeatures/CSharpTest/RefactoringHelpers/RefactoringHelpersTests.cs b/src/EditorFeatures/CSharpTest/RefactoringHelpers/RefactoringHelpersTests.cs index 4fc8cd96b88cf82927fcbbf8f988ad416bb0bbda..61320d80412d39fb87758bbf5b9ac7d824fa7b04 100644 --- a/src/EditorFeatures/CSharpTest/RefactoringHelpers/RefactoringHelpersTests.cs +++ b/src/EditorFeatures/CSharpTest/RefactoringHelpers/RefactoringHelpersTests.cs @@ -1187,5 +1187,83 @@ C LocalFunction(C c) await TestMissingAsync(testText); } #endregion + + #region Test Ifs + [WorkItem(35525, "https://github.com/dotnet/roslyn/issues/35525")] + [Fact] + public async Task TestMultiline_IfElseIfElseSelection1() + { + await TestAsync( +@"class A +{ + void Goo() + { + {|result:[|if (a) + { + a(); + }|] + else if (b) + { + b(); + } + else + { + c(); + }|} + } +}"); + } + + [WorkItem(35525, "https://github.com/dotnet/roslyn/issues/35525")] + [Fact] + public async Task TestMultiline_IfElseIfElseSelection2() + { + await TestAsync( +@"class A +{ + void Goo() + { + {|result:[|if (a) + { + a(); + } + else if (b) + { + b(); + } + else + { + c(); + }|]|} + } +}"); + } + + [WorkItem(35525, "https://github.com/dotnet/roslyn/issues/35525")] + [Fact] + public async Task TestMissingMultiline_IfElseIfElseSelection() + { + await TestMissingAsync( +@"class A +{ + void Goo() + { + if (a) + { + a(); + } + [|else if (b) + { + b(); + } + else + { + c(); + }|] + } +}"); + } + + #endregion } } diff --git a/src/Features/CSharp/Portable/InvertIf/CSharpInvertIfCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/InvertIf/CSharpInvertIfCodeRefactoringProvider.cs index 63ccde3de31e6a5559b61b1646192ba18d780e0c..f0241145ab74f68e7455b4709f2bbe96dcd29300 100644 --- a/src/Features/CSharp/Portable/InvertIf/CSharpInvertIfCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/InvertIf/CSharpInvertIfCodeRefactoringProvider.cs @@ -58,13 +58,6 @@ protected override StatementSyntax GetEmptyEmbeddedStatement() protected override StatementSyntax GetElseBody(IfStatementSyntax ifNode) => ifNode.Else.Statement; - protected override TextSpan GetHeaderSpan(IfStatementSyntax ifNode) - { - return TextSpan.FromBounds( - ifNode.IfKeyword.SpanStart, - ifNode.CloseParenToken.Span.End); - } - protected override bool CanControlFlowOut(SyntaxNode node) { switch (node.Kind()) diff --git a/src/Features/Core/Portable/CodeRefactorings/AbstractRefactoringHelpersService.cs b/src/Features/Core/Portable/CodeRefactorings/AbstractRefactoringHelpersService.cs index da351ca75cae8d22a1972f00b2c5195f3f7cde81..831796008ef0a1877ebcbadb32ac409afee96b09 100644 --- a/src/Features/Core/Portable/CodeRefactorings/AbstractRefactoringHelpersService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/AbstractRefactoringHelpersService.cs @@ -13,6 +13,9 @@ namespace Microsoft.CodeAnalysis.CodeRefactorings { internal abstract class AbstractRefactoringHelpersService : IRefactoringHelpersService { + public async Task TryGetSelectedNodeAsync(CodeRefactoringContext context) where TSyntaxNode : SyntaxNode + => await TryGetSelectedNodeAsync(context.Document, context.Span, context.CancellationToken).ConfigureAwait(false); + public async Task TryGetSelectedNodeAsync( Document document, TextSpan selection, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode { @@ -389,6 +392,12 @@ protected virtual IEnumerable ExtractNodesIfInHeader(SyntaxNode root { yield return localDeclaration; } + + // Header: `if(...)`{ }; + if (syntaxFacts.IsOnIfStatementHeader(root, location, out var ifStatement)) + { + yield return ifStatement; + } } } } diff --git a/src/Features/Core/Portable/CodeRefactorings/IRefactoringHelpersService.cs b/src/Features/Core/Portable/CodeRefactorings/IRefactoringHelpersService.cs index f4b9d4b047c6c7982ae00d0c5598d4f9a5799024..21a2616012176fbb7de2516e84ca86e62240e97a 100644 --- a/src/Features/Core/Portable/CodeRefactorings/IRefactoringHelpersService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/IRefactoringHelpersService.cs @@ -34,5 +34,6 @@ internal interface IRefactoringHelpersService : ILanguageService /// /// Task TryGetSelectedNodeAsync(Document document, TextSpan selection, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode; + Task TryGetSelectedNodeAsync(CodeRefactoringContext context) where TSyntaxNode : SyntaxNode; } } diff --git a/src/Features/Core/Portable/InvertIf/AbstractInvertIfCodeRefactoringProvider.cs b/src/Features/Core/Portable/InvertIf/AbstractInvertIfCodeRefactoringProvider.cs index b20a0bf03ede9ec8505d108bcd0e8ba873b5a6dc..b8e8850705fb2357f091628cf2cd0a3e21a1a71f 100644 --- a/src/Features/Core/Portable/InvertIf/AbstractInvertIfCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/InvertIf/AbstractInvertIfCodeRefactoringProvider.cs @@ -37,30 +37,14 @@ private enum InvertIfStyle public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { - var textSpan = context.Span; - if (!textSpan.IsEmpty) - { - return; - } - var document = context.Document; var cancellationToken = context.CancellationToken; var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(textSpan.Start); - var ifNode = token.GetAncestor(); - if (ifNode == null) - { - return; - } - - if (ifNode.OverlapsHiddenPosition(cancellationToken)) - { - return; - } + var helperService = document.GetLanguageService(); + var ifNode = await helperService.TryGetSelectedNodeAsync(context).ConfigureAwait(false); - var headerSpan = GetHeaderSpan(ifNode); - if (!headerSpan.IntersectsWith(textSpan)) + if (ifNode == null || ifNode.OverlapsHiddenPosition(cancellationToken)) { return; } @@ -417,7 +401,6 @@ private ImmutableArray GetSubsequentStatementRanges(TIfStatement protected abstract StatementRange GetIfBodyStatementRange(TIfStatementSyntax ifNode); protected abstract SyntaxNode GetCondition(TIfStatementSyntax ifNode); - protected abstract TextSpan GetHeaderSpan(TIfStatementSyntax ifNode); protected abstract IEnumerable UnwrapBlock(TEmbeddedStatement ifBody); protected abstract TEmbeddedStatement GetIfBody(TIfStatementSyntax ifNode); diff --git a/src/Features/VisualBasic/Portable/InvertIf/VisualBasicInvertIfCodeRefactoringProvider.MultiLine.vb b/src/Features/VisualBasic/Portable/InvertIf/VisualBasicInvertIfCodeRefactoringProvider.MultiLine.vb index d4a6210c3dee284e91f9db0f1a4413c3e8aeacb1..b498a2b94e10e84316662dc1bcb994c9e210b934 100644 --- a/src/Features/VisualBasic/Portable/InvertIf/VisualBasicInvertIfCodeRefactoringProvider.MultiLine.vb +++ b/src/Features/VisualBasic/Portable/InvertIf/VisualBasicInvertIfCodeRefactoringProvider.MultiLine.vb @@ -14,12 +14,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.InvertIf Public Sub New() End Sub - Protected Overrides Function GetHeaderSpan(ifNode As MultiLineIfBlockSyntax) As TextSpan - Return TextSpan.FromBounds( - ifNode.IfStatement.IfKeyword.SpanStart, - ifNode.IfStatement.Condition.Span.End) - End Function - Protected Overrides Function IsElseless(ifNode As MultiLineIfBlockSyntax) As Boolean Return ifNode.ElseBlock Is Nothing End Function diff --git a/src/Features/VisualBasic/Portable/InvertIf/VisualBasicInvertIfCodeRefactoringProvider.SingleLine.vb b/src/Features/VisualBasic/Portable/InvertIf/VisualBasicInvertIfCodeRefactoringProvider.SingleLine.vb index 68cfa6594db90645075c2c9690f2fb896b549b59..85a707dec5773b4b3ab7d7fa1a9724f67bd6017a 100644 --- a/src/Features/VisualBasic/Portable/InvertIf/VisualBasicInvertIfCodeRefactoringProvider.SingleLine.vb +++ b/src/Features/VisualBasic/Portable/InvertIf/VisualBasicInvertIfCodeRefactoringProvider.SingleLine.vb @@ -14,12 +14,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.InvertIf Public Sub New() End Sub - Protected Overrides Function GetHeaderSpan(ifNode As SingleLineIfStatementSyntax) As TextSpan - Return TextSpan.FromBounds( - ifNode.IfKeyword.SpanStart, - ifNode.Condition.Span.End) - End Function - Protected Overrides Function IsElseless(ifNode As SingleLineIfStatementSyntax) As Boolean Return ifNode.ElseClause Is Nothing End Function diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index c8371a97dca810bbd3d4f060c41aa96421d185a6..8e24cb4e2d0e00cb4dcef4276a26124e101efd00 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs @@ -1757,6 +1757,18 @@ public bool IsOnLocalDeclarationHeader(SyntaxNode root, int position, out Syntax return IsOnHeader(position, node, node, holes: initializersExpressions); } + public bool IsOnIfStatementHeader(SyntaxNode root, int position, out SyntaxNode ifStatement) + { + var node = TryGetAncestorForLocation(position, root); + ifStatement = node; + if (ifStatement == null) + { + return false; + } + + return IsOnHeader(position, node, node.CloseParenToken); + } + public bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position) { var token = root.FindToken(position); diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs index 0a6e8cec4b537ae089bc38098f3e389c9fc85259..8bb0be504ea560ccb175fcf67e7143069b0a08b8 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs @@ -418,6 +418,7 @@ internal interface ISyntaxFactsService : ILanguageService bool IsOnMethodHeader(SyntaxNode root, int position, out SyntaxNode method); bool IsOnLocalFunctionHeader(SyntaxNode root, int position, out SyntaxNode localFunction); bool IsOnLocalDeclarationHeader(SyntaxNode root, int position, out SyntaxNode localDeclaration); + bool IsOnIfStatementHeader(SyntaxNode root, int position, out SyntaxNode ifStatement); bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position); // Walks the tree, starting from contextNode, looking for the first construct diff --git a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb index 988710f88b4488732f9c3a1976d73af204309b04..7610677f7485e998a8c6e3cab47854739154d431 100644 --- a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb +++ b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb @@ -1796,6 +1796,24 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return IsOnHeader(position, node, node, initializersExpressions) End Function + Public Function IsOnIfStatementHeader(root As SyntaxNode, position As Integer, ByRef ifStatement As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsOnIfStatementHeader + ifStatement = Nothing + + Dim multipleLineNode = TryGetAncestorForLocation(Of MultiLineIfBlockSyntax)(position, root) + If multipleLineNode IsNot Nothing Then + ifStatement = multipleLineNode + Return IsOnHeader(position, multipleLineNode.IfStatement, multipleLineNode.IfStatement) + End If + + Dim singleLineNode = TryGetAncestorForLocation(Of SingleLineIfStatementSyntax)(position, root) + If singleLineNode IsNot Nothing Then + ifStatement = singleLineNode + Return IsOnHeader(position, singleLineNode, singleLineNode.Condition) + End If + + Return False + End Function + Public Function IsBetweenTypeMembers(sourceText As SourceText, root As SyntaxNode, position As Integer) As Boolean Implements ISyntaxFactsService.IsBetweenTypeMembers Dim token = root.FindToken(position) Dim typeDecl = token.GetAncestor(Of TypeBlockSyntax)