File: Structure\Providers\BlockSyntaxStructureProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Structure;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Structure;
 
internal sealed class BlockSyntaxStructureProvider : AbstractSyntaxNodeStructureProvider<BlockSyntax>
{
    protected override void CollectBlockSpans(
        SyntaxToken previousToken,
        BlockSyntax node,
        ArrayBuilder<BlockSpan> spans,
        BlockStructureOptions options,
        CancellationToken cancellationToken)
    {
        var parent = node.GetRequiredParent();
        var parentKind = parent.Kind();
 
        // For most types of statements, just consider the block 'attached' to the parent node.  That means we'll
        // show the parent node header when doing things like hovering over the indent guide.
        //
        // This also works nicely as the close brace for these constructs will always align with the start of these
        // statements.
 
        // If we're on the topmost if-statement in a chain if/else-if/else-if/else, then all the else-if and
        // final-else blocks as subheadings as well.  This is so that sticky scroll knows what to show when we're
        // nested within one of the deeper elses.
        if (parent is IfStatementSyntax { Parent: not ElseClauseSyntax } ifStatement)
        {
            using var subHeadings = TemporaryArray<(TextSpan textSpan, TextSpan hintSpan, string type)>.Empty;
 
            for (var currentElse = ifStatement.Else; currentElse != null;)
            {
                StatementSyntax elseStatement;
                if (currentElse.Statement is IfStatementSyntax nextIfStatement)
                {
                    elseStatement = nextIfStatement.Statement;
                    currentElse = nextIfStatement.Else;
                }
                else
                {
                    elseStatement = currentElse.Statement;
                    currentElse = null;
                }
 
                if (elseStatement is BlockSyntax { IsMissing: false } elseBlock)
                    subHeadings.Add((GetTextSpan(elseBlock), GetHintSpan(elseBlock), BlockTypes.Conditional));
            }
 
            spans.Add(new BlockSpan(
                BlockTypes.Conditional,
                isCollapsible: true,
                GetTextSpan(node),
                GetHintSpan(node),
                subHeadings.ToImmutableAndClear(),
                autoCollapse: false));
        }
        else if (parent is TryStatementSyntax tryStatement)
        {
            using var subHeadings = TemporaryArray<(TextSpan textSpan, TextSpan hintSpan, string type)>.Empty;
 
            foreach (var catchClause in tryStatement.Catches)
            {
                if (!catchClause.Block.IsMissing)
                    subHeadings.Add((GetTextSpan(catchClause.Block), GetHintSpan(catchClause.Block), BlockTypes.Statement));
            }
 
            if (tryStatement.Finally?.Block is { IsMissing: false } finallyBlock)
                subHeadings.Add((GetTextSpan(finallyBlock), GetHintSpan(finallyBlock), BlockTypes.Statement));
 
            spans.Add(new BlockSpan(
                BlockTypes.Statement,
                isCollapsible: true,
                GetTextSpan(node),
                GetHintSpan(node),
                subHeadings.ToImmutableAndClear(),
                autoCollapse: false));
        }
        else if (parentKind == SyntaxKind.ElseClause || IsNonBlockStatement(parent))
        {
            var autoCollapse = false;
 
            // Treat a local function at the top level as if it was a definition.  Similarly, if the user has asked
            // to always collapse local functions, then respect that option.
            if (parentKind == SyntaxKind.LocalFunctionStatement)
            {
                autoCollapse =
                    options.CollapseLocalFunctionsWhenCollapsingToDefinitions ||
                    parent.IsParentKind(SyntaxKind.GlobalStatement);
            }
 
            var type = GetType(parent);
            if (type != null)
            {
                spans.Add(new BlockSpan(
                    isCollapsible: true,
                    textSpan: GetTextSpan(node),
                    hintSpan: GetHintSpan(node),
                    type: type,
                    autoCollapse: autoCollapse));
            }
        }
 
        // Nested blocks aren't attached to anything.  Just collapse them as is.
        // Switch sections are also special.  Say you have the following:
        //
        //      case 0:
        //          {
        //          
        //          }
        //
        // We don't want to consider the block parented by the case, because 
        // that would cause us to draw the following:
        // 
        //      case 0:
        //      |   {
        //      |   
        //      |   }
        //
        // Which would obviously be wonky.  So in this case, we just use the
        // spanof the block alone, without consideration for the case clause.
        if (parentKind is SyntaxKind.Block or SyntaxKind.SwitchSection)
        {
            var type = GetType(parent);
            if (type != null)
            {
                spans.Add(new BlockSpan(
                    isCollapsible: true,
                    textSpan: node.Span,
                    hintSpan: node.Span,
                    type: type));
            }
        }
    }
 
    private static bool IsNonBlockStatement(SyntaxNode node)
        => node is StatementSyntax(kind: not SyntaxKind.Block);
 
    private static TextSpan GetHintSpan(BlockSyntax node)
    {
        var parent = node.GetRequiredParent();
        if (parent.IsKind(SyntaxKind.IfStatement) && parent.IsParentKind(SyntaxKind.ElseClause))
        {
            parent = parent.GetRequiredParent();
        }
 
        var start = parent.Span.Start;
        var end = GetEnd(node);
        return TextSpan.FromBounds(start, end);
    }
 
    private static TextSpan GetTextSpan(BlockSyntax node)
    {
        var previousToken = node.GetFirstToken().GetPreviousToken();
        if (previousToken.IsKind(SyntaxKind.None))
        {
            return node.Span;
        }
 
        return TextSpan.FromBounds(previousToken.Span.End, GetEnd(node));
    }
 
    private static int GetEnd(BlockSyntax node)
    {
        if (node.Parent.IsKind(SyntaxKind.IfStatement))
        {
            // For an if-statement, just collapse up to the end of the block.
            // We don't want collapse the whole statement just for the 'true'
            // portion.  Also, while outlining might be ok, the Indent-Guide
            // would look very strange for nodes like:
            //
            //      if (goo)
            //      {
            //      }
            //      else
            //          return a ||
            //                 b;
            return node.Span.End;
        }
        else
        {
            // For all other constructs, we collapse up to the end of the parent
            // construct.
            return node.GetRequiredParent().Span.End;
        }
    }
 
    private static string? GetType(SyntaxNode? parent)
        => (parent?.Kind()) switch
        {
            SyntaxKind.ForStatement => BlockTypes.Loop,
            SyntaxKind.ForEachStatement => BlockTypes.Loop,
            SyntaxKind.ForEachVariableStatement => BlockTypes.Loop,
            SyntaxKind.WhileStatement => BlockTypes.Loop,
            SyntaxKind.DoStatement => BlockTypes.Loop,
            SyntaxKind.TryStatement => BlockTypes.Statement,
            SyntaxKind.CatchClause => BlockTypes.Statement,
            SyntaxKind.FinallyClause => BlockTypes.Statement,
            SyntaxKind.UnsafeStatement => BlockTypes.Statement,
            SyntaxKind.FixedStatement => BlockTypes.Statement,
            SyntaxKind.LockStatement => BlockTypes.Statement,
            SyntaxKind.UsingStatement => BlockTypes.Statement,
            SyntaxKind.IfStatement => BlockTypes.Conditional,
            SyntaxKind.ElseClause => BlockTypes.Conditional,
            SyntaxKind.SwitchSection => BlockTypes.Conditional,
            SyntaxKind.Block => BlockTypes.Statement,
            SyntaxKind.LocalFunctionStatement => BlockTypes.Statement,
            _ => null,
        };
}