diff --git a/src/EditorFeatures/CSharpTest/Structure/ArrowExpressionClauseStructureTests.cs b/src/EditorFeatures/CSharpTest/Structure/ArrowExpressionClauseStructureTests.cs index 6c94cfeeaa124..b3d2faa1b6676 100644 --- a/src/EditorFeatures/CSharpTest/Structure/ArrowExpressionClauseStructureTests.cs +++ b/src/EditorFeatures/CSharpTest/Structure/ArrowExpressionClauseStructureTests.cs @@ -7,12 +7,13 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Structure; [Trait(Traits.Feature, Traits.Features.Outlining)] -public class ArrowExpressionClauseStructureTests : AbstractCSharpSyntaxNodeStructureTests +public sealed class ArrowExpressionClauseStructureTests : AbstractCSharpSyntaxNodeStructureTests { internal override AbstractSyntaxStructureProvider CreateProvider() => new ArrowExpressionClauseStructureProvider(); @@ -22,13 +23,13 @@ public async Task TestArrowExpressionClause_Method1() { await VerifyBlockSpansAsync( """ - class C - { - {|hintspan:void M(){|textspan: $$=> expression - ? trueCase - : falseCase;|}|}; - } - """, + class C + { + {|hintspan:void M(){|textspan: $$=> expression + ? trueCase + : falseCase;|}|}; + } + """, Region("textspan", "hintspan", CSharpStructureHelpers.Ellipsis, autoCollapse: true)); } @@ -196,4 +197,41 @@ void M() """, Region("textspan", "hintspan", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76820")] + public async Task TestArrowExpressionClause_DirectiveOutsideOfArrow() + { + await VerifyBlockSpansAsync( + """ + class C + { + #if true + {|hintspan:int M(){|textspan: $$=> + 0;|}|}; + #else + int M() => + 1; + #endif + } + """, + Region("textspan", "hintspan", CSharpStructureHelpers.Ellipsis, autoCollapse: true)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76820")] + public async Task TestArrowExpressionClause_DirectiveInsideOfArrow() + { + await VerifyBlockSpansAsync( + """ + class C + { + {|hintspan:int M(){|textspan: $$=> + #if true + 0; + #else + 1; + #endif|}|} + } + """, + Region("textspan", "hintspan", CSharpStructureHelpers.Ellipsis, autoCollapse: true)); + } } diff --git a/src/Features/CSharp/Portable/Structure/Providers/ArrowExpressionClauseStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/ArrowExpressionClauseStructureProvider.cs index 1f192ca31d718..7b96a7b77c167 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/ArrowExpressionClauseStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/ArrowExpressionClauseStructureProvider.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - +using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Structure; using Microsoft.CodeAnalysis.Text; @@ -22,11 +22,47 @@ protected override void CollectBlockSpans( BlockStructureOptions options, CancellationToken cancellationToken) { + var parent = node.GetRequiredParent(); + var end = GetEndPoint(); spans.Add(new BlockSpan( isCollapsible: true, - textSpan: TextSpan.FromBounds(previousToken.Span.End, node.Parent.Span.End), - hintSpan: node.Parent.Span, + textSpan: TextSpan.FromBounds(previousToken.Span.End, end), + hintSpan: TextSpan.FromBounds(parent.Span.Start, end), type: BlockTypes.Nonstructural, - autoCollapse: !node.IsParentKind(SyntaxKind.LocalFunctionStatement))); + autoCollapse: parent.Kind() != SyntaxKind.LocalFunctionStatement)); + + int GetEndPoint() + { + // If we have a directive that starts within the node we're collapsing, but ends outside of it, then we want + // to collapse all the directives along with the node itself since we're collapsing the node. + var endToken = parent.GetLastToken(); + var nextToken = endToken.GetNextToken(); + if (nextToken != default) + { + foreach (var trivia in nextToken.LeadingTrivia) + { + if (trivia.IsDirective) + { + var directive = (DirectiveTriviaSyntax)trivia.GetStructure()!; + var matchingDirectives = directive.GetMatchingConditionalDirectives(cancellationToken); + + // Check that: + // 1. The first directive is within the node we're collapsing. + // 2. All the directives end before the next construct starts. + // + // In that case, we want to collapse all the directives along with the node itself. + if (matchingDirectives.Length > 0 && + matchingDirectives[0].Span.Start >= parent.Span.Start && + matchingDirectives.All(d => d.Span.End <= nextToken.Span.Start)) + { + var lastDirective = matchingDirectives.Last(); + return lastDirective.Span.End; + } + } + } + } + + return parent.Span.End; + } } }