File: Extensions\RazorCodeDocumentExtensions_ClassifiedSpans.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.Extensions.ObjectPool;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
internal static partial class RazorCodeDocumentExtensions
{
    private enum SpanKind
    {
        Transition,
        MetaCode,
        Comment,
        Code,
        Markup,
        None
    }
 
    private record struct ClassifiedSpan(SourceSpan Span, SpanKind Kind);
 
    private sealed class ClassifiedSpanVisitor : SyntaxWalker, IPoolableObject
    {
        private enum BlockKind
        {
            // Code
            Statement,
            Directive,
            Expression,
 
            // Markup
            Markup,
            Template,
 
            // Special
            Comment,
            Tag,
            HtmlComment
        }
 
        // Significantly larger than DefaultPool.MaximumObjectSize as there shouldn't be much concurrency
        // of these arrays (we limit the number of pooled items to 5) and they are commonly large
        public const int MaximumObjectSize = DefaultPool.DefaultMaximumObjectSize * 32;
 
        private static readonly ObjectPool<ClassifiedSpanVisitor> s_pool = DefaultPool.Create(static () => new ClassifiedSpanVisitor(), poolSize: 5);
 
        private readonly ImmutableArray<ClassifiedSpan>.Builder _spans;
 
        private RazorSourceDocument _source;
        private BlockKind _currentBlockKind;
 
        private ClassifiedSpanVisitor()
        {
            _spans = ImmutableArray.CreateBuilder<ClassifiedSpan>();
            _source = null!;
        }
 
        private void Initialize(RazorSourceDocument source)
        {
            _source = source;
            _currentBlockKind = BlockKind.Markup;
        }
 
        public static ImmutableArray<ClassifiedSpan> VisitRoot(RazorSyntaxTree syntaxTree)
        {
            using var _ = s_pool.GetPooledObject(out var visitor);
 
            visitor.Initialize(syntaxTree.Source);
            visitor.Visit(syntaxTree.Root);
 
            return visitor.GetSpansAndClear();
        }
 
        private ImmutableArray<ClassifiedSpan> GetSpansAndClear()
            => _spans.ToImmutableAndClear();
 
        public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node)
        {
            using (CommentBlock())
            {
                AddSpan(node.StartCommentTransition, SpanKind.Transition);
                AddSpan(node.StartCommentStar, SpanKind.MetaCode);
 
                var comment = node.Comment;
 
                if (comment.IsMissing)
                {
                    // We need to generate a classified span at this position. So insert a marker in its place.
                    comment = SyntaxFactory.Token(SyntaxKind.Marker, parent: node, position: node.StartCommentStar.EndPosition);
                }
 
                AddSpan(comment, SpanKind.Comment);
 
                AddSpan(node.EndCommentStar, SpanKind.MetaCode);
                AddSpan(node.EndCommentTransition, SpanKind.Transition);
            }
        }
 
        public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node)
        {
            if (node.Parent is CSharpStatementBodySyntax or
                               CSharpExplicitExpressionBodySyntax or
                               CSharpImplicitExpressionBodySyntax or
                               RazorDirectiveBodySyntax ||
                (_currentBlockKind == BlockKind.Directive && node.Children is [CSharpStatementLiteralSyntax]))
            {
                base.VisitCSharpCodeBlock(node);
                return;
            }
 
            using (StatementBlock())
            {
                base.VisitCSharpCodeBlock(node);
            }
        }
 
        public override void VisitCSharpStatement(CSharpStatementSyntax node)
        {
            using (StatementBlock())
            {
                base.VisitCSharpStatement(node);
            }
        }
 
        public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node)
        {
            using (ExpressionBlock())
            {
                base.VisitCSharpExplicitExpression(node);
            }
        }
 
        public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node)
        {
            using (ExpressionBlock())
            {
                base.VisitCSharpImplicitExpression(node);
            }
        }
 
        public override void VisitRazorUsingDirective(RazorUsingDirectiveSyntax node)
        {
            using (DirectiveBlock())
            {
                base.VisitRazorUsingDirective(node);
            }
        }
 
        public override void VisitRazorDirective(RazorDirectiveSyntax node)
        {
            using (DirectiveBlock())
            {
                base.VisitRazorDirective(node);
            }
        }
 
        public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node)
        {
            using (TemplateBlock())
            {
                base.VisitCSharpTemplateBlock(node);
            }
        }
 
        public override void VisitMarkupBlock(MarkupBlockSyntax node)
        {
            using (MarkupBlock())
            {
                base.VisitMarkupBlock(node);
            }
        }
 
        public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttributeValueSyntax node)
        {
            // We don't generate a classified span when the attribute value is a simple literal value.
            // This is done so we maintain the classified spans generated in 2.x which
            // used ConditionalAttributeCollapser (combines markup literal attribute values into one span with no block parent).
            if (!IsSimpleLiteralValue(node))
            {
                base.VisitMarkupTagHelperAttributeValue(node);
                return;
            }
 
            using (MarkupBlock())
            {
                base.VisitMarkupTagHelperAttributeValue(node);
            }
 
            static bool IsSimpleLiteralValue(MarkupTagHelperAttributeValueSyntax node)
            {
                return node.Children is [MarkupDynamicAttributeValueSyntax] or { Count: > 1 };
            }
        }
 
        public override void VisitMarkupStartTag(MarkupStartTagSyntax node)
        {
            using (TagBlock())
            {
                var children = SyntaxUtilities.GetRewrittenMarkupStartTagChildren(node, includeEditHandler: true);
                foreach (var child in children)
                {
                    Visit(child);
                }
            }
        }
 
        public override void VisitMarkupEndTag(MarkupEndTagSyntax node)
        {
            using (TagBlock())
            {
                var children = SyntaxUtilities.GetRewrittenMarkupEndTagChildren(node, includeEditHandler: true);
 
                foreach (var child in children)
                {
                    Visit(child);
                }
            }
        }
 
        public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node)
        {
            using (TagBlock())
            {
                base.VisitMarkupTagHelperElement(node);
            }
        }
 
        public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node)
        {
            foreach (var child in node.Attributes)
            {
                if (child is MarkupTagHelperAttributeSyntax or
                             MarkupTagHelperDirectiveAttributeSyntax or
                             MarkupMinimizedTagHelperDirectiveAttributeSyntax)
                {
                    Visit(child);
                }
            }
        }
 
        public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node)
        {
            // We don't want to generate a classified span for a tag helper end tag. Do nothing.
        }
 
        public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
        {
            using (MarkupBlock())
            {
                // For attributes, we add a single span from the start of the name prefix to the end of the value prefix.
                var spanComputer = new SpanComputer();
                spanComputer.Add(node.NamePrefix);
                spanComputer.Add(node.Name);
                spanComputer.Add(node.NameSuffix);
                spanComputer.Add(node.EqualsToken);
                spanComputer.Add(node.ValuePrefix);
 
                var sourceSpan = spanComputer.ToSourceSpan(_source);
 
                AddSpan(sourceSpan, SpanKind.Markup);
 
                // Visit the value and value suffix separately.
                Visit(node.Value);
                Visit(node.ValueSuffix);
            }
        }
 
        public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node)
        {
            Visit(node.Value);
        }
 
        public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirectiveAttributeSyntax node)
        {
            Visit(node.Transition);
            Visit(node.Colon);
            Visit(node.Value);
        }
 
        public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinimizedTagHelperDirectiveAttributeSyntax node)
        {
            Visit(node.Transition);
            Visit(node.Colon);
        }
 
        public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node)
        {
            using (MarkupBlock())
            {
                // For minimized attributes, we add a single span for the attribute name along with the name prefix.
                var spanComputer = new SpanComputer();
                spanComputer.Add(node.NamePrefix);
                spanComputer.Add(node.Name);
 
                var sourceSpan = spanComputer.ToSourceSpan(_source);
 
                AddSpan(sourceSpan, SpanKind.Markup);
            }
        }
 
        public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node)
        {
            using (HtmlCommentBlock())
            {
                base.VisitMarkupCommentBlock(node);
            }
        }
 
        public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node)
        {
            using (MarkupBlock())
            {
                base.VisitMarkupDynamicAttributeValue(node);
            }
        }
 
        public override void VisitRazorMetaCode(RazorMetaCodeSyntax node)
        {
            AddSpan(node, SpanKind.MetaCode);
            base.VisitRazorMetaCode(node);
        }
 
        public override void VisitCSharpTransition(CSharpTransitionSyntax node)
        {
            AddSpan(node, SpanKind.Transition);
            base.VisitCSharpTransition(node);
        }
 
        public override void VisitMarkupTransition(MarkupTransitionSyntax node)
        {
            AddSpan(node, SpanKind.Transition);
            base.VisitMarkupTransition(node);
        }
 
        public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node)
        {
            AddSpan(node, SpanKind.Code);
            base.VisitCSharpStatementLiteral(node);
        }
 
        public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node)
        {
            AddSpan(node, SpanKind.Code);
            base.VisitCSharpExpressionLiteral(node);
        }
 
        public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node)
        {
            AddSpan(node, SpanKind.Code);
            base.VisitCSharpEphemeralTextLiteral(node);
        }
 
        public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node)
        {
            AddSpan(node, SpanKind.None);
            base.VisitUnclassifiedTextLiteral(node);
        }
 
        public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
        {
            AddSpan(node, SpanKind.Markup);
            base.VisitMarkupLiteralAttributeValue(node);
        }
 
        public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node)
        {
            if (node.Parent is MarkupLiteralAttributeValueSyntax)
            {
                base.VisitMarkupTextLiteral(node);
                return;
            }
 
            AddSpan(node, SpanKind.Markup);
            base.VisitMarkupTextLiteral(node);
        }
 
        public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node)
        {
            AddSpan(node, SpanKind.Markup);
            base.VisitMarkupEphemeralTextLiteral(node);
        }
 
        private BlockSaver CommentBlock()
            => Block(BlockKind.Comment);
 
        private BlockSaver DirectiveBlock()
            => Block(BlockKind.Directive);
 
        private BlockSaver ExpressionBlock()
            => Block(BlockKind.Expression);
 
        private BlockSaver HtmlCommentBlock()
            => Block(BlockKind.HtmlComment);
 
        private BlockSaver MarkupBlock()
            => Block(BlockKind.Markup);
 
        private BlockSaver StatementBlock()
            => Block(BlockKind.Statement);
 
        private BlockSaver TagBlock()
            => Block(BlockKind.Tag);
 
        private BlockSaver TemplateBlock()
            => Block(BlockKind.Template);
 
        private BlockSaver Block(BlockKind kind)
        {
            var saver = new BlockSaver(this);
 
            _currentBlockKind = kind;
 
            return saver;
        }
 
        private readonly ref struct BlockSaver(ClassifiedSpanVisitor visitor)
        {
            private readonly BlockKind _previousKind = visitor._currentBlockKind;
 
            public void Dispose()
            {
                visitor._currentBlockKind = _previousKind;
            }
        }
 
        private void AddSpan(SyntaxNode node, SpanKind kind)
        {
            if (node.IsMissing)
            {
                return;
            }
 
            var nodeSpan = node.GetSourceSpan(_source);
 
            AddSpan(nodeSpan, kind);
        }
 
        private void AddSpan(SyntaxToken token, SpanKind kind)
        {
            if (token.IsMissing)
            {
                return;
            }
 
            var tokenSpan = token.GetSourceSpan(_source);
 
            AddSpan(tokenSpan, kind);
        }
 
        private void AddSpan(SourceSpan span, SpanKind kind)
            => _spans.Add(new(span, kind));
 
        void IPoolableObject.Reset()
        {
            _spans.Clear();
 
            if (_spans.Capacity > MaximumObjectSize)
            {
                // Differs from ArrayBuilderPool.Policy's behavior as we allow our array to grow significantly larger
                _spans.Capacity = 0;
            }
 
            _source = null!;
            _currentBlockKind = BlockKind.Markup;
        }
    }
}