diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/BaseMarkupStartTagSyntax.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/BaseMarkupStartTagSyntax.cs index ac6f08cb6e6..5cb281d4a1c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/BaseMarkupStartTagSyntax.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/BaseMarkupStartTagSyntax.cs @@ -21,6 +21,13 @@ public SyntaxList LegacyChildren } } + public bool IsSelfClosing() + { + return ForwardSlash.Kind != SyntaxKind.None && + !ForwardSlash.IsMissing && + !CloseAngle.IsMissing; + } + /// /// This method returns the children of this start tag in legacy format. /// This is needed to generate the same classified spans as the legacy syntax tree. diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/MarkupStartTagSyntax.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/MarkupStartTagSyntax.cs index f0e9a3fb5f8..6b20b7387b4 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/MarkupStartTagSyntax.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/MarkupStartTagSyntax.cs @@ -12,13 +12,6 @@ public string GetTagNameWithOptionalBang() return Name.IsMissing ? string.Empty : Bang.Content + Name.Content; } - public bool IsSelfClosing() - { - return ForwardSlash.Kind != SyntaxKind.None && - !ForwardSlash.IsMissing && - !CloseAngle.IsMissing; - } - public bool IsVoidElement() { return ParserHelpers.VoidElements.Contains(Name.Content); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs index 5f300cb9c96..19d31766f04 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs @@ -171,7 +171,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V // For C# we run the edit through our formatting engine Debug.Assert(positionInfo.LanguageKind == RazorLanguageKind.CSharp); - var options = RazorFormattingOptions.From(originalRequest.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + var options = RazorFormattingOptions.From(originalRequest.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine, _optionsMonitor.CurrentValue.AttributeIndentStyle); var csharpSourceText = await documentContext.GetCSharpSourceTextAsync(cancellationToken).ConfigureAwait(false); var textChange = csharpSourceText.GetTextChange(delegatedResponse.TextEdit); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs index f73bc3baf3c..1bbce9f198f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs @@ -99,7 +99,7 @@ private async Task PostProcessCompletionItemAsync( return resolvedCompletionItem; } - var options = RazorFormattingOptions.From(formattingOptions, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + var options = RazorFormattingOptions.From(formattingOptions, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine, _optionsMonitor.CurrentValue.AttributeIndentStyle); return await DelegatedCompletionHelper.FormatCSharpCompletionItemAsync( resolvedCompletionItem, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs index 231a7529ee0..c180fe4a727 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Settings; @@ -95,13 +96,14 @@ internal RazorLSPOptions BuildOptions(JsonObject[] result) } else { - ExtractVSCodeOptions(result, out var formatting, out var autoClosingTags, out var commitElementsWithSpace, out var codeBlockBraceOnNextLine); + ExtractVSCodeOptions(result, out var formatting, out var autoClosingTags, out var commitElementsWithSpace, out var codeBlockBraceOnNextLine, out var attributeIndentStyle); return RazorLSPOptions.Default with { Formatting = formatting, AutoClosingTags = autoClosingTags, CommitElementsWithSpace = commitElementsWithSpace, - CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine + CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine, + AttributeIndentStyle = attributeIndentStyle, }; } } @@ -111,7 +113,8 @@ private void ExtractVSCodeOptions( out FormattingFlags formatting, out bool autoClosingTags, out bool commitElementsWithSpace, - out bool codeBlockBraceOnNextLine) + out bool codeBlockBraceOnNextLine, + out AttributeIndentStyle attributeIndentStyle) { var razor = result[0]; var html = result[1]; @@ -122,6 +125,7 @@ private void ExtractVSCodeOptions( // Deliberately not using the "default" here because we want a different default for VS Code, as // this matches VS Code's html servers commit behaviour commitElementsWithSpace = false; + attributeIndentStyle = RazorLSPOptions.Default.AttributeIndentStyle; if (razor.TryGetPropertyValue("format", out var parsedFormatNode) && parsedFormatNode?.AsObject() is { } parsedFormat) @@ -145,6 +149,12 @@ private void ExtractVSCodeOptions( { codeBlockBraceOnNextLine = GetObjectOrDefault(parsedCodeBlockBraceOnNextLine, codeBlockBraceOnNextLine); } + + if (parsedFormat.TryGetPropertyValue("attributeIndentStyle", out var parsedAttributeIndentStyle) && + parsedAttributeIndentStyle is not null) + { + attributeIndentStyle = GetEnumValue(parsedAttributeIndentStyle, AttributeIndentStyle.AlignWithFirst); + } } if (razor.TryGetPropertyValue("completion", out var parsedCompletionNode) && @@ -212,4 +222,26 @@ private T GetObjectOrDefault(JsonNode token, T defaultValue, [CallerArgumentE return defaultValue; } } + + private T GetEnumValue(JsonNode token, T defaultValue, [CallerArgumentExpression(nameof(defaultValue))] string? expression = null) + where T : struct + { + try + { + // GetValue could potentially throw here if the user provides malformed options. + // If this occurs, catch the exception and return the default value. + if (token.GetValue() is { } stringValue && + Enum.TryParse(stringValue, ignoreCase: true, out var parsed)) + { + return parsed; + } + + return defaultValue; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Malformed option: Token {token} cannot be converted to type {typeof(T)} for {expression}."); + return defaultValue; + } + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs index d51eaca2e37..59aed893731 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs @@ -49,7 +49,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentFormattingParams var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine, _optionsMonitor.CurrentValue.AttributeIndentStyle); if (await _htmlFormatter.GetDocumentFormattingEditsAsync( documentContext.Snapshot, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs index 77666e570f4..6f654e8b3e2 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -91,7 +91,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormatting cancellationToken.ThrowIfCancellationRequested(); - var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine, _optionsMonitor.CurrentValue.AttributeIndentStyle); ImmutableArray formattedChanges; if (triggerCharacterKind == RazorLanguageKind.CSharp) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs index ea308fee08c..2908fccb9f2 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs @@ -61,7 +61,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentRangeFormattingP } } - var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine, _optionsMonitor.CurrentValue.AttributeIndentStyle); if (await _htmlFormatter.GetDocumentFormattingEditsAsync( documentContext.Snapshot, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs index 509bf9c6a58..e13b7c17421 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs @@ -19,6 +19,7 @@ internal sealed record RazorLSPOptions( bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, + AttributeIndentStyle AttributeIndentStyle, bool CommitElementsWithSpace, ImmutableArray TaskListDescriptors) { @@ -31,6 +32,7 @@ internal sealed record RazorLSPOptions( AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, + AttributeIndentStyle: AttributeIndentStyle.AlignWithFirst, CommitElementsWithSpace: true, TaskListDescriptors: []); @@ -47,16 +49,17 @@ public ImmutableArray TaskListDescriptors /// internal static RazorLSPOptions From(ClientSettings settings) => new(GetFormattingFlags(settings), - settings.AdvancedSettings.AutoClosingTags, - !settings.ClientSpaceSettings.IndentWithTabs, - settings.ClientSpaceSettings.IndentSize, - settings.ClientCompletionSettings.AutoShowCompletion, - settings.ClientCompletionSettings.AutoListParams, - settings.AdvancedSettings.AutoInsertAttributeQuotes, - settings.AdvancedSettings.ColorBackground, - settings.AdvancedSettings.CodeBlockBraceOnNextLine, - settings.AdvancedSettings.CommitElementsWithSpace, - settings.AdvancedSettings.TaskListDescriptors); + settings.AdvancedSettings.AutoClosingTags, + !settings.ClientSpaceSettings.IndentWithTabs, + settings.ClientSpaceSettings.IndentSize, + settings.ClientCompletionSettings.AutoShowCompletion, + settings.ClientCompletionSettings.AutoListParams, + settings.AdvancedSettings.AutoInsertAttributeQuotes, + settings.AdvancedSettings.ColorBackground, + settings.AdvancedSettings.CodeBlockBraceOnNextLine, + settings.AdvancedSettings.AttributeIndentStyle, + settings.AdvancedSettings.CommitElementsWithSpace, + settings.AdvancedSettings.TaskListDescriptors); private static FormattingFlags GetFormattingFlags(ClientSettings settings) { @@ -86,6 +89,7 @@ public bool Equals(RazorLSPOptions? other) AutoInsertAttributeQuotes == other.AutoInsertAttributeQuotes && ColorBackground == other.ColorBackground && CodeBlockBraceOnNextLine == other.CodeBlockBraceOnNextLine && + AttributeIndentStyle == other.AttributeIndentStyle && CommitElementsWithSpace == other.CommitElementsWithSpace && TaskListDescriptors.SequenceEqual(other.TaskListDescriptors); } @@ -102,6 +106,7 @@ public override int GetHashCode() hash.Add(AutoInsertAttributeQuotes); hash.Add(ColorBackground); hash.Add(CodeBlockBraceOnNextLine); + hash.Add(AttributeIndentStyle); hash.Add(CommitElementsWithSpace); hash.Add(TaskListDescriptors); return hash; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs index 6cb84700da1..aa8ceb209d8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -103,7 +103,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalInlineCompleti continue; } - var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine, _optionsMonitor.CurrentValue.AttributeIndentStyle); var formattingContext = FormattingContext.Create( documentContext.Snapshot, codeDocument, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/CodeActions/CohostCodeActionsResolveEndpoint.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/CodeActions/CohostCodeActionsResolveEndpoint.cs index 1e2f376c4cf..9f91658e878 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/CodeActions/CohostCodeActionsResolveEndpoint.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/CodeActions/CohostCodeActionsResolveEndpoint.cs @@ -86,7 +86,8 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie { InsertSpaces = !clientSettings.ClientSpaceSettings.IndentWithTabs, TabSize = clientSettings.ClientSpaceSettings.IndentSize, - CodeBlockBraceOnNextLine = clientSettings.AdvancedSettings.CodeBlockBraceOnNextLine + CodeBlockBraceOnNextLine = clientSettings.AdvancedSettings.CodeBlockBraceOnNextLine, + AttributeIndentStyle = clientSettings.AdvancedSettings.AttributeIndentStyle, }; return await _remoteServiceInvoker.TryInvokeAsync( diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Completion/CohostDocumentCompletionResolveEndpoint.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Completion/CohostDocumentCompletionResolveEndpoint.cs index fb56b0ee2b7..3c674219b79 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Completion/CohostDocumentCompletionResolveEndpoint.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Completion/CohostDocumentCompletionResolveEndpoint.cs @@ -135,7 +135,8 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie { InsertSpaces = !clientSettings.ClientSpaceSettings.IndentWithTabs, TabSize = clientSettings.ClientSpaceSettings.IndentSize, - CodeBlockBraceOnNextLine = clientSettings.AdvancedSettings.CodeBlockBraceOnNextLine + CodeBlockBraceOnNextLine = clientSettings.AdvancedSettings.CodeBlockBraceOnNextLine, + AttributeIndentStyle = clientSettings.AdvancedSettings.AttributeIndentStyle, }; // Couldn't find an associated completion list, so its either Razor or C#. Either way, over to OOP diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostDocumentFormattingEndpoint.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostDocumentFormattingEndpoint.cs index e5cc179abda..33ce3bcd442 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostDocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostDocumentFormattingEndpoint.cs @@ -85,6 +85,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie var options = RazorFormattingOptions.From( request.Options, _clientSettingsManager.GetClientSettings().AdvancedSettings.CodeBlockBraceOnNextLine, + _clientSettingsManager.GetClientSettings().AdvancedSettings.AttributeIndentStyle, csharpSyntaxFormattingOptions); _logger.LogDebug($"Calling OOP with the {htmlChanges.Length} html edits, so it can fill in the rest"); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostOnTypeFormattingEndpoint.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostOnTypeFormattingEndpoint.cs index 0d9d4fb0526..4eb185d1086 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostOnTypeFormattingEndpoint.cs @@ -113,7 +113,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie } var csharpSyntaxFormattingOptions = RazorCSharpFormattingInteractionService.GetRazorCSharpSyntaxFormattingOptions(razorDocument.Project.Solution.Services); - var options = RazorFormattingOptions.From(request.Options, clientSettings.AdvancedSettings.CodeBlockBraceOnNextLine, csharpSyntaxFormattingOptions); + var options = RazorFormattingOptions.From(request.Options, clientSettings.AdvancedSettings.CodeBlockBraceOnNextLine, clientSettings.AdvancedSettings.AttributeIndentStyle, csharpSyntaxFormattingOptions); _logger.LogDebug($"Calling OOP with the {htmlChanges.Length} html edits, so it can fill in the rest"); var remoteResult = await _remoteServiceInvoker.TryInvokeAsync>( diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostRangeFormattingEndpoint.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostRangeFormattingEndpoint.cs index 26e19d3bf2d..d503be7b1c5 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Formatting/CohostRangeFormattingEndpoint.cs @@ -88,6 +88,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie var options = RazorFormattingOptions.From( request.Options, _clientSettingsManager.GetClientSettings().AdvancedSettings.CodeBlockBraceOnNextLine, + _clientSettingsManager.GetClientSettings().AdvancedSettings.AttributeIndentStyle, csharpSyntaxFormattingOptions); _logger.LogDebug($"Calling OOP with the {htmlChanges.Length} html edits, so it can fill in the rest"); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/OnAutoInsert/CohostOnAutoInsertEndpoint.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/OnAutoInsert/CohostOnAutoInsertEndpoint.cs index 9d37546fbc0..ac4939fd606 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/OnAutoInsert/CohostOnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/OnAutoInsert/CohostOnAutoInsertEndpoint.cs @@ -17,6 +17,7 @@ using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert; using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Razor.Settings; using Microsoft.CodeAnalysis.Razor.Workspaces.Settings; using Microsoft.VisualStudio.Razor.LanguageClient.Cohost; using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; @@ -90,7 +91,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie _logger.LogDebug($"Resolving auto-insertion for {razorDocument.FilePath}"); var clientSettings = _clientSettingsManager.GetClientSettings(); - var razorFormattingOptions = RazorFormattingOptions.From(request.Options, codeBlockBraceOnNextLine: false); + var razorFormattingOptions = RazorFormattingOptions.From(request.Options, codeBlockBraceOnNextLine: false, attributeIndentStyle: AttributeIndentStyle.AlignWithFirst); var autoInsertOptions = RemoteAutoInsertOptions.From(clientSettings, razorFormattingOptions); _logger.LogDebug($"Calling OOP to resolve insertion at {request.Position} invoked by typing '{request.Character}'"); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs index 0d44955b109..579a24eedbf 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs @@ -176,7 +176,8 @@ private async Task GenerateMethodInCodeBlockAsync( { TabSize = options.TabSize, InsertSpaces = options.InsertSpaces, - CodeBlockBraceOnNextLine = options.CodeBlockBraceOnNextLine + CodeBlockBraceOnNextLine = options.CodeBlockBraceOnNextLine, + AttributeIndentStyle = options.AttributeIndentStyle, }; var formattedChange = await _razorFormattingService.TryGetCSharpCodeActionEditAsync( diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs index 4fd830ad193..33932af8231 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs @@ -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; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxTreeExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxTreeExtensions.cs index 078ee0b95e2..0daaa71f569 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxTreeExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxTreeExtensions.cs @@ -48,10 +48,10 @@ public static IEnumerable EnumerateDirectives( yield return directive; } } + } - static bool MayContainDirectives(SyntaxNode node) - { - return node is RazorDocumentSyntax or MarkupBlockSyntax or CSharpCodeBlockSyntax; - } + public static bool MayContainDirectives(this SyntaxNode node) + { + return node is RazorDocumentSyntax or MarkupBlockSyntax or CSharpCodeBlockSyntax; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs index 6c825d0c7b8..76c4cb5158b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -172,6 +172,11 @@ private static ImmutableArray GetFormattingSpans(RazorSyntaxTree /// A whitespace string representing the indentation level based on the configuration. public string GetIndentationLevelString(int indentationLevel) { + if (indentationLevel == 0) + { + return ""; + } + var indentation = GetIndentationOffsetForLevel(indentationLevel); var indentationString = FormattingUtilities.GetIndentationString(indentation, Options.InsertSpaces, Options.TabSize); return indentationString; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs index e4fbc2fca03..079e1a1b376 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs @@ -152,9 +152,7 @@ public static int GetIndentationLevel(TextLine line, int firstNonWhitespaceChara var currentIndentationWidth = firstNonWhitespaceCharacterPosition - line.Start; if (insertSpaces) { - var indentationLevel = currentIndentationWidth / tabSize; - additionalIndentation = new string(' ', currentIndentationWidth % tabSize); - return indentationLevel; + return GetIndentationLevel(currentIndentationWidth, tabSize, out additionalIndentation); } // For tabs, we just count the tabs, and additional is any spaces at the end. @@ -178,6 +176,16 @@ public static int GetIndentationLevel(TextLine line, int firstNonWhitespaceChara return tabCount; } + public static int GetIndentationLevel(int length, int tabSize, out string additionalIndentation) + { + var indentationLevel = length / tabSize; + var additionalIndentationLength = length % tabSize; + additionalIndentation = additionalIndentationLength == 0 + ? "" + : new string(' ', additionalIndentationLength); + return indentationLevel; + } + /// /// Given a amount of characters, generate a string representing the configured indentation. /// 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 b0adf2f1dd2..bd587cc945e 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 @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Features; using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Settings; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; @@ -49,12 +50,12 @@ internal partial class CSharpFormattingPass /// /// The generator will go through that syntax tree and produce the following C# document: /// - /// // <div> + /// { // <div> /// @if (true) /// { /// // Some code /// } - /// // </div> + /// } // </div> /// /// class F /// { @@ -64,9 +65,11 @@ internal partial class CSharpFormattingPass /// /// /// The class definition is clearly not present in the source Razor document, but it represents the intended - /// indentation that the user would expect to see for the property declaration. Additionally the indentation - /// of the @if block is recorded, so that after formatting the C#, which Roslyn will set back to column 0, we - /// reapply it so we end up with the C# indentation and Html indentation combined. + /// indentation that the user would expect to see for the property declaration. The Html element start and + /// end tags are emited as braces so they cause the right amount of indentation, and the indentation of the + /// @if block is recorded, so that any quirks of Roslyn formatting are the same as what the user would expect + /// when looking at equivalent code in C#. ie, there might only be one level of indentation in C# for "Some code" + /// but conceptually the user is expecting two, due to the Html element. /// /// /// For more complete examples, the full test log for every formatting test includes the generated C# document. @@ -154,6 +157,7 @@ private sealed class Generator( private readonly SyntaxNode _csharpSyntaxRoot = csharpSyntaxRoot; private readonly bool _insertSpaces = options.InsertSpaces; private readonly int _tabSize = options.TabSize; + private readonly AttributeIndentStyle _attributeIndentStyle = options.AttributeIndentStyle; private readonly RazorCSharpSyntaxFormattingOptions? _csharpSyntaxFormattingOptions = options.CSharpSyntaxFormattingOptions; private readonly StringBuilder _builder = builder; private readonly ImmutableArray.Builder _lineInfoBuilder = lineInfoBuilder; @@ -167,13 +171,14 @@ private sealed class Generator( private RazorSyntaxToken _previousCurrentToken; /// - /// The line number of the last line of the current element, if we're inside one. + /// The line number of the last line of an element, if we're inside one, where we care about the Html formatters indentation /// /// /// This is used to track if the syntax node at the start of each line is parented by an element node, without - /// having to do lots of tree traversal. + /// having to do lots of tree traversal. We use this to make sure we format the contents of script and style tags as + /// the Html formatter intends. /// - private int? _elementEndLine; + private int? _honourHtmlFormattingUntilLine; /// /// The line number of the last line of a block where formatting should be completely ignored /// @@ -239,10 +244,10 @@ public void Generate() } // If we're inside an element that ends on this line, clear the field that tracks it. - if (_elementEndLine is { } endLine && + if (_honourHtmlFormattingUntilLine is { } endLine && endLine == line.LineNumber) { - _elementEndLine = null; + _honourHtmlFormattingUntilLine = null; } if (_ignoreUntilLine is { } endLine2 && @@ -267,7 +272,7 @@ private void AddAdditionalLineFormattingContent(StringBuilder additionalLinesBui // what the user expects. if (node is { Parent.Parent: MarkupTagHelperAttributeSyntax attribute } && attribute is { Parent.Parent: MarkupTagHelperElementSyntax element } && - element.TagHelperInfo.BindingResult.TagHelpers is [{ } descriptor] && + element.TagHelperInfo.BindingResult.TagHelpers is [{ } descriptor, ..] && descriptor.IsGenericTypedComponent() && descriptor.BoundAttributes.FirstOrDefault(d => d.Name == attribute.TagHelperAttributeInfo.Name) is { } boundAttribute && boundAttribute.IsTypeParameterProperty()) @@ -356,11 +361,12 @@ _sourceText.Lines[nodeStartLine] is { } previousLine && { // A special case here is if we're inside an explicit expression body, one of the bits of content after the node will // be the final close parens, so we need to emit that or the C# expression won't be valid, and we can't trust the formatter. - if (node.Parent.Parent is CSharpExplicitExpressionBodySyntax explicitExpression && - _sourceText.GetLinePosition(explicitExpression.EndPosition).Line == _currentLine.LineNumber) + var potentialExplicitExpression = node.Parent.Parent; + if (potentialExplicitExpression is CSharpExplicitExpressionBodySyntax or CSharpImplicitExpressionBodySyntax && + _sourceText.GetLinePosition(potentialExplicitExpression.EndPosition).Line == _currentLine.LineNumber) { isEndOfExplicitExpression = true; - end = explicitExpression.EndPosition; + end = potentialExplicitExpression.EndPosition; } else { @@ -382,14 +388,19 @@ _sourceText.Lines[nodeStartLine] is { } previousLine && _builder.Append(';'); } - // Append a comment at the end so whitespace isn't removed, as Roslyn thinks its the end of the line, but we know it isn't. + // Append a comment at the end so whitespace isn't removed, as Roslyn thinks its the end of the line, but it might not be. // eg, given "4, 5, @
", we want Roslyn to keep the space after the last comma, because there is something after it, // but we can't let Roslyn see the "@
" that it is. // We use a multi-line comment because Roslyn has a desire to line up "//" comments with the previous line, which we could interpret // as Roslyn suggesting we indent some trailing Html. - const string EndOfLineComment = " /* */"; - offsetFromEnd += EndOfLineComment.Length; - _builder.AppendLine(EndOfLineComment); + if (end != _currentLine.End) + { + const string EndOfLineComment = " /* */"; + offsetFromEnd += EndOfLineComment.Length; + _builder.Append(EndOfLineComment); + } + + _builder.AppendLine(); // Final quirk: If we're inside an Html attribute, it means the Html formatter won't have formatted this line, as multi-line // Html attributes are not valid. @@ -406,6 +417,17 @@ _sourceText.Lines[nodeStartLine] is { } previousLine && additionalIndentation = new string(' ', startChar % _tabSize); } + if (offsetFromEnd == 0) + { + // If we're not doing any extra emitting of our own, then we can safely check for newlines + return CreateLineInfo( + skipPreviousLine: skipPreviousLine, + processFormatting: true, + htmlIndentLevel: htmlIndentLevel, + additionalIndentation: additionalIndentation, + checkForNewLines: true); + } + return CreateLineInfo( skipPreviousLine: skipPreviousLine, processFormatting: true, @@ -422,12 +444,16 @@ public override LineInfo VisitCSharpStatementLiteral(CSharpStatementLiteralSynta // If this is the end of a multi-line CSharp template (ie, RenderFragment) then we need to close // out the lambda expression that we started when we opened it. - if (node.LiteralTokens.Where(static t => !t.IsWhitespace()).FirstOrDefault() is { Content: ";" } && + // When there are two templates directly following each other, the parser will put the semicolon of one + // and the declaration of the next in the same literal, so we have to be careful to only do this when the + // semicolon we find is on the line we're trying to process. + if (node.LiteralTokens.FirstOrDefault(static t => !t.IsWhitespace()) is { Content: ";" } semicolon && + GetLineNumber(semicolon) == GetLineNumber(_currentToken) && node.TryGetPreviousSibling(out var previousSibling) && previousSibling is CSharpTemplateBlockSyntax && GetLineNumber(previousSibling.GetFirstToken()) != GetLineNumber(previousSibling.GetLastToken())) { - _builder.AppendLine("};"); + _builder.AppendLine(";"); return CreateLineInfo(); } @@ -479,8 +505,8 @@ private LineInfo VisitCSharpLiteral(RazorSyntaxNode node, RazorSyntaxToken lastT // then that position will be different. If we're not given the options then we assume the default behaviour of // Roslyn which is to insert the newline. formattedOffsetFromEndOfLine = _csharpSyntaxFormattingOptions?.NewLines.IsFlagSet(RazorNewLinePlacement.BeforeOpenBraceInLambdaExpressionBody) ?? true - ? 5 - : 7; + ? 5 // "() =>".Length + : 7; // "() => {".Length } else { @@ -526,15 +552,22 @@ public override LineInfo VisitMarkupStartTag(MarkupStartTagSyntax node) { // The contents of textareas is significant, so we never want any formatting to happen inside them _ignoreUntilLine = GetLineNumber(element.EndTag?.CloseAngle ?? element.StartTag.CloseAngle); + + return EmitCurrentLineAsComment(); } - else if (_elementEndLine is null) + + var result = VisitStartTag(node); + + if (RazorSyntaxFacts.IsScriptOrStyleBlock(element) && + _honourHtmlFormattingUntilLine is null) { // If this is an element at the root level, we want to record where it ends. We can't rely on the Visit method - // for it, because it might not be at the start of a line. - _elementEndLine = GetLineNumber(element.EndTag?.CloseAngle ?? element.StartTag.CloseAngle); + // for it, because it might not be at the start of a line. We only care about contents though, so thats why + // we are doing this after emitting this line, and subtracting one from the end element line number. + _honourHtmlFormattingUntilLine = GetLineNumber(element.EndTag?.CloseAngle ?? element.StartTag.CloseAngle) - 1; } - return EmitCurrentLineAsComment(); + return result; } public override LineInfo VisitMarkupEndTag(MarkupEndTagSyntax node) @@ -544,6 +577,10 @@ public override LineInfo VisitMarkupEndTag(MarkupEndTagSyntax node) private LineInfo VisitEndTag(BaseMarkupEndTagSyntax node) { + // End tags are emited as close braces, to remove the indent their start tag caused. We don't need to worry + // about single-line elements here, because these Visit methods only ever see the first node on a line. + _builder.Append('}'); + // If this is the last line of a multi-line CSharp template (ie, RenderFragment), and the semicolon that ends // if is on the same line, then we need to close out the lambda expression that we started when we opened it. // If the semicolon is on the next line, then we'll take care of that when we get to it. @@ -554,28 +591,68 @@ private LineInfo VisitEndTag(BaseMarkupEndTagSyntax node) semiColonToken.Content == ";" && GetLineNumber(semiColonToken) == GetLineNumber(_currentToken)) { - _builder.AppendLine("};"); - return CreateLineInfo(); + _builder.Append(';'); } - return EmitCurrentLineAsComment(); + _builder.AppendLine(); + return CreateLineInfo(); } public override LineInfo VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node) { - // If this is an element at the root level, we want to record where it ends. We can't rely on the Visit method - // for it, because it might not be at the start of a line. - if (_elementEndLine is null) + return VisitStartTag(node); + } + + private LineInfo VisitStartTag(BaseMarkupStartTagSyntax node) + { + if (ElementCausesIndentation(node)) { - var element = (MarkupTagHelperElementSyntax)node.Parent; - _elementEndLine = GetLineNumber(element.EndTag?.CloseAngle ?? element.StartTag.CloseAngle); + // When an element causes indentation, we emit an open brace to tell the C# formatter to indent. + return EmitOpenBraceLine(); } + // This is a single line element, so it doesn't cause indentation return EmitCurrentLineAsComment(); } + private bool ElementCausesIndentation(BaseMarkupStartTagSyntax node) + { + if (node.Name.Content.Equals("html", StringComparison.OrdinalIgnoreCase)) + { + // doesn't cause indentation in Visual Studio or VS Code, so we honour that. + return false; + } + + if (node.IsSelfClosing()) + { + // Self-closing elements don't cause indentation + return false; + } + + if (node is MarkupStartTagSyntax startTag && startTag.IsVoidElement()) + { + // Void elements don't cause indentation + return false; + } + + if (node.GetEndTag() is { } endTag && + GetLineNumber(endTag) == GetLineNumber(node.CloseAngle)) + { + // End tag is on the same line as the start tag's close angle bracket + return false; + } + + return true; + } + public override LineInfo VisitRazorMetaCode(RazorMetaCodeSyntax node) { + // This could be a directive attribute, like @bind-Value="asdf" + if (TryVisitAttribute(node) is { } result) + { + return result; + } + // Meta code is a few things, and mostly they're valid C#, but one case we have to specifically handle is // bound attributes that start on their own line, eg the second line of: // @@ -611,26 +688,65 @@ public override LineInfo VisitRazorMetaCode(RazorMetaCodeSyntax node) public override LineInfo VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node) { // A MarkupEphemeralTextLiteral is an escaped @ sign, eg in CSS "@@font-face". We just treat it like markup text - return VisitMarkupLiteral(); + return EmitCurrentLineAsComment(); } public override LineInfo VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) { - return VisitMarkupLiteral(); + if (node.Parent is MarkupCommentBlockSyntax comment) + { + return VisitMarkupCommentBlock(comment); + } + + if (TryVisitAttribute(node) is { } result) + { + return result; + } + + return EmitCurrentLineAsComment(); } - private LineInfo VisitMarkupLiteral() + public override LineInfo VisitMarkupCommentBlock(MarkupCommentBlockSyntax node) { - // For markup text literal, we always want to honour the Html formatter, so we supply the Html indent. - // Normally that would only happen if we were inside a markup element -#if DEBUG - _builder.AppendLine($"// {_currentLine}"); -#else - _builder.AppendLine($"//"); -#endif - return CreateLineInfo( - htmlIndentLevel: FormattingUtilities.GetIndentationLevel(_currentLine, _currentFirstNonWhitespacePosition, _insertSpaces, _tabSize, out var additionalIndentation), - additionalIndentation: additionalIndentation); + // The contents of html comments might be significant (eg tables or ASCII flow charts), + // so we never want any formatting to happen inside them. + _ignoreUntilLine = _sourceText.Lines.GetLineFromPosition(node.EndPosition).LineNumber; + + return EmitCurrentLineAsComment(); + } + + private LineInfo? TryVisitAttribute(RazorSyntaxNode node) + { + if (RazorSyntaxFacts.IsAttributeName(node, out var startTag)) + { + // If we're just indenting attributes by one level, then we don't need to do anything special here. + var htmlIndentLevel = 1; + var additionalIndentation = ""; + + // Otherwise, attributes can be configured to align with the first attribute in their tag. + if (_attributeIndentStyle == AttributeIndentStyle.AlignWithFirst) + { + var firstAttribute = startTag.Attributes[0]; + var nameSpan = RazorSyntaxFacts.GetFullAttributeNameSpan(firstAttribute); + + // We need to line up with the first attribute, but the start tag might not be the first thing on the line, + // so it's really relative to the first non-whitespace character on the line. We use the line that the attribute + // is on, just in case its not on the same line as the start tag. + var lineStart = _sourceText.Lines[GetLineNumber(nameSpan)].GetFirstNonWhitespacePosition().GetValueOrDefault(); + htmlIndentLevel = FormattingUtilities.GetIndentationLevel(nameSpan.Start - lineStart, _tabSize, out additionalIndentation); + } + + // If the element has caused indentation, then we'll want to take one level off our attribute indentation to + // compensate. Need to be careful here because things like ` 0) + { + htmlIndentLevel--; + } + + return EmitCurrentLineAsComment(htmlIndentLevel: htmlIndentLevel, additionalIndentation: additionalIndentation); + } + + return null; } public override LineInfo VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node) @@ -710,6 +826,17 @@ public override LineInfo VisitCSharpImplicitExpression(CSharpImplicitExpressionS // so we can actually just emit these lines as a comment so the indentation is correct, and then let the code above // handle them. Essentially, whether these are at the start or int he middle of a line is irrelevant. + // The exception to this is if the implicit expressions are multi-line. In that case, it's possible that the contents + // of this line (ie, the first line) will affect the indentation of subsequent lines. Emitting this as a comment, won't + // help when we emit the following lines in their original form. So lets do that for this line too. Since it's multi-line + // we know, by definition, there can't be more than one on this line anyway. + + if (_sourceText.GetLinePositionSpan(node.Span).SpansMultipleLines()) + { + var csharpCode = ((CSharpImplicitExpressionBodySyntax)node.Body).CSharpCode; + return VisitCSharpCodeBlock(node, csharpCode); + } + return EmitCurrentLineAsComment(); } @@ -719,21 +846,52 @@ public override LineInfo VisitCSharpExplicitExpression(CSharpExplicitExpressionS // of whether its at the start or in the middle of the line. var body = (CSharpExplicitExpressionBodySyntax)node.Body; var closeParen = body.CloseParen; + var csharpCode = body.CSharpCode; if (GetLineNumber(closeParen) == GetLineNumber(node)) { return EmitCurrentLineAsComment(); } + return VisitCSharpCodeBlock(node, csharpCode); + } + + private LineInfo VisitCSharpCodeBlock(RazorSyntaxNode node, CSharpCodeBlockSyntax csharpCode) + { // If this spans multiple lines however, the indentation of this line will affect the next, so we handle it in the // same way we handle a C# literal syntax. That includes checking if the C# doesn't go to the end of the line. // If the whole explicit expression is C#, then the children will be a single CSharpExpressionLiteral. If not, there // will be multiple children, and the second one is not C#, so thats the one we need to exclude from the generated // document. - if (body.CSharpCode.Children is [_, { } secondChild, ..] && + if (csharpCode.Children is [_, { } secondChild, ..] && GetLineNumber(secondChild) == GetLineNumber(node)) { + // Emit the whitespace, so user spacing is honoured if possible + _builder.Append(_sourceText.ToString(TextSpan.FromBounds(_currentLine.Start, _currentFirstNonWhitespacePosition))); var span = TextSpan.FromBounds(_currentFirstNonWhitespacePosition + 1, secondChild.Position); _builder.Append(_sourceText.ToString(span)); + + if (secondChild is CSharpTemplateBlockSyntax template && + _sourceText.GetLinePositionSpan(template.Span).SpansMultipleLines()) + { + _builder.AppendLine("() => {"); + // We only want to format up to the text we added, but if Roslyn inserted a newline before the brace + // then that position will be different. If we're not given the options then we assume the default behaviour of + // Roslyn which is to insert the newline. + var formattedOffsetFromEndOfLine = _csharpSyntaxFormattingOptions?.NewLines.IsFlagSet(RazorNewLinePlacement.BeforeOpenBraceInLambdaExpressionBody) ?? true + ? 5 + : 7; + + return CreateLineInfo( + skipNextLineIfBrace: true, + originOffset: 1, + formattedLength: span.Length, + formattedOffsetFromEndOfLine: formattedOffsetFromEndOfLine, + processFormatting: true, + // We turn off check for new lines because that only works if the content doesn't change from the original, + // but we're deliberately leaving out a bunch of the original file because it would confuse the Roslyn formatter. + checkForNewLines: false); + } + // Append a comment at the end so whitespace isn't removed, as Roslyn thinks its the end of the line, but we know it isn't. _builder.AppendLine(" //"); @@ -747,10 +905,13 @@ public override LineInfo VisitCSharpExplicitExpression(CSharpExplicitExpressionS checkForNewLines: false); } + // Multi-line expressions are often not formatted by Roslyn much at all, but it will often move subsequent lines + // relative to the first, so make sure we include the users indentation so everything moves together, and is stable. + _builder.Append(_sourceText.GetSubTextString(TextSpan.FromBounds(_currentLine.Start, _currentFirstNonWhitespacePosition))); _builder.AppendLine(_sourceText.GetSubTextString(TextSpan.FromBounds(_currentToken.Position + 1, _currentLine.End))); return CreateLineInfo( processFormatting: true, - checkForNewLines: false, + checkForNewLines: true, originOffset: 1, formattedOffset: 0); } @@ -784,8 +945,7 @@ public override LineInfo VisitCSharpStatement(CSharpStatementSyntax node) // We don't need to worry about formatting, or offsetting, because the RazorFormattingPass will // have ensured this node is followed by a newline, and if there was a space between the "@" and "{" // then it wouldn't be a CSharpStatementSyntax so we wouldn't be here! - _builder.AppendLine("{"); - return CreateLineInfo(); + return EmitOpenBraceLine(); } public override LineInfo VisitRazorDirective(RazorDirectiveSyntax node) @@ -822,8 +982,7 @@ body.CSharpCode is CSharpCodeBlockSyntax code && // If the open brace is on the same line as the directive, then we need to ensure the contents are indented. GetLineNumber(brace) == GetLineNumber(_currentToken)) { - _builder.AppendLine("{"); - return CreateLineInfo(); + return EmitOpenBraceLine(); } // If the brace is on a different line, then we don't need to do anything, as the brace will be output when @@ -885,6 +1044,9 @@ private LineInfo VisitAttributeDirective(RazorSyntaxNode attribute) formattedOffset: 0); } + private int GetLineNumber(TextSpan span) + => _sourceText.GetLinePositionSpan(span).Start.Line; + private int GetLineNumber(RazorSyntaxNode node) => _sourceText.Lines.GetLineFromPosition(node.Position).LineNumber; @@ -897,13 +1059,17 @@ private LineInfo EmitCurrentLineAsCSharp() return CreateLineInfo(processFormatting: true, checkForNewLines: true); } - private LineInfo EmitCurrentLineAsComment() + private LineInfo EmitCurrentLineAsComment(int htmlIndentLevel = 0, string? additionalIndentation = null) { -#if DEBUG - _builder.AppendLine($"// {_currentLine}"); -#else _builder.AppendLine($"//"); -#endif + return CreateLineInfo(htmlIndentLevel: htmlIndentLevel, additionalIndentation: additionalIndentation); + } + + private LineInfo EmitOpenBraceLine() + { + // Any open brace we emit that represents something "real" must have something after it to avoid + // us skipping it due to SkipNextLineIfOpenBrace on the previous line. + _builder.AppendLine("{ /* */"); return CreateLineInfo(); } @@ -927,17 +1093,14 @@ private LineInfo CreateLineInfo( int formattedOffsetFromEndOfLine = 0, string? additionalIndentation = null) { - // We want to honour the indentation that the Html formatter supplied, but annoyingly it only actually indents - // the contents of elements, not anything which is not contained in an element. This makes sense from the point - // of view of Html, as it would expect the element to always be present, but that is not true in Razor. - // So we have to check if we're inside an element before we record the indentation, otherwise we could be - // recording incorrect information. + // We sometimes want to honour the indentation that the Html formatter supplied, when inside the right type of tag + // but we will also have added our own C# indentation on top of that, so we need to subtract one level to compensate. if (additionalIndentation is null && htmlIndentLevel == 0 && - _elementEndLine is { } endLine && + _honourHtmlFormattingUntilLine is { } endLine && endLine >= _currentLine.LineNumber) { - htmlIndentLevel = FormattingUtilities.GetIndentationLevel(_currentLine, _currentFirstNonWhitespacePosition, _insertSpaces, _tabSize, out additionalIndentation); + htmlIndentLevel = FormattingUtilities.GetIndentationLevel(_currentLine, _currentFirstNonWhitespacePosition, _insertSpaces, _tabSize, out additionalIndentation) - 1; } return new( diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs index eac8d041e9a..da01e570564 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs @@ -73,6 +73,8 @@ public async Task> ExecuteAsync(FormattingContext con break; } + string? indentationString = null; + var formattedLine = formattedCSharpText.Lines[iFormatted]; if (lineInfo.ProcessIndentation && formattedLine.GetFirstNonWhitespaceOffset() is { } formattedIndentation) @@ -85,7 +87,7 @@ public async Task> ExecuteAsync(FormattingContext con // First up, we take the indentation from the formatted file, and add on the Html indentation level from the line info, and // replace whatever was in the original file with it. var htmlIndentString = context.GetIndentationLevelString(lineInfo.HtmlIndentLevel); - var indentationString = formattedCSharpText.ToString(new TextSpan(formattedLine.Start, formattedIndentation)) + indentationString = formattedCSharpText.ToString(new TextSpan(formattedLine.Start, formattedIndentation)) + htmlIndentString + lineInfo.AdditionalIndentation; formattingChanges.Add(new TextChange(new TextSpan(originalLine.Start, originalLineOffset), indentationString)); @@ -173,6 +175,20 @@ public async Task> ExecuteAsync(FormattingContext con if (NextLineIsOnlyAnOpenBrace(changedText, iOriginal)) { iOriginal++; + + // We're skipping a line in the original document, because Roslyn brought it up to the previous + // line, but the fact is the opening brace was in the original document, and might need its indentation + // adjusted. Since we can't reason about this line in any way, because Roslyn has changed it, we just + // apply the indentation from the previous line. + // + // If we didn't adjust the indentation of the previous line, then we really have no information to go + // on at all, so hopefully the user is happy with where their open brace is. + if (indentationString is not null) + { + var originalLine = changedText.Lines[iOriginal]; + var originalLineOffset = originalLine.GetFirstNonWhitespaceOffset().GetValueOrDefault(); + formattingChanges.Add(new TextChange(new TextSpan(originalLine.Start, originalLineOffset), indentationString)); + } } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs index 2b496293d3d..69c7664a540 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs @@ -87,13 +87,25 @@ private async Task> FilterIncomingChangesAsync(Format var syntaxRoot = codeDocument.GetRequiredSyntaxRoot(); var sourceText = codeDocument.Source.Text; SyntaxNode? csharpSyntaxRoot = null; + ImmutableArray scriptAndStyleElementContentSpans = default; using var changesToKeep = new PooledArrayBuilder(capacity: changes.Length); foreach (var change in changes) { - // We don't keep changes that start inside of a razor comment block. + // 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 a change to line indentation, but not in a script or style block"); + continue; + } + var node = syntaxRoot.FindInnermostNode(change.Span.Start); + // We don't keep changes that start inside of a razor comment block. var comment = node?.FirstAncestorOrSelf(); if (comment is not null && change.Span.Start > comment.SpanStart) { @@ -107,7 +119,7 @@ private async Task> FilterIncomingChangesAsync(Format // void Foo() // { // Render(@); - // { + // } // } // // This is popular in some libraries, like bUnit. The issue here is that @@ -190,6 +202,63 @@ async Task 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. + InitializeElementContentSpanArray(); + + // If there aren't any elements we're interested in, we don't need to do anything + if (scriptAndStyleElementContentSpans.Length == 0) + { + return false; + } + + var index = scriptAndStyleElementContentSpans.BinarySearchBy(change.Span.Start, static (span, pos) => + { + if (span.Contains(pos)) + { + return 0; + } + + return span.Start.CompareTo(pos); + }); + + // If we got a hit, then we're inside a tag we care about + return index >= 0; + } + + void InitializeElementContentSpanArray() + { + if (!scriptAndStyleElementContentSpans.IsDefault) + { + return; + } + + using var boundsBuilder = new PooledArrayBuilder(); + + // We only care about "top level" block type structures (ie, a script tag isn't going to appear in the middle of a C# expression) so + // we only need to descend into the document itself, or nodes that might contain directives, or other elements in case of nesting. + foreach (var element in syntaxRoot.DescendantNodes(static node => node is MarkupElementSyntax || node.MayContainDirectives()).OfType()) + { + if (RazorSyntaxFacts.IsScriptOrStyleBlock(element) && + element.GetLinePositionSpan(codeDocument.Source).SpansMultipleLines()) + { + + // We only want the contents of the script tag to be included, but not whitespace before the end tag if + // there is only whitespace before the tag, so the calculation of the end is a little annoying. + var endTagline = sourceText.Lines.GetLineFromPosition(element.EndTag.SpanStart); + var firstNonWhitespace = endTagline.GetFirstNonWhitespacePosition(); + var end = firstNonWhitespace == element.EndTag.SpanStart + ? endTagline.Start + : firstNonWhitespace.GetValueOrDefault() + 1; // Add one to the end, because we want end to be inclusive, and the parameter is exclusive + boundsBuilder.Add(TextSpan.FromBounds(element.StartTag.EndPosition, end)); + } + } + + scriptAndStyleElementContentSpans = boundsBuilder.ToImmutableAndClear(); + } } internal TestAccessor GetTestAccessor() => new(this); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs index efecbec1a76..4a2c88865af 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs @@ -4,6 +4,7 @@ using System.Runtime.Serialization; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Features; +using Microsoft.CodeAnalysis.Razor.Settings; namespace Microsoft.CodeAnalysis.Razor.Formatting; @@ -17,26 +18,30 @@ internal readonly record struct RazorFormattingOptions [DataMember(Order = 2)] public bool CodeBlockBraceOnNextLine { get; init; } = false; [DataMember(Order = 3)] + public AttributeIndentStyle AttributeIndentStyle { get; init; } = AttributeIndentStyle.AlignWithFirst; + [DataMember(Order = 4)] public RazorCSharpSyntaxFormattingOptions? CSharpSyntaxFormattingOptions { get; init; } public RazorFormattingOptions() { } - public static RazorFormattingOptions From(FormattingOptions options, bool codeBlockBraceOnNextLine) + public static RazorFormattingOptions From(FormattingOptions options, bool codeBlockBraceOnNextLine, AttributeIndentStyle attributeIndentStyle) => new() { InsertSpaces = options.InsertSpaces, TabSize = options.TabSize, - CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine + CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine, + AttributeIndentStyle = attributeIndentStyle, }; - public static RazorFormattingOptions From(FormattingOptions options, bool codeBlockBraceOnNextLine, RazorCSharpSyntaxFormattingOptions csharpSyntaxFormattingOptions) + public static RazorFormattingOptions From(FormattingOptions options, bool codeBlockBraceOnNextLine, AttributeIndentStyle attributeIndentStyle, RazorCSharpSyntaxFormattingOptions csharpSyntaxFormattingOptions) => new() { InsertSpaces = options.InsertSpaces, TabSize = options.TabSize, CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine, + AttributeIndentStyle = attributeIndentStyle, CSharpSyntaxFormattingOptions = csharpSyntaxFormattingOptions }; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs index ccc5c6fcf3c..92d5a1ebbb7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs @@ -84,10 +84,12 @@ public static bool TryGetFullAttributeNameSpan(RazorCodeDocument codeDocument, i return attributeNameSpan != default; } - private static TextSpan GetFullAttributeNameSpan(RazorSyntaxNode? node) + public static TextSpan GetFullAttributeNameSpan(RazorSyntaxNode? node) { return node switch { + MarkupAttributeBlockSyntax att => att.Name.Span, + MarkupMinimizedAttributeBlockSyntax att => att.Name.Span, MarkupTagHelperAttributeSyntax att => att.Name.Span, MarkupMinimizedTagHelperAttributeSyntax att => att.Name.Span, MarkupTagHelperDirectiveAttributeSyntax att => CalculateFullSpan(att.Name, att.ParameterName, att.Transition), @@ -208,18 +210,29 @@ 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) + internal static bool IsAttributeName(RazorSyntaxNode node, [NotNullWhen(true)] out BaseMarkupStartTagSyntax? startTag) { - return IsElementWithName(node, "script"); + startTag = null; + + if (node.Parent.IsAnyAttributeSyntax() && + GetFullAttributeNameSpan(node.Parent).Start == node.SpanStart) + { + startTag = node.Parent.Parent as BaseMarkupStartTagSyntax; + } + + return startTag is not null; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/AttributeIndentStyle.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/AttributeIndentStyle.cs new file mode 100644 index 00000000000..b4a77e6fe09 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/AttributeIndentStyle.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.CodeAnalysis.Razor.Settings; + +internal enum AttributeIndentStyle +{ + /// + /// Matches the behaviour of the Html formatter in VS and VS Code, and makes attributes on subsequent lines + /// align with the first attribute on the first line of a tag + /// + AlignWithFirst, + /// + /// Indents attributes on subsequent lines by one more level than the indentation level of the line the tag starts on + /// + IndentByOne +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs index c3caff33e9f..a34ce02d689 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs @@ -40,6 +40,7 @@ internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, + AttributeIndentStyle AttributeIndentStyle, bool CommitElementsWithSpace, SnippetSetting SnippetSetting, LogLevel LogLevel, @@ -51,6 +52,7 @@ internal sealed record ClientAdvancedSettings(bool FormatOnType, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, + AttributeIndentStyle: AttributeIndentStyle.AlignWithFirst, CommitElementsWithSpace: true, SnippetSetting.All, LogLevel.Warning, @@ -65,6 +67,7 @@ public bool Equals(ClientAdvancedSettings? other) AutoInsertAttributeQuotes == other.AutoInsertAttributeQuotes && ColorBackground == other.ColorBackground && CodeBlockBraceOnNextLine == other.CodeBlockBraceOnNextLine && + AttributeIndentStyle == other.AttributeIndentStyle && CommitElementsWithSpace == other.CommitElementsWithSpace && SnippetSetting == other.SnippetSetting && LogLevel == other.LogLevel && @@ -80,6 +83,7 @@ public override int GetHashCode() hash.Add(AutoInsertAttributeQuotes); hash.Add(ColorBackground); hash.Add(CodeBlockBraceOnNextLine); + hash.Add(AttributeIndentStyle); hash.Add(CommitElementsWithSpace); hash.Add(SnippetSetting); hash.Add(LogLevel); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostInlineCompletionEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostInlineCompletionEndpoint.cs index 75de28abe78..0a5b89911fa 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostInlineCompletionEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostInlineCompletionEndpoint.cs @@ -94,7 +94,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie if (result.Range is not null) { - var options = RazorFormattingOptions.From(formattingOptions, _clientSettingsManager.GetClientSettings().AdvancedSettings.CodeBlockBraceOnNextLine); + var options = RazorFormattingOptions.From(formattingOptions, _clientSettingsManager.GetClientSettings().AdvancedSettings.CodeBlockBraceOnNextLine, _clientSettingsManager.GetClientSettings().AdvancedSettings.AttributeIndentStyle); var span = result.Range.ToLinePositionSpan(); var formattedInfo = await _remoteServiceInvoker.TryInvokeAsync( razorDocument.Project.Solution, diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/OptionsStorage.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/OptionsStorage.cs index 01db1266b91..b2f79c4947a 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/OptionsStorage.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/OptionsStorage.cs @@ -104,6 +104,7 @@ public ClientAdvancedSettings GetAdvancedSettings() GetBool(SettingsNames.AutoInsertAttributeQuotes, defaultValue: true), GetBool(SettingsNames.ColorBackground, defaultValue: false), GetBool(SettingsNames.CodeBlockBraceOnNextLine, defaultValue: false), + GetEnum(SettingsNames.AttributeIndentStyle, AttributeIndentStyle.AlignWithFirst), GetBool(SettingsNames.CommitElementsWithSpace, defaultValue: true), GetEnum(SettingsNames.Snippets, SnippetSetting.All), GetEnum(SettingsNames.LogLevel, LogLevel.Warning), diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/SettingsNames.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/SettingsNames.cs index 522b0b34d21..a7f5e4ad565 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/SettingsNames.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/SettingsNames.cs @@ -12,6 +12,7 @@ internal static class SettingsNames public static readonly string AutoInsertAttributeQuotes = UnifiedCollection + ".autoInsertAttributeQuotes"; public static readonly string ColorBackground = UnifiedCollection + ".colorBackground"; public static readonly string CodeBlockBraceOnNextLine = UnifiedCollection + ".codeBlockBraceOnNextLine"; + public static readonly string AttributeIndentStyle = UnifiedCollection + ".attributeIndentStyle"; public static readonly string CommitElementsWithSpace = UnifiedCollection + ".commitElementsWithSpace"; public static readonly string Snippets = UnifiedCollection + ".snippets"; public static readonly string LogLevel = UnifiedCollection + ".logLevel"; @@ -24,6 +25,7 @@ internal static class SettingsNames AutoInsertAttributeQuotes, ColorBackground, CodeBlockBraceOnNextLine, + AttributeIndentStyle, CommitElementsWithSpace, Snippets, LogLevel, diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef index 17befdb8d65..0868efcb927 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef @@ -55,4 +55,4 @@ [$RootKey$\SettingsManifests\{13b72f58-279e-49e0-a56d-296be02f0805}] @="Microsoft.VisualStudio.RazorExtension.RazorPackage" "ManifestPath"="$PackageFolder$\UnifiedSettings\razor.registration.json" -"CacheTag"=qword:91324F976E73CC56 +"CacheTag"=qword:91324F976E73CC57 diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json index e6ae2e717e8..628f0260943 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json @@ -84,11 +84,22 @@ "pass": { "input": { "store": "VsUserSettingsRegistry", - "path": "Razor\\CodeBlockBraceOnNextLined" + "path": "Razor\\CodeBlockBraceOnNextLine" } } } }, + "languages.razor.advanced.attributeIndentStyle": { + "type": "string", + "default": "alignWithFirst", + "enum": [ "alignWithFirst", "indentByOne" ], + "enumItemLabels": [ + "@Setting_AttributeIndentStyleAlignWithFirst;{13b72f58-279e-49e0-a56d-296be02f0805}", + "@Setting_AttributeIndentStyleIndentByOne;{13b72f58-279e-49e0-a56d-296be02f0805}" + ], + "title": "@Setting_AttributeIndentStyleDisplayName;{13b72f58-279e-49e0-a56d-296be02f0805}", + "description": "@Setting_AttributeIndentStyleDescription;{13b72f58-279e-49e0-a56d-296be02f0805}" + }, "languages.razor.advanced.commitElementsWithSpace": { "type": "boolean", "default": true, @@ -106,7 +117,7 @@ "languages.razor.advanced.snippets": { "type": "string", "default": "all", - "enum": ["all", "custom", "none"], + "enum": [ "all", "custom", "none" ], "enumItemLabels": [ "@Setting_SnippetsAll;{13b72f58-279e-49e0-a56d-296be02f0805}", "@Setting_SnippetsCustom;{13b72f58-279e-49e0-a56d-296be02f0805}", diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx index 3668a911591..b7f70e6969c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx @@ -237,4 +237,16 @@ Typing + + Attribute Indent Style + + + Defines how Html and component tag attributes should be indented + + + Align with first attribute + + + Indent by one level + \ No newline at end of file diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf index c12b785defd..65744e7dd69 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted Při hodnotě true se budou automaticky vkládat uzavírací značky. diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.de.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.de.xlf index cf637c2f4c4..d971fa849d3 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.de.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.de.xlf @@ -42,6 +42,26 @@ Razor-Syntax-Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted Bei "true" werden schließende Tags automatisch eingefügt. diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.es.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.es.xlf index 7800c1b8c6a..390029ed7ca 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.es.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.es.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted Si es verdadero, las etiquetas de cierre se insertarán automáticamente diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.fr.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.fr.xlf index 0c271310789..1c1bbf3d92b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.fr.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.fr.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted Si la valeur est true, les balises de fermeture sont automatiquement insérées diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.it.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.it.xlf index 51b4cac3329..bcff8d6a5bb 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.it.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.it.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted Se true, i tag di chiusura verranno inseriti automaticamente diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ja.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ja.xlf index 21e20d304db..b4f8383d6b4 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ja.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ja.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted true の場合、終了タグは自動的に挿入されます diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ko.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ko.xlf index fab0ddfb438..7787025a6bb 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ko.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ko.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted true이면 닫는 태그가 자동으로 삽입됩니다. diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pl.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pl.xlf index 88b01a257d9..9fdc0182cb3 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pl.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pl.xlf @@ -42,6 +42,26 @@ Syntax Visualizer Razor + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted W przypadku wartości true tagi zamykające będą wstawiane automatycznie diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pt-BR.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pt-BR.xlf index f3072c64291..859edd352cd 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pt-BR.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pt-BR.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted Se verdadeiro, as tags de fechamento serão inseridas automaticamente diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ru.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ru.xlf index d8f4e972cc1..fa36a6f8603 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ru.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ru.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted При значении true закрывающие теги вставляются автоматически diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.tr.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.tr.xlf index dc9d7ef13ad..172c3526c24 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.tr.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.tr.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted True ise, kapanış etiketleri otomatik olarak eklenir diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hans.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hans.xlf index 9fe3ccda9c3..e4d6e38e829 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hans.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hans.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted 如果为 true,将自动插入结束标记 diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hant.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hant.xlf index 32a598b3f36..84cc650689e 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hant.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hant.xlf @@ -42,6 +42,26 @@ Razor Syntax Visualizer + + Align with first attribute + Align with first attribute + + + + Defines how Html and component tag attributes should be indented + Defines how Html and component tag attributes should be indented + + + + Attribute Indent Style + Attribute Indent Style + + + + Indent by one level + Indent by one level + + If true, closing tags will be automatically inserted 若為 True,將會自動插入結尾標記 diff --git a/src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/CohostConfigurationChangedService.cs b/src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/CohostConfigurationChangedService.cs index b721ab6653d..86e674e85da 100644 --- a/src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/CohostConfigurationChangedService.cs +++ b/src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/CohostConfigurationChangedService.cs @@ -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.Composition; using System.Text.Json.Nodes; using System.Threading; @@ -47,6 +48,7 @@ private async Task RefreshOptionsAsync(RazorCohostRequestContext requestContext, Items = [ //TODO: new ConfigurationItem { Section = "razor.format.enable" }, new ConfigurationItem { Section = "razor.format.code_block_brace_on_next_line" }, + new ConfigurationItem { Section = "razor.format.attribute_indent_style" }, new ConfigurationItem { Section = "razor.completion.commit_elements_with_space" }, ] }; @@ -60,7 +62,8 @@ private async Task RefreshOptionsAsync(RazorCohostRequestContext requestContext, var settings = current with { CodeBlockBraceOnNextLine = GetBooleanOptionValue(options[0], current.CodeBlockBraceOnNextLine), - CommitElementsWithSpace = GetBooleanOptionValue(options[1], current.CommitElementsWithSpace), + AttributeIndentStyle = GetEnumOptionValue(options[1], current.AttributeIndentStyle), + CommitElementsWithSpace = GetBooleanOptionValue(options[2], current.CommitElementsWithSpace), }; _clientSettingsManager.Update(settings); @@ -75,4 +78,19 @@ private static bool GetBooleanOptionValue(JsonNode? jsonNode, bool defaultValue) return jsonNode.ToString() == "true"; } + + private static T GetEnumOptionValue(JsonNode? jsonNode, T defaultValue) where T : struct + { + if (jsonNode is null) + { + return defaultValue; + } + + if (Enum.TryParse(jsonNode.GetValue(), out var value)) + { + return value; + } + + return defaultValue; + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs index da59332bea5..a8f1ec9c03b 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.Razor.CodeActions.Razor; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Settings; using Microsoft.CodeAnalysis.Testing; using Xunit; using Xunit.Abstractions; @@ -843,6 +844,7 @@ public async Task Handle_GenerateMethod_VaryIndentSize(bool insertSpaces, int ta AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, + AttributeIndentStyle: AttributeIndentStyle.AlignWithFirst, CommitElementsWithSpace: true, TaskListDescriptors: []); var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs index 428661a9ce2..d8089b556cd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.CodeAnalysis.Razor.Settings; using Moq; using Xunit; using Xunit.Abstractions; @@ -21,7 +22,7 @@ public async Task GetLatestOptionsAsync_ReturnsExpectedOptions() { // Arrange var expectedOptions = new RazorLSPOptions( - FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false, TaskListDescriptors: []); + FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, AttributeIndentStyle: AttributeIndentStyle.AlignWithFirst, CommitElementsWithSpace: false, TaskListDescriptors: []); var razorJsonString = """ @@ -93,12 +94,13 @@ public void BuildOptions_VSCodeOptionsOnly_ReturnsExpected() { // Arrange - purposely choosing options opposite of default var expectedOptions = new RazorLSPOptions( - FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false, TaskListDescriptors: []); + FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, AttributeIndentStyle: AttributeIndentStyle.IndentByOne, CommitElementsWithSpace: false, TaskListDescriptors: []); var razorJsonString = """ { "format": { "enable": false, - "codeBlockBraceOnNextLine": true + "codeBlockBraceOnNextLine": true, + "attributeIndentStyle": "indentByOne" } } @@ -130,7 +132,7 @@ public void BuildOptions_VSOptionsOnly_ReturnsExpected() { // Arrange - purposely choosing options opposite of default var expectedOptions = new RazorLSPOptions( - FormattingFlags.Enabled, AutoClosingTags: false, InsertSpaces: false, TabSize: 8, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: false, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false, TaskListDescriptors: []); + FormattingFlags.Enabled, AutoClosingTags: false, InsertSpaces: false, TabSize: 8, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: false, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false, AttributeIndentStyle: AttributeIndentStyle.IndentByOne, TaskListDescriptors: []); var razorJsonString = """ { } @@ -151,7 +153,8 @@ public void BuildOptions_VSOptionsOnly_ReturnsExpected() "FormatOnType": false, "AutoClosingTags": false, "AutoInsertAttributeQuotes": false, - "CommitElementsWithSpace": false + "CommitElementsWithSpace": false, + "AttributeIndentStyle": 1 } } """; 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 cdaff390f00..93e060e69ef 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 @@ -472,14 +472,14 @@ protected void ToggleIconMenu(bool iconMenuActive) """, expected: """
- @code + @code + { + private bool IconMenuActive { get; set; } = false; + protected void ToggleIconMenu(bool iconMenuActive) { - private bool IconMenuActive { get; set; } = false; - protected void ToggleIconMenu(bool iconMenuActive) - { - IconMenuActive = iconMenuActive; - } + IconMenuActive = iconMenuActive; } + }
""", csharpSyntaxFormattingOptions: RazorCSharpSyntaxFormattingOptions.Default with @@ -653,15 +653,15 @@ private void IncrementCount() """, expected: """ - @code - { - private int currentCount = 0; + @code + { + private int currentCount = 0; - private void IncrementCount() - { - currentCount++; - } + private void IncrementCount() + { + currentCount++; } + } """, inGlobalNamespace: inGlobalNamespace); @@ -686,15 +686,15 @@ private void IncrementCount() """, expected: """ - @code - { - private int currentCount = 0; + @code + { + private int currentCount = 0; - private void IncrementCount() - { - currentCount++; - } + private void IncrementCount() + { + currentCount++; } + } """); } @@ -718,15 +718,15 @@ private void IncrementCount() """, expected: """ - @code - { - private int currentCount = 0; + @code + { + private int currentCount = 0; - private void IncrementCount() - { - currentCount++; - } + private void IncrementCount() + { + currentCount++; } + } """); } @@ -1896,7 +1896,7 @@ void Method() { }

Hello, world!

- Welcome to your new app. + Welcome to your new app. @@ -2495,6 +2495,31 @@ await RunFormattingTestAsync( """); } + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12635")] + public Task TwoRenderFragmentsAfterEachOther() + => RunFormattingTestAsync( + input: """ + @{ + Func<(bool b1, bool b2), object> o1 = @ +
+
; + Func<(bool b1, bool b2), object> o2 = @ +
+
; + } + """, + expected: """ + @{ + Func<(bool b1, bool b2), object> o1 = @ +
+
; + Func<(bool b1, bool b2), object> o2 = @ +
+
; + } + """); + [FormattingTestFact] [WorkItem("https://github.com/dotnet/razor/issues/6090")] public async Task FormatHtmlCommentsInsideCSharp1() @@ -3392,7 +3417,7 @@ class C
@(new C() - .M("Hello") + .M("Hello") .M("World") .M(source => { @@ -3405,15 +3430,15 @@ class C @(DateTime.Now) @(DateTime - .Now - .ToString()) + .Now + .ToString()) @(Html.DisplayNameFor(@

) - .ToString()) + .ToString()) @{ var x = @

Hi there!

; @@ -3477,29 +3502,81 @@ await RunFormattingTestAsync(

) - .ToString()) + .ToString()) @(Html.DisplayNameFor(@
, - 1, 3, 4)) + 1, 3, 4)) @(Html.DisplayNameFor(@
, - 1, 3, @
, - 2, 4)) + 1, 3, @
, + 2, 4)) @(Html.DisplayNameFor( - 1, 3, @
, - 2, 4)) + 1, 3, @
, + 2, 4)) @(Html.DisplayNameFor( - 1, 3, - 2, 4)) + 1, 3, + 2, 4)) @(Html.DisplayNameFor( - 2, 4, - 1, 3, @
, - 2, 4, - 1, 3, @
, - 4)) + 2, 4, + 1, 3, @
, + 2, 4, + 1, 3, @
, + 4)) +
+ """, + fileKind: RazorFileKind.Legacy); + } + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/6110")] + public async Task FormatExplicitCSharpInsideHtml3() + { + await RunFormattingTestAsync( + input: """ + @using System.Text; + +
+ @(new C() + .M("Hello") + .M("World") + .M(source => + { + if (source.Length > 0) + { + source.ToString(); + } + })) + + @(DateTime.Now) + + @(DateTime + .Now + .ToString()) +
+ """, + expected: """ + @using System.Text; + +
+ @(new C() + .M("Hello") + .M("World") + .M(source => + { + if (source.Length > 0) + { + source.ToString(); + } + })) + + @(DateTime.Now) + + @(DateTime + .Now + .ToString())
""", fileKind: RazorFileKind.Legacy); @@ -3546,8 +3623,8 @@ void M() { } """, expected: """
@@ -7296,7 +7375,7 @@ public Task NestedExplicitExpression3() @((((true) ? 123d : 0d) + (true ? 123d : 0d) ).ToString("F2", CultureInfo.InvariantCulture) - ) € + ) €
@@ -7304,7 +7383,7 @@ public Task NestedExplicitExpression3() ((true) ? 123d : 0d) + (true ? 123d : 0d) ).ToString("F2", CultureInfo.InvariantCulture) - ) € + ) € } """); @@ -7339,7 +7418,7 @@ public Task NestedExplicitExpression4() @((((true) ? 123d : 0d) + (true ? 123d : 0d) ).ToString("F2", CultureInfo.InvariantCulture) - ) € + ) €
@@ -7347,7 +7426,7 @@ public Task NestedExplicitExpression4() ((true) ? 123d : 0d) + (true ? 123d : 0d) ).ToString("F2", CultureInfo.InvariantCulture) - ) € + ) € } """); @@ -7368,4 +7447,508 @@ public Task TypeParameterAttribute() """); + + [FormattingTestFact] + public Task HtmlAttributes() + => RunFormattingTestAsync( + input: """ + + """, + expected: """ + + """); + + [FormattingTestFact] + public Task HtmlAttributes_FirstAttributeOnNextLine() + => RunFormattingTestAsync( + input: """ +
+
+ """, + expected: """ +
+
+ """); + + [FormattingTestFact] + public Task HtmlAttributes_IndentByOne() + => RunFormattingTestAsync( + input: """ +
+ """, + expected: """ + + """, + attributeIndentStyle: CodeAnalysis.Razor.Settings.AttributeIndentStyle.IndentByOne); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12223")] + public Task ExplicitExpression_InIf() + => RunFormattingTestAsync( + input: """ + @if (true) + { + @(Html.Grid() + .Render()) + } + """, + expected: """ + @if (true) + { + @(Html.Grid() + .Render()) + } + """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12554")] + public Task ObjectInitializers1() + => RunFormattingTestAsync( + input: """ + @{ + Func RenderTest = @
Test X: @item.X, Y: @item.Y
; + } + + @RenderTest(new Test() + { + X = 10, + Y = 20, + }) + +
+ @RenderTest(new Test() + { + X = 1, + Y = 2, + }) +
+ """, + expected: """ + @{ + Func RenderTest = @
Test X: @item.X, Y: @item.Y
; + } + + @RenderTest(new Test() + { + X = 10, + Y = 20, + }) + +
+ @RenderTest(new Test() + { + X = 1, + Y = 2, + }) +
+ """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12554")] + public Task ObjectInitializers2() + => RunFormattingTestAsync( + input: """ +
+ @if (true) + { + @Html.TextBox(new Test() + { + test = 5 + }) + } +
+ """, + expected: """ +
+ @if (true) + { + @Html.TextBox(new Test() + { + test = 5 + }) + } +
+ """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12554")] + public Task ObjectInitializers3() + => RunFormattingTestAsync( + input: """ +
+ @{ + var a = new int[] { 1, 2, 3 } + .Where(i => i % 2 == 0) + .ToArray(); + } +
+ """, + expected: """ +
+ @{ + var a = new int[] { 1, 2, 3 } + .Where(i => i % 2 == 0) + .ToArray(); + } +
+ """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12622")] + public Task ObjectInitializers4() + => RunFormattingTestAsync( + input: """ +
+ @if (true) + { + @Html.TextBox(new Test() + { + test = 5 + }) +
+ } +
+ """, + expected: """ +
+ @if (true) + { + @Html.TextBox(new Test() + { + test = 5 + }) +
+ } +
+ """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12622")] + public Task ObjectInitializers5() + => RunFormattingTestAsync( + input: """ +
+ @if (true) + { + @Html.TextBox(new Test() { test = 5 }) +
+ } +
+ """, + expected: """ +
+ @if (true) + { + @Html.TextBox(new Test() { test = 5 }) +
+ } +
+ """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12622")] + public Task ObjectInitializers6() + => RunFormattingTestAsync( + input: """ + @if (true) + { + @Html.TextBox(new Test() + { + test = 5 + }) +
+ } + """, + expected: """ + @if (true) + { + @Html.TextBox(new Test() + { + test = 5 + }) +
+ } + """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12622")] + public Task ObjectInitializers7() + => RunFormattingTestAsync( + input: """ +
+
+ @Html.TextBox(new + { + test = 5, + }) +
+
+ @Html.TextBox(new + { + test = 5, + }) +
+
+ """, + expected: """ +
+
+ @Html.TextBox(new + { + test = 5, + }) +
+
+ @Html.TextBox(new + { + test = 5, + }) +
+
+ """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12622")] + public Task ObjectInitializers8() + => RunFormattingTestAsync( + input: """ + @if (true) + { + @Html.TextBox(new Test() { + test = 5 + }) +
+ } + """, + expected: """ + @if (true) + { + @Html.TextBox(new Test() + { + test = 5 + }) +
+ } + """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12622")] + public Task ObjectInitializers9() + => RunFormattingTestAsync( + input: """ + @if (true) + { + @Html.TextBox(new Test() { + test = 5 + }) +
+ } + """, + expected: """ + @if (true) + { + @Html.TextBox(new Test() { + test = 5 + }) +
+ } + """, + csharpSyntaxFormattingOptions: RazorCSharpSyntaxFormattingOptions.Default with + { + NewLines = RazorCSharpSyntaxFormattingOptions.Default.NewLines & ~RazorNewLinePlacement.BeforeOpenBraceInObjectCollectionArrayInitializers + }); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12622")] + public Task ObjectInitializers10() + => RunFormattingTestAsync( + input: """ + @if (true) + { + @Html.TextBox(new Test() + { + test = 5 + }) +
+ } + """, + expected: """ + @if (true) + { + @Html.TextBox(new Test() { + test = 5 + }) +
+ } + """, + csharpSyntaxFormattingOptions: RazorCSharpSyntaxFormattingOptions.Default with + { + NewLines = RazorCSharpSyntaxFormattingOptions.Default.NewLines & ~RazorNewLinePlacement.BeforeOpenBraceInObjectCollectionArrayInitializers + }); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12622")] + public Task ObjectInitializers11() + => RunFormattingTestAsync( + input: """ +
+
+
+ @if (true) + { +
+ @Html.TextBox(new + { + test = 6 + }) +
+ +
+ } +
+
+
+ """, + expected: """ +
+
+
+ @if (true) + { +
+ @Html.TextBox(new + { + test = 6 + }) +
+ +
+ } +
+
+
+ """); + + [FormattingTestFact] + [WorkItem("https://github.com/dotnet/razor/issues/12631")] + public Task ObjectInitializers12() + => RunFormattingTestAsync( + input: """ + @await Component.InvokeAsync("ReviewAndPublishModal", + new { + id = "ReviewPublishModal", + title = "Review and publish", + text = Model.ReviewNotes, + state = Model.State, + allowSave = allowSaveReview, + allowPublish = allowPublish, + isPublished =isCurrentPublished + } + ) + """, + expected: """ + @await Component.InvokeAsync("ReviewAndPublishModal", + new + { + id = "ReviewPublishModal", + title = "Review and publish", + text = Model.ReviewNotes, + state = Model.State, + allowSave = allowSaveReview, + allowPublish = allowPublish, + isPublished = isCurrentPublished + } + ) + """); + + [FormattingTestFact] + public Task PartialDocument() + => RunFormattingTestAsync( + input: """ + + +
+ """, + expected: """ + + +
+ + """, + allowDiagnostics: true); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs index 5b45402332f..0f88488a3c2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs @@ -24,7 +24,7 @@ using Microsoft.CodeAnalysis.Razor.ProjectEngineHost; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Telemetry; +using Microsoft.CodeAnalysis.Razor.Settings; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Moq; @@ -59,13 +59,18 @@ private protected async Task RunFormattingTestAsync( TagHelperCollection? tagHelpers = null, bool allowDiagnostics = false, bool codeBlockBraceOnNextLine = false, + AttributeIndentStyle attributeIndentStyle = AttributeIndentStyle.AlignWithFirst, bool inGlobalNamespace = false, bool debugAssertsEnabled = true, RazorCSharpSyntaxFormattingOptions? csharpSyntaxFormattingOptions = null) { (input, expected) = ProcessFormattingContext(input, expected); - var razorLSPOptions = RazorLSPOptions.Default with { CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine }; + var razorLSPOptions = RazorLSPOptions.Default with + { + CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine, + AttributeIndentStyle = attributeIndentStyle, + }; csharpSyntaxFormattingOptions ??= RazorCSharpSyntaxFormattingOptions.Default; @@ -106,7 +111,7 @@ private async Task RunFormattingTestInternalAsync( TabSize = tabSize, InsertSpaces = insertSpaces, }; - var razorOptions = RazorFormattingOptions.From(options, codeBlockBraceOnNextLine: razorLSPOptions?.CodeBlockBraceOnNextLine ?? false, csharpSyntaxFormattingOptions); + var razorOptions = RazorFormattingOptions.From(options, codeBlockBraceOnNextLine: razorLSPOptions?.CodeBlockBraceOnNextLine ?? false, razorLSPOptions?.AttributeIndentStyle ?? AttributeIndentStyle.AlignWithFirst, csharpSyntaxFormattingOptions); var languageServerFeatureOptions = new TestLanguageServerFeatureOptions(); @@ -172,7 +177,7 @@ private protected async Task RunOnTypeFormattingTestAsync( TabSize = tabSize, InsertSpaces = insertSpaces, }; - var razorOptions = RazorFormattingOptions.From(options, codeBlockBraceOnNextLine: razorLSPOptions?.CodeBlockBraceOnNextLine ?? false); + var razorOptions = RazorFormattingOptions.From(options, codeBlockBraceOnNextLine: razorLSPOptions?.CodeBlockBraceOnNextLine ?? false, razorLSPOptions?.AttributeIndentStyle ?? AttributeIndentStyle.AlignWithFirst); var documentContext = new DocumentContext(uri, documentSnapshot, projectContext: null); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingTest.cs index 23f85daf973..cc4d3f49566 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingTest.cs @@ -263,7 +263,7 @@ await RunFormattingTestAsync( tagHelpers: [.. GetComponents()]); } - [FormattingTestFact(Skip = "Requires fix")] + [FormattingTestFact] [WorkItem("https://github.com/dotnet/razor/issues/8228")] public async Task FormatNestedComponents4() { @@ -279,9 +279,9 @@ await RunFormattingTestAsync( expected: """ @{ RenderFragment fragment = - @ - ; + @ + ; } """, tagHelpers: [.. GetComponents()]); @@ -307,7 +307,7 @@ await RunFormattingTestAsync( @{ RenderFragment fragment = @ + Caption="Title"> ; } @@ -464,11 +464,11 @@ await RunFormattingTestAsync(
@{ - #if DEBUG - } -
- @{ - #endif + #if DEBUG + } +
+ @{ + #endif }
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs index df1f08b8bb0..0664ab51bb8 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.Settings; using Moq; using Xunit; using Xunit.Abstractions; @@ -24,6 +25,7 @@ public class RazorLSPOptionsMonitorTest(ITestOutputHelper testOutput) : ToolingT AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, + AttributeIndentStyle: AttributeIndentStyle.AlignWithFirst, CommitElementsWithSpace: true, TaskListDescriptors: []); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs index ec292c917e5..a278ffe81f0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs @@ -21,6 +21,7 @@ using Microsoft.CodeAnalysis.Razor.ProjectEngineHost; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Settings; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Xunit.Abstractions; @@ -138,6 +139,7 @@ private protected static RazorLSPOptionsMonitor GetOptionsMonitor( bool autoInsertAttributeQuotes = true, bool colorBackground = false, bool codeBlockBraceOnNextLine = false, + AttributeIndentStyle attributeIndentStyle = AttributeIndentStyle.AlignWithFirst, bool commitElementsWithSpace = true, bool formatOnPaste = true) { @@ -153,6 +155,7 @@ private protected static RazorLSPOptionsMonitor GetOptionsMonitor( autoInsertAttributeQuotes, colorBackground, codeBlockBraceOnNextLine, + attributeIndentStyle, commitElementsWithSpace, TaskListDescriptors: []); var optionsMonitor = new RazorLSPOptionsMonitor(configService, options); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingLogTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingLogTest.cs index 1f31c6067b0..d45e315dcd5 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingLogTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingLogTest.cs @@ -44,7 +44,7 @@ public async Task UnexpectedFalseInIndentBlockOperation() var sourceText = await document.GetTextAsync(); var htmlEdits = htmlChanges.Select(c => sourceText.GetTextEdit(c.ToTextChange())).ToArray(); - await GetFormattingEditsAsync(document, htmlEdits, span: default, options.CodeBlockBraceOnNextLine, options.InsertSpaces, options.TabSize, options.CSharpSyntaxFormattingOptions.AssumeNotNull()); + await GetFormattingEditsAsync(document, htmlEdits, span: default, options.CodeBlockBraceOnNextLine, options.AttributeIndentStyle, options.InsertSpaces, options.TabSize, options.CSharpSyntaxFormattingOptions.AssumeNotNull()); } [Fact] @@ -91,7 +91,7 @@ public async Task CSharpStringLiteral() var sourceText = await document.GetTextAsync(); var htmlEdits = htmlChanges.Select(c => sourceText.GetTextEdit(c.ToTextChange())).ToArray(); - return await GetFormattingEditsAsync(document, htmlEdits, span: default, options.CodeBlockBraceOnNextLine, options.InsertSpaces, options.TabSize, RazorCSharpSyntaxFormattingOptions.Default); + return await GetFormattingEditsAsync(document, htmlEdits, span: default, options.CodeBlockBraceOnNextLine, options.AttributeIndentStyle, options.InsertSpaces, options.TabSize, RazorCSharpSyntaxFormattingOptions.Default); } private string GetResource(string name, [CallerMemberName] string? testName = null) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingTestBase.cs index ec4a0569e77..2c188ed6297 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/FormattingTestBase.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Razor.Settings; using Microsoft.CodeAnalysis.Razor.Workspaces.Settings; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Razor.Settings; @@ -41,6 +42,7 @@ private protected async Task RunFormattingTestAsync( RazorFileKind? fileKind = null, bool inGlobalNamespace = false, bool codeBlockBraceOnNextLine = false, + AttributeIndentStyle attributeIndentStyle = AttributeIndentStyle.AlignWithFirst, bool insertSpaces = true, int tabSize = 4, bool allowDiagnostics = false, @@ -82,7 +84,7 @@ private protected async Task RunFormattingTestAsync( ? spans.First() : default; - var edits = await GetFormattingEditsAsync(document, htmlEdits, span, codeBlockBraceOnNextLine, insertSpaces, tabSize, csharpSyntaxFormattingOptions); + var edits = await GetFormattingEditsAsync(document, htmlEdits, span, codeBlockBraceOnNextLine, attributeIndentStyle, insertSpaces, tabSize, csharpSyntaxFormattingOptions); if (edits is null) { @@ -97,12 +99,16 @@ private protected async Task RunFormattingTestAsync( AssertEx.EqualOrDiff(expected, finalText.ToString()); } - private protected async Task GetFormattingEditsAsync(TextDocument document, TextEdit[]? htmlEdits, TextSpan span, bool codeBlockBraceOnNextLine, bool insertSpaces, int tabSize, RazorCSharpSyntaxFormattingOptions csharpSyntaxFormattingOptions) + private protected async Task GetFormattingEditsAsync(TextDocument document, TextEdit[]? htmlEdits, TextSpan span, bool codeBlockBraceOnNextLine, AttributeIndentStyle attributeIndentStyle, bool insertSpaces, int tabSize, RazorCSharpSyntaxFormattingOptions csharpSyntaxFormattingOptions) { var requestInvoker = new TestHtmlRequestInvoker([(Methods.TextDocumentFormattingName, htmlEdits)]); var clientSettingsManager = new ClientSettingsManager(changeTriggers: []); - clientSettingsManager.Update(clientSettingsManager.GetClientSettings().AdvancedSettings with { CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine }); + clientSettingsManager.Update(clientSettingsManager.GetClientSettings().AdvancedSettings with + { + CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine, + AttributeIndentStyle = attributeIndentStyle, + }); var edits = await GetFormattingEditsAsync(span, insertSpaces, tabSize, document, requestInvoker, clientSettingsManager, csharpSyntaxFormattingOptions); return edits; diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/HtmlFormattingPassTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/HtmlFormattingPassTest.cs index d5d16db81d0..c152faf74a5 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/HtmlFormattingPassTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/HtmlFormattingPassTest.cs @@ -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.Collections.Immutable; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Test.Common; @@ -12,6 +13,7 @@ using Microsoft.VisualStudio.Razor.LanguageClient.Cohost.Formatting; using Xunit; using Xunit.Abstractions; +using AssertEx = Roslyn.Test.Utilities.AssertEx; namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost.Formatting; @@ -45,7 +47,60 @@ public async Task RemoveEditThatSplitsStringLiteral(string prefix, string suffix Assert.Empty(edits); } - private async Task> GetHtmlFormattingEditsAsync(CodeAnalysis.TextDocument document, TextChange change) + [Fact] + public async Task FilterOutHtmlEdits() + { + TestCode input = """ +
+
+
+ + Test + +
+ +
+ +
+ +
+ +
+ +
+ +
+ + """; + + var document = CreateProjectAndRazorDocument(input.Text); + var sourceText = SourceText.From(input.Text); + var changes = ImmutableArray.CreateBuilder(); + + // Create an edit to "indent" every line. Using $$ makes test assertions easier. + foreach (var line in sourceText.Lines) + { + changes.Add(new TextChange(new TextSpan(line.Start, 0), "$$")); + } + + var edits = await GetHtmlFormattingEditsAsync(document, changes.ToImmutable()); + + var newDoc = sourceText.WithChanges(edits); + AssertEx.EqualOrDiff(input.OriginalInput, newDoc.ToString()); + } + + private async Task> GetHtmlFormattingEditsAsync(CodeAnalysis.TextDocument document, params ImmutableArray changes) { var documentMappingService = OOPExportProvider.GetExportedValue(); var pass = new HtmlFormattingPass(documentMappingService); @@ -61,7 +116,7 @@ public async Task RemoveEditThatSplitsStringLiteral(string prefix, string suffix new RazorFormattingOptions(), logger); - var edits = await pass.GetTestAccessor().FilterIncomingChangesAsync(context, [change], DisposalToken); + var edits = await pass.GetTestAccessor().FilterIncomingChangesAsync(context, changes, DisposalToken); return edits; } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/HtmlFormattingTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/HtmlFormattingTest.cs index 28a4c998049..6b319453a74 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/HtmlFormattingTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/Formatting/HtmlFormattingTest.cs @@ -1,13 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Features; using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.Settings; using Microsoft.VisualStudio.Razor.LanguageClient.Cohost; using Microsoft.VisualStudio.Razor.LanguageClient.Cohost.Formatting; using Xunit; using Xunit.Abstractions; +using AssertEx = Roslyn.Test.Utilities.AssertEx; namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost.Formatting; @@ -293,7 +297,7 @@ await RunFormattingTestAsync( @{ RenderFragment fragment = @ + Caption="Title"> ; } @@ -408,11 +412,11 @@ await RunFormattingTestAsync(
@{ - #if DEBUG - } -
- @{ - #endif + #if DEBUG + } +
+ @{ + #endif }
@@ -423,6 +427,56 @@ await RunFormattingTestAsync( allowDiagnostics: true); } + [FormattingTestTheory] + [CombinatorialData] + internal async Task HtmlAttributes_FirstNotOnSameLine(AttributeIndentStyle attributeIndentStyle) + { + // This test looks different because it explicitly doesn't call the html formatter, because we don't + // want it to "fix" the first attribute placement, and put it on the same line as the start tag. + + var contents = """ +
+ Content +
+
+ Content +
+ """; + var expected = """ +
+ Content +
+
+ Content +
+ """; + + var document = CreateProjectAndRazorDocument(contents); + var options = new RazorFormattingOptions(); + + var formattingService = (RazorFormattingService)OOPExportProvider.GetExportedValue(); + formattingService.GetTestAccessor().SetFormattingLoggerFactory(new TestFormattingLoggerFactory(TestOutputHelper)); + + var htmlEdits = new TextEdit[0]; + var edits = await GetFormattingEditsAsync(document, htmlEdits, span: default, options.CodeBlockBraceOnNextLine, attributeIndentStyle, options.InsertSpaces, options.TabSize, RazorCSharpSyntaxFormattingOptions.Default); + + Assert.NotNull(edits); + + var inputText = await document.GetTextAsync(DisposalToken); + var changes = edits.Select(inputText.GetTextChange); + var finalText = inputText.WithChanges(changes); + + AssertEx.EqualOrDiff(expected, finalText.ToString()); + } + private Task RunFormattingTestAsync( TestCode input, string expected) @@ -492,5 +546,6 @@ @typeparam TValue protected string StringValue => Value?.ToString(); } """)]); + } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs index 8f117a6e6a4..2e87701686c 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs @@ -90,6 +90,7 @@ public void Update_TriggersChangedIfAdvancedSettingsAreDifferent() AutoInsertAttributeQuotes: true, ColorBackground: true, CodeBlockBraceOnNextLine: false, + AttributeIndentStyle: AttributeIndentStyle.AlignWithFirst, CommitElementsWithSpace: false, SnippetSetting: SnippetSetting.All, LogLevel: LogLevel.None,