77using Microsoft . AspNetCore . Razor . Language ;
88using Microsoft . AspNetCore . Razor . Language . Syntax ;
99using Microsoft . AspNetCore . Razor . PooledObjects ;
10+ using Microsoft . CodeAnalysis . Razor . DocumentMapping ;
1011using Microsoft . CodeAnalysis . Razor . TextDifferencing ;
1112using Microsoft . CodeAnalysis . Text ;
1213
1314namespace Microsoft . CodeAnalysis . Razor . Formatting ;
1415
15- internal sealed class HtmlFormattingPass : IFormattingPass
16+ internal sealed class HtmlFormattingPass ( IDocumentMappingService documentMappingService ) : IFormattingPass
1617{
17- public Task < ImmutableArray < TextChange > > ExecuteAsync ( FormattingContext context , ImmutableArray < TextChange > changes , CancellationToken cancellationToken )
18+ private readonly IDocumentMappingService _documentMappingService = documentMappingService ;
19+
20+ public async Task < ImmutableArray < TextChange > > ExecuteAsync ( FormattingContext context , ImmutableArray < TextChange > changes , CancellationToken cancellationToken )
1821 {
1922 var changedText = context . SourceText ;
2023
2124 if ( changes . Length > 0 )
2225 {
23- var filteredChanges = FilterIncomingChanges ( context . CodeDocument . GetRequiredSyntaxTree ( ) , changes ) ;
26+ var filteredChanges = await FilterIncomingChangesAsync ( context , changes , cancellationToken ) . ConfigureAwait ( false ) ;
2427 changedText = changedText . WithChanges ( filteredChanges ) ;
2528
2629 context . Logger ? . LogSourceText ( "AfterHtmlFormatter" , changedText ) ;
2730 }
2831
29- return Task . FromResult ( SourceTextDiffer . GetMinimalTextChanges ( context . SourceText , changedText , DiffKind . Char ) ) ;
32+ return SourceTextDiffer . GetMinimalTextChanges ( context . SourceText , changedText , DiffKind . Char ) ;
3033 }
3134
32- private static ImmutableArray < TextChange > FilterIncomingChanges ( RazorSyntaxTree syntaxTree , ImmutableArray < TextChange > changes )
35+ private async Task < ImmutableArray < TextChange > > FilterIncomingChangesAsync ( FormattingContext context , ImmutableArray < TextChange > changes , CancellationToken cancellationToken )
3336 {
34- var sourceText = syntaxTree . Source . Text ;
37+ var codeDocument = context . CodeDocument ;
38+ var csharpDocument = codeDocument . GetRequiredCSharpDocument ( ) ;
39+ var syntaxRoot = codeDocument . GetRequiredSyntaxRoot ( ) ;
40+ var sourceText = codeDocument . Source . Text ;
41+ SyntaxNode ? csharpSyntaxRoot = null ;
3542
3643 using var changesToKeep = new PooledArrayBuilder < TextChange > ( capacity : changes . Length ) ;
3744
3845 foreach ( var change in changes )
3946 {
40- // Don't keep changes that start inside of a razor comment block.
41- var comment = syntaxTree . Root . FindInnermostNode ( change . Span . Start ) ? . FirstAncestorOrSelf < RazorCommentBlockSyntax > ( ) ;
47+ // We don't keep changes that start inside of a razor comment block.
48+ var node = syntaxRoot . FindInnermostNode ( change . Span . Start ) ;
49+ var comment = node ? . FirstAncestorOrSelf < RazorCommentBlockSyntax > ( ) ;
4250 if ( comment is not null && change . Span . Start > comment . SpanStart )
4351 {
4452 continue ;
4553 }
4654
47- // Normally we don't touch Html changes much but there is one
48- // edge case when including render fragments in a C# code block, eg:
55+ // When render fragments are inside a C# code block, eg:
4956 //
5057 // @code {
5158 // void Foo()
@@ -56,22 +63,68 @@ private static ImmutableArray<TextChange> FilterIncomingChanges(RazorSyntaxTree
5663 //
5764 // This is popular in some libraries, like bUnit. The issue here is that
5865 // the Html formatter sees ~~~~~<SurveyPrompt /> and puts a newline before
59- // the tag, but obviously that breaks things.
66+ // the tag, but obviously that breaks things by separating the transition and the tag .
6067 //
6168 // It's straight forward enough to just check for this situation and ignore the change.
6269
6370 // There needs to be a newline being inserted between an '@' and a '<'.
64- if ( change . NewText is [ '\r ' or '\n ' , ..] &&
65- sourceText . Length > 1 &&
66- sourceText [ change . Span . Start - 1 ] == '@' &&
67- sourceText [ change . Span . Start ] == '<' )
71+ if ( change . NewText is [ '\r ' or '\n ' , ..] )
6872 {
69- continue ;
73+ if ( change . Span . Start > 0 &&
74+ sourceText . Length > 1 &&
75+ sourceText [ change . Span . Start - 1 ] == '@' &&
76+ sourceText [ change . Span . Start ] == '<' )
77+ {
78+ continue ;
79+ }
80+
81+ // The Html formatter in VS Code wraps long lines, based on a user setting, but when there
82+ // are long C# string literals that ends up breaking the code. For example:
83+ //
84+ // @("this is a long string that spans past some user set maximum limit")
85+ //
86+ // could become
87+ //
88+ // @("this is a long string that spans past
89+ // some user set maximum limit")
90+ //
91+ // That doesn't compile, and depending on the scenario, can even cause a crash inside the
92+ // Roslyn formatter.
93+ //
94+ // Strictly speaking if literal is a verbatim string, or multiline raw string literal, then
95+ // it would compile, but it would also change the value of the string, and since these edits
96+ // come from the Html formatter which clearly has no idea it's doing that, it is safer to
97+ // disregard them all equally, and let the user make the final decision.
98+ //
99+ // In order to avoid hard coding all of the various string syntax kinds here, we can just check
100+ // for any literal, as the only literals that can contain spaces, which is what the Html formatter
101+ // will wrap on, are strings. And if it did decide to insert a newline into a number, or the 'null'
102+ // keyword, that would be pretty bad too.
103+ if ( csharpSyntaxRoot is null )
104+ {
105+ var csharpSyntaxTree = await context . OriginalSnapshot . GetCSharpSyntaxTreeAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
106+ csharpSyntaxRoot = await csharpSyntaxTree . GetRootAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
107+ }
108+
109+ if ( _documentMappingService . TryMapToCSharpDocumentPosition ( csharpDocument , change . Span . Start , out _ , out var csharpIndex ) &&
110+ csharpSyntaxRoot . FindNode ( new TextSpan ( csharpIndex , 0 ) , getInnermostNodeForTie : true ) is { } csharpNode &&
111+ csharpNode is CSharp . Syntax . LiteralExpressionSyntax or CSharp . Syntax . InterpolatedStringTextSyntax )
112+ {
113+ continue ;
114+ }
70115 }
71116
72117 changesToKeep . Add ( change ) ;
73118 }
74119
75120 return changesToKeep . ToImmutableAndClear ( ) ;
76121 }
122+
123+ internal TestAccessor GetTestAccessor ( ) => new ( this ) ;
124+
125+ internal readonly struct TestAccessor ( HtmlFormattingPass pass )
126+ {
127+ public Task < ImmutableArray < TextChange > > FilterIncomingChangesAsync ( FormattingContext context , ImmutableArray < TextChange > changes , CancellationToken cancellationToken )
128+ => pass . FilterIncomingChangesAsync ( context , changes , cancellationToken ) ;
129+ }
77130}
0 commit comments