Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a6b2bd6
Simplify helper method
davidwengier Dec 24, 2025
71c5305
Drop Html indentation changes unless they're in script or style tags
davidwengier Dec 24, 2025
c06c22e
Drop all html formatting changes before the first non-whitespace char…
davidwengier Dec 24, 2025
94d066b
Indent attributes in line with the first attribute on an element
davidwengier Dec 24, 2025
d8a3fca
Add basic Html attribute test, and fix bug with generic type params
davidwengier Dec 24, 2025
eb6d9a2
Format RenderFragments that start in explicit expressions
davidwengier Dec 24, 2025
e19c614
Copy indentation from the previous line when ignoring an open brace
davidwengier Dec 24, 2025
e9795e9
Code blocks are always at column 0
davidwengier Dec 24, 2025
60b6257
Text outside of Html elements if formatted to column 0
davidwengier Dec 24, 2025
0b2b168
Update implicit and explicit expression test outputs
davidwengier Dec 24, 2025
b4b9a6d
Update "bad input" test output
davidwengier Dec 24, 2025
143126c
Update RenderFragment test outputs
davidwengier Dec 24, 2025
8ac743c
Add a few more tests to demonstrate fixed issues
davidwengier Dec 24, 2025
4b6666c
Don't do any formatting in Html comments
davidwengier Dec 24, 2025
a12f688
Update doc comment to represent the new behaviour
davidwengier Dec 24, 2025
df04f2a
Fix first attribute alignment calculation, add more test coverage
davidwengier Dec 24, 2025
7a3f323
Fix test baseline
davidwengier Dec 24, 2025
e3ef464
Fix formatting in release mode
davidwengier Dec 25, 2025
61a0354
Unskip test, because I accidentally fixed it :)
davidwengier Dec 25, 2025
d3a6f15
Emit the start of multiline implicit expressions the same as explicit…
davidwengier Dec 29, 2025
385c2fe
Only emit a trailing comment if necessary, so we can check for newlin…
davidwengier Dec 29, 2025
63e4db3
Tests
davidwengier Dec 29, 2025
e823f02
Add new option to control attribute indent style
davidwengier Dec 29, 2025
38c5bac
Implement attribute indent style option
davidwengier Dec 29, 2025
b57255c
Fix multiline implicit expressions with braces that move
davidwengier Jan 5, 2026
419de94
Make sure we only process the closing semicolon when its on the line …
davidwengier Jan 5, 2026
b9a8fc3
PR Feedback
davidwengier Jan 6, 2026
ee8f1bd
PR feedback
davidwengier Jan 6, 2026
e728adf
Fix off by one error
davidwengier Jan 6, 2026
77f4e63
(Most) PR feedback
davidwengier Jan 6, 2026
0047c0a
Fix test baselines after fixing the off-by-one issue
davidwengier Jan 6, 2026
9d5cf02
Merge remote-tracking branch 'upstream/main' into dev/dawengie/LetsDo…
davidwengier Jan 6, 2026
ec1da85
Update more test baselines
davidwengier Jan 6, 2026
71686f6
Fix issue when first attribute is not on the same line as the start t…
davidwengier Jan 7, 2026
e4070bd
Fix logic for when elements cause indentation
davidwengier Jan 7, 2026
ea3f882
Change the algorithm to find script and style tags, to simplify the s…
davidwengier Jan 7, 2026
ce5daef
Remove debug logging
davidwengier Jan 7, 2026
d73855c
Remove unused using
davidwengier Jan 7, 2026
3c3052e
Merge branch 'dev/dawengie/LetsDoHtmlFormatting' into dev/dawengie/Fi…
davidwengier Jan 7, 2026
227b67b
Update src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/…
davidwengier Jan 7, 2026
b326574
Emit the start of multiline implicit expressions the same as explicit…
davidwengier Jan 7, 2026
3d72258
Fix formatting with adjacent C# templates (#12636)
davidwengier Jan 7, 2026
8dfee13
Merge remote-tracking branch 'upstream/dev/dawengie/LetsDoHtmlFormatt…
davidwengier Jan 7, 2026
7aac958
Update test for both indent styles
davidwengier Jan 7, 2026
e4feaa9
Add new option to control attribute indent style (#12625)
davidwengier Jan 7, 2026
794ac7b
Fix test in the old server
davidwengier Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ static bool IsInScriptOrStyleOrHtmlComment(AspNetCore.Razor.Language.Syntax.Synt
{
if (node is MarkupElementSyntax elementNode)
{
if (RazorSyntaxFacts.IsStyleBlock(elementNode) || RazorSyntaxFacts.IsScriptBlock(elementNode))
if (RazorSyntaxFacts.IsScriptOrStyleBlock(elementNode))
{
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 System.Linq;
using System.Threading;
Expand Down Expand Up @@ -87,13 +88,26 @@ private async Task<ImmutableArray<TextChange>> FilterIncomingChangesAsync(Format
var syntaxRoot = codeDocument.GetRequiredSyntaxRoot();
var sourceText = codeDocument.Source.Text;
SyntaxNode? csharpSyntaxRoot = null;
ImmutableArray<int> scriptAndStyleElementBounds = default;

using var changesToKeep = new PooledArrayBuilder<TextChange>(capacity: changes.Length);

foreach (var change in changes)
{
// We don't keep changes that start inside of a razor comment block.
var node = syntaxRoot.FindInnermostNode(change.Span.Start);

// We don't keep indentation changes that aren't in a script or style block
// As a quick check, we only care about dropping edits that affect indentation - ie, are before the first
// whitespace char on a line
var line = sourceText.Lines.GetLineFromPosition(change.Span.Start);
if (change.Span.Start < line.GetFirstNonWhitespacePosition() &&
!ChangeIsInsideScriptOrStyleElement(change))
{
context.Logger?.LogMessage($"Dropping change {change} because it's not in a script or style block");
Comment thread
davidwengier marked this conversation as resolved.
Outdated
continue;
}

// We don't keep changes that start inside of a razor comment block.
Comment thread
davidwengier marked this conversation as resolved.
var comment = node?.FirstAncestorOrSelf<RazorCommentBlockSyntax>();
if (comment is not null && change.Span.Start > comment.SpanStart)
{
Expand Down Expand Up @@ -190,6 +204,60 @@ async Task<bool> ChangeIsInStringLiteralAsync(FormattingContext context, RazorCS

return false;
}

bool ChangeIsInsideScriptOrStyleElement(TextChange change)
{
// Rather than ascend up the tree for every change, and look for script/style elements, we build up an index
// first and just reuse it for every subsequent edit.
InitializeElementBoundsArray();

// If there aren't any elements we're interested in, we don't need to do anything
if (scriptAndStyleElementBounds.Length == 0)
{
return false;
}

var index = scriptAndStyleElementBounds.BinarySearch(change.Span.Start);

// If we got a hit, then we're on the tag itself, which is not inside the tag
if (index >= 0)
{
return false;
}

// If we don't get a hit, the complement of the result is the index to the next largest item in the array. Since we add
// things to the array in pairs, we know that if that index is even, we're outside an element, so we want to ignore identation.
// This only works if the array is ordered, but we can assume it is because we built it in order, and it makes no sense to nest
// script tags within style tags, or vice versa.
index = ~index;
if (index % 2 == 0)
{
return false;
}

return true;
}

void InitializeElementBoundsArray()
{
if (!scriptAndStyleElementBounds.IsDefault)
{
return;
}

using var boundsBuilder = new PooledArrayBuilder<int>();

foreach (var element in syntaxRoot.DescendantNodes(static node => node is RazorDocumentSyntax or MarkupBlockSyntax or MarkupElementSyntax or CSharpCodeBlockSyntax).OfType<MarkupElementSyntax>())
Comment thread
davidwengier marked this conversation as resolved.
Outdated
{
if (RazorSyntaxFacts.IsScriptOrStyleBlock(element))
{
boundsBuilder.Add(element.SpanStart);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boundsBuilder.Add(element.SpanStart);

I assume doing the adding two ints and then doing the mod 2 checking inside ChangeIsInsideScriptOrStyleElement was cleaner than just adding in the TextSpan and doing a BinarySearchBy call?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly never thought of that. I guess it would work though, if the binary search fails you know you're not in a tag? Not sure it's worth changing, but I guess I'm open to it if you think this way is confusing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, if binary search fails, not in a matching span. I think it would be a bit easier to understand, but it's not like this is that complicated anyways.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Played around with this, and yeah I like it much better. Some interesting edges at trying to get the span of just the contents of the tag, because the compiler can be annoying with whitespace, but definitely easier to understand a complicated span calculation and simple search, than the other way around. Also added direct tests

boundsBuilder.Add(element.Span.End);
}
}

scriptAndStyleElementBounds = boundsBuilder.ToImmutableAndClear();
}
}

internal TestAccessor GetTestAccessor() => new(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,18 +208,19 @@ internal static bool IsInUsingDirective(RazorSyntaxNode node)
return false;
}

internal static bool IsElementWithName(MarkupElementSyntax? element, string name)
internal static bool IsScriptOrStyleBlock(MarkupElementSyntax? element)
{
return string.Equals(element?.StartTag.Name.Content, name, StringComparison.OrdinalIgnoreCase);
}
// StartTag is annotated as not nullable, but on invalid documents it can be. The 'Format_DocumentWithDiagnostics' test
// illustrates this.
if (element?.StartTag?.Name.Content is not { } tagName)
{
return false;
}

internal static bool IsStyleBlock(MarkupElementSyntax? node)
{
return IsElementWithName(node, "style");
return string.Equals(tagName, "script", StringComparison.OrdinalIgnoreCase) ||
string.Equals(tagName, "style", StringComparison.OrdinalIgnoreCase);
}

internal static bool IsScriptBlock(MarkupElementSyntax? node)
{
return IsElementWithName(node, "script");
}
}