|
// 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;
}
}
|