Skip to content

Commit 3372435

Browse files
[release/dev17.14] Fix formatting issues (#11913)
Backport of #11908 and #11911 to release/dev17.14 Fixes [developercommunity.visualstudio.com/t/Format-Document-in-Razor-pages-no-longer/10903159](https://developercommunity.visualstudio.com/t/Format-Document-in-Razor-pages-no-longer/10903159) Fixes [devdiv.visualstudio.com/DevDiv/_workitems/edit/2471065](https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2471065) /cc @davidwengier ## Customer Impact ## Regression - [ ] Yes - [ ] No [If yes, specify when the regression was introduced. Provide the PR or commit if known.] ## Testing [How was the fix verified? How was the issue missed previously? What tests were added?] ## Risk [High/Medium/Low. Justify the indication by mentioning how risks were measured and addressed.]
2 parents 6ec16a8 + 82f6315 commit 3372435

File tree

10 files changed

+284
-24
lines changed

10 files changed

+284
-24
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/TextLineExtensions.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

4+
using System.Diagnostics;
45
using Microsoft.AspNetCore.Razor;
5-
using Microsoft.CodeAnalysis.Razor;
66

77
namespace Microsoft.CodeAnalysis.Text;
88

99
internal static class TextLineExtensions
1010
{
11+
public static char CharAt(this TextLine line, int offset)
12+
{
13+
ArgHelper.ThrowIfNegative(offset);
14+
ArgHelper.ThrowIfGreaterThanOrEqual(offset, line.SpanIncludingLineBreak.Length);
15+
16+
var text = line.Text.AssumeNotNull();
17+
var index = line.Start + offset;
18+
Debug.Assert(index < text.Length, "This should be impossible as we validated offset against the line length above.");
19+
20+
return text[index];
21+
}
22+
1123
public static string GetLeadingWhitespace(this TextLine line)
1224
{
13-
return line.ToString().GetLeadingWhitespace();
25+
return line.GetFirstNonWhitespaceOffset() is int offset
26+
? line.Text.AssumeNotNull().ToString(TextSpan.FromBounds(line.Start, line.Start + offset))
27+
: string.Empty;
1428
}
1529

1630
public static int GetIndentationSize(this TextLine line, long tabSize)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/New/CSharpFormattingPass.CSharpDocumentGenerator.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,8 @@ private LineInfo VisitCodeOrFunctionsDirective(RazorSyntaxNode openBrace)
651651
_builder.AppendLine("class F");
652652
_builder.AppendLine("{");
653653

654-
return CreateLineInfo(skipNextLine: true);
654+
// Roslyn might move our brace to the previous line, so we might _not_ need to skip it 🤦‍
655+
return CreateLineInfo(skipNextLineIfBrace: true);
655656
}
656657

657658
// If the braces are on different lines, then we can do nothing, unless its an @code or @functions
@@ -733,6 +734,7 @@ private LineInfo CreateLineInfo(
733734
bool checkForNewLines = false,
734735
bool skipPreviousLine = false,
735736
bool skipNextLine = false,
737+
bool skipNextLineIfBrace = false,
736738
int htmlIndentLevel = 0,
737739
int originOffset = 0,
738740
int formattedLength = 0,
@@ -759,6 +761,7 @@ _elementEndLine is { } endLine &&
759761
CheckForNewLines: checkForNewLines,
760762
SkipPreviousLine: skipPreviousLine,
761763
SkipNextLine: skipNextLine,
764+
SkipNextLineIfBrace: skipNextLineIfBrace,
762765
HtmlIndentLevel: htmlIndentLevel,
763766
OriginOffset: originOffset,
764767
FormattedLength: formattedLength,

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/New/CSharpFormattingPass.LineInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal partial class CSharpFormattingPass
1313
/// <param name="CheckForNewLines">Whether the origin document text could have overflowed to multiple lines in the formatted document</param>
1414
/// <param name="SkipPreviousLine">Whether to skip the previous line in the formatted document, since it doesn't represent anything in the origin document</param>
1515
/// <param name="SkipNextLine">Whether to skip the next line in the formatted document, since it doesn't represent anything in the origin document</param>
16+
/// <param name="SkipNextLineIfBrace">Whether to skip the next line in the formatted document, like <see cref="SkipNextLine" />, but only skips if the next line is a brace</param>
1617
/// <param name="HtmlIndentLevel">The indent level that the Html formatter applied to this line</param>
1718
/// <param name="OriginOffset">How many characters after the first non-whitespace character of the origin line should be skipped before applying formatting</param>
1819
/// <param name="FormattedLength">How many characters of the origin line the formatted line represents</param>
@@ -25,6 +26,7 @@ private readonly record struct LineInfo(
2526
bool CheckForNewLines,
2627
bool SkipPreviousLine,
2728
bool SkipNextLine,
29+
bool SkipNextLineIfBrace,
2830
int HtmlIndentLevel,
2931
int OriginOffset,
3032
int FormattedLength,

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/New/CSharpFormattingPass.cs

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ internal sealed partial class CSharpFormattingPass(IHostServicesProvider hostSer
2121
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CSharpFormattingPass>();
2222
private readonly IHostServicesProvider _hostServicesProvider = hostServicesProvider;
2323

24+
private Func<SourceText, SourceText>? _formattedCSharpDocumentModifierFunc = null;
25+
2426
public async Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext context, ImmutableArray<TextChange> changes, CancellationToken cancellationToken)
2527
{
2628
// Process changes from previous passes
@@ -36,6 +38,12 @@ public async Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext con
3638
var formattedCSharpText = await FormatCSharpAsync(generatedCSharpText, context.Options.ToIndentationOptions(), cancellationToken).ConfigureAwait(false);
3739
_logger.LogTestOnly($"Formatted generated C# document:\r\n{formattedCSharpText}");
3840

41+
if (_formattedCSharpDocumentModifierFunc is { } func)
42+
{
43+
formattedCSharpText = func(formattedCSharpText);
44+
_logger.LogTestOnly($"Formatted generated C# document (after func):\r\n{formattedCSharpText}");
45+
}
46+
3947
// We now have a formatted C# document, and an original document, but we can't just apply the changes to the original
4048
// document as they come from very different places. What we want to do is go through each line of the generated document,
4149
// take the indentation that is in it, and apply it to the original document, and then take any formatting changes
@@ -94,16 +102,37 @@ public async Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext con
94102
// making it run over two lines, or even "string Prop { get" and making it span three lines.
95103
// Since we assume Roslyn won't change anything non-whitespace, we just keep inserting the formatted lines
96104
// of C# until we match the original line contents.
105+
// Of course, Roslyn could just as easily remove whitespace, eg making a "class Goo {" into "class Goo\n{",
106+
// so whilst the same theory applies, instead of inserting formatted lines, we eat the original lines.
97107
while (!changedText.NonWhitespaceContentEquals(formattedCSharpText, originalStart, originalLine.End, formattedStart, formattedLine.End))
98108
{
99-
// Sanity check: Because we're looking ahead through lines until the original line content is fully matches, we could loop forever if there is a bug somewhere
100-
Debug.Assert(
101-
FormattingUtilities.CountNonWhitespaceChars(changedText, originalStart, originalLine.End) >= FormattingUtilities.CountNonWhitespaceChars(formattedCSharpText, formattedStart, formattedLine.End),
102-
"Infinite loop in formatting! A bug in our visitor, or has Roslyn changed a non-whitespace char?");
103-
104-
iFormatted++;
105-
formattedLine = formattedCSharpText.Lines[iFormatted];
106-
formattingChanges.Add(new TextChange(new(originalLine.EndIncludingLineBreak, 0), htmlIndentString + formattedCSharpText.ToString(formattedLine.SpanIncludingLineBreak)));
109+
// If there are more non-whitespace chars in the original line, then its something like "if (true) {" to "if (true)", so keep inserting formatted lines until we're past the brace.
110+
if (FormattingUtilities.CountNonWhitespaceChars(changedText, originalStart, originalLine.End) >= FormattingUtilities.CountNonWhitespaceChars(formattedCSharpText, formattedStart, formattedLine.End))
111+
{
112+
iFormatted++;
113+
if (iFormatted >= formattedCSharpText.Lines.Count)
114+
{
115+
_logger.LogError($"Ran out of formatted lines while trying to process formatted changes after {iOriginal} lines. Abandoning further formatting to not corrupt the source file, please report this issue.");
116+
break;
117+
}
118+
119+
formattedLine = formattedCSharpText.Lines[iFormatted];
120+
formattingChanges.Add(new TextChange(new(originalLine.EndIncludingLineBreak, 0), htmlIndentString + formattedCSharpText.ToString(formattedLine.SpanIncludingLineBreak)));
121+
}
122+
else
123+
{
124+
// Otherwise, there are more whitespace chars in the formatted line, so "if (true)" to "if (true) {", so we need to remove the original lines until we're past the brace.
125+
var oldEnd = originalLine.End;
126+
iOriginal++;
127+
if (iOriginal >= changedText.Lines.Count)
128+
{
129+
_logger.LogError("Ran out of lines while trying to process formatted changes. Abandoning further formatting to not corrupt the source file, please report this issue.");
130+
break;
131+
}
132+
133+
originalLine = changedText.Lines[iOriginal];
134+
formattingChanges.Add(new TextChange(TextSpan.FromBounds(oldEnd, originalLine.End), ""));
135+
}
107136
}
108137
}
109138
}
@@ -113,6 +142,17 @@ public async Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext con
113142
{
114143
iFormatted++;
115144
}
145+
else if (lineInfo.SkipNextLineIfBrace)
146+
{
147+
// If the next line is a brace, we skip it, otherwise we don't. This is used to skip the opening brace of a class
148+
// that we insert, but Roslyn settings might place on the same like as the class declaration.
149+
if (iFormatted + 1 < formattedCSharpText.Lines.Count &&
150+
formattedCSharpText.Lines[iFormatted + 1] is { Span.Length: > 0 } nextLine &&
151+
nextLine.CharAt(0) == '{')
152+
{
153+
iFormatted++;
154+
}
155+
}
116156
}
117157

118158
// We're finished processing the original file, which means we've done all of the indentation for the file, and we've done
@@ -182,4 +222,14 @@ private async Task<SourceText> FormatCSharpAsync(SourceText generatedCSharpText,
182222
[Obsolete("Only for the syntax visualizer, do not call")]
183223
internal static string GetFormattingDocumentContentsForSyntaxVisualizer(RazorCodeDocument codeDocument)
184224
=> CSharpDocumentGenerator.Generate(codeDocument, new()).SourceText.ToString();
225+
226+
internal TestAccessor GetTestAccessor() => new TestAccessor(this);
227+
228+
internal readonly struct TestAccessor(CSharpFormattingPass instance)
229+
{
230+
public void SetFormattedCSharpDocumentModifierFunc(Func<SourceText, SourceText> func)
231+
{
232+
instance._formattedCSharpDocumentModifierFunc = func;
233+
}
234+
}
185235
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Collections.Generic;
55
using System.Collections.Immutable;
6-
using System.Diagnostics;
76
using System.Diagnostics.CodeAnalysis;
87
using System.Linq;
98
using System.Threading;
@@ -404,8 +403,15 @@ private bool FormatBlock(FormattingContext context, RazorSourceDocument source,
404403
{
405404
var openBraceLineNumber = openBraceNode.GetLinePositionSpan(source).Start.Line;
406405
var openBraceLine = source.Text.Lines[openBraceLineNumber];
407-
Debug.Assert(openBraceLine.GetFirstNonWhitespacePosition().HasValue);
408-
additionalIndentation = source.Text.GetSubTextString(TextSpan.FromBounds(openBraceLine.Start, openBraceLine.GetFirstNonWhitespacePosition().GetValueOrDefault()));
406+
407+
// The open brace node might actually start with a newline on the line before, which could be blank,
408+
// so make sure we find some actual content.
409+
while (!openBraceLine.GetFirstNonWhitespacePosition().HasValue)
410+
{
411+
openBraceLine = source.Text.Lines[++openBraceLineNumber];
412+
}
413+
414+
additionalIndentation = openBraceLine.GetLeadingWhitespace();
409415
}
410416
}
411417

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Frozen;
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
8+
using System.Diagnostics;
89
using System.Linq;
910
using System.Threading;
1011
using System.Threading.Tasks;
@@ -321,8 +322,8 @@ private static ImmutableArray<TextChange> UnwrapCSharpSnippets(ImmutableArray<Te
321322
}
322323

323324
/// <summary>
324-
/// This method counts the occurrences of CRLF and LF line endings in the original text.
325-
/// If LF line endings are more prevalent, it removes any CR characters from the text changes
325+
/// This method counts the occurrences of CRLF and LF line endings in the original text.
326+
/// If LF line endings are more prevalent, it removes any CR characters from the text changes
326327
/// to ensure consistency with the LF style.
327328
/// </summary>
328329
private static ImmutableArray<TextChange> NormalizeLineEndings(SourceText originalText, ImmutableArray<TextChange> changes)
@@ -355,9 +356,20 @@ private static ImmutableArray<TextChange> ReplaceInChanges(ImmutableArray<TextCh
355356
return changes.DrainToImmutable();
356357
}
357358

358-
internal static class TestAccessor
359+
internal TestAccessor GetTestAccessor() => new TestAccessor(this);
360+
361+
internal class TestAccessor(RazorFormattingService service)
359362
{
360363
public static FrozenSet<string> GetCSharpTriggerCharacterSet() => s_csharpTriggerCharacterSet;
361364
public static FrozenSet<string> GetHtmlTriggerCharacterSet() => s_htmlTriggerCharacterSet;
365+
366+
public void SetFormattedCSharpDocumentModifierFunc(Func<SourceText, SourceText> func)
367+
{
368+
// This is only valid for the new formatting engine, so a test that sets it for the old formatter is probably written incorrectly.
369+
Debug.Assert(service._languageServerFeatureOptions.UseNewFormattingEngine);
370+
371+
var pass = service._documentFormattingPasses.OfType<New.CSharpFormattingPass>().Single();
372+
pass.GetTestAccessor().SetFormattedCSharpDocumentModifierFunc(func);
373+
}
362374
}
363375
}

0 commit comments

Comments
 (0)