Skip to content

Commit abea493

Browse files
authored
Add formatting option to force open brace onto the next line after a @code or @functions block (#10018)
Fixes: Being nerdsniped by `u/featheredsnake` on Reddit: https://www.reddit.com/r/Blazor/comments/1b47odh/am_i_the_only_one_bothered_that_code_brackets/ ![27c833c0-aa9c-4be6-9519-3b3ecb9f3f54](https://github.com/dotnet/razor/assets/754264/d3c16ea4-9b75-4972-a4bd-ad4bf986b54d) VS Code option: dotnet/vscode-csharp#6939
2 parents 9e4a93f + 823069a commit abea493

File tree

29 files changed

+396
-68
lines changed

29 files changed

+396
-68
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,30 @@ internal RazorLSPOptions BuildOptions(JObject[] result)
9595
}
9696
else
9797
{
98-
ExtractVSCodeOptions(result, out var enableFormatting, out var autoClosingTags, out var commitElementsWithSpace);
99-
return new RazorLSPOptions(enableFormatting, autoClosingTags, commitElementsWithSpace, ClientSettings.Default);
98+
ExtractVSCodeOptions(result, out var enableFormatting, out var autoClosingTags, out var commitElementsWithSpace, out var codeBlockBraceOnNextLine);
99+
return RazorLSPOptions.Default with
100+
{
101+
EnableFormatting = enableFormatting,
102+
AutoClosingTags = autoClosingTags,
103+
CommitElementsWithSpace = commitElementsWithSpace,
104+
CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine
105+
};
100106
}
101107
}
102108

103109
private void ExtractVSCodeOptions(
104110
JObject[] result,
105111
out bool enableFormatting,
106112
out bool autoClosingTags,
107-
out bool commitElementsWithSpace)
113+
out bool commitElementsWithSpace,
114+
out bool codeBlockBraceOnNextLine)
108115
{
109116
var razor = result[0];
110117
var html = result[1];
111118

112119
enableFormatting = RazorLSPOptions.Default.EnableFormatting;
113120
autoClosingTags = RazorLSPOptions.Default.AutoClosingTags;
121+
codeBlockBraceOnNextLine = RazorLSPOptions.Default.CodeBlockBraceOnNextLine;
114122
// Deliberately not using the "default" here because we want a different default for VS Code, as
115123
// this matches VS Code's html servers commit behaviour
116124
commitElementsWithSpace = false;
@@ -119,10 +127,17 @@ private void ExtractVSCodeOptions(
119127
{
120128
if (razor.TryGetValue("format", out var parsedFormat))
121129
{
122-
if (parsedFormat is JObject jObject &&
123-
jObject.TryGetValue("enable", out var parsedEnableFormatting))
130+
if (parsedFormat is JObject jObject)
124131
{
125-
enableFormatting = GetObjectOrDefault(parsedEnableFormatting, enableFormatting);
132+
if (jObject.TryGetValue("enable", out var parsedEnableFormatting))
133+
{
134+
enableFormatting = GetObjectOrDefault(parsedEnableFormatting, enableFormatting);
135+
}
136+
137+
if (jObject.TryGetValue("codeBlockBraceOnNextLine", out var parsedCodeBlockBraceOnNextLine))
138+
{
139+
codeBlockBraceOnNextLine = GetObjectOrDefault(parsedCodeBlockBraceOnNextLine, codeBlockBraceOnNextLine);
140+
}
126141
}
127142
}
128143

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/RazorFormattingPass.cs

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,33 @@
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;
54
using System.Collections.Generic;
65
using System.Diagnostics.CodeAnalysis;
76
using System.Linq;
87
using System.Threading;
98
using System.Threading.Tasks;
109
using Microsoft.AspNetCore.Razor.Language;
10+
using Microsoft.AspNetCore.Razor.Language.Components;
1111
using Microsoft.AspNetCore.Razor.Language.Extensions;
1212
using Microsoft.AspNetCore.Razor.Language.Syntax;
1313
using Microsoft.AspNetCore.Razor.LanguageServer.Extensions;
1414
using Microsoft.CodeAnalysis.Razor.Logging;
1515
using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions;
1616
using Microsoft.Extensions.Logging;
17+
using Microsoft.Extensions.Options;
1718
using Microsoft.VisualStudio.LanguageServer.Protocol;
1819

1920
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
2021

21-
internal class RazorFormattingPass : FormattingPassBase
22+
internal class RazorFormattingPass(
23+
IRazorDocumentMappingService documentMappingService,
24+
IClientConnection clientConnection,
25+
IOptionsMonitor<RazorLSPOptions> optionsMonitor,
26+
IRazorLoggerFactory loggerFactory)
27+
: FormattingPassBase(documentMappingService, clientConnection)
2228
{
23-
private readonly ILogger _logger;
24-
25-
public RazorFormattingPass(
26-
IRazorDocumentMappingService documentMappingService,
27-
IClientConnection clientConnection,
28-
IRazorLoggerFactory loggerFactory)
29-
: base(documentMappingService, clientConnection)
30-
{
31-
if (loggerFactory is null)
32-
{
33-
throw new ArgumentNullException(nameof(loggerFactory));
34-
}
35-
36-
_logger = loggerFactory.CreateLogger<RazorFormattingPass>();
37-
}
29+
private readonly ILogger _logger = loggerFactory.CreateLogger<RazorFormattingPass>();
30+
private readonly IOptionsMonitor<RazorLSPOptions> _optionsMonitor = optionsMonitor;
3831

3932
// Run after the C# formatter pass.
4033
public override int Order => DefaultOrder - 4;
@@ -76,7 +69,7 @@ public async override Task<FormattingResult> ExecuteAsync(FormattingContext cont
7669
return new FormattingResult(finalEdits);
7770
}
7871

79-
private static IEnumerable<TextEdit> FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree)
72+
private IEnumerable<TextEdit> FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree)
8073
{
8174
var edits = new List<TextEdit>();
8275
var source = syntaxTree.Source;
@@ -93,7 +86,7 @@ private static IEnumerable<TextEdit> FormatRazor(FormattingContext context, Razo
9386
return edits;
9487
}
9588

96-
private static void TryFormatBlocks(FormattingContext context, List<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
89+
private void TryFormatBlocks(FormattingContext context, List<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
9790
{
9891
// We only want to run one of these
9992
_ = TryFormatFunctionsBlock(context, edits, source, node) ||
@@ -122,8 +115,8 @@ directiveCode.Children is [RazorDirectiveSyntax directive] &&
122115
if (TryGetWhitespace(children, out var whitespaceBeforeSectionName, out var whitespaceAfterSectionName))
123116
{
124117
// For whitespace we normalize it differently depending on if its multi-line or not
125-
FormatWhitespaceBetweenDirectiveAndBrace(whitespaceBeforeSectionName, directive, edits, source, context);
126-
FormatWhitespaceBetweenDirectiveAndBrace(whitespaceAfterSectionName, directive, edits, source, context);
118+
FormatWhitespaceBetweenDirectiveAndBrace(whitespaceBeforeSectionName, directive, edits, source, context, forceNewLine: false);
119+
FormatWhitespaceBetweenDirectiveAndBrace(whitespaceAfterSectionName, directive, edits, source, context, forceNewLine: false);
127120

128121
return true;
129122
}
@@ -178,8 +171,7 @@ private static bool TryFormatFunctionsBlock(FormattingContext context, IList<Tex
178171
// }
179172
if (node is CSharpCodeBlockSyntax { Children: [RazorDirectiveSyntax { Body: RazorDirectiveBodySyntax body } directive] })
180173
{
181-
var keywordContent = body.Keyword.GetContent();
182-
if (keywordContent != "functions" && keywordContent != "code")
174+
if (!IsCodeOrFunctionsBlock(body.Keyword))
183175
{
184176
return false;
185177
}
@@ -265,7 +257,7 @@ private static bool TryFormatHtmlInCSharp(FormattingContext context, IList<TextE
265257
return false;
266258
}
267259

268-
private static void TryFormatCSharpBlockStructure(FormattingContext context, List<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
260+
private void TryFormatCSharpBlockStructure(FormattingContext context, List<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
269261
{
270262
// We're looking for a code block like this:
271263
//
@@ -284,11 +276,16 @@ private static void TryFormatCSharpBlockStructure(FormattingContext context, Lis
284276
!directive.ContainsDiagnostics &&
285277
directive.DirectiveDescriptor?.Kind == DirectiveKind.CodeBlock)
286278
{
279+
// If we're formatting a @code or @functions directive, the user might have indicated they always want a newline
280+
var forceNewLine = _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine &&
281+
directive.Body is RazorDirectiveBodySyntax { Keyword: { } keyword } &&
282+
IsCodeOrFunctionsBlock(keyword);
283+
287284
var children = code.Children;
288285
if (TryGetLeadingWhitespace(children, out var whitespace))
289286
{
290287
// For whitespace we normalize it differently depending on if its multi-line or not
291-
FormatWhitespaceBetweenDirectiveAndBrace(whitespace, directive, edits, source, context);
288+
FormatWhitespaceBetweenDirectiveAndBrace(whitespace, directive, edits, source, context, forceNewLine);
292289
}
293290
else if (children.TryGetOpenBraceToken(out var brace))
294291
{
@@ -297,7 +294,9 @@ private static void TryFormatCSharpBlockStructure(FormattingContext context, Lis
297294
var edit = new TextEdit
298295
{
299296
Range = new Range { Start = start, End = start },
300-
NewText = " "
297+
NewText = forceNewLine
298+
? context.NewLineString + FormattingUtilities.GetIndentationString(directive.GetLinePositionSpan(source).Start.Character, context.Options.InsertSpaces, context.Options.TabSize)
299+
: " "
301300
};
302301
edits.Add(edit);
303302
}
@@ -355,9 +354,9 @@ static bool IsSingleLineDirective(SyntaxNode node, [NotNullWhen(true)] out Synta
355354
}
356355
}
357356

358-
private static void FormatWhitespaceBetweenDirectiveAndBrace(SyntaxNode node, RazorDirectiveSyntax directive, List<TextEdit> edits, RazorSourceDocument source, FormattingContext context)
357+
private static void FormatWhitespaceBetweenDirectiveAndBrace(SyntaxNode node, RazorDirectiveSyntax directive, List<TextEdit> edits, RazorSourceDocument source, FormattingContext context, bool forceNewLine)
359358
{
360-
if (node.ContainsOnlyWhitespace(includingNewLines: false))
359+
if (node.ContainsOnlyWhitespace(includingNewLines: false) && !forceNewLine)
361360
{
362361
ShrinkToSingleSpace(node, edits, source);
363362
}
@@ -393,6 +392,7 @@ private static bool FormatBlock(FormattingContext context, RazorSourceDocument s
393392

394393
var openBraceRange = openBraceNode.GetRangeWithoutWhitespace(source);
395394
var codeRange = codeNode.GetRangeWithoutWhitespace(source);
395+
396396
if (openBraceRange is not null &&
397397
codeRange is not null &&
398398
openBraceRange.End.Line == codeRange.Start.Line &&
@@ -536,4 +536,11 @@ static int GetTrailingWhitespaceLength(SyntaxNode node, FormattingContext contex
536536
}
537537
}
538538
}
539+
540+
private static bool IsCodeOrFunctionsBlock(RazorSyntaxNode keyword)
541+
{
542+
var keywordContent = keyword.GetContent();
543+
return keywordContent == FunctionsDirective.Directive.Directive ||
544+
keywordContent == ComponentCodeDirective.Directive.Directive;
545+
}
539546
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLSPOptions.cs

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,9 @@ internal record RazorLSPOptions(
1515
bool FormatOnType,
1616
bool AutoInsertAttributeQuotes,
1717
bool ColorBackground,
18+
bool CodeBlockBraceOnNextLine,
1819
bool CommitElementsWithSpace)
1920
{
20-
public RazorLSPOptions(bool enableFormatting, bool autoClosingTags, bool commitElementsWithSpace, ClientSettings settings)
21-
: this(enableFormatting,
22-
autoClosingTags,
23-
!settings.ClientSpaceSettings.IndentWithTabs,
24-
settings.ClientSpaceSettings.IndentSize,
25-
settings.ClientCompletionSettings.AutoShowCompletion,
26-
settings.ClientCompletionSettings.AutoListParams,
27-
settings.AdvancedSettings.FormatOnType,
28-
settings.AdvancedSettings.AutoInsertAttributeQuotes,
29-
settings.AdvancedSettings.ColorBackground,
30-
commitElementsWithSpace)
31-
{
32-
}
33-
3421
public readonly static RazorLSPOptions Default = new(EnableFormatting: true,
3522
AutoClosingTags: true,
3623
AutoListParams: true,
@@ -40,15 +27,23 @@ public RazorLSPOptions(bool enableFormatting, bool autoClosingTags, bool commitE
4027
FormatOnType: true,
4128
AutoInsertAttributeQuotes: true,
4229
ColorBackground: false,
30+
CodeBlockBraceOnNextLine: false,
4331
CommitElementsWithSpace: true);
4432

4533
/// <summary>
4634
/// Initializes the LSP options with the settings from the passed in client settings, and default values for anything
4735
/// not defined in client settings.
4836
/// </summary>
49-
internal static RazorLSPOptions From(ClientSettings clientSettings)
37+
internal static RazorLSPOptions From(ClientSettings settings)
5038
=> new(Default.EnableFormatting,
51-
clientSettings.AdvancedSettings.AutoClosingTags,
52-
clientSettings.AdvancedSettings.CommitElementsWithSpace,
53-
clientSettings);
39+
settings.AdvancedSettings.AutoClosingTags,
40+
!settings.ClientSpaceSettings.IndentWithTabs,
41+
settings.ClientSpaceSettings.IndentSize,
42+
settings.ClientCompletionSettings.AutoShowCompletion,
43+
settings.ClientCompletionSettings.AutoListParams,
44+
settings.AdvancedSettings.FormatOnType,
45+
settings.AdvancedSettings.AutoInsertAttributeQuotes,
46+
settings.AdvancedSettings.ColorBackground,
47+
settings.AdvancedSettings.CodeBlockBraceOnNextLine,
48+
settings.AdvancedSettings.CommitElementsWithSpace);
5449
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ internal sealed record ClientSpaceSettings(bool IndentWithTabs, int IndentSize)
3232
public int IndentSize { get; } = IndentSize >= 0 ? IndentSize : throw new ArgumentOutOfRangeException(nameof(IndentSize));
3333
}
3434

35-
internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoClosingTags, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CommitElementsWithSpace, SnippetSetting SnippetSetting, LogLevel LogLevel)
35+
internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoClosingTags, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, bool CommitElementsWithSpace, SnippetSetting SnippetSetting, LogLevel LogLevel)
3636
{
37-
public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CommitElementsWithSpace: true, SnippetSetting.All, LogLevel.Warning);
37+
public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true, SnippetSetting.All, LogLevel.Warning);
3838
}

src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Options/OptionsStorage.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ public bool ColorBackground
5353
set => SetBool(SettingsNames.ColorBackground.LegacyName, value);
5454
}
5555

56+
public bool CodeBlockBraceOnNextLine
57+
{
58+
get => GetBool(SettingsNames.CodeBlockBraceOnNextLine.LegacyName, defaultValue: false);
59+
set => SetBool(SettingsNames.CodeBlockBraceOnNextLine.LegacyName, value);
60+
}
61+
5662
public bool CommitElementsWithSpace
5763
{
5864
get => GetBool(SettingsNames.CommitElementsWithSpace.LegacyName, defaultValue: true);
@@ -101,7 +107,7 @@ public async Task OnChangedAsync(Action<ClientAdvancedSettings> changed)
101107

102108
private EventHandler<ClientAdvancedSettingsChangedEventArgs>? _changed;
103109

104-
public ClientAdvancedSettings GetAdvancedSettings() => new(FormatOnType, AutoClosingTags, AutoInsertAttributeQuotes, ColorBackground, CommitElementsWithSpace, Snippets, LogLevel);
110+
public ClientAdvancedSettings GetAdvancedSettings() => new(FormatOnType, AutoClosingTags, AutoInsertAttributeQuotes, ColorBackground, CodeBlockBraceOnNextLine, CommitElementsWithSpace, Snippets, LogLevel);
105111

106112
public bool GetBool(string name, bool defaultValue)
107113
{

src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Options/SettingsNames.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public record Setting(string LegacyName, string UnifiedName);
1414
public static readonly Setting AutoClosingTags = new("AutoClosingTags", UnifiedCollection + ".autoClosingTags");
1515
public static readonly Setting AutoInsertAttributeQuotes = new("AutoInsertAttributeQuotes", UnifiedCollection + ".autoInsertAttributeQuotes");
1616
public static readonly Setting ColorBackground = new("ColorBackground", UnifiedCollection + ".colorBackground");
17+
public static readonly Setting CodeBlockBraceOnNextLine = new("CodeBlockBraceOnNextLine", UnifiedCollection + ".codeBlockBraceOnNextLine");
1718
public static readonly Setting CommitElementsWithSpace = new("CommitElementsWithSpace", UnifiedCollection + ".commitCharactersWithSpace");
1819
public static readonly Setting Snippets = new("Snippets", UnifiedCollection + ".snippets");
1920
public static readonly Setting LogLevel = new("LogLevel", UnifiedCollection + ".logLevel");
@@ -24,6 +25,7 @@ public record Setting(string LegacyName, string UnifiedName);
2425
AutoClosingTags,
2526
AutoInsertAttributeQuotes,
2627
ColorBackground,
28+
CodeBlockBraceOnNextLine,
2729
CommitElementsWithSpace,
2830
Snippets,
2931
LogLevel,

src/Razor/src/Microsoft.VisualStudio.RazorExtension/Options/AdvancedOptionPage.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal class AdvancedOptionPage : DialogPage
2121
private bool? _autoClosingTags;
2222
private bool? _autoInsertAttributeQuotes;
2323
private bool? _colorBackground;
24+
private bool? _codeBlockBraceOnNextLine;
2425
private bool? _commitElementsWithSpace;
2526
private SnippetSetting? _snippets;
2627
private LogLevel? _logLevel;
@@ -81,6 +82,15 @@ public bool ColorBackground
8182
set => _colorBackground = value;
8283
}
8384

85+
[LocCategory(nameof(VSPackage.Formatting))]
86+
[LocDescription(nameof(VSPackage.Setting_CodeBlockBraceOnNextLineDescription))]
87+
[LocDisplayName(nameof(VSPackage.Setting_CodeBlockBraceOnNextLineDisplayName))]
88+
public bool CodeBlockBraceOnNextLine
89+
{
90+
get => _codeBlockBraceOnNextLine ?? _optionsStorage.Value.CodeBlockBraceOnNextLine;
91+
set => _codeBlockBraceOnNextLine = value;
92+
}
93+
8494
[LocCategory(nameof(VSPackage.Completion))]
8595
[LocDescription(nameof(VSPackage.Setting_SnippetsDescription))]
8696
[LocDisplayName(nameof(VSPackage.Setting_SnippetsDisplayName))]

src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@
6161
}
6262
}
6363
},
64+
"textEditor.razor.advanced.codeBlockBraceOnNextLine": {
65+
"type": "boolean",
66+
"default": false,
67+
"title": "@Setting_CodeBlockBraceOnNextLinedDisplayName;{13b72f58-279e-49e0-a56d-296be02f0805}",
68+
"description": "@Setting_CodeBlockBraceOnNextLinedDescription;{13b72f58-279e-49e0-a56d-296be02f0805}",
69+
"migration": {
70+
"pass": {
71+
"input": {
72+
"store": "VsUserSettingsRegistry",
73+
"path": "Razor\\CodeBlockBraceOnNextLined"
74+
}
75+
}
76+
}
77+
},
6478
"textEditor.razor.advanced.commitElementsWithSpace": {
6579
"type": "boolean",
6680
"default": true,

src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@
162162
<data name="Setting_AutoInsertAttributeQuotesDisplayName" xml:space="preserve">
163163
<value>Auto Insert Attribute Quotes</value>
164164
</data>
165+
<data name="Setting_CodeBlockBraceOnNextLineDescription" xml:space="preserve">
166+
<value>Forces the open brace after an @code or @functions directive to be on the following line</value>
167+
</data>
168+
<data name="Setting_CodeBlockBraceOnNextLineDisplayName" xml:space="preserve">
169+
<value>Code/Functions block open brace on next line</value>
170+
</data>
165171
<data name="Setting_ColorBackgroundDescription" xml:space="preserve">
166172
<value>If true, gives C# code in Razor files a background color</value>
167173
</data>

0 commit comments

Comments
 (0)