File: Structure\Providers\DisabledTextTriviaStructureProvider.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.Threading;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Structure;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Structure;
 
internal sealed class DisabledTextTriviaStructureProvider : AbstractSyntaxTriviaStructureProvider
{
    public override void CollectBlockSpans(
        SyntaxTrivia trivia,
        ArrayBuilder<BlockSpan> spans,
        BlockStructureOptions options,
        CancellationToken cancellationToken)
    {
        Contract.ThrowIfNull(trivia.SyntaxTree);
        CollectBlockSpans(trivia.SyntaxTree, trivia, spans, cancellationToken);
    }
 
    public static void CollectBlockSpans(
        SyntaxTree syntaxTree, SyntaxTrivia trivia,
        ArrayBuilder<BlockSpan> spans, CancellationToken cancellationToken)
    {
        // We'll always be leading trivia of some token.
        var startPos = trivia.FullSpan.Start;
 
        var parentTriviaList = trivia.Token.LeadingTrivia;
        var indexInParent = parentTriviaList.IndexOf(trivia);
 
        // Note: in some error cases (for example when all future tokens end up being skipped)
        // the parser may end up attaching pre-processor directives as trailing trivia to a 
        // preceding token.
        if (indexInParent < 0)
        {
            parentTriviaList = trivia.Token.TrailingTrivia;
            indexInParent = parentTriviaList.IndexOf(trivia);
        }
 
        if (indexInParent <= 0)
        {
            return;
        }
 
        if (!parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.IfDirectiveTrivia) &&
            !parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.ElifDirectiveTrivia) &&
            !parentTriviaList[indexInParent - 1].IsKind(SyntaxKind.ElseDirectiveTrivia))
        {
            return;
        }
 
        var endTrivia = GetCorrespondingEndTrivia(trivia, parentTriviaList, indexInParent);
        var endPos = GetEndPositionExludingLastNewLine(syntaxTree, endTrivia, cancellationToken);
 
        var span = TextSpan.FromBounds(startPos, endPos);
        spans.Add(new BlockSpan(
            isCollapsible: true,
            textSpan: span,
            type: BlockTypes.PreprocessorRegion,
            bannerText: CSharpStructureHelpers.Ellipsis,
            autoCollapse: true));
    }
 
    private static int GetEndPositionExludingLastNewLine(SyntaxTree syntaxTree, SyntaxTrivia trivia, CancellationToken cancellationToken)
    {
        var endPos = trivia.FullSpan.End;
        var text = syntaxTree.GetText(cancellationToken);
        return endPos >= 2 && text[endPos - 1] == '\n' && text[endPos - 2] == '\r' ? endPos - 2 :
               endPos >= 1 && SyntaxFacts.IsNewLine(text[endPos - 1]) ? endPos - 1 : endPos;
    }
 
    private static SyntaxTrivia GetCorrespondingEndTrivia(
        SyntaxTrivia trivia, SyntaxTriviaList triviaList, int index)
    {
        // Look through our parent token's trivia, to extend the span to the end of the last
        // disabled trivia.
        //
        // The issue is that if there are other pre-processor directives (like #regions or
        // #lines) mixed in the disabled code, they will be interleaved.  Keep walking past
        // them to the next thing that will actually end a disabled block. When we encounter
        // one, we must also consider which opening block they end. In case of nested pre-processor
        // directives, the inner most end block should match the inner most open block and so on.
 
        var nestedIfDirectiveTrivia = 0;
        for (var i = index; i < triviaList.Count; i++)
        {
            var currentTrivia = triviaList[i];
            switch (currentTrivia.Kind())
            {
                case SyntaxKind.IfDirectiveTrivia:
                    // Hit a nested #if directive.  Keep track of this so we can ensure
                    // that our actual disabled region reached the right end point.
                    nestedIfDirectiveTrivia++;
                    continue;
 
                case SyntaxKind.EndIfDirectiveTrivia:
                    if (nestedIfDirectiveTrivia > 0)
                    {
                        // This #endif corresponded to a nested #if, pop our stack
                        // and keep searching.
                        nestedIfDirectiveTrivia--;
                        continue;
                    }
 
                    // Found an #endif corresponding to our original #if/#elif/#else region we
                    // started with. Mark up to the trivia before this as the range to collapse.
                    return triviaList[i - 1];
 
                case SyntaxKind.ElseDirectiveTrivia:
                case SyntaxKind.ElifDirectiveTrivia:
                    if (nestedIfDirectiveTrivia > 0)
                    {
                        // This #else/#elif corresponded to a nested #if, ignore as
                        // they're not relevant to the original construct we started 
                        // on.
                        continue;
                    }
 
                    // We found the next #else/#elif corresponding to our original #if/#elif/#else
                    // region we started with. Mark up to the trivia before this as the range
                    // to collapse.
                    return triviaList[i - 1];
            }
        }
 
        // Couldn't find a future trivia to collapse up to.  Just collapse the original 
        // disabled text trivia we started with.
        return trivia;
    }
}