66using System . Diagnostics ;
77using System . Linq ;
88using System . Text ;
9+ using Microsoft . AspNetCore . Razor ;
910using Microsoft . AspNetCore . Razor . Language ;
1011using Microsoft . AspNetCore . Razor . Language . Syntax ;
1112using Microsoft . AspNetCore . Razor . PooledObjects ;
13+ using Microsoft . CodeAnalysis . ExternalAccess . Razor . Features ;
1214using Microsoft . CodeAnalysis . Razor . Workspaces ;
1315using Microsoft . CodeAnalysis . Text ;
1416using RazorSyntaxNode = Microsoft . AspNetCore . Razor . Language . Syntax . SyntaxNode ;
@@ -147,6 +149,7 @@ private sealed class Generator(
147149 private readonly RazorCodeDocument _codeDocument = codeDocument ;
148150 private readonly bool _insertSpaces = options . InsertSpaces ;
149151 private readonly int _tabSize = options . TabSize ;
152+ private readonly RazorCSharpSyntaxFormattingOptions ? _csharpSyntaxFormattingOptions = options . CSharpSyntaxFormattingOptions ;
150153 private readonly StringBuilder _builder = builder ;
151154 private readonly ImmutableArray < LineInfo > . Builder _lineInfoBuilder = lineInfoBuilder ;
152155
@@ -386,6 +389,18 @@ _sourceText.Lines[nodeStartLine] is { } previousLine &&
386389 public override LineInfo VisitCSharpStatementLiteral ( CSharpStatementLiteralSyntax node )
387390 {
388391 Debug . Assert ( node . LiteralTokens . Count > 0 ) ;
392+
393+ // If this is the end of a multi-line CSharp template (ie, RenderFragment) then we need to close
394+ // out the lambda expression that we started when we opened it.
395+ if ( node . LiteralTokens . Where ( static t => ! t . IsWhitespace ( ) ) . FirstOrDefault ( ) is { Content : ";" } &&
396+ node . TryGetPreviousSibling ( out var previousSibling ) &&
397+ previousSibling is CSharpTemplateBlockSyntax &&
398+ GetLineNumber ( previousSibling . GetFirstToken ( ) ) != GetLineNumber ( previousSibling . GetLastToken ( ) ) )
399+ {
400+ _builder . AppendLine ( "};" ) ;
401+ return CreateLineInfo ( ) ;
402+ }
403+
389404 return VisitCSharpLiteral ( node , node . LiteralTokens [ ^ 1 ] ) ;
390405 }
391406
@@ -412,22 +427,47 @@ private LineInfo VisitCSharpLiteral(RazorSyntaxNode node, RazorSyntaxToken lastT
412427 // Now emit the contents
413428 var span = TextSpan . FromBounds ( _currentFirstNonWhitespacePosition , node . EndPosition ) ;
414429 _builder . Append ( _sourceText . ToString ( span ) ) ;
415- // Append a comment at the end so whitespace isn't removed, as Roslyn thinks its the end of the line, but we know it isn't.
416- _builder . AppendLine ( " //" ) ;
417430
418431 // Putting a semi-colon on the end might make for invalid C#, but it means this line won't cause indentation,
419432 // which is all we need. If we're in an explicit expression body though, we don't want to do this, as the
420433 // close paren of the expression will do the same job (and the semi-colon would confuse that).
421434 var emitSemiColon = node . Parent . Parent is not CSharpExplicitExpressionBodySyntax ;
435+
436+ var skipNextLineIfBrace = false ;
437+ int formattedOffsetFromEndOfLine ;
438+
439+ // If the template is multiline we emit a lambda expression, otherwise just a null statement so there
440+ // is something there. See VisitMarkupTransition for more info
441+ if ( token . Parent ? . Parent . Parent is CSharpTemplateBlockSyntax template &&
442+ _sourceText . GetLinePositionSpan ( template . Span ) . SpansMultipleLines ( ) )
443+ {
444+ emitSemiColon = false ;
445+ skipNextLineIfBrace = true ;
446+ _builder . AppendLine ( "() => {" ) ;
447+
448+ // We only want to format up to the text we added, but if Roslyn inserted a newline before the brace
449+ // then that position will be different. If we're not given the options then we assume the default behaviour of
450+ // Roslyn which is to insert the newline.
451+ formattedOffsetFromEndOfLine = _csharpSyntaxFormattingOptions ? . NewLines . IsFlagSet ( RazorNewLinePlacement . BeforeOpenBraceInLambdaExpressionBody ) ?? true
452+ ? 5
453+ : 7 ;
454+ }
455+ else
456+ {
457+ _builder . AppendLine ( "null" ) ;
458+ formattedOffsetFromEndOfLine = 4 ;
459+ }
460+
422461 if ( emitSemiColon )
423462 {
424463 _builder . AppendLine ( ";" ) ;
425464 }
426465
427466 return CreateLineInfo (
428467 skipNextLine : emitSemiColon ,
468+ skipNextLineIfBrace : skipNextLineIfBrace ,
429469 formattedLength : span . Length ,
430- formattedOffsetFromEndOfLine : 3 ,
470+ formattedOffsetFromEndOfLine : formattedOffsetFromEndOfLine ,
431471 processFormatting : true ,
432472 // We turn off check for new lines because that only works if the content doesn't change from the original,
433473 // but we're deliberately leaving out a bunch of the original file because it would confuse the Roslyn formatter.
@@ -458,8 +498,25 @@ public override LineInfo VisitMarkupStartTag(MarkupStartTagSyntax node)
458498
459499 public override LineInfo VisitMarkupEndTag ( MarkupEndTagSyntax node )
460500 {
461- // Since this visitor only sees nodes at the start of a line, an end tag always means de-dent.
462- //return new("}");
501+ return VisitEndTag ( node ) ;
502+ }
503+
504+ private LineInfo VisitEndTag ( BaseMarkupEndTagSyntax node )
505+ {
506+ // If this is the last line of a multi-line CSharp template (ie, RenderFragment), and the semicolon that ends
507+ // if is on the same line, then we need to close out the lambda expression that we started when we opened it.
508+ // If the semicolon is on the next line, then we'll take care of that when we get to it.
509+ if ( node . Parent . Parent . Parent is CSharpTemplateBlockSyntax template &&
510+ GetLineNumber ( template . GetLastToken ( ) ) == GetLineNumber ( _currentToken ) &&
511+ GetLineNumber ( template . GetFirstToken ( ) ) != GetLineNumber ( template . GetLastToken ( ) ) &&
512+ template . GetLastToken ( ) . GetNextToken ( ) is { } semiColonToken &&
513+ semiColonToken . Content == ";" &&
514+ GetLineNumber ( semiColonToken ) == GetLineNumber ( _currentToken ) )
515+ {
516+ _builder . AppendLine ( "};" ) ;
517+ return CreateLineInfo ( ) ;
518+ }
519+
463520 return EmitCurrentLineAsComment ( ) ;
464521 }
465522
@@ -537,29 +594,45 @@ private LineInfo VisitMarkupLiteral()
537594
538595 public override LineInfo VisitMarkupTagHelperEndTag ( MarkupTagHelperEndTagSyntax node )
539596 {
540- // Since this visitor only sees nodes at the start of a line, an end tag always means de-dent.
541- //return new("}");
542- return EmitCurrentLineAsComment ( ) ;
597+ return VisitEndTag ( node ) ;
543598 }
544599
545600 public override LineInfo VisitMarkupTransition ( MarkupTransitionSyntax node )
546601 {
547- // A transition to Html is treated the same as Html, which is to say nothing interesting.
548- // We could emit as a comment, so C# indentation is handled, but it is often that a markup transition
549- // appears after assigning a RenderFragment, eg
602+ // A transition to Html means the start of a RenderFragment. These are challenging because conceptually
603+ // they are like a Write() call, because their contents are sent to the output, but they can also contain
604+ // statements. eg:
550605 //
551606 // RenderFragment f =
552607 // @<div>
553- // <p>Some text</p>
608+ // @if (true)
609+ // {
610+ // <p>Some text</p>
611+ // }
554612 // </div>;
555613 //
556- // If we just emit a comment there, the C# formatter will not indent it, and it will leave a hanging
557- // expression which affects future indentation. So instead we emit some fake C# just to make sure
558- // nothing is left open. A single semi-colon will suffice.
614+ // If we convert that to C# the way we normally do, we end up with statements in a C# context where only
615+ // expressions are valid. To avoid that, we need to emit C# such that we can be sure we're in a context
616+ // where statements are valid. To do this we emit a block bodied lambda expression. Ironically this whole
617+ // formatting engine arguably exists because the compiler loves to emit lambda expressions, but they're
618+ // really annoying to format. This just happens to be the one case where a lambda is the right choice.
619+
559620 // Emit the whitespace, so user spacing is honoured if possible
560621 _builder . Append ( _sourceText . ToString ( TextSpan . FromBounds ( _currentLine . Start , _currentFirstNonWhitespacePosition ) ) ) ;
561- _builder . AppendLine ( ";" ) ;
562- return CreateLineInfo ( ) ;
622+
623+ // If its a one-line render fragment, then we don't need to worry.
624+ if ( GetLineNumber ( node . Parent . GetLastToken ( ) ) == GetLineNumber ( _currentToken ) )
625+ {
626+ _builder . AppendLine ( "null;" ) ;
627+ return CreateLineInfo ( ) ;
628+ }
629+
630+ // Roslyn may move the opening brace to the next line, depending on its options. Unlike with code block
631+ // formatting where we put the opening brace on the next line ourselves (and Roslyn might bring it back)
632+ // if we do that for lambdas, Roslyn won't adjust the opening brace position at all. See, told you lambdas
633+ // were annoying to format.
634+ _builder . AppendLine ( "() => {" ) ;
635+ return CreateLineInfo ( skipNextLineIfBrace : true ) ;
563636 }
564637
565638 public override LineInfo VisitRazorCommentBlock ( RazorCommentBlockSyntax node )
0 commit comments