diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.CSharpDocumentGenerator.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.CSharpDocumentGenerator.cs index 7644e4253b4..f97f95fb86f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.CSharpDocumentGenerator.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.CSharpDocumentGenerator.cs @@ -8,6 +8,7 @@ using System.Text; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Features; @@ -214,12 +215,7 @@ public void Generate() var node = root.FindInnermostNode(originalSpan.AbsoluteIndex); if (node is CSharpExpressionLiteralSyntax) { - // Rather than bother to store more data about the formatted file, since we don't actually know where - // these will end up in that file once it's all said and done, we are just going to use a simple comment - // format that we can easily parse. - additionalLinesBuilder.AppendLine(GetAdditionalLineComment(originalSpan)); - additionalLinesBuilder.AppendLine(_sourceText.GetSubTextString(originalSpan.ToTextSpan())); - additionalLinesBuilder.AppendLine(";"); + AddAdditionalLineFormattingContent(additionalLinesBuilder, node, originalSpan); } iMapping++; @@ -254,6 +250,34 @@ public void Generate() _builder.AppendLine(additionalLinesBuilder.ToString()); } + private void AddAdditionalLineFormattingContent(StringBuilder additionalLinesBuilder, RazorSyntaxNode node, SourceSpan originalSpan) + { + // Rather than bother to store more data about the formatted file, since we don't actually know where + // these will end up in that file once it's all said and done, we are just going to use a simple comment + // format that we can easily parse. + + // Special case, for attributes that represent generic type parameters, we want to output something such + // that Roslyn knows to format it as a type. For example, the meaning and spacing around "?"s should be + // what the user expects. + if (node is { Parent.Parent: MarkupTagHelperAttributeSyntax attribute } && + attribute is { Parent.Parent: MarkupTagHelperElementSyntax element } && + element.TagHelperInfo.BindingResult.Descriptors is [{ } descriptor] && + descriptor.IsGenericTypedComponent() && + descriptor.BoundAttributes.FirstOrDefault(d => d.Name == attribute.TagHelperAttributeInfo.Name) is { } boundAttribute && + boundAttribute.IsTypeParameterProperty()) + { + additionalLinesBuilder.AppendLine("F<"); + additionalLinesBuilder.AppendLine(GetAdditionalLineComment(originalSpan)); + additionalLinesBuilder.AppendLine(_sourceText.GetSubTextString(originalSpan.ToTextSpan())); + additionalLinesBuilder.AppendLine("> x;"); + return; + } + + additionalLinesBuilder.AppendLine(GetAdditionalLineComment(originalSpan)); + additionalLinesBuilder.AppendLine(_sourceText.GetSubTextString(originalSpan.ToTextSpan())); + additionalLinesBuilder.AppendLine(";"); + } + public override LineInfo Visit(RazorSyntaxNode? node) { // Sometimes we are in a block where we want to do no formatting at all diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentFormattingTest.cs index 94cec39477a..8068c52b5a0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentFormattingTest.cs @@ -7303,4 +7303,21 @@ public Task NestedExplicitExpression4() } """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12445")] + public Task TypeParameterAttribute() + => RunFormattingTestAsync( + input: """ +