diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs deleted file mode 100644 index b8f38697657..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs +++ /dev/null @@ -1,397 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using Microsoft.AspNetCore.Razor.Language.Legacy; -using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.Extensions.ObjectPool; - -namespace Microsoft.AspNetCore.Razor.Language; - -internal class ClassifiedSpanVisitor : SyntaxWalker -{ - private static readonly ObjectPool.Builder> Pool = DefaultPool.Create(Policy.Instance, size: 5); - - private readonly RazorSourceDocument _source; - private readonly ImmutableArray.Builder _spans; - - private readonly Action _baseVisitCSharpCodeBlock; - private readonly Action _baseVisitCSharpStatement; - private readonly Action _baseVisitCSharpExplicitExpression; - private readonly Action _baseVisitCSharpImplicitExpression; - private readonly Action _baseVisitRazorDirective; - private readonly Action _baseVisitCSharpTemplateBlock; - private readonly Action _baseVisitMarkupBlock; - private readonly Action _baseVisitMarkupTagHelperAttributeValue; - private readonly Action _baseVisitMarkupTagHelperElement; - private readonly Action _baseVisitMarkupCommentBlock; - private readonly Action _baseVisitMarkupDynamicAttributeValue; - - private BlockKindInternal _currentBlockKind; - private SyntaxNode? _currentBlock; - - private ClassifiedSpanVisitor(RazorSourceDocument source, ImmutableArray.Builder spans) - { - _source = source; - _spans = spans; - - _baseVisitCSharpCodeBlock = base.VisitCSharpCodeBlock; - _baseVisitCSharpStatement = base.VisitCSharpStatement; - _baseVisitCSharpExplicitExpression = base.VisitCSharpExplicitExpression; - _baseVisitCSharpImplicitExpression = base.VisitCSharpImplicitExpression; - _baseVisitRazorDirective = base.VisitRazorDirective; - _baseVisitCSharpTemplateBlock = base.VisitCSharpTemplateBlock; - _baseVisitMarkupBlock = base.VisitMarkupBlock; - _baseVisitMarkupTagHelperAttributeValue = base.VisitMarkupTagHelperAttributeValue; - _baseVisitMarkupTagHelperElement = base.VisitMarkupTagHelperElement; - _baseVisitMarkupCommentBlock = base.VisitMarkupCommentBlock; - _baseVisitMarkupDynamicAttributeValue = base.VisitMarkupDynamicAttributeValue; - - _currentBlockKind = BlockKindInternal.Markup; - } - - public static ImmutableArray VisitRoot(RazorSyntaxTree syntaxTree) - { - using var _ = Pool.GetPooledObject(out var builder); - - var visitor = new ClassifiedSpanVisitor(syntaxTree.Source, builder); - visitor.Visit(syntaxTree.Root); - - return builder.ToImmutableAndClear(); - } - - public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) - { - WriteBlock(node, BlockKindInternal.Comment, razorCommentSyntax => - { - WriteSpan(razorCommentSyntax.StartCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); - WriteSpan(razorCommentSyntax.StartCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); - - var comment = razorCommentSyntax.Comment; - if (comment.IsMissing) - { - // We need to generate a classified span at this position. So insert a marker in its place. - comment = new(razorCommentSyntax, Syntax.InternalSyntax.SyntaxFactory.Token(SyntaxKind.Marker, string.Empty), razorCommentSyntax.StartCommentStar.EndPosition, index: 0); - } - - WriteSpan(comment, SpanKindInternal.Comment, AcceptedCharactersInternal.Any); - - WriteSpan(razorCommentSyntax.EndCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); - WriteSpan(razorCommentSyntax.EndCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); - }); - } - - public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node) - { - if (node.Parent is CSharpStatementBodySyntax || - node.Parent is CSharpExplicitExpressionBodySyntax || - node.Parent is CSharpImplicitExpressionBodySyntax || - node.Parent is RazorDirectiveBodySyntax || - (_currentBlockKind == BlockKindInternal.Directive && - node.Children.Count == 1 && - node.Children[0] is CSharpStatementLiteralSyntax)) - { - base.VisitCSharpCodeBlock(node); - return; - } - - WriteBlock(node, BlockKindInternal.Statement, _baseVisitCSharpCodeBlock); - } - - public override void VisitCSharpStatement(CSharpStatementSyntax node) - { - WriteBlock(node, BlockKindInternal.Statement, _baseVisitCSharpStatement); - } - - public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node) - { - WriteBlock(node, BlockKindInternal.Expression, _baseVisitCSharpExplicitExpression); - } - - public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node) - { - WriteBlock(node, BlockKindInternal.Expression, _baseVisitCSharpImplicitExpression); - } - - public override void VisitRazorDirective(RazorDirectiveSyntax node) - { - WriteBlock(node, BlockKindInternal.Directive, _baseVisitRazorDirective); - } - - public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node) - { - WriteBlock(node, BlockKindInternal.Template, _baseVisitCSharpTemplateBlock); - } - - public override void VisitMarkupBlock(MarkupBlockSyntax node) - { - WriteBlock(node, BlockKindInternal.Markup, _baseVisitMarkupBlock); - } - - 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 (node.Children.Count > 1 || - (node.Children.Count == 1 && node.Children[0] is MarkupDynamicAttributeValueSyntax)) - { - WriteBlock(node, BlockKindInternal.Markup, _baseVisitMarkupTagHelperAttributeValue); - return; - } - - base.VisitMarkupTagHelperAttributeValue(node); - } - - public override void VisitMarkupStartTag(MarkupStartTagSyntax node) - { - WriteBlock(node, BlockKindInternal.Tag, n => - { - var children = SyntaxUtilities.GetRewrittenMarkupStartTagChildren(node, includeEditHandler: true); - foreach (var child in children) - { - Visit(child); - } - }); - } - - public override void VisitMarkupEndTag(MarkupEndTagSyntax node) - { - WriteBlock(node, BlockKindInternal.Tag, n => - { - var children = SyntaxUtilities.GetRewrittenMarkupEndTagChildren(node, includeEditHandler: true); - foreach (var child in children) - { - Visit(child); - } - }); - } - - public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) - { - WriteBlock(node, BlockKindInternal.Tag, _baseVisitMarkupTagHelperElement); - } - - public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node) - { - foreach (var child in node.Attributes) - { - if (child is MarkupTagHelperAttributeSyntax || - child is MarkupTagHelperDirectiveAttributeSyntax || - child is 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) - { - WriteBlock(node, BlockKindInternal.Markup, n => - { - var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new SyntaxTokenList(node.EqualsToken), chunkGenerator: null); - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix); - Visit(mergedAttributePrefix); - 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) - { - WriteBlock(node, BlockKindInternal.Markup, n => - { - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name); - Visit(mergedAttributePrefix); - }); - } - - public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node) - { - WriteBlock(node, BlockKindInternal.HtmlComment, _baseVisitMarkupCommentBlock); - } - - public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node) - { - WriteBlock(node, BlockKindInternal.Markup, _baseVisitMarkupDynamicAttributeValue); - } - - public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) - { - WriteSpan(node, SpanKindInternal.MetaCode); - base.VisitRazorMetaCode(node); - } - - public override void VisitCSharpTransition(CSharpTransitionSyntax node) - { - WriteSpan(node, SpanKindInternal.Transition); - base.VisitCSharpTransition(node); - } - - public override void VisitMarkupTransition(MarkupTransitionSyntax node) - { - WriteSpan(node, SpanKindInternal.Transition); - base.VisitMarkupTransition(node); - } - - public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node) - { - WriteSpan(node, SpanKindInternal.Code); - base.VisitCSharpStatementLiteral(node); - } - - public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node) - { - WriteSpan(node, SpanKindInternal.Code); - base.VisitCSharpExpressionLiteral(node); - } - - public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node) - { - WriteSpan(node, SpanKindInternal.Code); - base.VisitCSharpEphemeralTextLiteral(node); - } - - public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node) - { - WriteSpan(node, SpanKindInternal.None); - base.VisitUnclassifiedTextLiteral(node); - } - - public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node) - { - WriteSpan(node, SpanKindInternal.Markup); - base.VisitMarkupLiteralAttributeValue(node); - } - - public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) - { - if (node.Parent is MarkupLiteralAttributeValueSyntax) - { - base.VisitMarkupTextLiteral(node); - return; - } - - WriteSpan(node, SpanKindInternal.Markup); - base.VisitMarkupTextLiteral(node); - } - - public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node) - { - WriteSpan(node, SpanKindInternal.Markup); - base.VisitMarkupEphemeralTextLiteral(node); - } - - private void WriteBlock(TNode node, BlockKindInternal kind, Action handler) where TNode : SyntaxNode - { - var previousBlock = _currentBlock; - var previousKind = _currentBlockKind; - - _currentBlock = node; - _currentBlockKind = kind; - - handler(node); - - _currentBlock = previousBlock; - _currentBlockKind = previousKind; - } - - private void WriteSpan(SyntaxNode node, SpanKindInternal kind, AcceptedCharactersInternal? acceptedCharacters = null) - { - if (node.IsMissing) - { - return; - } - - var spanSource = node.GetSourceSpan(_source); - var blockSource = _currentBlock.GetSourceSpan(_source); - if (!acceptedCharacters.HasValue) - { - acceptedCharacters = AcceptedCharactersInternal.Any; - var context = node.GetEditHandler(); - if (context != null) - { - acceptedCharacters = context.AcceptedCharacters; - } - } - - var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters.Value); - _spans.Add(span); - } - - private void WriteSpan(SyntaxToken token, SpanKindInternal kind, AcceptedCharactersInternal? acceptedCharacters = null) - { - if (token.IsMissing) - { - return; - } - - var spanSource = token.GetSourceSpan(_source); - var blockSource = _currentBlock.GetSourceSpan(_source); - if (!acceptedCharacters.HasValue) - { - acceptedCharacters = AcceptedCharactersInternal.Any; - var context = token.GetEditHandler(); - if (context != null) - { - acceptedCharacters = context.AcceptedCharacters; - } - } - - var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters.Value); - _spans.Add(span); - } - - private sealed class Policy : IPooledObjectPolicy.Builder> - { - public static readonly Policy Instance = new(); - - // 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.MaximumObjectSize * 32; - - private Policy() - { - } - - public ImmutableArray.Builder Create() => ImmutableArray.CreateBuilder(); - - public bool Return(ImmutableArray.Builder builder) - { - builder.Clear(); - - if (builder.Capacity > MaximumObjectSize) - { - // Differs from ArrayBuilderPool.Policy's behavior as we allow our array to grow significantly larger - builder.Capacity = 0; - } - - return true; - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs new file mode 100644 index 00000000000..c3cc20f56b6 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs @@ -0,0 +1,473 @@ +// 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 System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy; + +internal sealed class ClassifiedSpanVisitor : SyntaxWalker +{ + private static readonly ObjectPool Pool = DefaultPool.Create(Policy.Instance, size: 5); + + private readonly ImmutableArray.Builder _spans; + + private RazorSourceDocument _source = null!; + private SyntaxNode? _currentBlock; + private SourceSpan? _currentBlockSpan; + private BlockKindInternal _currentBlockKind; + + private ClassifiedSpanVisitor() + { + _spans = ImmutableArray.CreateBuilder(); + _source = null!; + } + + private void Initialize(RazorSourceDocument source) + { + _source = source; + _currentBlockKind = BlockKindInternal.Markup; + } + + public static ImmutableArray VisitRoot(RazorSyntaxTree syntaxTree) + { + using var _ = Pool.GetPooledObject(out var visitor); + + visitor.Initialize(syntaxTree.Source); + visitor.Visit(syntaxTree.Root); + + return visitor.GetSpansAndClear(); + } + + private ImmutableArray GetSpansAndClear() + => _spans.ToImmutableAndClear(); + + public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) + { + using (CommentBlock(node)) + { + AddSpan(node.StartCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); + AddSpan(node.StartCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + + 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, SpanKindInternal.Comment, AcceptedCharactersInternal.Any); + + AddSpan(node.EndCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + AddSpan(node.EndCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); + } + } + + public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node) + { + if (node.Parent is CSharpStatementBodySyntax or + CSharpExplicitExpressionBodySyntax or + CSharpImplicitExpressionBodySyntax or + RazorDirectiveBodySyntax || + (_currentBlockKind == BlockKindInternal.Directive && node.Children is [CSharpStatementLiteralSyntax])) + { + base.VisitCSharpCodeBlock(node); + return; + } + + using (StatementBlock(node)) + { + base.VisitCSharpCodeBlock(node); + } + } + + public override void VisitCSharpStatement(CSharpStatementSyntax node) + { + using (StatementBlock(node)) + { + base.VisitCSharpStatement(node); + } + } + + public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node) + { + using (ExpressionBlock(node)) + { + base.VisitCSharpExplicitExpression(node); + } + } + + public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node) + { + using (ExpressionBlock(node)) + { + base.VisitCSharpImplicitExpression(node); + } + } + + public override void VisitRazorDirective(RazorDirectiveSyntax node) + { + using (DirectiveBlock(node)) + { + base.VisitRazorDirective(node); + } + } + + public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node) + { + using (TemplateBlock(node)) + { + base.VisitCSharpTemplateBlock(node); + } + } + + public override void VisitMarkupBlock(MarkupBlockSyntax node) + { + using (MarkupBlock(node)) + { + 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(node)) + { + base.VisitMarkupTagHelperAttributeValue(node); + } + + static bool IsSimpleLiteralValue(MarkupTagHelperAttributeValueSyntax node) + { + return node.Children is [MarkupDynamicAttributeValueSyntax] or { Count: > 1 }; + } + } + + public override void VisitMarkupStartTag(MarkupStartTagSyntax node) + { + using (TagBlock(node)) + { + var children = SyntaxUtilities.GetRewrittenMarkupStartTagChildren(node, includeEditHandler: true); + foreach (var child in children) + { + Visit(child); + } + } + } + + public override void VisitMarkupEndTag(MarkupEndTagSyntax node) + { + using (TagBlock(node)) + { + var children = SyntaxUtilities.GetRewrittenMarkupEndTagChildren(node, includeEditHandler: true); + + foreach (var child in children) + { + Visit(child); + } + } + } + + public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) + { + using (TagBlock(node)) + { + 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(node)) + { + // 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, SpanKindInternal.Markup, AcceptedCharactersInternal.Any); + + // 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(node)) + { + // 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, SpanKindInternal.Markup, AcceptedCharactersInternal.Any); + } + } + + public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node) + { + using (HtmlCommentBlock(node)) + { + base.VisitMarkupCommentBlock(node); + } + } + + public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node) + { + using (MarkupBlock(node)) + { + base.VisitMarkupDynamicAttributeValue(node); + } + } + + public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) + { + AddSpan(node, SpanKindInternal.MetaCode); + base.VisitRazorMetaCode(node); + } + + public override void VisitCSharpTransition(CSharpTransitionSyntax node) + { + AddSpan(node, SpanKindInternal.Transition); + base.VisitCSharpTransition(node); + } + + public override void VisitMarkupTransition(MarkupTransitionSyntax node) + { + AddSpan(node, SpanKindInternal.Transition); + base.VisitMarkupTransition(node); + } + + public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node) + { + AddSpan(node, SpanKindInternal.Code); + base.VisitCSharpStatementLiteral(node); + } + + public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node) + { + AddSpan(node, SpanKindInternal.Code); + base.VisitCSharpExpressionLiteral(node); + } + + public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node) + { + AddSpan(node, SpanKindInternal.Code); + base.VisitCSharpEphemeralTextLiteral(node); + } + + public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node) + { + AddSpan(node, SpanKindInternal.None); + base.VisitUnclassifiedTextLiteral(node); + } + + public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node) + { + AddSpan(node, SpanKindInternal.Markup); + base.VisitMarkupLiteralAttributeValue(node); + } + + public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) + { + if (node.Parent is MarkupLiteralAttributeValueSyntax) + { + base.VisitMarkupTextLiteral(node); + return; + } + + AddSpan(node, SpanKindInternal.Markup); + base.VisitMarkupTextLiteral(node); + } + + public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node) + { + AddSpan(node, SpanKindInternal.Markup); + base.VisitMarkupEphemeralTextLiteral(node); + } + + private BlockSaver CommentBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Comment); + + private BlockSaver DirectiveBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Directive); + + private BlockSaver ExpressionBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Expression); + + private BlockSaver HtmlCommentBlock(SyntaxNode node) + => Block(node, BlockKindInternal.HtmlComment); + + private BlockSaver MarkupBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Markup); + + private BlockSaver StatementBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Statement); + + private BlockSaver TagBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Tag); + + private BlockSaver TemplateBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Template); + + private BlockSaver Block(SyntaxNode node, BlockKindInternal kind) + { + var saver = new BlockSaver(this); + + _currentBlock = node; + _currentBlockKind = kind; + + // This is a new block, so we reset the current block span. + // It will be computed when the first span is written. + _currentBlockSpan = null; + + return saver; + } + + private readonly ref struct BlockSaver(ClassifiedSpanVisitor visitor) + { + private readonly SyntaxNode? _previousBlock = visitor._currentBlock; + private readonly SourceSpan? _previousBlockSpan = visitor._currentBlockSpan; + private readonly BlockKindInternal _previousKind = visitor._currentBlockKind; + + public void Dispose() + { + visitor._currentBlock = _previousBlock; + visitor._currentBlockSpan = _previousBlockSpan; + visitor._currentBlockKind = _previousKind; + } + } + + private SourceSpan CurrentBlockSpan + => _currentBlockSpan ??= _currentBlock.AssumeNotNull().GetSourceSpan(_source); + + private void AddSpan(SyntaxNode node, SpanKindInternal kind) + { + if (node.IsMissing) + { + return; + } + + Debug.Assert(_currentBlock != null, "Current block should not be null when writing a span for a node."); + + var nodeSpan = node.GetSourceSpan(_source); + + var acceptedCharacters = node.GetEditHandler() is { } context + ? context.AcceptedCharacters + : AcceptedCharactersInternal.Any; + + AddSpan(nodeSpan, kind, acceptedCharacters); + } + + private void AddSpan(SyntaxToken token, SpanKindInternal kind, AcceptedCharactersInternal acceptedCharacters) + { + if (token.IsMissing) + { + return; + } + + Debug.Assert(_currentBlock != null, "Current block should not be null when writing a span for a token."); + + var tokenSpan = token.GetSourceSpan(_source); + + AddSpan(tokenSpan, kind, acceptedCharacters); + } + + private void AddSpan(SourceSpan span, SpanKindInternal kind, AcceptedCharactersInternal acceptedCharacters) + => _spans.Add(new(span, CurrentBlockSpan, kind, _currentBlockKind, acceptedCharacters)); + + private void Reset() + { + _spans.Clear(); + + if (_spans.Capacity > Policy.MaximumObjectSize) + { + // Differs from ArrayBuilderPool.Policy's behavior as we allow our array to grow significantly larger + _spans.Capacity = 0; + } + + _source = null!; + _currentBlock = null!; + _currentBlockSpan = null; + _currentBlockKind = BlockKindInternal.Markup; + } + + private sealed class Policy : IPooledObjectPolicy + { + public static readonly Policy Instance = new(); + + // 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.MaximumObjectSize * 32; + + private Policy() + { + } + + public ClassifiedSpanVisitor Create() => new(); + + public bool Return(ClassifiedSpanVisitor visitor) + { + visitor.Reset(); + + return true; + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RazorSyntaxTreeExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RazorSyntaxTreeExtensions.cs index 67b03324925..016dcc9e5bc 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RazorSyntaxTreeExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RazorSyntaxTreeExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Immutable; namespace Microsoft.AspNetCore.Razor.Language.Legacy; @@ -10,20 +9,14 @@ internal static class RazorSyntaxTreeExtensions { public static ImmutableArray GetClassifiedSpans(this RazorSyntaxTree syntaxTree) { - if (syntaxTree == null) - { - throw new ArgumentNullException(nameof(syntaxTree)); - } + ArgHelper.ThrowIfNull(syntaxTree); return ClassifiedSpanVisitor.VisitRoot(syntaxTree); } public static ImmutableArray GetTagHelperSpans(this RazorSyntaxTree syntaxTree) { - if (syntaxTree == null) - { - throw new ArgumentNullException(nameof(syntaxTree)); - } + ArgHelper.ThrowIfNull(syntaxTree); return TagHelperSpanVisitor.VisitRoot(syntaxTree); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperSpanVisitor.cs similarity index 89% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperSpanVisitor.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperSpanVisitor.cs index f812a1c240a..f680a1a00d9 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperSpanVisitor.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; -using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; -namespace Microsoft.AspNetCore.Razor.Language; +namespace Microsoft.AspNetCore.Razor.Language.Legacy; -internal class TagHelperSpanVisitor : SyntaxWalker +internal sealed class TagHelperSpanVisitor : SyntaxWalker { private readonly RazorSourceDocument _source; private readonly ImmutableArray.Builder _spans; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SpanComputer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SpanComputer.cs new file mode 100644 index 00000000000..b450434c1bc --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SpanComputer.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.AspNetCore.Razor.Language; + +/// +/// Helper that can be used to efficiently build up a or +/// from a set of syntax tokens. +/// +internal ref struct SpanComputer() +{ + private SyntaxToken _firstToken; + private SyntaxToken _lastToken; + + public void Add(SyntaxToken token) + { + if (token.Kind == SyntaxKind.None) + { + return; + } + + if (_firstToken.Kind == SyntaxKind.None) + { + _firstToken = token; + } + + _lastToken = token; + } + + public void Add(SyntaxTokenList tokenList) + { + if (tokenList.Count == 0) + { + return; + } + + if (_firstToken.Kind == SyntaxKind.None) + { + _firstToken = tokenList[0]; + } + + _lastToken = tokenList[^1]; + } + + public void Add(SyntaxTokenList? tokenList) + { + if (tokenList is not [_, ..] tokens) + { + return; + } + + if (_firstToken.Kind == SyntaxKind.None) + { + _firstToken = tokens[0]; + } + + _lastToken = tokens[^1]; + } + + public void Add(CSharpEphemeralTextLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(CSharpExpressionLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(CSharpStatementLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(MarkupEphemeralTextLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(MarkupTextLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(UnclassifiedTextLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public readonly SourceSpan ToSourceSpan(RazorSourceDocument source) + { + if (_firstToken.Kind == SyntaxKind.None) + { + return default; + } + + Debug.Assert(_lastToken.Kind != SyntaxKind.None, "Last token should not be None when first token is set."); + + var start = _firstToken.Span.Start; + var end = _lastToken.Span.End; + + Debug.Assert(start <= end, "Start position should not be greater than end position."); + + var length = end - start; + + var text = source.Text; + var startLinePosition = text.Lines.GetLinePosition(start); + var endLinePosition = text.Lines.GetLinePosition(end); + var lineCount = endLinePosition.Line - startLinePosition.Line; + + return new SourceSpan(source.FilePath, absoluteIndex: start, startLinePosition.Line, startLinePosition.Character, length, lineCount, endLinePosition.Character); + } + + public readonly TextSpan ToTextSpan() + { + if (_firstToken.Kind == SyntaxKind.None) + { + return default; + } + + Debug.Assert(_lastToken.Kind != SyntaxKind.None, "Last token should not be None when first token is set."); + + var start = _firstToken.Span.Start; + var end = _lastToken.Span.End; + + Debug.Assert(start <= end, "Start position should not be greater than end position."); + + return TextSpan.FromBounds(start, end); + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxFactory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxFactory.cs index 34f99b4ccc4..51506537b4d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxFactory.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxFactory.cs @@ -10,19 +10,29 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax; internal static partial class SyntaxFactory { public static SyntaxToken Token(SyntaxKind kind, params RazorDiagnostic[] diagnostics) - { - return Token(kind, content: string.Empty, diagnostics: diagnostics); - } + => Token(kind, content: string.Empty, parent: null, position: 0, index: 0, diagnostics: diagnostics); public static SyntaxToken Token(SyntaxKind kind, string content, params RazorDiagnostic[] diagnostics) - { - return new SyntaxToken(parent: null, InternalSyntax.SyntaxFactory.Token(kind, content, diagnostics), position: 0, index: 0); - } + => Token(kind, content, parent: null, position: 0, index: 0, diagnostics); + + public static SyntaxToken Token( + SyntaxKind kind, SyntaxNode? parent, int position, params RazorDiagnostic[] diagnostics) + => Token(kind, string.Empty, parent, position, index: 0, diagnostics); + + public static SyntaxToken Token( + SyntaxKind kind, string content, SyntaxNode? parent, int position, params RazorDiagnostic[] diagnostics) + => Token(kind, content, parent, position, index: 0, diagnostics); + + public static SyntaxToken Token( + SyntaxKind kind, SyntaxNode? parent, int position, int index, params RazorDiagnostic[] diagnostics) + => Token(kind, string.Empty, parent, position, index, diagnostics); + + public static SyntaxToken Token( + SyntaxKind kind, string content, SyntaxNode? parent, int position, int index, params RazorDiagnostic[] diagnostics) + => new(parent, InternalSyntax.SyntaxFactory.Token(kind, content, diagnostics), position, index); internal static SyntaxToken MissingToken(SyntaxKind kind, params RazorDiagnostic[] diagnostics) - { - return new SyntaxToken(parent: null, InternalSyntax.SyntaxFactory.MissingToken(kind, diagnostics), position: 0, index: 0); - } + => new(parent: null, InternalSyntax.SyntaxFactory.MissingToken(kind, diagnostics), position: 0, index: 0); public static SyntaxList List() where TNode : SyntaxNode diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxUtilities.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxUtilities.cs index ed227f7b2d4..0efeb668a9e 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxUtilities.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxUtilities.cs @@ -9,39 +9,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax; internal static class SyntaxUtilities { - public static MarkupTextLiteralSyntax MergeTextLiterals(params ReadOnlySpan literals) - { - SyntaxNode? parent = null; - var position = 0; - var seenFirstLiteral = false; - - using PooledArrayBuilder builder = []; - - foreach (var literal in literals) - { - if (literal == null) - { - continue; - } - - if (!seenFirstLiteral) - { - // Set the parent and position of the merged literal to the value of the first non-null literal. - parent = literal.Parent; - position = literal.Position; - seenFirstLiteral = true; - } - - builder.AddRange(literal.LiteralTokens); - } - - return (MarkupTextLiteralSyntax)InternalSyntax.SyntaxFactory - .MarkupTextLiteral( - literalTokens: builder.ToGreenListNode().ToGreenList(), - chunkGenerator: null) - .CreateRed(parent, position); - } - internal static SyntaxList GetRewrittenMarkupStartTagChildren( MarkupStartTagSyntax node, bool includeEditHandler = false) { @@ -120,6 +87,39 @@ void AddLiteralIsIfNeeded() } } + private static MarkupTextLiteralSyntax MergeTextLiterals(params ReadOnlySpan literals) + { + SyntaxNode? parent = null; + var position = 0; + var seenFirstLiteral = false; + + using PooledArrayBuilder builder = []; + + foreach (var literal in literals) + { + if (literal == null) + { + continue; + } + + if (!seenFirstLiteral) + { + // Set the parent and position of the merged literal to the value of the first non-null literal. + parent = literal.Parent; + position = literal.Position; + seenFirstLiteral = true; + } + + builder.AddRange(literal.LiteralTokens); + } + + return (MarkupTextLiteralSyntax)InternalSyntax.SyntaxFactory + .MarkupTextLiteral( + literalTokens: builder.ToGreenListNode().ToGreenList(), + chunkGenerator: null) + .CreateRed(parent, position); + } + internal static SyntaxList GetRewrittenMarkupEndTagChildren( MarkupEndTagSyntax node, bool includeEditHandler = false) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs index 5ca184053bf..7bfd9bc5895 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs @@ -6,7 +6,8 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading; -using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor.Threading; @@ -39,8 +40,8 @@ private sealed class CachedData(RazorCodeDocument codeDocument) private readonly SemaphoreSlim _stateLock = new(initialCount: 1); private SyntaxTree? _syntaxTree; - private ImmutableArray? _classifiedSpans; - private ImmutableArray? _tagHelperSpans; + private ImmutableArray? _classifiedSpans; + private ImmutableArray? _tagHelperSpans; public SyntaxTree GetOrParseCSharpSyntaxTree(CancellationToken cancellationToken) { @@ -61,7 +62,7 @@ static SyntaxTree ParseSyntaxTree(RazorCodeDocument codeDocument, CancellationTo } } - public ImmutableArray GetOrComputeClassifiedSpans(CancellationToken cancellationToken) + public ImmutableArray GetOrComputeClassifiedSpans(CancellationToken cancellationToken) { if (_classifiedSpans is { } classifiedSpans) { @@ -70,11 +71,11 @@ public ImmutableArray GetOrComputeClassifiedSpans(Cancel using (_stateLock.DisposableWait(cancellationToken)) { - return _classifiedSpans ??= _codeDocument.GetRequiredSyntaxTree().GetClassifiedSpans(); + return _classifiedSpans ??= ClassifiedSpanVisitor.VisitRoot(_codeDocument.GetRequiredSyntaxTree()); } } - public ImmutableArray GetOrComputeTagHelperSpans(CancellationToken cancellationToken) + public ImmutableArray GetOrComputeTagHelperSpans(CancellationToken cancellationToken) { if (_tagHelperSpans is { } tagHelperSpans) { @@ -83,7 +84,25 @@ public ImmutableArray GetOrComputeTagHelperSpans(Cancella using (_stateLock.DisposableWait(cancellationToken)) { - return _tagHelperSpans ??= _codeDocument.GetRequiredSyntaxTree().GetTagHelperSpans(); + return _tagHelperSpans ??= ComputeTagHelperSpans(_codeDocument.GetRequiredSyntaxTree()); + } + + static ImmutableArray ComputeTagHelperSpans(RazorSyntaxTree syntaxTree) + { + using var builder = new PooledArrayBuilder(); + + foreach (var node in syntaxTree.Root.DescendantNodes()) + { + if (node is not MarkupTagHelperElementSyntax tagHelperElement || + tagHelperElement.TagHelperInfo is null) + { + continue; + } + + builder.Add(tagHelperElement.GetSourceSpan(syntaxTree.Source)); + } + + return builder.ToImmutableAndClear(); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs index 8a459a1ed8d..ea1abdf6280 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Threading; using Microsoft.AspNetCore.Razor.Language.Intermediate; -using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Protocol; @@ -150,15 +150,15 @@ public static RazorLanguageKind GetLanguageKind(this RazorCodeDocument codeDocum return GetLanguageKindCore(classifiedSpans, tagHelperSpans, hostDocumentIndex, documentLength, rightAssociative); } - private static ImmutableArray GetClassifiedSpans(RazorCodeDocument document) + private static ImmutableArray GetClassifiedSpans(RazorCodeDocument document) => GetCachedData(document).GetOrComputeClassifiedSpans(CancellationToken.None); - private static ImmutableArray GetTagHelperSpans(RazorCodeDocument document) + private static ImmutableArray GetTagHelperSpans(RazorCodeDocument document) => GetCachedData(document).GetOrComputeTagHelperSpans(CancellationToken.None); private static RazorLanguageKind GetLanguageKindCore( - ImmutableArray classifiedSpans, - ImmutableArray tagHelperSpans, + ImmutableArray classifiedSpans, + ImmutableArray tagHelperSpans, int hostDocumentIndex, int hostDocumentLength, bool rightAssociative) @@ -178,7 +178,7 @@ private static RazorLanguageKind GetLanguageKindCore( { // We're at an edge. - if (classifiedSpan.SpanKind is SpanKindInternal.MetaCode or SpanKindInternal.Transition) + if (classifiedSpan.Kind is SpanKind.MetaCode or SpanKind.Transition) { // If we're on an edge of a transition of some kind (MetaCode representing an open or closing piece of syntax such as <|, // and Transition representing an explicit transition to/from razor syntax, such as @|), prefer to classify to the span @@ -206,10 +206,8 @@ private static RazorLanguageKind GetLanguageKindCore( } } - foreach (var tagHelperSpan in tagHelperSpans) + foreach (var span in tagHelperSpans) { - var span = tagHelperSpan.Span; - if (span.AbsoluteIndex <= hostDocumentIndex) { var end = span.AbsoluteIndex + span.Length; @@ -238,13 +236,13 @@ private static RazorLanguageKind GetLanguageKindCore( // Default to Razor return RazorLanguageKind.Razor; - static RazorLanguageKind GetLanguageFromClassifiedSpan(ClassifiedSpanInternal classifiedSpan) + static RazorLanguageKind GetLanguageFromClassifiedSpan(ClassifiedSpan classifiedSpan) { // Overlaps with request - return classifiedSpan.SpanKind switch + return classifiedSpan.Kind switch { - SpanKindInternal.Markup => RazorLanguageKind.Html, - SpanKindInternal.Code => RazorLanguageKind.CSharp, + SpanKind.Markup => RazorLanguageKind.Html, + SpanKind.Code => RazorLanguageKind.CSharp, // Content type was non-C# or Html or we couldn't find a classified span overlapping the request position. // All other classified span kinds default back to Razor diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions_ClassifiedSpans.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions_ClassifiedSpans.cs new file mode 100644 index 00000000000..c03724adf70 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions_ClassifiedSpans.cs @@ -0,0 +1,486 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +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 +{ + // This is modified copy of the ClassifiedSpanVisitor from the legacy compiler APIs. + // It is streamlined to only produce the information needed for tooling's + // GetLanguageKind() API. + // + // Note that the legacy ClassifiedSpanVisitor will be removed with the legacy editor. + + private enum SpanKind + { + Transition, + MetaCode, + Comment, + Code, + Markup, + None + } + + private record struct ClassifiedSpan(SourceSpan Span, SpanKind Kind); + + private sealed class ClassifiedSpanVisitor : SyntaxWalker + { + private enum BlockKind + { + // Code + Statement, + Directive, + Expression, + + // Markup + Markup, + Template, + + // Special + Comment, + Tag, + HtmlComment + } + + private static readonly ObjectPool s_pool = DefaultPool.Create(Policy.Instance, size: 5); + + private readonly ImmutableArray.Builder _spans; + + private RazorSourceDocument _source; + private BlockKind _currentBlockKind; + + private ClassifiedSpanVisitor() + { + _spans = ImmutableArray.CreateBuilder(); + _source = null!; + } + + private void Initialize(RazorSourceDocument source) + { + _source = source; + _currentBlockKind = BlockKind.Markup; + } + + public static ImmutableArray VisitRoot(RazorSyntaxTree syntaxTree) + { + using var _ = s_pool.GetPooledObject(out var visitor); + + visitor.Initialize(syntaxTree.Source); + visitor.Visit(syntaxTree.Root); + + return visitor.GetSpansAndClear(); + } + + private ImmutableArray 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 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)); + + private void Reset() + { + _spans.Clear(); + + if (_spans.Capacity > Policy.MaximumObjectSize) + { + // Differs from ArrayBuilderPool.Policy's behavior as we allow our array to grow significantly larger + _spans.Capacity = 0; + } + + _source = null!; + _currentBlockKind = BlockKind.Markup; + } + + private sealed class Policy : IPooledObjectPolicy + { + public static readonly Policy Instance = new(); + + // 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.MaximumObjectSize * 32; + + private Policy() + { + } + + public ClassifiedSpanVisitor Create() => new(); + + public bool Return(ClassifiedSpanVisitor visitor) + { + visitor.Reset(); + + return true; + } + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs index 2f2ac128c9b..31de89f1551 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; @@ -19,7 +20,7 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal sealed class FormattingContext { - private IReadOnlyList? _formattingSpans; + private ImmutableArray? _formattingSpans; private IReadOnlyDictionary? _indentations; private readonly bool _useNewFormattingEngine; @@ -143,25 +144,27 @@ public IReadOnlyDictionary GetIndentations() return _indentations; } - private IReadOnlyList GetFormattingSpans() + private ImmutableArray GetFormattingSpans() { - if (_formattingSpans is null) + return _formattingSpans ??= ComputeFormattingSpans(CodeDocument); + + static ImmutableArray ComputeFormattingSpans(RazorCodeDocument codeDocument) { - var syntaxTree = CodeDocument.GetRequiredSyntaxTree(); - var inGlobalNamespace = CodeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace) && + var syntaxTree = codeDocument.GetRequiredSyntaxTree(); + var inGlobalNamespace = codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace) && string.IsNullOrEmpty(@namespace); - _formattingSpans = GetFormattingSpans(syntaxTree, inGlobalNamespace: inGlobalNamespace); - } - return _formattingSpans; + return GetFormattingSpans(syntaxTree, inGlobalNamespace: inGlobalNamespace); + } } - private static IReadOnlyList GetFormattingSpans(RazorSyntaxTree syntaxTree, bool inGlobalNamespace) + private static ImmutableArray GetFormattingSpans(RazorSyntaxTree syntaxTree, bool inGlobalNamespace) { - var visitor = new FormattingVisitor(inGlobalNamespace: inGlobalNamespace); - visitor.Visit(syntaxTree.Root); + using var _ = ArrayBuilderPool.GetPooledObject(out var formattingSpans); + + FormattingVisitor.VisitRoot(syntaxTree, formattingSpans, inGlobalNamespace); - return visitor.FormattingSpans; + return formattingSpans.ToImmutableAndClear(); } /// diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs index 315f8d2916f..bab1ab25072 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs @@ -3,28 +3,23 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; -using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis.Text; -using RazorSyntaxToken = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxToken; -using RazorSyntaxTokenList = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxTokenList; -using RazorSyntaxWalker = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxWalker; - namespace Microsoft.CodeAnalysis.Razor.Formatting; -// There is already RazorSyntaxNode so not following that pattern for this alias -using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; +using Microsoft.AspNetCore.Razor.Language.Syntax; -internal class FormattingVisitor : RazorSyntaxWalker +internal sealed class FormattingVisitor : SyntaxWalker { private const string HtmlTag = "html"; + private readonly ImmutableArray.Builder _spans; private readonly bool _inGlobalNamespace; - private readonly List _spans; private FormattingBlockKind _currentBlockKind; private SyntaxNode? _currentBlock; private int _currentHtmlIndentationLevel = 0; @@ -32,35 +27,39 @@ internal class FormattingVisitor : RazorSyntaxWalker private int _currentComponentIndentationLevel = 0; private bool _isInClassBody = false; - public FormattingVisitor(bool inGlobalNamespace) + private FormattingVisitor(ImmutableArray.Builder spans, bool inGlobalNamespace) { _inGlobalNamespace = inGlobalNamespace; - _spans = new List(); + _spans = spans; _currentBlockKind = FormattingBlockKind.Markup; } - public IReadOnlyList FormattingSpans => _spans; + public static void VisitRoot( + RazorSyntaxTree syntaxTree, ImmutableArray.Builder spans, bool inGlobalNamespace) + { + var visitor = new FormattingVisitor(spans, inGlobalNamespace); + visitor.Visit(syntaxTree.Root); + } public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Comment, razorCommentSyntax => + using (CommentBlock(node)) { // We only want to move the start of the comment into the right spot, so we only // create spans for the start. // The body of the comment, including whitespace before the "*@" is left exactly // as the user has it in the file. - WriteSpan(razorCommentSyntax.StartCommentTransition, FormattingSpanKind.Transition); - WriteSpan(razorCommentSyntax.StartCommentStar, FormattingSpanKind.MetaCode); - }); + AddSpan(node.StartCommentTransition, FormattingSpanKind.Transition); + AddSpan(node.StartCommentStar, FormattingSpanKind.MetaCode); + } } public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node) { - if (node.Parent is CSharpStatementBodySyntax || - node.Parent is CSharpImplicitExpressionBodySyntax || - node.Parent is RazorDirectiveBodySyntax || - (_currentBlockKind == FormattingBlockKind.Directive && - node.Parent?.Parent is RazorDirectiveBodySyntax)) + if (node.Parent is CSharpStatementBodySyntax or + CSharpImplicitExpressionBodySyntax or + RazorDirectiveBodySyntax || + (_currentBlockKind == FormattingBlockKind.Directive && node.Parent?.Parent is RazorDirectiveBodySyntax)) { // If we get here, it means we don't want this code block to be considered significant. // Without this, we would have double indentation in places where @@ -97,37 +96,58 @@ node.Parent is RazorDirectiveBodySyntax || return; } - WriteBlock(node, FormattingBlockKind.Statement, base.VisitCSharpCodeBlock); + using (StatementBlock(node)) + { + base.VisitCSharpCodeBlock(node); + } } public override void VisitCSharpStatement(CSharpStatementSyntax node) { - WriteBlock(node, FormattingBlockKind.Statement, base.VisitCSharpStatement); + using (StatementBlock(node)) + { + base.VisitCSharpStatement(node); + } } public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node) { - WriteBlock(node, FormattingBlockKind.Expression, base.VisitCSharpExplicitExpression); + using (ExpressionBlock(node)) + { + base.VisitCSharpExplicitExpression(node); + } } public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node) { - WriteBlock(node, FormattingBlockKind.Expression, base.VisitCSharpImplicitExpression); + using (ExpressionBlock(node)) + { + base.VisitCSharpImplicitExpression(node); + } } public override void VisitRazorDirective(RazorDirectiveSyntax node) { - WriteBlock(node, FormattingBlockKind.Directive, base.VisitRazorDirective); + using (DirectiveBlock(node)) + { + base.VisitRazorDirective(node); + } } public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Template, base.VisitCSharpTemplateBlock); + using (TemplateBlock(node)) + { + base.VisitCSharpTemplateBlock(node); + } } public override void VisitMarkupBlock(MarkupBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, base.VisitMarkupBlock); + using (MarkupBlock(node)) + { + base.VisitMarkupBlock(node); + } } public override void VisitMarkupElement(MarkupElementSyntax node) @@ -160,19 +180,20 @@ public override void VisitMarkupElement(MarkupElementSyntax node) public override void VisitMarkupStartTag(MarkupStartTagSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { var children = SyntaxUtilities.GetRewrittenMarkupStartTagChildren(node); + foreach (var child in children) { Visit(child); } - }); + } } public override void VisitMarkupEndTag(MarkupEndTagSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { var children = SyntaxUtilities.GetRewrittenMarkupEndTagChildren(node); @@ -180,7 +201,7 @@ public override void VisitMarkupEndTag(MarkupEndTagSyntax node) { Visit(child); } - }); + } } public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) @@ -299,48 +320,68 @@ static bool HasUnspecifiedCascadingTypeParameter(MarkupTagHelperElementSyntax no public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { - foreach (var child in n.LegacyChildren) + foreach (var child in node.LegacyChildren) { Visit(child); } - }); + } } public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { - foreach (var child in n.LegacyChildren) + foreach (var child in node.LegacyChildren) { Visit(child); } - }); + } } public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, n => + using (MarkupBlock(node)) { - var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new RazorSyntaxTokenList(node.EqualsToken), chunkGenerator: null); - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix); - Visit(mergedAttributePrefix); + // 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 textSpan = spanComputer.ToTextSpan(); + + AddSpan(textSpan, FormattingSpanKind.Markup); + + // Visit the value and value suffix separately. Visit(node.Value); Visit(node.ValueSuffix); - }); + } } public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { - var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new RazorSyntaxTokenList(node.EqualsToken), chunkGenerator: null); - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix); - Visit(mergedAttributePrefix); + // 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 textSpan = spanComputer.ToTextSpan(); + + AddSpan(textSpan, FormattingSpanKind.Markup); + + // Visit the value and value suffix separately. Visit(node.Value); Visit(node.ValueSuffix); - }); + } } public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirectiveAttributeSyntax node) @@ -358,26 +399,41 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, n => + using (MarkupBlock(node)) { - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name); - Visit(mergedAttributePrefix); - }); + // 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 textSpan = spanComputer.ToTextSpan(); + + AddSpan(textSpan, FormattingSpanKind.Markup); + } } public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.HtmlComment, base.VisitMarkupCommentBlock); + using (HtmlCommentBlock(node)) + { + base.VisitMarkupCommentBlock(node); + } } public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, base.VisitMarkupDynamicAttributeValue); + using (MarkupBlock(node)) + { + base.VisitMarkupDynamicAttributeValue(node); + } } public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttributeValueSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, base.VisitMarkupTagHelperAttributeValue); + using (MarkupBlock(node)) + { + base.VisitMarkupTagHelperAttributeValue(node); + } } public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) @@ -385,11 +441,11 @@ public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) if (node.Parent is MarkupTagHelperDirectiveAttributeSyntax { TagHelperAttributeInfo.Bound: true }) { // For @bind attributes we want to pretend that we're in a Html context, so write this span as markup - WriteSpan(node, FormattingSpanKind.Markup); + AddSpan(node, FormattingSpanKind.Markup); } else { - WriteSpan(node, FormattingSpanKind.MetaCode); + AddSpan(node, FormattingSpanKind.MetaCode); } base.VisitRazorMetaCode(node); @@ -397,13 +453,13 @@ public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) public override void VisitCSharpTransition(CSharpTransitionSyntax node) { - WriteSpan(node, FormattingSpanKind.Transition); + AddSpan(node, FormattingSpanKind.Transition); base.VisitCSharpTransition(node); } public override void VisitMarkupTransition(MarkupTransitionSyntax node) { - WriteSpan(node, FormattingSpanKind.Transition); + AddSpan(node, FormattingSpanKind.Transition); base.VisitMarkupTransition(node); } @@ -420,7 +476,7 @@ public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax no // being "inside" the block. if (node.LiteralTokens is not [{ Kind: SyntaxKind.Marker }]) { - WriteSpan(node, FormattingSpanKind.Code); + AddSpan(node, FormattingSpanKind.Code); } base.VisitCSharpStatementLiteral(node); @@ -428,25 +484,25 @@ public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax no public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node) { - WriteSpan(node, FormattingSpanKind.Code); + AddSpan(node, FormattingSpanKind.Code); base.VisitCSharpExpressionLiteral(node); } public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node) { - WriteSpan(node, FormattingSpanKind.Code); + AddSpan(node, FormattingSpanKind.Code); base.VisitCSharpEphemeralTextLiteral(node); } public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node) { - WriteSpan(node, FormattingSpanKind.None); + AddSpan(node, FormattingSpanKind.None); base.VisitUnclassifiedTextLiteral(node); } public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node) { - WriteSpan(node, FormattingSpanKind.Markup); + AddSpan(node, FormattingSpanKind.Markup); base.VisitMarkupLiteralAttributeValue(node); } @@ -458,56 +514,85 @@ public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) return; } - WriteSpan(node, FormattingSpanKind.Markup); + AddSpan(node, FormattingSpanKind.Markup); base.VisitMarkupTextLiteral(node); } public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node) { - WriteSpan(node, FormattingSpanKind.Markup); + AddSpan(node, FormattingSpanKind.Markup); base.VisitMarkupEphemeralTextLiteral(node); } - private void WriteBlock(TNode node, FormattingBlockKind kind, Action handler) where TNode : SyntaxNode + private BlockSaver CommentBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Comment); + + private BlockSaver DirectiveBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Directive); + + private BlockSaver ExpressionBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Expression); + + private BlockSaver HtmlCommentBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.HtmlComment); + + private BlockSaver MarkupBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Markup); + + private BlockSaver StatementBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Statement); + + private BlockSaver TagBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Tag); + + private BlockSaver TemplateBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Template); + + private BlockSaver Block(SyntaxNode node, FormattingBlockKind kind) { - var previousBlock = _currentBlock; - var previousKind = _currentBlockKind; + var saver = new BlockSaver(this); _currentBlock = node; _currentBlockKind = kind; - handler(node); + return saver; + } - _currentBlock = previousBlock; - _currentBlockKind = previousKind; + private readonly ref struct BlockSaver(FormattingVisitor visitor) + { + private readonly SyntaxNode? _previousBlock = visitor._currentBlock; + private readonly FormattingBlockKind _previousKind = visitor._currentBlockKind; + + public void Dispose() + { + visitor._currentBlock = _previousBlock; + visitor._currentBlockKind = _previousKind; + } } - private void WriteSpan(SyntaxNode node, FormattingSpanKind kind) + private void AddSpan(SyntaxNode node, FormattingSpanKind kind) { if (node.IsMissing) { return; } - Assumes.NotNull(_currentBlock); + AddSpan(node.Span, kind); + } - var span = new FormattingSpan( - node.Span, - _currentBlock.Span, - kind, - _currentBlockKind, - _currentRazorIndentationLevel, - _currentHtmlIndentationLevel, - isInGlobalNamespace: _inGlobalNamespace, - isInClassBody: _isInClassBody, - _currentComponentIndentationLevel); + private void AddSpan(SyntaxToken token, FormattingSpanKind kind) + { + if (token.IsMissing) + { + return; + } - _spans.Add(span); + AddSpan(token.Span, kind); } - private void WriteSpan(RazorSyntaxToken token, FormattingSpanKind kind) + private void AddSpan(TextSpan textSpan, FormattingSpanKind kind) { - if (token.IsMissing) + if (textSpan.IsEmpty) { return; } @@ -515,7 +600,7 @@ private void WriteSpan(RazorSyntaxToken token, FormattingSpanKind kind) Assumes.NotNull(_currentBlock); var span = new FormattingSpan( - token.Span, + textSpan, _currentBlock.Span, kind, _currentBlockKind,