From 97ac5367c76246e972838dbc99502acc091953ae Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 14:18:10 +1000 Subject: [PATCH 01/26] Move add usings helper code down to the workspaces layer This commit is just moves, no functionality changes. Minor tweaks to method visibility, and one rename of a static field :) --- .../AddUsingsCodeActionProviderHelper.cs | 190 ------------ .../TypeAccessibilityCodeActionProvider.cs | 5 +- .../Razor/AddUsingsCodeActionResolver.cs | 206 +++++-------- ...omponentAccessibilityCodeActionProvider.cs | 2 +- .../LspCSharpOnTypeFormattingPass.cs | 2 +- .../Formatting/AddUsingsHelper.cs | 272 ++++++++++++++++++ .../AddUsingsCodeActionProviderFactoryTest.cs | 114 -------- .../AddUsingsCodeActionResolverTest.cs | 42 +++ .../Formatting/AddUsingsHelperTest.cs | 71 +++++ 9 files changed, 455 insertions(+), 449 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/AddUsingsCodeActionProviderHelper.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/AddUsingsHelper.cs delete mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/AddUsingsCodeActionProviderFactoryTest.cs create mode 100644 src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Formatting/AddUsingsHelperTest.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/AddUsingsCodeActionProviderHelper.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/AddUsingsCodeActionProviderHelper.cs deleted file mode 100644 index a216287aea..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/AddUsingsCodeActionProviderHelper.cs +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; - -internal static class AddUsingsCodeActionProviderHelper -{ - public static async Task GetUsingStatementEditsAsync(RazorCodeDocument codeDocument, SourceText originalCSharpText, SourceText changedCSharpText, CancellationToken cancellationToken) - { - // Now that we're done with everything, lets see if there are any using statements to fix up - // We do this by comparing the original generated C# code, and the changed C# code, and look for a difference - // in using statements. We can't use edits for this for two main reasons: - // - // 1. Using statements in the generated code might come from _Imports.razor, or from this file, and C# will shove them anywhere - // 2. The edit might not be clean. eg given: - // using System; - // using System.Text; - // Adding "using System.Linq;" could result in an insert of "Linq;\r\nusing System." on line 2 - // - // So because of the above, we look for a difference in C# using directive nodes directly from the C# syntax tree, and apply them manually - // to the Razor document. - - var oldUsings = await FindUsingDirectiveStringsAsync(originalCSharpText, cancellationToken).ConfigureAwait(false); - var newUsings = await FindUsingDirectiveStringsAsync(changedCSharpText, cancellationToken).ConfigureAwait(false); - - using var edits = new PooledArrayBuilder(); - foreach (var usingStatement in newUsings.Except(oldUsings)) - { - // This identifier will be eventually thrown away. - Debug.Assert(codeDocument.Source.FilePath != null); - var identifier = new OptionalVersionedTextDocumentIdentifier { Uri = new Uri(codeDocument.Source.FilePath, UriKind.Relative) }; - var workspaceEdit = AddUsingsCodeActionResolver.CreateAddUsingWorkspaceEdit(usingStatement, additionalEdit: null, codeDocument, codeDocumentIdentifier: identifier); - edits.AddRange(workspaceEdit.DocumentChanges!.Value.First.First().Edits); - } - - return edits.ToArray(); - } - - private static async Task> FindUsingDirectiveStringsAsync(SourceText originalCSharpText, CancellationToken cancellationToken) - { - var syntaxTree = CSharpSyntaxTree.ParseText(originalCSharpText, cancellationToken: cancellationToken); - var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - - // We descend any compilation unit (ie, the file) or and namespaces because the compiler puts all usings inside - // the namespace node. - var usings = syntaxRoot.DescendantNodes(n => n is BaseNamespaceDeclarationSyntax or CompilationUnitSyntax) - // Filter to using directives - .OfType() - // Select everything after the initial "using " part of the statement, and excluding the ending semi-colon. The - // semi-colon is valid in Razor, but users find it surprising. This is slightly lazy, for sure, but has - // the advantage of us not caring about changes to C# syntax, we just grab whatever Roslyn wanted to put in, so - // we should still work in C# v26 - .Select(u => u.ToString()["using ".Length..^1]); - - return usings; - } - - internal static readonly Regex AddUsingVSCodeAction = new Regex("@?using ([^;]+);?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); - - // Internal for testing - internal static string GetNamespaceFromFQN(string fullyQualifiedName) - { - if (!TrySplitNamespaceAndType(fullyQualifiedName.AsSpan(), out var namespaceName, out _)) - { - return string.Empty; - } - - return namespaceName.ToString(); - } - - internal static bool TryCreateAddUsingResolutionParams(string fullyQualifiedName, Uri uri, TextDocumentEdit? additionalEdit, [NotNullWhen(true)] out string? @namespace, [NotNullWhen(true)] out RazorCodeActionResolutionParams? resolutionParams) - { - @namespace = GetNamespaceFromFQN(fullyQualifiedName); - if (string.IsNullOrEmpty(@namespace)) - { - @namespace = null; - resolutionParams = null; - return false; - } - - var actionParams = new AddUsingsCodeActionParams - { - Uri = uri, - Namespace = @namespace, - AdditionalEdit = additionalEdit - }; - - resolutionParams = new RazorCodeActionResolutionParams - { - Action = LanguageServerConstants.CodeActions.AddUsing, - Language = LanguageServerConstants.CodeActions.Languages.Razor, - Data = actionParams, - }; - - return true; - } - - /// - /// Extracts the namespace from a C# add using statement provided by Visual Studio - /// - /// Add using statement of the form `using System.X;` - /// Extract namespace `System.X` - /// The prefix to show, before the namespace, if any - /// - internal static bool TryExtractNamespace(string csharpAddUsing, out string @namespace, out string prefix) - { - // We must remove any leading/trailing new lines from the add using edit - csharpAddUsing = csharpAddUsing.Trim(); - var regexMatchedTextEdit = AddUsingVSCodeAction.Match(csharpAddUsing); - if (!regexMatchedTextEdit.Success || - - // Two Regex matching groups are expected - // 1. `using namespace;` - // 2. `namespace` - regexMatchedTextEdit.Groups.Count != 2) - { - // Text edit in an unexpected format - @namespace = string.Empty; - prefix = string.Empty; - return false; - } - - @namespace = regexMatchedTextEdit.Groups[1].Value; - prefix = csharpAddUsing[..regexMatchedTextEdit.Index]; - return true; - } - - internal static bool TrySplitNamespaceAndType(ReadOnlySpan fullTypeName, out ReadOnlySpan @namespace, out ReadOnlySpan typeName) - { - @namespace = default; - typeName = default; - - if (fullTypeName.IsEmpty) - { - return false; - } - - var nestingLevel = 0; - var splitLocation = -1; - for (var i = fullTypeName.Length - 1; i >= 0; i--) - { - var c = fullTypeName[i]; - if (c == Type.Delimiter && nestingLevel == 0) - { - splitLocation = i; - break; - } - else if (c == '>') - { - nestingLevel++; - } - else if (c == '<') - { - nestingLevel--; - } - } - - if (splitLocation == -1) - { - typeName = fullTypeName; - return true; - } - - @namespace = fullTypeName[..splitLocation]; - - var typeNameStartLocation = splitLocation + 1; - if (typeNameStartLocation < fullTypeName.Length) - { - typeName = fullTypeName[typeNameStartLocation..]; - } - - return true; - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs index 0cbd6d6f4a..679eb58c84 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Threading; using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -138,7 +139,7 @@ internal sealed class TypeAccessibilityCodeActionProvider : ICSharpCodeActionPro var fqnCodeAction = CreateFQNCodeAction(context, diagnostic, codeAction, fqn); typeAccessibilityCodeActions.Add(fqnCodeAction); - if (AddUsingsCodeActionProviderHelper.TryCreateAddUsingResolutionParams(fqn, context.Request.TextDocument.Uri, additionalEdit: null, out var @namespace, out var resolutionParams)) + if (AddUsingsCodeActionResolver.TryCreateAddUsingResolutionParams(fqn, context.Request.TextDocument.Uri, additionalEdit: null, out var @namespace, out var resolutionParams)) { var addUsingCodeAction = RazorCodeActionFactory.CreateAddComponentUsing(@namespace, newTagName: null, resolutionParams); typeAccessibilityCodeActions.Add(addUsingCodeAction); @@ -191,7 +192,7 @@ internal sealed class TypeAccessibilityCodeActionProvider : ICSharpCodeActionPro // For add using suggestions, the code action title is of the form: // `using System.Net;` else if (codeAction.Name is not null && codeAction.Name.Equals(RazorPredefinedCodeFixProviderNames.AddImport, StringComparison.Ordinal) && - AddUsingsCodeActionProviderHelper.TryExtractNamespace(codeAction.Title, out var @namespace, out var prefix)) + AddUsingsHelper.TryExtractNamespace(codeAction.Title, out var @namespace, out var prefix)) { codeAction.Title = $"{prefix}@using {@namespace}"; typeAccessibilityCodeActions.Add(codeAction.WrapResolvableCodeAction(context, LanguageServerConstants.CodeActions.Default)); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/AddUsingsCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/AddUsingsCodeActionResolver.cs index 5923d2ed86..f0516a5358 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/AddUsingsCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/AddUsingsCodeActionResolver.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Threading; @@ -16,6 +17,7 @@ using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -50,169 +52,91 @@ internal sealed class AddUsingsCodeActionResolver(IDocumentContextFactory docume } var codeDocumentIdentifier = new OptionalVersionedTextDocumentIdentifier() { Uri = actionParams.Uri }; - return CreateAddUsingWorkspaceEdit(actionParams.Namespace, actionParams.AdditionalEdit, codeDocument, codeDocumentIdentifier); + return AddUsingsHelper.CreateAddUsingWorkspaceEdit(actionParams.Namespace, actionParams.AdditionalEdit, codeDocument, codeDocumentIdentifier); } - internal static WorkspaceEdit CreateAddUsingWorkspaceEdit(string @namespace, TextDocumentEdit? additionalEdit, RazorCodeDocument codeDocument, OptionalVersionedTextDocumentIdentifier codeDocumentIdentifier) + internal static bool TryCreateAddUsingResolutionParams(string fullyQualifiedName, Uri uri, TextDocumentEdit? additionalEdit, [NotNullWhen(true)] out string? @namespace, [NotNullWhen(true)] out RazorCodeActionResolutionParams? resolutionParams) { - /* The heuristic is as follows: - * - * - If no @using, @namespace, or @page directives are present, insert the statements at the top of the - * file in alphabetical order. - * - If a @namespace or @page are present, the statements are inserted after the last line-wise in - * alphabetical order. - * - If @using directives are present and alphabetized with System directives at the top, the statements - * will be placed in the correct locations according to that ordering. - * - Otherwise it's kind of undefined; it's only geared to insert based on alphabetization. - * - * This is generally sufficient for our current situation (inserting a single @using statement to include a - * component), however it has holes if we eventually use it for other purposes. If we want to deal with - * that now I can come up with a more sophisticated heuristic (something along the lines of checking if - * there's already an ordering, etc.). - */ - using var documentChanges = new PooledArrayBuilder(); - - // Need to add the additional edit first, as the actual usings go at the top of the file, and would - // change the ranges needed in the additional edit if they went in first - if (additionalEdit is not null) + @namespace = GetNamespaceFromFQN(fullyQualifiedName); + if (string.IsNullOrEmpty(@namespace)) { - documentChanges.Add(additionalEdit); + @namespace = null; + resolutionParams = null; + return false; } - using var usingDirectives = new PooledArrayBuilder(); - CollectUsingDirectives(codeDocument, ref usingDirectives.AsRef()); - if (usingDirectives.Count > 0) + var actionParams = new AddUsingsCodeActionParams { - // Interpolate based on existing @using statements - var edits = GenerateSingleUsingEditsInterpolated(codeDocument, codeDocumentIdentifier, @namespace, in usingDirectives); - documentChanges.Add(edits); - } - else - { - // Just throw them at the top - var edits = GenerateSingleUsingEditsAtTop(codeDocument, codeDocumentIdentifier, @namespace); - documentChanges.Add(edits); - } - - return new WorkspaceEdit() - { - DocumentChanges = documentChanges.ToArray(), + Uri = uri, + Namespace = @namespace, + AdditionalEdit = additionalEdit }; + + resolutionParams = new RazorCodeActionResolutionParams + { + Action = LanguageServerConstants.CodeActions.AddUsing, + Language = LanguageServerConstants.CodeActions.Languages.Razor, + Data = actionParams, + }; + + return true; } - private static TextDocumentEdit GenerateSingleUsingEditsInterpolated( - RazorCodeDocument codeDocument, - OptionalVersionedTextDocumentIdentifier codeDocumentIdentifier, - string newUsingNamespace, - ref readonly PooledArrayBuilder existingUsingDirectives) + // Internal for testing + internal static string GetNamespaceFromFQN(string fullyQualifiedName) { - Debug.Assert(existingUsingDirectives.Count > 0); - - using var edits = new PooledArrayBuilder(); - var newText = $"@using {newUsingNamespace}{Environment.NewLine}"; - - foreach (var usingDirective in existingUsingDirectives) + if (!TrySplitNamespaceAndType(fullyQualifiedName.AsSpan(), out var namespaceName, out _)) { - // Skip System directives; if they're at the top we don't want to insert before them - var usingDirectiveNamespace = usingDirective.Statement.ParsedNamespace; - if (usingDirectiveNamespace.StartsWith("System", StringComparison.Ordinal)) - { - continue; - } + return string.Empty; + } - if (string.CompareOrdinal(newUsingNamespace, usingDirectiveNamespace) < 0) + return namespaceName.ToString(); + } + + private static bool TrySplitNamespaceAndType(ReadOnlySpan fullTypeName, out ReadOnlySpan @namespace, out ReadOnlySpan typeName) + { + @namespace = default; + typeName = default; + + if (fullTypeName.IsEmpty) + { + return false; + } + + var nestingLevel = 0; + var splitLocation = -1; + for (var i = fullTypeName.Length - 1; i >= 0; i--) + { + var c = fullTypeName[i]; + if (c == Type.Delimiter && nestingLevel == 0) { - var usingDirectiveLineIndex = codeDocument.Source.Text.GetLinePosition(usingDirective.Node.Span.Start).Line; - var edit = VsLspFactory.CreateTextEdit(line: usingDirectiveLineIndex, character: 0, newText); - edits.Add(edit); + splitLocation = i; break; } - } - - // If we haven't actually found a place to insert the using directive, do so at the end - if (edits.Count == 0) - { - var endIndex = existingUsingDirectives[^1].Node.Span.End; - var lineIndex = GetLineIndexOrEnd(codeDocument, endIndex - 1) + 1; - var edit = VsLspFactory.CreateTextEdit(line: lineIndex, character: 0, newText); - edits.Add(edit); - } - - return new TextDocumentEdit() - { - TextDocument = codeDocumentIdentifier, - Edits = edits.ToArray() - }; - } - - private static TextDocumentEdit GenerateSingleUsingEditsAtTop( - RazorCodeDocument codeDocument, - OptionalVersionedTextDocumentIdentifier codeDocumentIdentifier, - string newUsingNamespace) - { - var insertPosition = (0, 0); - - // If we don't have usings, insert after the last namespace or page directive, which ever comes later - var syntaxTreeRoot = codeDocument.GetSyntaxTree().Root; - var lastNamespaceOrPageDirective = syntaxTreeRoot - .DescendantNodes() - .LastOrDefault(IsNamespaceOrPageDirective); - - if (lastNamespaceOrPageDirective != null) - { - var lineIndex = GetLineIndexOrEnd(codeDocument, lastNamespaceOrPageDirective.Span.End - 1) + 1; - insertPosition = (lineIndex, 0); - } - - // Insert all usings at the given point - return new TextDocumentEdit - { - TextDocument = codeDocumentIdentifier, - Edits = [VsLspFactory.CreateTextEdit(insertPosition, newText: $"@using {newUsingNamespace}{Environment.NewLine}")] - }; - } - - private static int GetLineIndexOrEnd(RazorCodeDocument codeDocument, int endIndex) - { - if (endIndex < codeDocument.Source.Text.Length) - { - return codeDocument.Source.Text.GetLinePosition(endIndex).Line; - } - else - { - return codeDocument.Source.Text.Lines.Count; - } - } - - private static void CollectUsingDirectives(RazorCodeDocument codeDocument, ref PooledArrayBuilder directives) - { - var syntaxTreeRoot = codeDocument.GetSyntaxTree().Root; - foreach (var node in syntaxTreeRoot.DescendantNodes()) - { - if (node is RazorDirectiveSyntax directiveNode) + else if (c == '>') { - foreach (var child in directiveNode.DescendantNodes()) - { - if (child.GetChunkGenerator() is AddImportChunkGenerator { IsStatic: false } usingStatement) - { - directives.Add(new RazorUsingDirective(directiveNode, usingStatement)); - } - } + nestingLevel++; + } + else if (c == '<') + { + nestingLevel--; } } - } - private static bool IsNamespaceOrPageDirective(SyntaxNode node) - { - if (node is RazorDirectiveSyntax directiveNode) + if (splitLocation == -1) { - return directiveNode.DirectiveDescriptor == ComponentPageDirective.Directive || - directiveNode.DirectiveDescriptor == NamespaceDirective.Directive || - directiveNode.DirectiveDescriptor == PageDirective.Directive; + typeName = fullTypeName; + return true; } - return false; - } + @namespace = fullTypeName[..splitLocation]; - private readonly record struct RazorUsingDirective(RazorDirectiveSyntax Node, AddImportChunkGenerator Statement); + var typeNameStartLocation = splitLocation + 1; + if (typeNameStartLocation < fullTypeName.Length) + { + typeName = fullTypeName[typeNameStartLocation..]; + } + + return true; + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs index 2e8ec37c2f..8a551f300b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs @@ -176,7 +176,7 @@ internal sealed class ComponentAccessibilityCodeActionProvider : IRazorCodeActio // name to give the tag. if (!tagHelperPair.CaseInsensitiveMatch || newTagName is not null) { - if (AddUsingsCodeActionProviderHelper.TryCreateAddUsingResolutionParams(fullyQualifiedName, context.Request.TextDocument.Uri, additionalEdit, out var @namespace, out var resolutionParams)) + if (AddUsingsCodeActionResolver.TryCreateAddUsingResolutionParams(fullyQualifiedName, context.Request.TextDocument.Uri, additionalEdit, out var @namespace, out var resolutionParams)) { var addUsingCodeAction = RazorCodeActionFactory.CreateAddComponentUsing(@namespace, newTagName, resolutionParams); container.Add(addUsingCodeAction); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspCSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspCSharpOnTypeFormattingPass.cs index c163bc336d..a737698a1d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspCSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspCSharpOnTypeFormattingPass.cs @@ -29,7 +29,7 @@ internal sealed class LspCSharpOnTypeFormattingPass( // Because we need to parse the C# code twice for this operation, lets do a quick check to see if its even necessary if (textEdits.Any(e => e.NewText.IndexOf("using") != -1)) { - var usingStatementEdits = await AddUsingsCodeActionProviderHelper.GetUsingStatementEditsAsync(codeDocument, csharpText, originalTextWithChanges, cancellationToken).ConfigureAwait(false); + var usingStatementEdits = await AddUsingsHelper.GetUsingStatementEditsAsync(codeDocument, csharpText, originalTextWithChanges, cancellationToken).ConfigureAwait(false); finalEdits = [.. usingStatementEdits, .. finalEdits]; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/AddUsingsHelper.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/AddUsingsHelper.cs new file mode 100644 index 0000000000..b7bc5f33fc --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/AddUsingsHelper.cs @@ -0,0 +1,272 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; + +namespace Microsoft.CodeAnalysis.Razor.Formatting; + +internal static class AddUsingsHelper +{ + private static readonly Regex s_addUsingVSCodeAction = new Regex("@?using ([^;]+);?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); + + private readonly record struct RazorUsingDirective(RazorDirectiveSyntax Node, AddImportChunkGenerator Statement); + + public static async Task GetUsingStatementEditsAsync(RazorCodeDocument codeDocument, SourceText originalCSharpText, SourceText changedCSharpText, CancellationToken cancellationToken) + { + // Now that we're done with everything, lets see if there are any using statements to fix up + // We do this by comparing the original generated C# code, and the changed C# code, and look for a difference + // in using statements. We can't use edits for this for two main reasons: + // + // 1. Using statements in the generated code might come from _Imports.razor, or from this file, and C# will shove them anywhere + // 2. The edit might not be clean. eg given: + // using System; + // using System.Text; + // Adding "using System.Linq;" could result in an insert of "Linq;\r\nusing System." on line 2 + // + // So because of the above, we look for a difference in C# using directive nodes directly from the C# syntax tree, and apply them manually + // to the Razor document. + + var oldUsings = await FindUsingDirectiveStringsAsync(originalCSharpText, cancellationToken).ConfigureAwait(false); + var newUsings = await FindUsingDirectiveStringsAsync(changedCSharpText, cancellationToken).ConfigureAwait(false); + + using var edits = new PooledArrayBuilder(); + foreach (var usingStatement in newUsings.Except(oldUsings)) + { + // This identifier will be eventually thrown away. + Debug.Assert(codeDocument.Source.FilePath != null); + var identifier = new OptionalVersionedTextDocumentIdentifier { Uri = new Uri(codeDocument.Source.FilePath, UriKind.Relative) }; + var workspaceEdit = CreateAddUsingWorkspaceEdit(usingStatement, additionalEdit: null, codeDocument, codeDocumentIdentifier: identifier); + edits.AddRange(workspaceEdit.DocumentChanges!.Value.First.First().Edits); + } + + return edits.ToArray(); + } + + /// + /// Extracts the namespace from a C# add using statement provided by Visual Studio + /// + /// Add using statement of the form `using System.X;` + /// Extract namespace `System.X` + /// The prefix to show, before the namespace, if any + /// + public static bool TryExtractNamespace(string csharpAddUsing, out string @namespace, out string prefix) + { + // We must remove any leading/trailing new lines from the add using edit + csharpAddUsing = csharpAddUsing.Trim(); + var regexMatchedTextEdit = s_addUsingVSCodeAction.Match(csharpAddUsing); + if (!regexMatchedTextEdit.Success || + + // Two Regex matching groups are expected + // 1. `using namespace;` + // 2. `namespace` + regexMatchedTextEdit.Groups.Count != 2) + { + // Text edit in an unexpected format + @namespace = string.Empty; + prefix = string.Empty; + return false; + } + + @namespace = regexMatchedTextEdit.Groups[1].Value; + prefix = csharpAddUsing[..regexMatchedTextEdit.Index]; + return true; + } + + public static WorkspaceEdit CreateAddUsingWorkspaceEdit(string @namespace, TextDocumentEdit? additionalEdit, RazorCodeDocument codeDocument, OptionalVersionedTextDocumentIdentifier codeDocumentIdentifier) + { + /* The heuristic is as follows: + * + * - If no @using, @namespace, or @page directives are present, insert the statements at the top of the + * file in alphabetical order. + * - If a @namespace or @page are present, the statements are inserted after the last line-wise in + * alphabetical order. + * - If @using directives are present and alphabetized with System directives at the top, the statements + * will be placed in the correct locations according to that ordering. + * - Otherwise it's kind of undefined; it's only geared to insert based on alphabetization. + * + * This is generally sufficient for our current situation (inserting a single @using statement to include a + * component), however it has holes if we eventually use it for other purposes. If we want to deal with + * that now I can come up with a more sophisticated heuristic (something along the lines of checking if + * there's already an ordering, etc.). + */ + using var documentChanges = new PooledArrayBuilder(); + + // Need to add the additional edit first, as the actual usings go at the top of the file, and would + // change the ranges needed in the additional edit if they went in first + if (additionalEdit is not null) + { + documentChanges.Add(additionalEdit); + } + + using var usingDirectives = new PooledArrayBuilder(); + CollectUsingDirectives(codeDocument, ref usingDirectives.AsRef()); + if (usingDirectives.Count > 0) + { + // Interpolate based on existing @using statements + var edits = GenerateSingleUsingEditsInterpolated(codeDocument, codeDocumentIdentifier, @namespace, in usingDirectives); + documentChanges.Add(edits); + } + else + { + // Just throw them at the top + var edits = GenerateSingleUsingEditsAtTop(codeDocument, codeDocumentIdentifier, @namespace); + documentChanges.Add(edits); + } + + return new WorkspaceEdit() + { + DocumentChanges = documentChanges.ToArray(), + }; + } + + private static async Task> FindUsingDirectiveStringsAsync(SourceText originalCSharpText, CancellationToken cancellationToken) + { + var syntaxTree = CSharpSyntaxTree.ParseText(originalCSharpText, cancellationToken: cancellationToken); + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + + // We descend any compilation unit (ie, the file) or and namespaces because the compiler puts all usings inside + // the namespace node. + var usings = syntaxRoot.DescendantNodes(n => n is BaseNamespaceDeclarationSyntax or CompilationUnitSyntax) + // Filter to using directives + .OfType() + // Select everything after the initial "using " part of the statement, and excluding the ending semi-colon. The + // semi-colon is valid in Razor, but users find it surprising. This is slightly lazy, for sure, but has + // the advantage of us not caring about changes to C# syntax, we just grab whatever Roslyn wanted to put in, so + // we should still work in C# v26 + .Select(u => u.ToString()["using ".Length..^1]); + + return usings; + } + + private static TextDocumentEdit GenerateSingleUsingEditsInterpolated( + RazorCodeDocument codeDocument, + OptionalVersionedTextDocumentIdentifier codeDocumentIdentifier, + string newUsingNamespace, + ref readonly PooledArrayBuilder existingUsingDirectives) + { + Debug.Assert(existingUsingDirectives.Count > 0); + + using var edits = new PooledArrayBuilder(); + var newText = $"@using {newUsingNamespace}{Environment.NewLine}"; + + foreach (var usingDirective in existingUsingDirectives) + { + // Skip System directives; if they're at the top we don't want to insert before them + var usingDirectiveNamespace = usingDirective.Statement.ParsedNamespace; + if (usingDirectiveNamespace.StartsWith("System", StringComparison.Ordinal)) + { + continue; + } + + if (string.CompareOrdinal(newUsingNamespace, usingDirectiveNamespace) < 0) + { + var usingDirectiveLineIndex = codeDocument.Source.Text.GetLinePosition(usingDirective.Node.Span.Start).Line; + var edit = VsLspFactory.CreateTextEdit(line: usingDirectiveLineIndex, character: 0, newText); + edits.Add(edit); + break; + } + } + + // If we haven't actually found a place to insert the using directive, do so at the end + if (edits.Count == 0) + { + var endIndex = existingUsingDirectives[^1].Node.Span.End; + var lineIndex = GetLineIndexOrEnd(codeDocument, endIndex - 1) + 1; + var edit = VsLspFactory.CreateTextEdit(line: lineIndex, character: 0, newText); + edits.Add(edit); + } + + return new TextDocumentEdit() + { + TextDocument = codeDocumentIdentifier, + Edits = edits.ToArray() + }; + } + + private static TextDocumentEdit GenerateSingleUsingEditsAtTop( + RazorCodeDocument codeDocument, + OptionalVersionedTextDocumentIdentifier codeDocumentIdentifier, + string newUsingNamespace) + { + var insertPosition = (0, 0); + + // If we don't have usings, insert after the last namespace or page directive, which ever comes later + var syntaxTreeRoot = codeDocument.GetSyntaxTree().Root; + var lastNamespaceOrPageDirective = syntaxTreeRoot + .DescendantNodes() + .LastOrDefault(IsNamespaceOrPageDirective); + + if (lastNamespaceOrPageDirective != null) + { + var lineIndex = GetLineIndexOrEnd(codeDocument, lastNamespaceOrPageDirective.Span.End - 1) + 1; + insertPosition = (lineIndex, 0); + } + + // Insert all usings at the given point + return new TextDocumentEdit + { + TextDocument = codeDocumentIdentifier, + Edits = [VsLspFactory.CreateTextEdit(insertPosition, newText: $"@using {newUsingNamespace}{Environment.NewLine}")] + }; + } + + private static int GetLineIndexOrEnd(RazorCodeDocument codeDocument, int endIndex) + { + if (endIndex < codeDocument.Source.Text.Length) + { + return codeDocument.Source.Text.GetLinePosition(endIndex).Line; + } + else + { + return codeDocument.Source.Text.Lines.Count; + } + } + + private static void CollectUsingDirectives(RazorCodeDocument codeDocument, ref PooledArrayBuilder directives) + { + var syntaxTreeRoot = codeDocument.GetSyntaxTree().Root; + foreach (var node in syntaxTreeRoot.DescendantNodes()) + { + if (node is RazorDirectiveSyntax directiveNode) + { + foreach (var child in directiveNode.DescendantNodes()) + { + if (child.GetChunkGenerator() is AddImportChunkGenerator { IsStatic: false } usingStatement) + { + directives.Add(new RazorUsingDirective(directiveNode, usingStatement)); + } + } + } + } + } + + private static bool IsNamespaceOrPageDirective(RazorSyntaxNode node) + { + if (node is RazorDirectiveSyntax directiveNode) + { + return directiveNode.DirectiveDescriptor == ComponentPageDirective.Directive || + directiveNode.DirectiveDescriptor == NamespaceDirective.Directive || + directiveNode.DirectiveDescriptor == PageDirective.Directive; + } + + return false; + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/AddUsingsCodeActionProviderFactoryTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/AddUsingsCodeActionProviderFactoryTest.cs deleted file mode 100644 index 5ce053f830..0000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/AddUsingsCodeActionProviderFactoryTest.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Razor.Test.Common; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; - -public class AddUsingsCodeActionProviderFactoryTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) -{ - [Fact] - public void GetNamespaceFromFQN_Invalid_ReturnsEmpty() - { - // Arrange - var fqn = "Abc"; - - // Act - var namespaceName = AddUsingsCodeActionProviderHelper.GetNamespaceFromFQN(fqn); - - // Assert - Assert.Empty(namespaceName); - } - - [Fact] - public void GetNamespaceFromFQN_Valid_ReturnsNamespace() - { - // Arrange - var fqn = "Abc.Xyz"; - - // Act - var namespaceName = AddUsingsCodeActionProviderHelper.GetNamespaceFromFQN(fqn); - - // Assert - Assert.Equal("Abc", namespaceName); - } - - [Fact] - public void TryCreateAddUsingResolutionParams_CreatesResolutionParams() - { - // Arrange - var fqn = "Abc.Xyz"; - var docUri = new Uri("c:/path"); - - // Act - var result = AddUsingsCodeActionProviderHelper.TryCreateAddUsingResolutionParams(fqn, docUri, additionalEdit: null, out var @namespace, out var resolutionParams); - - // Assert - Assert.True(result); - Assert.Equal("Abc", @namespace); - Assert.NotNull(resolutionParams); - } - - [Fact] - public void TryExtractNamespace_Invalid_ReturnsFalse() - { - // Arrange - var csharpAddUsing = "Abc.Xyz;"; - - // Act - var res = AddUsingsCodeActionProviderHelper.TryExtractNamespace(csharpAddUsing, out var @namespace, out var prefix); - - // Assert - Assert.False(res); - Assert.Empty(@namespace); - Assert.Empty(prefix); - } - - [Fact] - public void TryExtractNamespace_ReturnsTrue() - { - // Arrange - var csharpAddUsing = "using Abc.Xyz;"; - - // Act - var res = AddUsingsCodeActionProviderHelper.TryExtractNamespace(csharpAddUsing, out var @namespace, out var prefix); - - // Assert - Assert.True(res); - Assert.Equal("Abc.Xyz", @namespace); - Assert.Empty(prefix); - } - - [Fact] - public void TryExtractNamespace_WithStatic_ReturnsTrue() - { - // Arrange - var csharpAddUsing = "using static X.Y.Z;"; - - // Act - var res = AddUsingsCodeActionProviderHelper.TryExtractNamespace(csharpAddUsing, out var @namespace, out var prefix); - - // Assert - Assert.True(res); - Assert.Equal("static X.Y.Z", @namespace); - Assert.Empty(prefix); - } - - [Fact] - public void TryExtractNamespace_WithTypeNameCorrection_ReturnsTrue() - { - // Arrange - var csharpAddUsing = "Goo - using X.Y.Z;"; - - // Act - var res = AddUsingsCodeActionProviderHelper.TryExtractNamespace(csharpAddUsing, out var @namespace, out var prefix); - - // Assert - Assert.True(res); - Assert.Equal("X.Y.Z", @namespace); - Assert.Equal("Goo - ", prefix); - } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/AddUsingsCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/AddUsingsCodeActionResolverTest.cs index 7d22fc8ee8..2808f79a5f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/AddUsingsCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/AddUsingsCodeActionResolverTest.cs @@ -20,6 +20,48 @@ public class AddUsingsCodeActionResolverTest(ITestOutputHelper testOutput) : Lan { private readonly IDocumentContextFactory _emptyDocumentContextFactory = new TestDocumentContextFactory(); + [Fact] + public void GetNamespaceFromFQN_Invalid_ReturnsEmpty() + { + // Arrange + var fqn = "Abc"; + + // Act + var namespaceName = AddUsingsCodeActionResolver.GetNamespaceFromFQN(fqn); + + // Assert + Assert.Empty(namespaceName); + } + + [Fact] + public void GetNamespaceFromFQN_Valid_ReturnsNamespace() + { + // Arrange + var fqn = "Abc.Xyz"; + + // Act + var namespaceName = AddUsingsCodeActionResolver.GetNamespaceFromFQN(fqn); + + // Assert + Assert.Equal("Abc", namespaceName); + } + + [Fact] + public void TryCreateAddUsingResolutionParams_CreatesResolutionParams() + { + // Arrange + var fqn = "Abc.Xyz"; + var docUri = new Uri("c:/path"); + + // Act + var result = AddUsingsCodeActionResolver.TryCreateAddUsingResolutionParams(fqn, docUri, additionalEdit: null, out var @namespace, out var resolutionParams); + + // Assert + Assert.True(result); + Assert.Equal("Abc", @namespace); + Assert.NotNull(resolutionParams); + } + [Fact] public async Task Handle_MissingFile() { diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Formatting/AddUsingsHelperTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Formatting/AddUsingsHelperTest.cs new file mode 100644 index 0000000000..3f2b206688 --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Formatting/AddUsingsHelperTest.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Test.Common; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Razor.Formatting; + +public class AddUsingsHelperTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) +{ + [Fact] + public void TryExtractNamespace_Invalid_ReturnsFalse() + { + // Arrange + var csharpAddUsing = "Abc.Xyz;"; + + // Act + var res = AddUsingsHelper.TryExtractNamespace(csharpAddUsing, out var @namespace, out var prefix); + + // Assert + Assert.False(res); + Assert.Empty(@namespace); + Assert.Empty(prefix); + } + + [Fact] + public void TryExtractNamespace_ReturnsTrue() + { + // Arrange + var csharpAddUsing = "using Abc.Xyz;"; + + // Act + var res = AddUsingsHelper.TryExtractNamespace(csharpAddUsing, out var @namespace, out var prefix); + + // Assert + Assert.True(res); + Assert.Equal("Abc.Xyz", @namespace); + Assert.Empty(prefix); + } + + [Fact] + public void TryExtractNamespace_WithStatic_ReturnsTrue() + { + // Arrange + var csharpAddUsing = "using static X.Y.Z;"; + + // Act + var res = AddUsingsHelper.TryExtractNamespace(csharpAddUsing, out var @namespace, out var prefix); + + // Assert + Assert.True(res); + Assert.Equal("static X.Y.Z", @namespace); + Assert.Empty(prefix); + } + + [Fact] + public void TryExtractNamespace_WithTypeNameCorrection_ReturnsTrue() + { + // Arrange + var csharpAddUsing = "Goo - using X.Y.Z;"; + + // Act + var res = AddUsingsHelper.TryExtractNamespace(csharpAddUsing, out var @namespace, out var prefix); + + // Assert + Assert.True(res); + Assert.Equal("X.Y.Z", @namespace); + Assert.Equal("Goo - ", prefix); + } +} From 8984db9496ea28e92a061bc560ad430ac7c90716 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 14:29:29 +1000 Subject: [PATCH 02/26] Combine back into one on type formatting pass --- .../IServiceCollectionExtensions.cs | 2 +- .../LspCSharpOnTypeFormattingPass.cs | 39 ------------------- ...sBase.cs => CSharpOnTypeFormattingPass.cs} | 27 +++++++++---- .../RemoteCSharpOnTypeFormattingPass.cs | 27 ------------- .../TestRazorFormattingService.cs | 2 +- 5 files changed, 21 insertions(+), 76 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspCSharpOnTypeFormattingPass.cs rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{CSharpOnTypeFormattingPassBase.cs => CSharpOnTypeFormattingPass.cs} (94%) delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteCSharpOnTypeFormattingPass.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 851e04822a..865632e5da 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -64,7 +64,7 @@ internal static class IServiceCollectionExtensions // Formatting Passes services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspCSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspCSharpOnTypeFormattingPass.cs deleted file mode 100644 index a737698a1d..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspCSharpOnTypeFormattingPass.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Formatting; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; - -internal sealed class LspCSharpOnTypeFormattingPass( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - : CSharpOnTypeFormattingPassBase(documentMappingService, loggerFactory) -{ - - protected override async Task AddUsingStatementEditsIfNecessaryAsync(CodeAnalysis.Razor.Formatting.FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken) - { - if (context.AutomaticallyAddUsings) - { - // Because we need to parse the C# code twice for this operation, lets do a quick check to see if its even necessary - if (textEdits.Any(e => e.NewText.IndexOf("using") != -1)) - { - var usingStatementEdits = await AddUsingsHelper.GetUsingStatementEditsAsync(codeDocument, csharpText, originalTextWithChanges, cancellationToken).ConfigureAwait(false); - finalEdits = [.. usingStatementEdits, .. finalEdits]; - } - } - - return finalEdits; - } -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs similarity index 94% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPassBase.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs index 8e17fba829..d20744517e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs @@ -24,14 +24,12 @@ using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.CodeAnalysis.Razor.Formatting; -using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; - -internal abstract class CSharpOnTypeFormattingPassBase( +internal sealed class CSharpOnTypeFormattingPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) : CSharpFormattingPassBase(documentMappingService) { - private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); + private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { @@ -208,7 +206,20 @@ internal abstract class CSharpOnTypeFormattingPassBase( return new FormattingResult(finalEdits); } - protected abstract Task AddUsingStatementEditsIfNecessaryAsync(FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken); + private static async Task AddUsingStatementEditsIfNecessaryAsync(CodeAnalysis.Razor.Formatting.FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken) + { + if (context.AutomaticallyAddUsings) + { + // Because we need to parse the C# code twice for this operation, lets do a quick check to see if its even necessary + if (textEdits.Any(e => e.NewText.IndexOf("using") != -1)) + { + var usingStatementEdits = await AddUsingsHelper.GetUsingStatementEditsAsync(codeDocument, csharpText, originalTextWithChanges, cancellationToken).ConfigureAwait(false); + finalEdits = [.. usingStatementEdits, .. finalEdits]; + } + } + + return finalEdits; + } // Returns the minimal TextSpan that encompasses all the differences between the old and the new text. private static SourceText ApplyChangesAndTrackChange(SourceText oldText, IEnumerable changes, out TextSpan spanBeforeChange, out TextSpan spanAfterChange) @@ -323,7 +334,7 @@ internal abstract class CSharpOnTypeFormattingPassBase( if (owner is CSharpStatementLiteralSyntax && owner.TryGetPreviousSibling(out var prevNode) && - prevNode.FirstAncestorOrSelf(a => a is CSharpTemplateBlockSyntax) is { } template && + prevNode.FirstAncestorOrSelf(a => a is CSharpTemplateBlockSyntax) is { } template && owner.SpanStart == template.Span.End && IsOnSingleLine(template, text)) { @@ -477,7 +488,7 @@ internal abstract class CSharpOnTypeFormattingPassBase( if (owner is CSharpStatementLiteralSyntax && owner.NextSpan() is { } nextNode && - nextNode.FirstAncestorOrSelf(a => a is CSharpTemplateBlockSyntax) is { } template && + nextNode.FirstAncestorOrSelf(a => a is CSharpTemplateBlockSyntax) is { } template && template.SpanStart == owner.Span.End && IsOnSingleLine(template, text)) { @@ -523,7 +534,7 @@ internal abstract class CSharpOnTypeFormattingPassBase( changes.Add(change); } - private static bool IsOnSingleLine(SyntaxNode node, SourceText text) + private static bool IsOnSingleLine(RazorSyntaxNode node, SourceText text) { var linePositionSpan = text.GetLinePositionSpan(node.Span); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteCSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteCSharpOnTypeFormattingPass.cs deleted file mode 100644 index 6d4e33d0af..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteCSharpOnTypeFormattingPass.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Formatting; -using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; - -internal sealed class RemoteCSharpOnTypeFormattingPass( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - : CSharpOnTypeFormattingPassBase(documentMappingService, loggerFactory) -{ - protected override Task AddUsingStatementEditsIfNecessaryAsync(CodeAnalysis.Razor.Formatting.FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken) - { - Debug.Fail("Implement this when code actions are migrated to cohosting"); - - return Task.FromResult(finalEdits); - } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs index 11e6b4f155..a1e3f585ba 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs @@ -57,7 +57,7 @@ internal static class TestRazorFormattingService { new HtmlFormattingPass(mappingService, client, versionCache, loggerFactory), new CSharpFormattingPass(mappingService, loggerFactory), - new LspCSharpOnTypeFormattingPass(mappingService, loggerFactory), + new CSharpOnTypeFormattingPass(mappingService, loggerFactory), new LspRazorFormattingPass(mappingService, optionsMonitor), new FormattingDiagnosticValidationPass(mappingService, loggerFactory), new FormattingContentValidationPass(mappingService, loggerFactory), From fba3f1e3042e41a4085d54e469240949dec59522 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 14:55:51 +1000 Subject: [PATCH 03/26] Rename some methods --- .../LanguageServer/RazorCSharpFormattingBenchmark.cs | 2 +- .../AutoInsert/OnAutoInsertEndpoint.cs | 4 ++-- .../CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs | 2 +- .../CodeActions/Html/DefaultHtmlCodeActionProvider.cs | 2 +- .../CodeActions/Razor/GenerateMethodCodeActionResolver.cs | 2 +- .../Delegation/DelegatedCompletionItemResolver.cs | 4 ++-- .../Formatting/DocumentFormattingEndpoint.cs | 2 +- .../Formatting/DocumentOnTypeFormattingEndpoint.cs | 2 +- .../Formatting/DocumentRangeFormattingEndpoint.cs | 2 +- .../Formatting/HtmlFormatter.cs | 2 +- .../WrapWithTag/WrapWithTagEndpoint.cs | 2 +- .../Formatting/IRazorFormattingService.cs | 8 ++++---- .../Formatting/RazorFormattingService.cs | 8 ++++---- .../CSharp/DefaultCSharpCodeActionResolverTest.cs | 2 +- .../Formatting_NetFx/FormattingLanguageServerTestBase.cs | 8 ++++---- .../Formatting_NetFx/FormattingTestBase.cs | 6 +++--- .../WrapWithTag/WrapWithTagEndpointTests.cs | 6 +++--- 17 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs index 2d965e9ce5..8c08ab2b48 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs @@ -118,7 +118,7 @@ public class RazorCSharpFormattingBenchmark : RazorLanguageServerBenchmarkBase var documentContext = new VersionedDocumentContext(DocumentUri, DocumentSnapshot, projectContext: null, version: 1); - var edits = await RazorFormattingService.FormatAsync(documentContext, range: null, options, CancellationToken.None); + var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, range: null, options, CancellationToken.None); #if DEBUG // For debugging purposes only. 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 cbcef4ece9..43f4e4fa6d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs @@ -206,8 +206,8 @@ internal class OnAutoInsertEndpoint( var edits = new[] { delegatedResponse.TextEdit }; var mappedEdits = delegatedResponse.TextEditFormat == InsertTextFormat.Snippet - ? await _razorFormattingService.FormatSnippetAsync(documentContext, positionInfo.LanguageKind, edits, originalRequest.Options, cancellationToken).ConfigureAwait(false) - : await _razorFormattingService.FormatOnTypeAsync(documentContext, positionInfo.LanguageKind, edits, originalRequest.Options, hostDocumentIndex: 0, triggerCharacter: '\0', cancellationToken).ConfigureAwait(false); + ? await _razorFormattingService.GetSnippetFormattingEditsAsync(documentContext, positionInfo.LanguageKind, edits, originalRequest.Options, cancellationToken).ConfigureAwait(false) + : await _razorFormattingService.GetOnTypeFormattingEditsAsync(documentContext, positionInfo.LanguageKind, edits, originalRequest.Options, hostDocumentIndex: 0, triggerCharacter: '\0', cancellationToken).ConfigureAwait(false); if (mappedEdits is not [{ } edit]) { return null; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs index 8c44e462dc..7a59a497c3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs @@ -80,7 +80,7 @@ internal sealed class DefaultCSharpCodeActionResolver( // Remaps the text edits from the generated C# to the razor file, // as well as applying appropriate formatting. - var formattedEdits = await _razorFormattingService.FormatCodeActionAsync( + var formattedEdits = await _razorFormattingService.GetCodeActionEditsAsync( documentContext, RazorLanguageKind.CSharp, csharpTextEdits, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs index 1ce1cba0a8..b5d22fabb6 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs @@ -54,7 +54,7 @@ internal sealed class DefaultHtmlCodeActionProvider(IEditMappingService editMapp foreach (var edit in documentEdits) { - edit.Edits = HtmlFormatter.FixHtmlTestEdits(htmlSourceText, edit.Edits); + edit.Edits = HtmlFormatter.FixHtmlTextEdits(htmlSourceText, edit.Edits); } codeAction.Edit = new WorkspaceEdit diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs index 34a71cd61b..bc6a10fc10 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs @@ -210,7 +210,7 @@ internal sealed class GenerateMethodCodeActionResolver( InsertSpaces = _razorLSPOptionsMonitor.CurrentValue.InsertSpaces, }; - var formattedEdits = await _razorFormattingService.FormatCodeActionAsync( + var formattedEdits = await _razorFormattingService.GetCodeActionEditsAsync( documentContext, RazorLanguageKind.CSharp, result, 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 4f3ae959b2..fdfcc2c0b7 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 @@ -122,7 +122,7 @@ internal class DelegatedCompletionItemResolver : CompletionItemResolver { if (resolvedCompletionItem.TextEdit.Value.TryGetFirst(out var textEdit)) { - var formattedTextEdit = await _formattingService.FormatSnippetAsync( + var formattedTextEdit = await _formattingService.GetSnippetFormattingEditsAsync( documentContext, RazorLanguageKind.CSharp, new[] { textEdit }, @@ -141,7 +141,7 @@ internal class DelegatedCompletionItemResolver : CompletionItemResolver if (resolvedCompletionItem.AdditionalTextEdits is not null) { - var formattedTextEdits = await _formattingService.FormatSnippetAsync( + var formattedTextEdits = await _formattingService.GetSnippetFormattingEditsAsync( documentContext, RazorLanguageKind.CSharp, resolvedCompletionItem.AdditionalTextEdits, 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 d25fff093b..f290028d35 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs @@ -56,7 +56,7 @@ internal class DocumentFormattingEndpoint : IRazorRequestHandler 0); - var formattedEdits = await _razorFormattingService.FormatOnTypeAsync(documentContext, triggerCharacterKind, Array.Empty(), request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); + var formattedEdits = await _razorFormattingService.GetOnTypeFormattingEditsAsync(documentContext, triggerCharacterKind, Array.Empty(), request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); if (formattedEdits.Length == 0) { _logger.LogInformation($"No formatting changes were necessary"); 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 4fa0207619..aedd7d1edf 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs @@ -56,7 +56,7 @@ internal class DocumentRangeFormattingEndpoint : IRazorRequestHandler // Internal for testing - public static TextEdit[] FixHtmlTestEdits(SourceText htmlSourceText, TextEdit[] edits) + public static TextEdit[] FixHtmlTextEdits(SourceText htmlSourceText, TextEdit[] edits) { // Avoid computing a minimal diff if we don't need to if (!edits.Any(e => e.NewText.Contains("~"))) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs index 990dbc07a6..743b84d60b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs @@ -116,7 +116,7 @@ internal class WrapWithTagEndpoint( if (htmlResponse.TextEdits is not null) { var htmlSourceText = await documentContext.GetHtmlSourceTextAsync(cancellationToken).ConfigureAwait(false); - htmlResponse.TextEdits = HtmlFormatter.FixHtmlTestEdits(htmlSourceText, htmlResponse.TextEdits); + htmlResponse.TextEdits = HtmlFormatter.FixHtmlTextEdits(htmlSourceText, htmlResponse.TextEdits); } return htmlResponse; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs index a5001dddd0..afca7fb228 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs @@ -11,13 +11,13 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal interface IRazorFormattingService { - Task FormatAsync( + Task GetDocumentFormattingEditsAsync( VersionedDocumentContext documentContext, Range? range, FormattingOptions options, CancellationToken cancellationToken); - Task FormatOnTypeAsync( + Task GetOnTypeFormattingEditsAsync( DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, @@ -26,14 +26,14 @@ internal interface IRazorFormattingService char triggerCharacter, CancellationToken cancellationToken); - Task FormatCodeActionAsync( + Task GetCodeActionEditsAsync( DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken); - Task FormatSnippetAsync( + Task GetSnippetFormattingEditsAsync( DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] edits, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 1b14b3fa01..4a638abfc0 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -36,7 +36,7 @@ internal class RazorFormattingService : IRazorFormattingService _workspaceFactory = workspaceFactory ?? throw new ArgumentNullException(nameof(workspaceFactory)); } - public async Task FormatAsync( + public async Task GetDocumentFormattingEditsAsync( VersionedDocumentContext documentContext, Range? range, FormattingOptions options, @@ -104,13 +104,13 @@ internal class RazorFormattingService : IRazorFormattingService return finalEdits; } - public Task FormatOnTypeAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + public Task GetOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) => ApplyFormattedEditsAsync(documentContext, kind, formattedEdits, options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); - public Task FormatCodeActionAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + public Task GetCodeActionEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) => ApplyFormattedEditsAsync(documentContext, kind, formattedEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: true, collapseEdits: false, automaticallyAddUsings: true, cancellationToken: cancellationToken); - public async Task FormatSnippetAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken) + public async Task GetSnippetFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken) { if (kind == RazorLanguageKind.CSharp) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs index 8ee8500337..b29742538c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs @@ -188,7 +188,7 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) : private static IRazorFormattingService CreateRazorFormattingService(Uri documentUri) { var razorFormattingService = Mock.Of( - rfs => rfs.FormatCodeActionAsync( + rfs => rfs.GetCodeActionEditsAsync( It.Is(c => c.Uri == documentUri), RazorLanguageKind.CSharp, It.IsAny(), diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs index 54ae3aa7ff..d3d2a80cc5 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs @@ -38,23 +38,23 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut { public bool Called { get; private set; } - public Task FormatAsync(VersionedDocumentContext documentContext, Range? range, FormattingOptions options, CancellationToken cancellationToken) + public Task GetDocumentFormattingEditsAsync(VersionedDocumentContext documentContext, Range? range, FormattingOptions options, CancellationToken cancellationToken) { Called = true; return SpecializedTasks.EmptyArray(); } - public Task FormatCodeActionAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + public Task GetCodeActionEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) { return Task.FromResult(formattedEdits); } - public Task FormatOnTypeAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + public Task GetOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) { return Task.FromResult(formattedEdits); } - public Task FormatSnippetAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + public Task GetSnippetFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) { return Task.FromResult(formattedEdits); } 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 497b2b175d..a503985815 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 @@ -81,7 +81,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); // Act - var edits = await formattingService.FormatAsync(documentContext, range, options, DisposalToken); + var edits = await formattingService.GetDocumentFormattingEditsAsync(documentContext, range, options, DisposalToken); // Assert var edited = ApplyEdits(source, edits); @@ -131,7 +131,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); // Act - var edits = await formattingService.FormatOnTypeAsync(documentContext, languageKind, Array.Empty(), options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); + var edits = await formattingService.GetOnTypeFormattingEditsAsync(documentContext, languageKind, Array.Empty(), options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); // Assert var edited = ApplyEdits(razorSourceText, edits); @@ -199,7 +199,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); // Act - var edits = await formattingService.FormatCodeActionAsync(documentContext, languageKind, codeActionEdits, options, DisposalToken); + var edits = await formattingService.GetCodeActionEditsAsync(documentContext, languageKind, codeActionEdits, options, DisposalToken); // Assert var edited = ApplyEdits(razorSourceText, edits); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs index 80a563b38d..b7c5955ea8 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs @@ -282,7 +282,7 @@ public class WrapWithTagEndpointTest(ITestOutputHelper testOutput) : LanguageSer }; var htmlSourceText = await context!.GetHtmlSourceTextAsync(DisposalToken); - var edits = HtmlFormatter.FixHtmlTestEdits(htmlSourceText, computedEdits); + var edits = HtmlFormatter.FixHtmlTextEdits(htmlSourceText, computedEdits); Assert.Same(computedEdits, edits); var finalText = inputSourceText.WithChanges(edits.Select(inputSourceText.GetTextChange)); @@ -322,7 +322,7 @@ public class WrapWithTagEndpointTest(ITestOutputHelper testOutput) : LanguageSer }; var htmlSourceText = await context!.GetHtmlSourceTextAsync(DisposalToken); - var edits = HtmlFormatter.FixHtmlTestEdits(htmlSourceText, computedEdits); + var edits = HtmlFormatter.FixHtmlTextEdits(htmlSourceText, computedEdits); Assert.NotSame(computedEdits, edits); var finalText = inputSourceText.WithChanges(edits.Select(inputSourceText.GetTextChange)); @@ -362,7 +362,7 @@ public class WrapWithTagEndpointTest(ITestOutputHelper testOutput) : LanguageSer }; var htmlSourceText = await context.GetHtmlSourceTextAsync(DisposalToken); - var edits = HtmlFormatter.FixHtmlTestEdits(htmlSourceText, computedEdits); + var edits = HtmlFormatter.FixHtmlTextEdits(htmlSourceText, computedEdits); Assert.NotSame(computedEdits, edits); var finalText = inputSourceText.WithChanges(edits.Select(inputSourceText.GetTextChange)); From 2c39c207c74f1608b9bff7adabefbfe19ea138fb Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 16:01:18 +1000 Subject: [PATCH 04/26] Random bits of cleanup before some bigger moves --- .../Formatting/DocumentFormattingEndpoint.cs | 16 ++-- .../Formatting/HtmlFormatter.cs | 39 +++------- .../Extensions/SourceTextExtensions.cs | 21 +++++ .../Formatting/CSharpOnTypeFormattingPass.cs | 14 +--- .../Formatting/RazorFormattingService.cs | 77 +++++-------------- 5 files changed, 56 insertions(+), 111 deletions(-) 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 f290028d35..7887eb319d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs @@ -12,18 +12,12 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; [RazorLanguageServerEndpoint(Methods.TextDocumentFormattingName)] -internal class DocumentFormattingEndpoint : IRazorRequestHandler, ICapabilitiesProvider +internal class DocumentFormattingEndpoint( + IRazorFormattingService razorFormattingService, + RazorLSPOptionsMonitor optionsMonitor) : IRazorRequestHandler, ICapabilitiesProvider { - private readonly IRazorFormattingService _razorFormattingService; - private readonly RazorLSPOptionsMonitor _optionsMonitor; - - public DocumentFormattingEndpoint( - IRazorFormattingService razorFormattingService, - RazorLSPOptionsMonitor optionsMonitor) - { - _razorFormattingService = razorFormattingService ?? throw new ArgumentNullException(nameof(razorFormattingService)); - _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); - } + private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; + private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; public bool MutatesSolutionState => false; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs index 5f5206fd8b..224ae300c6 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs @@ -15,31 +15,20 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -internal class HtmlFormatter +internal sealed class HtmlFormatter( + IClientConnection clientConnection, + IDocumentVersionCache documentVersionCache) { - private readonly IDocumentVersionCache _documentVersionCache; - private readonly IClientConnection _clientConnection; - - public HtmlFormatter( - IClientConnection clientConnection, - IDocumentVersionCache documentVersionCache) - { - _clientConnection = clientConnection; - _documentVersionCache = documentVersionCache; - } + private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache; + private readonly IClientConnection _clientConnection = clientConnection; public async Task FormatAsync( FormattingContext context, CancellationToken cancellationToken) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - if (!_documentVersionCache.TryGetDocumentVersion(context.OriginalSnapshot, out var documentVersion)) { - return Array.Empty(); + return []; } var @params = new RazorDocumentFormattingParams() @@ -57,7 +46,7 @@ internal class HtmlFormatter @params, cancellationToken).ConfigureAwait(false); - return result?.Edits ?? Array.Empty(); + return result?.Edits ?? []; } public async Task FormatOnTypeAsync( @@ -66,7 +55,7 @@ internal class HtmlFormatter { if (!_documentVersionCache.TryGetDocumentVersion(context.OriginalSnapshot, out var documentVersion)) { - return Array.Empty(); + return []; } var @params = new RazorDocumentOnTypeFormattingParams() @@ -83,7 +72,7 @@ internal class HtmlFormatter @params, cancellationToken).ConfigureAwait(false); - return result?.Edits ?? Array.Empty(); + return result?.Edits ?? []; } /// @@ -98,14 +87,6 @@ internal class HtmlFormatter if (!edits.Any(e => e.NewText.Contains("~"))) return edits; - // First we apply the edits that the Html language server wanted, to the Html document - var textChanges = edits.Select(htmlSourceText.GetTextChange); - var changedText = htmlSourceText.WithChanges(textChanges); - - // Now we use our minimal text differ algorithm to get the bare minimum of edits - var minimalChanges = SourceTextDiffer.GetMinimalTextChanges(htmlSourceText, changedText, DiffKind.Char); - var minimalEdits = minimalChanges.Select(htmlSourceText.GetTextEdit).ToArray(); - - return minimalEdits; + return htmlSourceText.NormalizeTextEdits(edits); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SourceTextExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SourceTextExtensions.cs index 6d72290f11..cf8b62aa8c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SourceTextExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SourceTextExtensions.cs @@ -3,9 +3,12 @@ using System; using System.Buffers; +using System.Linq; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.TextDifferencing; using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Text; @@ -269,4 +272,22 @@ internal static class SourceTextExtensions location = default; return false; } + + public static TextEdit[] NormalizeTextEdits(this SourceText text, TextEdit[] edits) + => NormalizeTextEdits(text, edits, out _); + + public static TextEdit[] NormalizeTextEdits(this SourceText text, TextEdit[] edits, out SourceText originalTextWithChanges) + { + var changes = edits.Select(text.GetTextChange); + originalTextWithChanges = text.WithChanges(changes); + + if (text.ContentEquals(originalTextWithChanges)) + { + return []; + } + + var cleanChanges = SourceTextDiffer.GetMinimalTextChanges(text, originalTextWithChanges, DiffKind.Char); + var cleanEdits = cleanChanges.Select(text.GetTextEdit).ToArray(); + return cleanEdits; + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs index d20744517e..1d96525057 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.AspNetCore.Razor.TextDifferencing; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; @@ -91,7 +90,7 @@ internal sealed class CSharpOnTypeFormattingPass( } } - var normalizedEdits = NormalizeTextEdits(csharpText, textEdits, out var originalTextWithChanges); + var normalizedEdits = csharpText.NormalizeTextEdits(textEdits, out var originalTextWithChanges); var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits, result.Kind); var filteredEdits = FilterCSharpTextEdits(context, mappedEdits); if (filteredEdits.Length == 0) @@ -206,7 +205,7 @@ internal sealed class CSharpOnTypeFormattingPass( return new FormattingResult(finalEdits); } - private static async Task AddUsingStatementEditsIfNecessaryAsync(CodeAnalysis.Razor.Formatting.FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken) + private static async Task AddUsingStatementEditsIfNecessaryAsync(FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken) { if (context.AutomaticallyAddUsings) { @@ -540,13 +539,4 @@ internal sealed class CSharpOnTypeFormattingPass( return linePositionSpan.Start.Line == linePositionSpan.End.Line; } - - private static TextEdit[] NormalizeTextEdits(SourceText originalText, TextEdit[] edits, out SourceText originalTextWithChanges) - { - var changes = edits.Select(originalText.GetTextChange); - originalTextWithChanges = originalText.WithChanges(changes); - var cleanChanges = SourceTextDiffer.GetMinimalTextChanges(originalText, originalTextWithChanges, DiffKind.Char); - var cleanEdits = cleanChanges.Select(originalText.GetTextEdit).ToArray(); - return cleanEdits; - } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 4a638abfc0..5db87f7a9c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -1,13 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.TextDifferencing; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; @@ -18,23 +17,12 @@ using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal class RazorFormattingService : IRazorFormattingService +internal class RazorFormattingService( + IEnumerable formattingPasses, + IAdhocWorkspaceFactory workspaceFactory) : IRazorFormattingService { - private readonly List _formattingPasses; - private readonly IAdhocWorkspaceFactory _workspaceFactory; - - public RazorFormattingService( - IEnumerable formattingPasses, - IAdhocWorkspaceFactory workspaceFactory) - { - if (formattingPasses is null) - { - throw new ArgumentNullException(nameof(formattingPasses)); - } - - _formattingPasses = formattingPasses.OrderBy(f => f.Order).ToList(); - _workspaceFactory = workspaceFactory ?? throw new ArgumentNullException(nameof(workspaceFactory)); - } + private readonly ImmutableArray _formattingPasses = formattingPasses.OrderByAsArray(f => f.Order); + private readonly IAdhocWorkspaceFactory _workspaceFactory = workspaceFactory; public async Task GetDocumentFormattingEditsAsync( VersionedDocumentContext documentContext, @@ -82,26 +70,9 @@ internal class RazorFormattingService : IRazorFormattingService var filteredEdits = range is null ? result.Edits - : result.Edits.Where(e => range.LineOverlapsWith(e.Range)); + : result.Edits.Where(e => range.LineOverlapsWith(e.Range)).ToArray(); - return GetMinimalEdits(originalText, filteredEdits); - } - - private static TextEdit[] GetMinimalEdits(SourceText originalText, IEnumerable filteredEdits) - { - // Make sure the edits actually change something, or its not worth responding - var textChanges = filteredEdits.Select(originalText.GetTextChange); - var changedText = originalText.WithChanges(textChanges); - if (changedText.ContentEquals(originalText)) - { - return Array.Empty(); - } - - // Only send back the minimum edits - var minimalChanges = SourceTextDiffer.GetMinimalTextChanges(originalText, changedText, DiffKind.Char); - var finalEdits = minimalChanges.Select(originalText.GetTextEdit).ToArray(); - - return finalEdits; + return originalText.NormalizeTextEdits(filteredEdits); } public Task GetOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) @@ -171,7 +142,7 @@ internal class RazorFormattingService : IRazorFormattingService } var originalText = context.SourceText; - var edits = GetMinimalEdits(originalText, result.Edits); + var edits = originalText.NormalizeTextEdits(result.Edits); if (collapseEdits) { @@ -179,10 +150,10 @@ internal class RazorFormattingService : IRazorFormattingService if (collapsedEdit.NewText.Length == 0 && collapsedEdit.Range.IsZeroWidth()) { - return Array.Empty(); + return []; } - return new[] { collapsedEdit }; + return [collapsedEdit]; } return edits; @@ -196,14 +167,7 @@ internal class RazorFormattingService : IRazorFormattingService return edits[0]; } - var textChanges = new List(); - foreach (var edit in edits) - { - var change = new TextChange(sourceText.GetTextSpan(edit.Range), edit.NewText); - textChanges.Add(change); - } - - var changedText = sourceText.WithChanges(textChanges); + var changedText = sourceText.WithChanges(edits.Select(sourceText.GetTextChange)); var affectedRange = changedText.GetEncompassingTextChangeRange(sourceText); var spanBeforeChange = affectedRange.Span; var spanAfterChange = new TextSpan(spanBeforeChange.Start, affectedRange.NewLength); @@ -218,26 +182,21 @@ internal class RazorFormattingService : IRazorFormattingService { // Currently this method only supports wrapping `$0`, any additional markers aren't formatted properly. - for (var i = 0; i < snippetEdits.Length; i++) + foreach (var snippetEdit in snippetEdits) { - var snippetEdit = snippetEdits[i]; - // Formatting doesn't work with syntax errors caused by the cursor marker ($0). // So, let's avoid the error by wrapping the cursor marker in a comment. - var wrappedText = snippetEdit.NewText.Replace("$0", "/*$0*/"); - snippetEdit.NewText = wrappedText; + snippetEdit.NewText = snippetEdit.NewText.Replace("$0", "/*$0*/"); } } private static void UnwrapCSharpSnippets(TextEdit[] snippetEdits) { - for (var i = 0; i < snippetEdits.Length; i++) + foreach (var snippetEdit in snippetEdits) { - var snippetEdit = snippetEdits[i]; - - // Unwrap the cursor marker. - var unwrappedText = snippetEdit.NewText.Replace("/*$0*/", "$0"); - snippetEdit.NewText = unwrappedText; + // Formatting doesn't work with syntax errors caused by the cursor marker ($0). + // So, let's avoid the error by wrapping the cursor marker in a comment. + snippetEdit.NewText = snippetEdit.NewText.Replace("/*$0*/", "$0"); } } } From e45f09468d5377aec2bd99d868cb877c8355687d Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 16:25:41 +1000 Subject: [PATCH 05/26] Make it more explicit that we only do code action formatting for C# --- .../CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs | 3 +-- .../CodeActions/Razor/GenerateMethodCodeActionResolver.cs | 3 +-- .../Formatting/IRazorFormattingService.cs | 3 +-- .../Formatting/RazorFormattingService.cs | 4 ++-- .../CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs | 3 +-- .../Formatting_NetFx/FormattingLanguageServerTestBase.cs | 2 +- 6 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs index 7a59a497c3..ec5b35b595 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs @@ -80,9 +80,8 @@ internal sealed class DefaultCSharpCodeActionResolver( // Remaps the text edits from the generated C# to the razor file, // as well as applying appropriate formatting. - var formattedEdits = await _razorFormattingService.GetCodeActionEditsAsync( + var formattedEdits = await _razorFormattingService.GetCSharpCodeActionEditsAsync( documentContext, - RazorLanguageKind.CSharp, csharpTextEdits, s_defaultFormattingOptions, cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs index bc6a10fc10..918e13fe06 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs @@ -210,9 +210,8 @@ internal sealed class GenerateMethodCodeActionResolver( InsertSpaces = _razorLSPOptionsMonitor.CurrentValue.InsertSpaces, }; - var formattedEdits = await _razorFormattingService.GetCodeActionEditsAsync( + var formattedEdits = await _razorFormattingService.GetCSharpCodeActionEditsAsync( documentContext, - RazorLanguageKind.CSharp, result, formattingOptions, cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs index afca7fb228..0c49c56f28 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs @@ -26,9 +26,8 @@ internal interface IRazorFormattingService char triggerCharacter, CancellationToken cancellationToken); - Task GetCodeActionEditsAsync( + Task GetCSharpCodeActionEditsAsync( DocumentContext documentContext, - RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 5db87f7a9c..5e5420010b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -78,8 +78,8 @@ internal class RazorFormattingService( public Task GetOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) => ApplyFormattedEditsAsync(documentContext, kind, formattedEdits, options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); - public Task GetCodeActionEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) - => ApplyFormattedEditsAsync(documentContext, kind, formattedEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: true, collapseEdits: false, automaticallyAddUsings: true, cancellationToken: cancellationToken); + public Task GetCSharpCodeActionEditsAsync(DocumentContext documentContext, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + => ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, formattedEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: true, collapseEdits: false, automaticallyAddUsings: true, cancellationToken: cancellationToken); public async Task GetSnippetFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs index b29742538c..fbaadf3db8 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs @@ -188,9 +188,8 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) : private static IRazorFormattingService CreateRazorFormattingService(Uri documentUri) { var razorFormattingService = Mock.Of( - rfs => rfs.GetCodeActionEditsAsync( + rfs => rfs.GetCSharpCodeActionEditsAsync( It.Is(c => c.Uri == documentUri), - RazorLanguageKind.CSharp, It.IsAny(), It.IsAny(), It.IsAny()) == Task.FromResult(s_defaultFormattedEdits), MockBehavior.Strict); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs index d3d2a80cc5..9f00c0c549 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs @@ -44,7 +44,7 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut return SpecializedTasks.EmptyArray(); } - public Task GetCodeActionEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + public Task GetCSharpCodeActionEditsAsync(DocumentContext documentContext, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) { return Task.FromResult(formattedEdits); } From a7027160acadff26eaf1f74f848d60f29efc3f52 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 17:06:55 +1000 Subject: [PATCH 06/26] Be SUPER specific about our APIs, what they accept, and what they return This probably looks like overkill, but hopefully the next commit will help explain a little about what is going on. --- .../RazorCSharpFormattingBenchmark.cs | 2 +- .../AutoInsert/OnAutoInsertEndpoint.cs | 13 ++++--- .../CSharp/DefaultCSharpCodeActionResolver.cs | 4 +- .../Razor/GenerateMethodCodeActionResolver.cs | 4 +- .../DelegatedCompletionItemResolver.cs | 33 ++++++---------- .../Formatting/DocumentFormattingEndpoint.cs | 5 ++- .../DocumentOnTypeFormattingEndpoint.cs | 18 ++++++++- .../DocumentRangeFormattingEndpoint.cs | 5 ++- .../Formatting/IRazorFormattingService.cs | 25 ++++++++---- .../Formatting/RazorFormattingService.cs | 39 +++++++++++-------- .../DefaultCSharpCodeActionResolverTest.cs | 8 ++-- .../FormattingLanguageServerTestBase.cs | 25 ++++++++---- .../Formatting_NetFx/FormattingTestBase.cs | 21 ++++++++-- 13 files changed, 128 insertions(+), 74 deletions(-) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs index 8c08ab2b48..2c74f91bc7 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs @@ -118,7 +118,7 @@ public class RazorCSharpFormattingBenchmark : RazorLanguageServerBenchmarkBase var documentContext = new VersionedDocumentContext(DocumentUri, DocumentSnapshot, projectContext: null, version: 1); - var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, range: null, options, CancellationToken.None); + var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, [], range: null, options, CancellationToken.None); #if DEBUG // For debugging purposes only. 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 43f4e4fa6d..4ec4479463 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -203,19 +204,19 @@ internal class OnAutoInsertEndpoint( } // For C# we run the edit through our formatting engine - var edits = new[] { delegatedResponse.TextEdit }; + Debug.Assert(positionInfo.LanguageKind == RazorLanguageKind.CSharp); - var mappedEdits = delegatedResponse.TextEditFormat == InsertTextFormat.Snippet - ? await _razorFormattingService.GetSnippetFormattingEditsAsync(documentContext, positionInfo.LanguageKind, edits, originalRequest.Options, cancellationToken).ConfigureAwait(false) - : await _razorFormattingService.GetOnTypeFormattingEditsAsync(documentContext, positionInfo.LanguageKind, edits, originalRequest.Options, hostDocumentIndex: 0, triggerCharacter: '\0', cancellationToken).ConfigureAwait(false); - if (mappedEdits is not [{ } edit]) + var mappedEdit = delegatedResponse.TextEditFormat == InsertTextFormat.Snippet + ? await _razorFormattingService.GetCSharpSnippetFormattingEditAsync(documentContext, [delegatedResponse.TextEdit], originalRequest.Options, cancellationToken).ConfigureAwait(false) + : await _razorFormattingService.GetSingleCSharpEditAsync(documentContext, delegatedResponse.TextEdit, originalRequest.Options, cancellationToken).ConfigureAwait(false); + if (mappedEdit is null) { return null; } return new VSInternalDocumentOnAutoInsertResponseItem() { - TextEdit = edit, + TextEdit = mappedEdit, TextEditFormat = delegatedResponse.TextEditFormat, }; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs index ec5b35b595..5dca63c913 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs @@ -80,7 +80,7 @@ internal sealed class DefaultCSharpCodeActionResolver( // Remaps the text edits from the generated C# to the razor file, // as well as applying appropriate formatting. - var formattedEdits = await _razorFormattingService.GetCSharpCodeActionEditsAsync( + var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync( documentContext, csharpTextEdits, s_defaultFormattingOptions, @@ -101,7 +101,7 @@ internal sealed class DefaultCSharpCodeActionResolver( new TextDocumentEdit() { TextDocument = codeDocumentIdentifier, - Edits = formattedEdits, + Edits = formattedEdit is null ? [] : [formattedEdit], } } }; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs index 918e13fe06..15a36e487a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs @@ -210,13 +210,13 @@ internal sealed class GenerateMethodCodeActionResolver( InsertSpaces = _razorLSPOptionsMonitor.CurrentValue.InsertSpaces, }; - var formattedEdits = await _razorFormattingService.GetCSharpCodeActionEditsAsync( + var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync( documentContext, result, formattingOptions, cancellationToken).ConfigureAwait(false); - edits = formattedEdits; + edits = formattedEdit is null ? [] : [formattedEdit]; } } 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 fdfcc2c0b7..4b4644be46 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 @@ -15,21 +15,14 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; -internal class DelegatedCompletionItemResolver : CompletionItemResolver +internal class DelegatedCompletionItemResolver( + IDocumentContextFactory documentContextFactory, + IRazorFormattingService formattingService, + IClientConnection clientConnection) : CompletionItemResolver { - private readonly IDocumentContextFactory _documentContextFactory; - private readonly IRazorFormattingService _formattingService; - private readonly IClientConnection _clientConnection; - - public DelegatedCompletionItemResolver( - IDocumentContextFactory documentContextFactory, - IRazorFormattingService formattingService, - IClientConnection clientConnection) - { - _documentContextFactory = documentContextFactory; - _formattingService = formattingService; - _clientConnection = clientConnection; - } + private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; + private readonly IRazorFormattingService _formattingService = formattingService; + private readonly IClientConnection _clientConnection = clientConnection; public override async Task ResolveAsync( VSInternalCompletionItem item, @@ -122,14 +115,13 @@ internal class DelegatedCompletionItemResolver : CompletionItemResolver { if (resolvedCompletionItem.TextEdit.Value.TryGetFirst(out var textEdit)) { - var formattedTextEdit = await _formattingService.GetSnippetFormattingEditsAsync( + var formattedTextEdit = await _formattingService.GetCSharpSnippetFormattingEditAsync( documentContext, - RazorLanguageKind.CSharp, - new[] { textEdit }, + [textEdit], formattingOptions, cancellationToken).ConfigureAwait(false); - resolvedCompletionItem.TextEdit = formattedTextEdit.FirstOrDefault(); + resolvedCompletionItem.TextEdit = formattedTextEdit; } else { @@ -141,14 +133,13 @@ internal class DelegatedCompletionItemResolver : CompletionItemResolver if (resolvedCompletionItem.AdditionalTextEdits is not null) { - var formattedTextEdits = await _formattingService.GetSnippetFormattingEditsAsync( + var formattedTextEdit = await _formattingService.GetCSharpSnippetFormattingEditAsync( documentContext, - RazorLanguageKind.CSharp, resolvedCompletionItem.AdditionalTextEdits, formattingOptions, cancellationToken).ConfigureAwait(false); - resolvedCompletionItem.AdditionalTextEdits = formattedTextEdits; + resolvedCompletionItem.AdditionalTextEdits = formattedTextEdit is null ? null : [formattedTextEdit]; } return resolvedCompletionItem; 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 7887eb319d..92b146fb50 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs @@ -50,7 +50,10 @@ internal class DocumentFormattingEndpoint( return null; } - var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, range: null, request.Options, cancellationToken).ConfigureAwait(false); + // TODO: In the next commit, get the Html edits from the Html formatter + var htmlEdits = Array.Empty(); + + var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range: null, request.Options, cancellationToken).ConfigureAwait(false); return edits; } } 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 0a8a1d3c3b..0f88d469b9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -114,7 +114,23 @@ internal class DocumentOnTypeFormattingEndpoint( Debug.Assert(request.Character.Length > 0); - var formattedEdits = await _razorFormattingService.GetOnTypeFormattingEditsAsync(documentContext, triggerCharacterKind, Array.Empty(), request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); + TextEdit[] formattedEdits; + if (triggerCharacterKind == RazorLanguageKind.CSharp) + { + formattedEdits = await _razorFormattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); + } + else if (triggerCharacterKind == RazorLanguageKind.Html) + { + // TODO: In the next commit, get the Html edits from the Html formatter + var htmlEdits = Array.Empty(); + formattedEdits = await _razorFormattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); + } + else + { + Assumed.Unreachable(); + return null; + } + if (formattedEdits.Length == 0) { _logger.LogInformation($"No formatting changes were necessary"); 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 aedd7d1edf..48ead55198 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs @@ -56,7 +56,10 @@ internal class DocumentRangeFormattingEndpoint : IRazorRequestHandler(); + + var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, request.Range, request.Options, cancellationToken).ConfigureAwait(false); return edits; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs index 0c49c56f28..54bfba4f88 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; @@ -13,28 +12,40 @@ internal interface IRazorFormattingService { Task GetDocumentFormattingEditsAsync( VersionedDocumentContext documentContext, + TextEdit[] htmlEdits, Range? range, FormattingOptions options, CancellationToken cancellationToken); - Task GetOnTypeFormattingEditsAsync( + Task GetHtmlOnTypeFormattingEditsAsync( DocumentContext documentContext, - RazorLanguageKind kind, - TextEdit[] formattedEdits, + TextEdit[] htmlEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken); - Task GetCSharpCodeActionEditsAsync( + Task GetCSharpOnTypeFormattingEditsAsync( + DocumentContext documentContext, + FormattingOptions options, + int hostDocumentIndex, + char triggerCharacter, + CancellationToken cancellationToken); + + Task GetSingleCSharpEditAsync( + DocumentContext documentContext, + TextEdit initialEdit, + FormattingOptions options, + CancellationToken cancellationToken); + + Task GetCSharpCodeActionEditAsync( DocumentContext documentContext, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken); - Task GetSnippetFormattingEditsAsync( + Task GetCSharpSnippetFormattingEditAsync( DocumentContext documentContext, - RazorLanguageKind kind, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 5e5420010b..eedc6f4989 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -26,6 +26,7 @@ internal class RazorFormattingService( public async Task GetDocumentFormattingEditsAsync( VersionedDocumentContext documentContext, + TextEdit[] htmlEdits, Range? range, FormattingOptions options, CancellationToken cancellationToken) @@ -61,7 +62,7 @@ internal class RazorFormattingService( using var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, _workspaceFactory); var originalText = context.SourceText; - var result = new FormattingResult([]); + var result = new FormattingResult(htmlEdits); foreach (var pass in _formattingPasses) { cancellationToken.ThrowIfCancellationRequested(); @@ -75,22 +76,31 @@ internal class RazorFormattingService( return originalText.NormalizeTextEdits(filteredEdits); } - public Task GetOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) - => ApplyFormattedEditsAsync(documentContext, kind, formattedEdits, options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); + public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + => ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, [], options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); - public Task GetCSharpCodeActionEditsAsync(DocumentContext documentContext, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) - => ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, formattedEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: true, collapseEdits: false, automaticallyAddUsings: true, cancellationToken: cancellationToken); + public Task GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + => ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.Html, htmlEdits, options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); - public async Task GetSnippetFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken) + public async Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit edit, FormattingOptions options, CancellationToken cancellationToken) { - if (kind == RazorLanguageKind.CSharp) - { - WrapCSharpSnippets(edits); - } + var formattedEdits = await ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, [edit], options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken).ConfigureAwait(false); + return formattedEdits.SingleOrDefault(); + } + + public async Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] initialEdits, FormattingOptions options, CancellationToken cancellationToken) + { + var edits = await ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, initialEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: true, collapseEdits: true, automaticallyAddUsings: true, cancellationToken: cancellationToken).ConfigureAwait(false); + return edits.SingleOrDefault(); + } + + public async Task GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken) + { + WrapCSharpSnippets(edits); var formattedEdits = await ApplyFormattedEditsAsync( documentContext, - kind, + RazorLanguageKind.CSharp, edits, options, hostDocumentIndex: 0, @@ -100,12 +110,9 @@ internal class RazorFormattingService( automaticallyAddUsings: false, cancellationToken: cancellationToken).ConfigureAwait(false); - if (kind == RazorLanguageKind.CSharp) - { - UnwrapCSharpSnippets(formattedEdits); - } + UnwrapCSharpSnippets(formattedEdits); - return formattedEdits; + return formattedEdits.SingleOrDefault(); } private async Task ApplyFormattedEditsAsync( diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs index fbaadf3db8..4bd0ec70df 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs @@ -39,7 +39,7 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) : } }; - private static readonly TextEdit[] s_defaultFormattedEdits = [VsLspFactory.CreateTextEdit(position: (0, 0), "Remapped & Formatted Edit")]; + private static readonly TextEdit s_defaultFormattedEdit = VsLspFactory.CreateTextEdit(position: (0, 0), "Remapped & Formatted Edit"); private static readonly CodeAction s_defaultUnresolvedCodeAction = new CodeAction() { @@ -63,7 +63,7 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) : var returnedEdits = returnedCodeAction.Edit.DocumentChanges.Value; Assert.True(returnedEdits.TryGetFirst(out var textDocumentEdits)); var returnedTextDocumentEdit = Assert.Single(textDocumentEdits[0].Edits); - Assert.Equal(s_defaultFormattedEdits.First(), returnedTextDocumentEdit); + Assert.Equal(s_defaultFormattedEdit, returnedTextDocumentEdit); } [Fact] @@ -188,11 +188,11 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) : private static IRazorFormattingService CreateRazorFormattingService(Uri documentUri) { var razorFormattingService = Mock.Of( - rfs => rfs.GetCSharpCodeActionEditsAsync( + rfs => rfs.GetCSharpCodeActionEditAsync( It.Is(c => c.Uri == documentUri), It.IsAny(), It.IsAny(), - It.IsAny()) == Task.FromResult(s_defaultFormattedEdits), MockBehavior.Strict); + It.IsAny()) == Task.FromResult(s_defaultFormattedEdit), MockBehavior.Strict); return razorFormattingService; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs index 9f00c0c549..e6771a34d0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Threading; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit.Abstractions; using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; @@ -38,25 +37,35 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut { public bool Called { get; private set; } - public Task GetDocumentFormattingEditsAsync(VersionedDocumentContext documentContext, Range? range, FormattingOptions options, CancellationToken cancellationToken) + public Task GetDocumentFormattingEditsAsync(VersionedDocumentContext documentContext, TextEdit[] htmlEdits, Range? range, FormattingOptions options, CancellationToken cancellationToken) { Called = true; return SpecializedTasks.EmptyArray(); } - public Task GetCSharpCodeActionEditsAsync(DocumentContext documentContext, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + public Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) { - return Task.FromResult(formattedEdits); + throw new NotImplementedException(); } - public Task GetOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) { - return Task.FromResult(formattedEdits); + throw new NotImplementedException(); } - public Task GetSnippetFormattingEditsAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + public Task GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken) { - return Task.FromResult(formattedEdits); + throw new NotImplementedException(); + } + + public Task GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + { + return Task.FromResult(htmlEdits); + } + + public Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit initialEdit, FormattingOptions options, CancellationToken cancellationToken) + { + throw new NotImplementedException(); } } } 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 a503985815..de1c65f5dc 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 @@ -80,8 +80,11 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, documentSnapshot, razorLSPOptions); var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); + // TODO: In the next commit, get the Html edits from the Html formatter + var htmlEdits = Array.Empty(); + // Act - var edits = await formattingService.GetDocumentFormattingEditsAsync(documentContext, range, options, DisposalToken); + var edits = await formattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range, options, DisposalToken); // Assert var edited = ApplyEdits(source, edits); @@ -131,7 +134,17 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); // Act - var edits = await formattingService.GetOnTypeFormattingEditsAsync(documentContext, languageKind, Array.Empty(), options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); + TextEdit[] edits; + if (languageKind == RazorLanguageKind.CSharp) + { + edits = await formattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); + } + else + { + // todo: + var htmlEdits = Array.Empty(); + edits = await formattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); + } // Assert var edited = ApplyEdits(razorSourceText, edits); @@ -199,10 +212,10 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); // Act - var edits = await formattingService.GetCodeActionEditsAsync(documentContext, languageKind, codeActionEdits, options, DisposalToken); + var edit = await formattingService.GetCSharpCodeActionEditAsync(documentContext, codeActionEdits, options, DisposalToken); // Assert - var edited = ApplyEdits(razorSourceText, edits); + var edited = ApplyEdits(razorSourceText, [edit]); var actual = edited.ToString(); AssertEx.EqualOrDiff(expected, actual); From e56b85e264775d3a32aec230eb4c56f736ff73f9 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 21:26:18 +1000 Subject: [PATCH 07/26] Pass Html edits into the formatting service, so that the service doesn't need LSP client dependencies --- .../Formatting/DocumentFormattingEndpoint.cs | 9 ++--- .../DocumentOnTypeFormattingEndpoint.cs | 8 +++-- .../DocumentRangeFormattingEndpoint.cs | 25 ++++++------- .../Formatting/HtmlFormatter.cs | 35 +++++++++++-------- .../Formatting/HtmlFormattingPass.cs | 18 ++-------- .../CodeActionEndToEndTest.NetFx.cs | 2 +- .../DocumentOnTypeFormattingEndpointTest.cs | 26 ++++++++++---- .../DocumentRangeFormattingEndpointTest.cs | 19 +++++++--- .../Formatting_NetFx/FormattingTestBase.cs | 28 +++++++++++---- .../TestRazorFormattingService.cs | 13 +------ 10 files changed, 101 insertions(+), 82 deletions(-) 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 92b146fb50..b4e4a4a937 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs @@ -1,11 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -14,10 +14,13 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; [RazorLanguageServerEndpoint(Methods.TextDocumentFormattingName)] internal class DocumentFormattingEndpoint( IRazorFormattingService razorFormattingService, + IClientConnection clientConnection, + IDocumentVersionCache documentVersionCache, RazorLSPOptionsMonitor optionsMonitor) : IRazorRequestHandler, ICapabilitiesProvider { private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; + private readonly HtmlFormatter _htmlFormatter = new HtmlFormatter(clientConnection, documentVersionCache); public bool MutatesSolutionState => false; @@ -50,9 +53,7 @@ internal class DocumentFormattingEndpoint( return null; } - // TODO: In the next commit, get the Html edits from the Html formatter - var htmlEdits = Array.Empty(); - + var htmlEdits = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false); var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range: null, request.Options, cancellationToken).ConfigureAwait(false); return edits; } 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 0f88d469b9..6a7b42d1c0 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Logging; @@ -21,6 +22,8 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; [RazorLanguageServerEndpoint(Methods.TextDocumentOnTypeFormattingName)] internal class DocumentOnTypeFormattingEndpoint( IRazorFormattingService razorFormattingService, + IClientConnection clientConnection, + IDocumentVersionCache documentVersionCache, IDocumentMappingService documentMappingService, RazorLSPOptionsMonitor optionsMonitor, ILoggerFactory loggerFactory) @@ -29,6 +32,7 @@ internal class DocumentOnTypeFormattingEndpoint( private readonly IRazorFormattingService _razorFormattingService = razorFormattingService ?? throw new ArgumentNullException(nameof(razorFormattingService)); private readonly IDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); + private readonly HtmlFormatter _htmlFormatter = new HtmlFormatter(clientConnection, documentVersionCache); private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); private static readonly IReadOnlyList s_csharpTriggerCharacters = new[] { "}", ";" }; @@ -74,7 +78,6 @@ internal class DocumentOnTypeFormattingEndpoint( } var documentContext = requestContext.DocumentContext; - if (documentContext is null) { _logger.LogWarning($"Failed to find document {request.TextDocument.Uri}."); @@ -121,8 +124,7 @@ internal class DocumentOnTypeFormattingEndpoint( } else if (triggerCharacterKind == RazorLanguageKind.Html) { - // TODO: In the next commit, get the Html edits from the Html formatter - var htmlEdits = Array.Empty(); + var htmlEdits = await _htmlFormatter.GetOnTypeFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Position, request.Character, request.Options, cancellationToken).ConfigureAwait(false); formattedEdits = await _razorFormattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); } else 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 48ead55198..c83d7ac3fd 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs @@ -1,29 +1,26 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; [RazorLanguageServerEndpoint(Methods.TextDocumentRangeFormattingName)] -internal class DocumentRangeFormattingEndpoint : IRazorRequestHandler, ICapabilitiesProvider +internal class DocumentRangeFormattingEndpoint( + IRazorFormattingService razorFormattingService, + IClientConnection clientConnection, + IDocumentVersionCache documentVersionCache, + RazorLSPOptionsMonitor optionsMonitor) : IRazorRequestHandler, ICapabilitiesProvider { - private readonly IRazorFormattingService _razorFormattingService; - private readonly RazorLSPOptionsMonitor _optionsMonitor; - - public DocumentRangeFormattingEndpoint( - IRazorFormattingService razorFormattingService, - RazorLSPOptionsMonitor optionsMonitor) - { - _razorFormattingService = razorFormattingService ?? throw new ArgumentNullException(nameof(razorFormattingService)); - _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); - } + private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; + private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; + private readonly HtmlFormatter _htmlFormatter = new HtmlFormatter(clientConnection, documentVersionCache); public bool MutatesSolutionState => false; @@ -56,9 +53,7 @@ internal class DocumentRangeFormattingEndpoint : IRazorRequestHandler(); - + var htmlEdits = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false); var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, request.Range, request.Options, cancellationToken).ConfigureAwait(false); return edits; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs index 224ae300c6..17f6a13235 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs @@ -6,8 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -using Microsoft.AspNetCore.Razor.TextDifferencing; -using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Protocol.Formatting; using Microsoft.CodeAnalysis.Text; @@ -22,11 +21,13 @@ internal sealed class HtmlFormatter( private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache; private readonly IClientConnection _clientConnection = clientConnection; - public async Task FormatAsync( - FormattingContext context, + public async Task GetDocumentFormattingEditsAsync( + IDocumentSnapshot documentSnapshot, + Uri uri, + FormattingOptions options, CancellationToken cancellationToken) { - if (!_documentVersionCache.TryGetDocumentVersion(context.OriginalSnapshot, out var documentVersion)) + if (!_documentVersionCache.TryGetDocumentVersion(documentSnapshot, out var documentVersion)) { return []; } @@ -35,10 +36,10 @@ internal sealed class HtmlFormatter( { TextDocument = new TextDocumentIdentifier { - Uri = context.Uri, + Uri = uri, }, HostDocumentVersion = documentVersion.Value, - Options = context.Options + Options = options }; var result = await _clientConnection.SendRequestAsync( @@ -49,21 +50,25 @@ internal sealed class HtmlFormatter( return result?.Edits ?? []; } - public async Task FormatOnTypeAsync( - FormattingContext context, - CancellationToken cancellationToken) + public async Task GetOnTypeFormattingEditsAsync( + IDocumentSnapshot documentSnapshot, + Uri uri, + Position position, + string triggerCharacter, + FormattingOptions options, + CancellationToken cancellationToken) { - if (!_documentVersionCache.TryGetDocumentVersion(context.OriginalSnapshot, out var documentVersion)) + if (!_documentVersionCache.TryGetDocumentVersion(documentSnapshot, out var documentVersion)) { return []; } var @params = new RazorDocumentOnTypeFormattingParams() { - Position = context.SourceText.GetPosition(context.HostDocumentIndex), - Character = context.TriggerCharacter.ToString(), - TextDocument = new TextDocumentIdentifier { Uri = context.Uri }, - Options = context.Options, + Position = position, + Character = triggerCharacter.ToString(), + TextDocument = new TextDocumentIdentifier { Uri = uri }, + Options = options, HostDocumentVersion = documentVersion.Value, }; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs index 3b3b4023e4..de1f7f8341 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Logging; @@ -19,12 +18,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; internal sealed class HtmlFormattingPass( IDocumentMappingService documentMappingService, - IClientConnection clientConnection, - IDocumentVersionCache documentVersionCache, ILoggerFactory loggerFactory) : FormattingPassBase(documentMappingService) { - private readonly HtmlFormatter _htmlFormatter = new HtmlFormatter(clientConnection, documentVersionCache); private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); // We want this to run first because it uses the client HTML formatter. @@ -36,22 +32,14 @@ internal sealed class HtmlFormattingPass( { var originalText = context.SourceText; - TextEdit[] htmlEdits; - - if (context.IsFormatOnType && result.Kind == RazorLanguageKind.Html) - { - htmlEdits = await _htmlFormatter.FormatOnTypeAsync(context, cancellationToken).ConfigureAwait(false); - } - else if (!context.IsFormatOnType) - { - htmlEdits = await _htmlFormatter.FormatAsync(context, cancellationToken).ConfigureAwait(false); - } - else + if (context.IsFormatOnType && result.Kind != RazorLanguageKind.Html) { // We don't want to handle on type formatting requests for other languages return result; } + var htmlEdits = result.Edits; + var changedText = originalText; var changedContext = context; 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 1090b3672e..5519014c08 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 @@ -1132,7 +1132,7 @@ public class CodeActionEndToEndTest(ITestOutputHelper testOutput) : SingleServer Assert.NotNull(codeActionToRun); - var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, documentContext.Snapshot, optionsMonitor?.CurrentValue); + var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, optionsMonitor?.CurrentValue); var changes = await GetEditsAsync( codeActionToRun, requestContext, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs index 17177a8e49..d53ad0ecbd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -27,8 +29,10 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); var optionsMonitor = GetOptionsMonitor(enableFormatting: false); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, documentMappingService, optionsMonitor, LoggerFactory); + formattingService, clientConnection, documentVersionCache, documentMappingService, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams { TextDocument = new TextDocumentIdentifier { Uri = uri, } }; var requestContext = CreateRazorRequestContext(documentContext: null); @@ -56,8 +60,10 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, documentMappingService, optionsMonitor, LoggerFactory); + formattingService, clientConnection, documentVersionCache, documentMappingService, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -91,8 +97,10 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, documentMappingService, optionsMonitor, LoggerFactory); + formattingService, clientConnection, documentVersionCache, documentMappingService, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -127,8 +135,10 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new Mock(MockBehavior.Strict); documentMappingService.Setup(s => s.GetLanguageKind(codeDocument, 17, false)).Returns(RazorLanguageKind.Html); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, documentMappingService.Object, optionsMonitor, LoggerFactory); + formattingService, clientConnection, documentVersionCache, documentMappingService.Object, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -163,8 +173,10 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new Mock(MockBehavior.Strict); documentMappingService.Setup(s => s.GetLanguageKind(codeDocument, 17, false)).Returns(RazorLanguageKind.Razor); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, documentMappingService.Object, optionsMonitor, LoggerFactory); + formattingService, clientConnection, documentVersionCache, documentMappingService.Object, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -198,8 +210,10 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new LspDocumentMappingService(FilePathService, documentContextFactory, LoggerFactory); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, documentMappingService, optionsMonitor, LoggerFactory); + formattingService, clientConnection, documentVersionCache, documentMappingService, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs index 964ed2d0ef..f1a3cbebc8 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs @@ -4,6 +4,8 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; using Xunit.Abstractions; @@ -22,9 +24,12 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : var documentContext = CreateDocumentContext(uri, codeDocument); var formattingService = new DummyRazorFormattingService(); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); + var optionsMonitor = GetOptionsMonitor(enableFormatting: true); var endpoint = new DocumentRangeFormattingEndpoint( - formattingService, optionsMonitor); + formattingService, clientConnection, documentVersionCache, optionsMonitor); var @params = new DocumentRangeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, } @@ -45,7 +50,9 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : // Arrange var formattingService = new DummyRazorFormattingService(); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var endpoint = new DocumentRangeFormattingEndpoint(formattingService, optionsMonitor); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); + var endpoint = new DocumentRangeFormattingEndpoint(formattingService, clientConnection, documentVersionCache, optionsMonitor); var uri = new Uri("file://path/test.razor"); var @params = new DocumentRangeFormattingParams() { @@ -71,7 +78,9 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : var documentContext = CreateDocumentContext(uri, codeDocument); var formattingService = new DummyRazorFormattingService(); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var endpoint = new DocumentRangeFormattingEndpoint(formattingService, optionsMonitor); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); + var endpoint = new DocumentRangeFormattingEndpoint(formattingService, clientConnection, documentVersionCache, optionsMonitor); var @params = new DocumentRangeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, } @@ -91,7 +100,9 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : // Arrange var formattingService = new DummyRazorFormattingService(); var optionsMonitor = GetOptionsMonitor(enableFormatting: false); - var endpoint = new DocumentRangeFormattingEndpoint(formattingService, optionsMonitor); + var clientConnection = StrictMock.Of(); + var documentVersionCache = StrictMock.Of(); + var endpoint = new DocumentRangeFormattingEndpoint(formattingService, clientConnection, documentVersionCache, optionsMonitor); var @params = new DocumentRangeFormattingParams(); var requestContext = CreateRazorRequestContext(documentContext: null); 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 de1c65f5dc..559eba75bf 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,6 +24,7 @@ using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Settings.Internal; using Moq; using Roslyn.Test.Utilities; using Xunit; @@ -77,11 +78,18 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase InsertSpaces = insertSpaces, }; - var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, documentSnapshot, razorLSPOptions); + var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, razorLSPOptions); var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); - // TODO: In the next commit, get the Html edits from the Html formatter - var htmlEdits = Array.Empty(); + var projectManager = StrictMock.Of(); + var versionCache = new DocumentVersionCache(projectManager); + versionCache.TrackDocumentVersion(documentSnapshot, version: 1); + + var client = new FormattingLanguageServerClient(LoggerFactory); + client.AddCodeDocument(codeDocument); + + var htmlFormatter = new HtmlFormatter(client, versionCache); + var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken); // Act var edits = await formattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range, options, DisposalToken); @@ -124,8 +132,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase filePathService, new TestDocumentContextFactory(), LoggerFactory); var languageKind = mappingService.GetLanguageKind(codeDocument, positionAfterTrigger, rightAssociative: false); - var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync( - LoggerFactory, codeDocument, documentSnapshot, razorLSPOptions); + var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, razorLSPOptions); var options = new FormattingOptions() { TabSize = tabSize, @@ -141,8 +148,15 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase } else { - // todo: - var htmlEdits = Array.Empty(); + var projectManager = StrictMock.Of(); + var versionCache = new DocumentVersionCache(projectManager); + versionCache.TrackDocumentVersion(documentSnapshot, version: 1); + + var client = new FormattingLanguageServerClient(LoggerFactory); + client.AddCodeDocument(codeDocument); + + var htmlFormatter = new HtmlFormatter(client, versionCache); + var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken); edits = await formattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs index a1e3f585ba..c9efe60bea 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs @@ -22,7 +22,6 @@ internal static class TestRazorFormattingService public static async Task CreateWithFullSupportAsync( ILoggerFactory loggerFactory, RazorCodeDocument? codeDocument = null, - IDocumentSnapshot? documentSnapshot = null, RazorLSPOptions? razorLSPOptions = null) { codeDocument ??= TestRazorCodeDocument.CreateEmpty(); @@ -30,16 +29,6 @@ internal static class TestRazorFormattingService var filePathService = new LSPFilePathService(TestLanguageServerFeatureOptions.Instance); var mappingService = new LspDocumentMappingService(filePathService, new TestDocumentContextFactory(), loggerFactory); - var projectManager = StrictMock.Of(); - var versionCache = new DocumentVersionCache(projectManager); - if (documentSnapshot is not null) - { - versionCache.TrackDocumentVersion(documentSnapshot, version: 1); - } - - var client = new FormattingLanguageServerClient(loggerFactory); - client.AddCodeDocument(codeDocument); - var configurationSyncService = new Mock(MockBehavior.Strict); configurationSyncService .Setup(c => c.GetLatestOptionsAsync(It.IsAny())) @@ -55,7 +44,7 @@ internal static class TestRazorFormattingService var passes = new List() { - new HtmlFormattingPass(mappingService, client, versionCache, loggerFactory), + new HtmlFormattingPass(mappingService, loggerFactory), new CSharpFormattingPass(mappingService, loggerFactory), new CSharpOnTypeFormattingPass(mappingService, loggerFactory), new LspRazorFormattingPass(mappingService, optionsMonitor), From 3f578100fe25abb36f825f52cd2b0158d0d8a47f Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 21:39:44 +1000 Subject: [PATCH 08/26] Get HtmlFormatter from DI --- .../IServiceCollectionExtensions.cs | 1 + .../Formatting/DocumentFormattingEndpoint.cs | 5 ++- .../DocumentOnTypeFormattingEndpoint.cs | 5 ++- .../DocumentRangeFormattingEndpoint.cs | 5 ++- .../Formatting/HtmlFormatter.cs | 2 +- .../Formatting/IHtmlFormatter.cs | 15 +++++++++ .../Formatting/RazorFormattingService.cs | 5 +++ .../DocumentOnTypeFormattingEndpointTest.cs | 32 +++++++------------ .../DocumentRangeFormattingEndpointTest.cs | 23 +++++-------- .../Formatting_NetFx/TestHtmlFormatter.cs | 24 ++++++++++++++ 10 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/IHtmlFormatter.cs create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestHtmlFormatter.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 865632e5da..b1888555f1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -59,6 +59,7 @@ internal static class IServiceCollectionExtensions public static void AddFormattingServices(this IServiceCollection services) { // Formatting + services.AddSingleton(); services.AddSingleton(); // Formatting Passes 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 b4e4a4a937..003ab22f19 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs @@ -14,13 +14,12 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; [RazorLanguageServerEndpoint(Methods.TextDocumentFormattingName)] internal class DocumentFormattingEndpoint( IRazorFormattingService razorFormattingService, - IClientConnection clientConnection, - IDocumentVersionCache documentVersionCache, + IHtmlFormatter htmlFormatter, RazorLSPOptionsMonitor optionsMonitor) : IRazorRequestHandler, ICapabilitiesProvider { private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; - private readonly HtmlFormatter _htmlFormatter = new HtmlFormatter(clientConnection, documentVersionCache); + private readonly IHtmlFormatter _htmlFormatter = htmlFormatter; public bool MutatesSolutionState => false; 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 6a7b42d1c0..d75cd09242 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -22,8 +22,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; [RazorLanguageServerEndpoint(Methods.TextDocumentOnTypeFormattingName)] internal class DocumentOnTypeFormattingEndpoint( IRazorFormattingService razorFormattingService, - IClientConnection clientConnection, - IDocumentVersionCache documentVersionCache, + IHtmlFormatter htmlFormatter, IDocumentMappingService documentMappingService, RazorLSPOptionsMonitor optionsMonitor, ILoggerFactory loggerFactory) @@ -32,7 +31,7 @@ internal class DocumentOnTypeFormattingEndpoint( private readonly IRazorFormattingService _razorFormattingService = razorFormattingService ?? throw new ArgumentNullException(nameof(razorFormattingService)); private readonly IDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); - private readonly HtmlFormatter _htmlFormatter = new HtmlFormatter(clientConnection, documentVersionCache); + private readonly IHtmlFormatter _htmlFormatter = htmlFormatter; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); private static readonly IReadOnlyList s_csharpTriggerCharacters = new[] { "}", ";" }; 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 c83d7ac3fd..bd27ec5b8f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs @@ -14,13 +14,12 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; [RazorLanguageServerEndpoint(Methods.TextDocumentRangeFormattingName)] internal class DocumentRangeFormattingEndpoint( IRazorFormattingService razorFormattingService, - IClientConnection clientConnection, - IDocumentVersionCache documentVersionCache, + IHtmlFormatter htmlFormatter, RazorLSPOptionsMonitor optionsMonitor) : IRazorRequestHandler, ICapabilitiesProvider { private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; - private readonly HtmlFormatter _htmlFormatter = new HtmlFormatter(clientConnection, documentVersionCache); + private readonly IHtmlFormatter _htmlFormatter = htmlFormatter; public bool MutatesSolutionState => false; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs index 17f6a13235..0522c8360f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; internal sealed class HtmlFormatter( IClientConnection clientConnection, - IDocumentVersionCache documentVersionCache) + IDocumentVersionCache documentVersionCache) : IHtmlFormatter { private readonly IDocumentVersionCache _documentVersionCache = documentVersionCache; private readonly IClientConnection _clientConnection = clientConnection; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/IHtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/IHtmlFormatter.cs new file mode 100644 index 0000000000..fd388022be --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/IHtmlFormatter.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; +internal interface IHtmlFormatter +{ + Task GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken); + Task GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index eedc6f4989..4f806d916e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -31,6 +32,8 @@ internal class RazorFormattingService( FormattingOptions options, CancellationToken cancellationToken) { + Debug.Assert(_formattingPasses[0].GetType().Name == "HtmlFormattingPass", "Formatting requires the first pass to be Html"); + var codeDocument = await documentContext.Snapshot.GetFormatterCodeDocumentAsync().ConfigureAwait(false); // Range formatting happens on every paste, and if there are Razor diagnostics in the file @@ -127,6 +130,8 @@ internal class RazorFormattingService( bool automaticallyAddUsings, CancellationToken cancellationToken) { + Debug.Assert(_formattingPasses[0].GetType().Name == "HtmlFormattingPass", "Formatting requires the first pass to be Html"); + // If we only received a single edit, let's always return a single edit back. // Otherwise, merge only if explicitly asked. collapseEdits |= formattedEdits.Length == 1; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs index d53ad0ecbd..74201d434c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -29,10 +27,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); var optionsMonitor = GetOptionsMonitor(enableFormatting: false); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); + var htmlFormatter = new TestHtmlFormatter(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, clientConnection, documentVersionCache, documentMappingService, optionsMonitor, LoggerFactory); + formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams { TextDocument = new TextDocumentIdentifier { Uri = uri, } }; var requestContext = CreateRazorRequestContext(documentContext: null); @@ -60,10 +57,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); + var htmlFormatter = new TestHtmlFormatter(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, clientConnection, documentVersionCache, documentMappingService, optionsMonitor, LoggerFactory); + formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -97,10 +93,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); + var htmlFormatter = new TestHtmlFormatter(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, clientConnection, documentVersionCache, documentMappingService, optionsMonitor, LoggerFactory); + formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -135,10 +130,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new Mock(MockBehavior.Strict); documentMappingService.Setup(s => s.GetLanguageKind(codeDocument, 17, false)).Returns(RazorLanguageKind.Html); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); + var htmlFormatter = new TestHtmlFormatter(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, clientConnection, documentVersionCache, documentMappingService.Object, optionsMonitor, LoggerFactory); + formattingService, htmlFormatter, documentMappingService.Object, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -173,10 +167,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new Mock(MockBehavior.Strict); documentMappingService.Setup(s => s.GetLanguageKind(codeDocument, 17, false)).Returns(RazorLanguageKind.Razor); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); + var htmlFormatter = new TestHtmlFormatter(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, clientConnection, documentVersionCache, documentMappingService.Object, optionsMonitor, LoggerFactory); + formattingService, htmlFormatter, documentMappingService.Object, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, @@ -210,10 +203,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) var documentMappingService = new LspDocumentMappingService(FilePathService, documentContextFactory, LoggerFactory); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); + var htmlFormatter = new TestHtmlFormatter(); var endpoint = new DocumentOnTypeFormattingEndpoint( - formattingService, clientConnection, documentVersionCache, documentMappingService, optionsMonitor, LoggerFactory); + formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory); var @params = new DocumentOnTypeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, }, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs index f1a3cbebc8..a497f70c2b 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs @@ -4,8 +4,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; using Xunit.Abstractions; @@ -24,12 +22,10 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : var documentContext = CreateDocumentContext(uri, codeDocument); var formattingService = new DummyRazorFormattingService(); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); - + var htmlFormatter = new TestHtmlFormatter(); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); var endpoint = new DocumentRangeFormattingEndpoint( - formattingService, clientConnection, documentVersionCache, optionsMonitor); + formattingService, htmlFormatter, optionsMonitor); var @params = new DocumentRangeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, } @@ -50,9 +46,8 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : // Arrange var formattingService = new DummyRazorFormattingService(); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); - var endpoint = new DocumentRangeFormattingEndpoint(formattingService, clientConnection, documentVersionCache, optionsMonitor); + var htmlFormatter = new TestHtmlFormatter(); + var endpoint = new DocumentRangeFormattingEndpoint(formattingService, htmlFormatter, optionsMonitor); var uri = new Uri("file://path/test.razor"); var @params = new DocumentRangeFormattingParams() { @@ -78,9 +73,8 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : var documentContext = CreateDocumentContext(uri, codeDocument); var formattingService = new DummyRazorFormattingService(); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); - var endpoint = new DocumentRangeFormattingEndpoint(formattingService, clientConnection, documentVersionCache, optionsMonitor); + var htmlFormatter = new TestHtmlFormatter(); + var endpoint = new DocumentRangeFormattingEndpoint(formattingService, htmlFormatter, optionsMonitor); var @params = new DocumentRangeFormattingParams() { TextDocument = new TextDocumentIdentifier { Uri = uri, } @@ -100,9 +94,8 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : // Arrange var formattingService = new DummyRazorFormattingService(); var optionsMonitor = GetOptionsMonitor(enableFormatting: false); - var clientConnection = StrictMock.Of(); - var documentVersionCache = StrictMock.Of(); - var endpoint = new DocumentRangeFormattingEndpoint(formattingService, clientConnection, documentVersionCache, optionsMonitor); + var htmlFormatter = new TestHtmlFormatter(); + var endpoint = new DocumentRangeFormattingEndpoint(formattingService, htmlFormatter, optionsMonitor); var @params = new DocumentRangeFormattingParams(); var requestContext = CreateRazorRequestContext(documentContext: null); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestHtmlFormatter.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestHtmlFormatter.cs new file mode 100644 index 0000000000..0774619dfa --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestHtmlFormatter.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Threading; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; + +internal class TestHtmlFormatter : IHtmlFormatter +{ + public Task GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken) + { + return SpecializedTasks.EmptyArray(); + } + + public Task GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken) + { + return SpecializedTasks.EmptyArray(); + } +} From 38868db9c26ef5ec9403294429c23e0d3fd82379 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 21:45:52 +1000 Subject: [PATCH 09/26] Minor cleanup --- .../Formatting/DocumentOnTypeFormattingEndpoint.cs | 9 +++++---- .../Formatting/IHtmlFormatter.cs | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) 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 d75cd09242..4279c55053 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -34,9 +35,9 @@ internal class DocumentOnTypeFormattingEndpoint( private readonly IHtmlFormatter _htmlFormatter = htmlFormatter; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - private static readonly IReadOnlyList s_csharpTriggerCharacters = new[] { "}", ";" }; - private static readonly IReadOnlyList s_htmlTriggerCharacters = new[] { "\n", "{", "}", ";" }; - private static readonly IReadOnlyList s_allTriggerCharacters = s_csharpTriggerCharacters.Concat(s_htmlTriggerCharacters).ToArray(); + private static readonly FrozenSet s_csharpTriggerCharacters = FrozenSet.ToFrozenSet(["}", ";"]); + private static readonly FrozenSet s_htmlTriggerCharacters = FrozenSet.ToFrozenSet(["\n", "{", "}", ";"]); + private static readonly FrozenSet s_allTriggerCharacters = FrozenSet.ToFrozenSet(s_csharpTriggerCharacters.Concat(s_htmlTriggerCharacters)); public bool MutatesSolutionState => false; @@ -44,7 +45,7 @@ internal class DocumentOnTypeFormattingEndpoint( { serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions { - FirstTriggerCharacter = s_allTriggerCharacters[0], + FirstTriggerCharacter = s_allTriggerCharacters.First(), MoreTriggerCharacter = s_allTriggerCharacters.Skip(1).ToArray(), }; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/IHtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/IHtmlFormatter.cs index fd388022be..cf3f9bf263 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/IHtmlFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/IHtmlFormatter.cs @@ -8,8 +8,9 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; + internal interface IHtmlFormatter { Task GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken); Task GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken); -} \ No newline at end of file +} From ca142becdc3258b786b935d65146229ff55f5cca Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 21 Aug 2024 21:57:49 +1000 Subject: [PATCH 10/26] Move HtmlFormattingPass down --- .../Formatting/HtmlFormattingPass.cs | 3 +-- .../Formatting/RazorFormattingService.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) rename src/Razor/src/{Microsoft.AspNetCore.Razor.LanguageServer => Microsoft.CodeAnalysis.Razor.Workspaces}/Formatting/HtmlFormattingPass.cs (98%) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs similarity index 98% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs index de1f7f8341..0bedcf7b3f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs @@ -8,13 +8,12 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; +namespace Microsoft.CodeAnalysis.Razor.Formatting; internal sealed class HtmlFormattingPass( IDocumentMappingService documentMappingService, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 4f806d916e..e3c61860f1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -32,7 +32,7 @@ internal class RazorFormattingService( FormattingOptions options, CancellationToken cancellationToken) { - Debug.Assert(_formattingPasses[0].GetType().Name == "HtmlFormattingPass", "Formatting requires the first pass to be Html"); + Debug.Assert(_formattingPasses[0] is HtmlFormattingPass, "Formatting requires the first pass to be Html"); var codeDocument = await documentContext.Snapshot.GetFormatterCodeDocumentAsync().ConfigureAwait(false); @@ -130,7 +130,7 @@ internal class RazorFormattingService( bool automaticallyAddUsings, CancellationToken cancellationToken) { - Debug.Assert(_formattingPasses[0].GetType().Name == "HtmlFormattingPass", "Formatting requires the first pass to be Html"); + Debug.Assert(_formattingPasses[0] is HtmlFormattingPass, "Formatting requires the first pass to be Html"); // If we only received a single edit, let's always return a single edit back. // Otherwise, merge only if explicitly asked. From 114390078e1f34b7aef598766b95f8f98bbe2ec9 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 09:13:11 +1000 Subject: [PATCH 11/26] Formalize Razor formatting options so we can have a single RazorFormattingPass --- .../RazorCSharpFormattingBenchmark.cs | 8 +-- .../AutoInsert/OnAutoInsertEndpoint.cs | 10 ++- .../CSharp/DefaultCSharpCodeActionResolver.cs | 17 +---- .../Razor/GenerateMethodCodeActionResolver.cs | 3 +- .../DelegatedCompletionItemResolver.cs | 9 ++- .../IServiceCollectionExtensions.cs | 2 +- .../Formatting/DocumentFormattingEndpoint.cs | 4 +- .../DocumentOnTypeFormattingEndpoint.cs | 6 +- .../DocumentRangeFormattingEndpoint.cs | 4 +- .../Formatting/LspRazorFormattingPass.cs | 15 ----- .../InlineCompletionEndPoint.cs | 8 ++- .../Formatting/FormattingContext.cs | 62 ++++--------------- .../Formatting/FormattingOptionsExtensions.cs | 16 ----- .../Formatting/IRazorFormattingService.cs | 12 ++-- .../Formatting/RazorFormattingOptions.cs | 37 +++++++++++ ...tingPassBase.cs => RazorFormattingPass.cs} | 6 +- .../Formatting/RazorFormattingService.cs | 14 ++--- .../Formatting/RemoteRazorFormattingPass.cs | 15 ----- ...Microsoft.CodeAnalysis.Remote.Razor.csproj | 4 ++ .../RazorOnAutoInsertProviderTestBase.cs | 2 +- .../DefaultCSharpCodeActionResolverTest.cs | 2 +- ...legatedCompletionItemResolverTest.NetFx.cs | 18 ++++-- .../DocumentRangeFormattingEndpointTest.cs | 3 +- .../FormattingContentValidationPassTest.cs | 2 +- .../FormattingDiagnosticValidationPassTest.cs | 2 +- .../FormattingLanguageServerTestBase.cs | 12 ++-- .../Formatting_NetFx/FormattingTestBase.cs | 13 ++-- .../TestRazorFormattingService.cs | 2 +- 28 files changed, 137 insertions(+), 171 deletions(-) delete mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspRazorFormattingPass.cs delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingOptionsExtensions.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{RazorFormattingPassBase.cs => RazorFormattingPass.cs} (99%) delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingPass.cs diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs index 2c74f91bc7..571eeb689c 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs @@ -110,15 +110,9 @@ public class RazorCSharpFormattingBenchmark : RazorLanguageServerBenchmarkBase [Benchmark(Description = "Formatting")] public async Task RazorCSharpFormattingAsync() { - var options = new FormattingOptions() - { - TabSize = 4, - InsertSpaces = true - }; - var documentContext = new VersionedDocumentContext(DocumentUri, DocumentSnapshot, projectContext: null, version: 1); - var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, [], range: null, options, CancellationToken.None); + var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, [], range: null, RazorFormattingOptions.Default, CancellationToken.None); #if DEBUG // For debugging purposes only. 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 4ec4479463..8cdbb296fc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs @@ -104,7 +104,9 @@ internal class OnAutoInsertEndpoint( var uri = request.TextDocument.Uri; var position = request.Position; - using var formattingContext = FormattingContext.Create(uri, documentContext.Snapshot, codeDocument, request.Options, _workspaceFactory); + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + + using var formattingContext = FormattingContext.Create(uri, documentContext.Snapshot, codeDocument, options, _workspaceFactory); foreach (var provider in applicableProviders) { if (provider.TryResolveInsertion(position, formattingContext, out var textEdit, out var format)) @@ -206,9 +208,11 @@ internal class OnAutoInsertEndpoint( // 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 mappedEdit = delegatedResponse.TextEditFormat == InsertTextFormat.Snippet - ? await _razorFormattingService.GetCSharpSnippetFormattingEditAsync(documentContext, [delegatedResponse.TextEdit], originalRequest.Options, cancellationToken).ConfigureAwait(false) - : await _razorFormattingService.GetSingleCSharpEditAsync(documentContext, delegatedResponse.TextEdit, originalRequest.Options, cancellationToken).ConfigureAwait(false); + ? await _razorFormattingService.GetCSharpSnippetFormattingEditAsync(documentContext, [delegatedResponse.TextEdit], options, cancellationToken).ConfigureAwait(false) + : await _razorFormattingService.GetSingleCSharpEditAsync(documentContext, delegatedResponse.TextEdit, options, cancellationToken).ConfigureAwait(false); if (mappedEdit is null) { return null; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs index 5dca63c913..4a1a8eb6c9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionResolver.cs @@ -22,21 +22,6 @@ internal sealed class DefaultCSharpCodeActionResolver( IClientConnection clientConnection, IRazorFormattingService razorFormattingService) : CSharpCodeActionResolver(clientConnection) { - // Usually when we need to format code, we utilize the formatting options provided - // by the platform. However, we aren't provided such options in the case of code actions - // so we use a default (and commonly used) configuration. - private static readonly FormattingOptions s_defaultFormattingOptions = new FormattingOptions() - { - TabSize = 4, - InsertSpaces = true, - OtherOptions = new Dictionary - { - { "trimTrailingWhitespace", true }, - { "insertFinalNewline", true }, - { "trimFinalNewlines", true }, - }, - }; - private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; @@ -83,7 +68,7 @@ internal sealed class DefaultCSharpCodeActionResolver( var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync( documentContext, csharpTextEdits, - s_defaultFormattingOptions, + RazorFormattingOptions.Default, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs index 15a36e487a..ae238376ce 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs @@ -204,10 +204,11 @@ internal sealed class GenerateMethodCodeActionResolver( if (result is not null) { - var formattingOptions = new FormattingOptions() + var formattingOptions = new RazorFormattingOptions() { TabSize = _razorLSPOptionsMonitor.CurrentValue.TabSize, InsertSpaces = _razorLSPOptionsMonitor.CurrentValue.InsertSpaces, + CodeBlockBraceOnNextLine = _razorLSPOptionsMonitor.CurrentValue.CodeBlockBraceOnNextLine }; var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync( 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 4b4644be46..df02fe2cfe 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 @@ -12,16 +12,19 @@ using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; +using static Nerdbank.Streams.MultiplexingStream; namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; internal class DelegatedCompletionItemResolver( IDocumentContextFactory documentContextFactory, IRazorFormattingService formattingService, + RazorLSPOptionsMonitor optionsMonitor, IClientConnection clientConnection) : CompletionItemResolver { private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; private readonly IRazorFormattingService _formattingService = formattingService; + private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; private readonly IClientConnection _clientConnection = clientConnection; public override async Task ResolveAsync( @@ -111,6 +114,8 @@ internal class DelegatedCompletionItemResolver( return resolvedCompletionItem; } + var options = RazorFormattingOptions.From(formattingOptions, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + if (resolvedCompletionItem.TextEdit is not null) { if (resolvedCompletionItem.TextEdit.Value.TryGetFirst(out var textEdit)) @@ -118,7 +123,7 @@ internal class DelegatedCompletionItemResolver( var formattedTextEdit = await _formattingService.GetCSharpSnippetFormattingEditAsync( documentContext, [textEdit], - formattingOptions, + options, cancellationToken).ConfigureAwait(false); resolvedCompletionItem.TextEdit = formattedTextEdit; @@ -136,7 +141,7 @@ internal class DelegatedCompletionItemResolver( var formattedTextEdit = await _formattingService.GetCSharpSnippetFormattingEditAsync( documentContext, resolvedCompletionItem.AdditionalTextEdits, - formattingOptions, + options, cancellationToken).ConfigureAwait(false); resolvedCompletionItem.AdditionalTextEdits = formattedTextEdit is null ? null : [formattedTextEdit]; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index b1888555f1..90d8e8e368 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -68,7 +68,7 @@ internal static class IServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); 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 003ab22f19..fd61a19455 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs @@ -52,8 +52,10 @@ internal class DocumentFormattingEndpoint( return null; } + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + var htmlEdits = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false); - var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range: null, request.Options, cancellationToken).ConfigureAwait(false); + var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range: null, options, cancellationToken).ConfigureAwait(false); return edits; } } 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 4279c55053..bd8903326a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -117,15 +117,17 @@ internal class DocumentOnTypeFormattingEndpoint( Debug.Assert(request.Character.Length > 0); + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + TextEdit[] formattedEdits; if (triggerCharacterKind == RazorLanguageKind.CSharp) { - formattedEdits = await _razorFormattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); + formattedEdits = await _razorFormattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); } else if (triggerCharacterKind == RazorLanguageKind.Html) { var htmlEdits = await _htmlFormatter.GetOnTypeFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Position, request.Character, request.Options, cancellationToken).ConfigureAwait(false); - formattedEdits = await _razorFormattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); + formattedEdits = await _razorFormattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false); } else { 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 bd27ec5b8f..0cd138bf0c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs @@ -52,8 +52,10 @@ internal class DocumentRangeFormattingEndpoint( return null; } + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + var htmlEdits = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false); - var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, request.Range, request.Options, cancellationToken).ConfigureAwait(false); + var edits = await _razorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, request.Range, options, cancellationToken).ConfigureAwait(false); return edits; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspRazorFormattingPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspRazorFormattingPass.cs deleted file mode 100644 index 49731bf21a..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspRazorFormattingPass.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Formatting; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; - -internal sealed class LspRazorFormattingPass( - IDocumentMappingService documentMappingService, - RazorLSPOptionsMonitor optionsMonitor) - : RazorFormattingPassBase(documentMappingService) -{ - protected override bool CodeBlockBraceOnNextLine => optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine; -} 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 0993fac51f..779cc1eb04 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -29,6 +29,7 @@ internal sealed class InlineCompletionEndpoint( IDocumentMappingService documentMappingService, IClientConnection clientConnection, IAdhocWorkspaceFactory adhocWorkspaceFactory, + RazorLSPOptionsMonitor optionsMonitor, ILoggerFactory loggerFactory) : IRazorRequestHandler, ICapabilitiesProvider { @@ -40,6 +41,7 @@ internal sealed class InlineCompletionEndpoint( private readonly IDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); private readonly IAdhocWorkspaceFactory _adhocWorkspaceFactory = adhocWorkspaceFactory ?? throw new ArgumentNullException(nameof(adhocWorkspaceFactory)); + private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); public bool MutatesSolutionState => false; @@ -123,7 +125,9 @@ internal sealed class InlineCompletionEndpoint( continue; } - using var formattingContext = FormattingContext.Create(request.TextDocument.Uri, documentContext.Snapshot, codeDocument, request.Options, _adhocWorkspaceFactory); + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); + + using var formattingContext = FormattingContext.Create(request.TextDocument.Uri, documentContext.Snapshot, codeDocument, options, _adhocWorkspaceFactory); if (!TryGetSnippetWithAdjustedIndentation(formattingContext, item.Text, hostDocumentIndex, out var newSnippetText)) { continue; @@ -148,7 +152,7 @@ internal sealed class InlineCompletionEndpoint( _logger.LogInformation($"Returning {items.Count} items."); return new VSInternalInlineCompletionList { - Items = items.ToArray() + Items = [.. items] }; } 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 e5329eccbc..b4e5f825ad 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -28,7 +28,7 @@ internal sealed class FormattingContext : IDisposable private IReadOnlyList? _formattingSpans; private IReadOnlyDictionary? _indentations; - private FormattingContext(IAdhocWorkspaceFactory workspaceFactory, Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, FormattingOptions options, + private FormattingContext(IAdhocWorkspaceFactory workspaceFactory, Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, RazorFormattingOptions options, bool isFormatOnType, bool automaticallyAddUsings, int hostDocumentIndex, char triggerCharacter) { _workspaceFactory = workspaceFactory; @@ -47,7 +47,7 @@ internal sealed class FormattingContext : IDisposable public Uri Uri { get; } public IDocumentSnapshot OriginalSnapshot { get; } public RazorCodeDocument CodeDocument { get; } - public FormattingOptions Options { get; } + public RazorFormattingOptions Options { get; } public bool IsFormatOnType { get; } public bool AutomaticallyAddUsings { get; } public int HostDocumentIndex { get; } @@ -86,7 +86,7 @@ internal sealed class FormattingContext : IDisposable { if (_indentations is null) { - var sourceText = this.SourceText; + var sourceText = SourceText; var indentations = new Dictionary(); var previousIndentationLevel = 0; @@ -100,7 +100,7 @@ internal sealed class FormattingContext : IDisposable // The existingIndentation above is measured in characters, and is used to create text edits // The below is measured in columns, so takes into account tab size. This is useful for creating // new indentation strings - var existingIndentationSize = line.GetIndentationSize(this.Options.TabSize); + var existingIndentationSize = line.GetIndentationSize(Options.TabSize); var emptyOrWhitespaceLine = false; if (nonWsPos is null) @@ -179,11 +179,6 @@ internal sealed class FormattingContext : IDisposable private static IReadOnlyList GetFormattingSpans(RazorSyntaxTree syntaxTree, bool inGlobalNamespace) { - if (syntaxTree is null) - { - throw new ArgumentNullException(nameof(syntaxTree)); - } - var visitor = new FormattingVisitor(inGlobalNamespace: inGlobalNamespace); visitor.Visit(syntaxTree.Root); @@ -227,11 +222,10 @@ internal sealed class FormattingContext : IDisposable public bool TryGetFormattingSpan(int absoluteIndex, [NotNullWhen(true)] out FormattingSpan? result) { result = null; - var formattingspans = GetFormattingSpans(); - for (var i = 0; i < formattingspans.Count; i++) + var formattingSpans = GetFormattingSpans(); + foreach (var formattingSpan in formattingSpans) { - var formattingspan = formattingspans[i]; - var span = formattingspan.Span; + var span = formattingSpan.Span; if (span.Start <= absoluteIndex && span.End >= absoluteIndex) { @@ -242,7 +236,7 @@ internal sealed class FormattingContext : IDisposable continue; } - result = formattingspan; + result = formattingSpan; return true; } } @@ -261,11 +255,6 @@ internal sealed class FormattingContext : IDisposable public async Task WithTextAsync(SourceText changedText) { - if (changedText is null) - { - throw new ArgumentNullException(nameof(changedText)); - } - var changedSnapshot = OriginalSnapshot.WithText(changedText); var codeDocument = await changedSnapshot.GetFormatterCodeDocumentAsync().ConfigureAwait(false); @@ -308,7 +297,7 @@ internal sealed class FormattingContext : IDisposable Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, - FormattingOptions options, + RazorFormattingOptions options, IAdhocWorkspaceFactory workspaceFactory, bool automaticallyAddUsings, int hostDocumentIndex, @@ -321,7 +310,7 @@ internal sealed class FormattingContext : IDisposable Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, - FormattingOptions options, + RazorFormattingOptions options, IAdhocWorkspaceFactory workspaceFactory) { return CreateCore(uri, originalSnapshot, codeDocument, options, workspaceFactory, isFormatOnType: false, automaticallyAddUsings: false, hostDocumentIndex: 0, triggerCharacter: '\0'); @@ -331,42 +320,17 @@ internal sealed class FormattingContext : IDisposable Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, - FormattingOptions options, + RazorFormattingOptions options, IAdhocWorkspaceFactory workspaceFactory, bool isFormatOnType, bool automaticallyAddUsings, int hostDocumentIndex, char triggerCharacter) { - if (uri is null) - { - throw new ArgumentNullException(nameof(uri)); - } - - if (originalSnapshot is null) - { - throw new ArgumentNullException(nameof(originalSnapshot)); - } - - if (codeDocument is null) - { - throw new ArgumentNullException(nameof(codeDocument)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (workspaceFactory is null) - { - throw new ArgumentNullException(nameof(workspaceFactory)); - } - // hostDocumentIndex, triggerCharacter and automaticallyAddUsings are only supported in on type formatting Debug.Assert(isFormatOnType || (hostDocumentIndex == 0 && triggerCharacter == '\0' && automaticallyAddUsings == false)); - var result = new FormattingContext( + return new FormattingContext( workspaceFactory, uri, originalSnapshot, @@ -377,7 +341,5 @@ internal sealed class FormattingContext : IDisposable hostDocumentIndex, triggerCharacter ); - - return result; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingOptionsExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingOptionsExtensions.cs deleted file mode 100644 index 63fcae2af9..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingOptionsExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using Microsoft.CodeAnalysis.ExternalAccess.Razor; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.CodeAnalysis.Razor.Formatting; - -internal static class FormattingOptionsExtensions -{ - public static RazorIndentationOptions GetIndentationOptions(this FormattingOptions options) - => new( - UseTabs: !options.InsertSpaces, - TabSize: options.TabSize, - IndentationSize: options.TabSize); -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs index 54bfba4f88..f2893792c9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs @@ -14,20 +14,20 @@ internal interface IRazorFormattingService VersionedDocumentContext documentContext, TextEdit[] htmlEdits, Range? range, - FormattingOptions options, + RazorFormattingOptions options, CancellationToken cancellationToken); Task GetHtmlOnTypeFormattingEditsAsync( DocumentContext documentContext, TextEdit[] htmlEdits, - FormattingOptions options, + RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken); Task GetCSharpOnTypeFormattingEditsAsync( DocumentContext documentContext, - FormattingOptions options, + RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken); @@ -35,18 +35,18 @@ internal interface IRazorFormattingService Task GetSingleCSharpEditAsync( DocumentContext documentContext, TextEdit initialEdit, - FormattingOptions options, + RazorFormattingOptions options, CancellationToken cancellationToken); Task GetCSharpCodeActionEditAsync( DocumentContext documentContext, TextEdit[] formattedEdits, - FormattingOptions options, + RazorFormattingOptions options, CancellationToken cancellationToken); Task GetCSharpSnippetFormattingEditAsync( DocumentContext documentContext, TextEdit[] edits, - FormattingOptions options, + RazorFormattingOptions options, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs new file mode 100644 index 0000000000..f830202a3b --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Formatting; + +internal sealed record RazorFormattingOptions +{ + public bool InsertSpaces { get; init; } + public int TabSize { get; init; } + public bool CodeBlockBraceOnNextLine { get; init; } + + public static RazorFormattingOptions Default => new RazorFormattingOptions() + { + InsertSpaces = true, + TabSize = 4, + CodeBlockBraceOnNextLine = false + }; + + public static RazorFormattingOptions From(FormattingOptions options, bool codeBlockBraceOnNextLine) + { + return new RazorFormattingOptions() + { + InsertSpaces = options.InsertSpaces, + TabSize = options.TabSize, + CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine + }; + } + + public RazorIndentationOptions GetIndentationOptions() + => new( + UseTabs: !InsertSpaces, + TabSize: TabSize, + IndentationSize: TabSize); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs similarity index 99% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPassBase.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs index 883538508d..f19aa15394 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs @@ -22,7 +22,7 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode; -internal abstract class RazorFormattingPassBase( +internal sealed class RazorFormattingPass( IDocumentMappingService documentMappingService) : FormattingPassBase(documentMappingService) { @@ -66,8 +66,6 @@ internal abstract class RazorFormattingPassBase( return new FormattingResult(finalEdits); } - protected abstract bool CodeBlockBraceOnNextLine { get; } - private IEnumerable FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree) { var edits = new List(); @@ -271,7 +269,7 @@ internal abstract class RazorFormattingPassBase( directive.DirectiveDescriptor?.Kind == DirectiveKind.CodeBlock) { // If we're formatting a @code or @functions directive, the user might have indicated they always want a newline - var forceNewLine = CodeBlockBraceOnNextLine && + var forceNewLine = context.Options.CodeBlockBraceOnNextLine && directive.Body is RazorDirectiveBodySyntax { Keyword: { } keyword } && IsCodeOrFunctionsBlock(keyword); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index e3c61860f1..de664da131 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -29,7 +29,7 @@ internal class RazorFormattingService( VersionedDocumentContext documentContext, TextEdit[] htmlEdits, Range? range, - FormattingOptions options, + RazorFormattingOptions options, CancellationToken cancellationToken) { Debug.Assert(_formattingPasses[0] is HtmlFormattingPass, "Formatting requires the first pass to be Html"); @@ -79,25 +79,25 @@ internal class RazorFormattingService( return originalText.NormalizeTextEdits(filteredEdits); } - public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) => ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, [], options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); - public Task GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + public Task GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) => ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.Html, htmlEdits, options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); - public async Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit edit, FormattingOptions options, CancellationToken cancellationToken) + public async Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit edit, RazorFormattingOptions options, CancellationToken cancellationToken) { var formattedEdits = await ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, [edit], options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken).ConfigureAwait(false); return formattedEdits.SingleOrDefault(); } - public async Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] initialEdits, FormattingOptions options, CancellationToken cancellationToken) + public async Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] initialEdits, RazorFormattingOptions options, CancellationToken cancellationToken) { var edits = await ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, initialEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: true, collapseEdits: true, automaticallyAddUsings: true, cancellationToken: cancellationToken).ConfigureAwait(false); return edits.SingleOrDefault(); } - public async Task GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken) + public async Task GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, RazorFormattingOptions options, CancellationToken cancellationToken) { WrapCSharpSnippets(edits); @@ -122,7 +122,7 @@ internal class RazorFormattingService( DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, - FormattingOptions options, + RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, bool bypassValidationPasses, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingPass.cs deleted file mode 100644 index 500d610a99..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingPass.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Formatting; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; - -internal sealed class RemoteRazorFormattingPass( - IDocumentMappingService documentMappingService) - : RazorFormattingPassBase(documentMappingService) -{ - // TODO: properly plumb this through - protected override bool CodeBlockBraceOnNextLine => true; -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj index 3357fd1eb7..60cd914522 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj @@ -57,4 +57,8 @@ + + + + diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/RazorOnAutoInsertProviderTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/RazorOnAutoInsertProviderTestBase.cs index 2d77fa619a..b9fac28aea 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/RazorOnAutoInsertProviderTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/RazorOnAutoInsertProviderTestBase.cs @@ -39,7 +39,7 @@ public abstract class RazorOnAutoInsertProviderTestBase : LanguageServerTestBase var path = "file:///path/to/document.razor"; var uri = new Uri(path); var codeDocument = CreateCodeDocument(source, uri.AbsolutePath, tagHelpers, fileKind: fileKind); - var options = new FormattingOptions() + var options = new RazorFormattingOptions() { TabSize = tabSize, InsertSpaces = insertSpaces, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs index 4bd0ec70df..72219c5e5d 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionResolverTest.cs @@ -191,7 +191,7 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) : rfs => rfs.GetCSharpCodeActionEditAsync( It.Is(c => c.Uri == documentUri), It.IsAny(), - It.IsAny(), + It.IsAny(), It.IsAny()) == Task.FromResult(s_defaultFormattedEdit), MockBehavior.Strict); return razorFormattingService; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs index c84c82b3b9..463d5246c9 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs @@ -80,7 +80,8 @@ public class DelegatedCompletionItemResolverTest : LanguageServerTestBase { // Arrange var server = TestDelegatedCompletionItemResolverServer.Create(); - var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), server); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); + var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), optionsMonitor, server); var item = new VSInternalCompletionItem(); var notContainingCompletionList = new VSInternalCompletionList(); var originalRequestContext = new object(); @@ -98,7 +99,8 @@ public class DelegatedCompletionItemResolverTest : LanguageServerTestBase { // Arrange var server = TestDelegatedCompletionItemResolverServer.Create(); - var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), server); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); + var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), optionsMonitor, server); var item = new VSInternalCompletionItem(); var containingCompletionList = new VSInternalCompletionList() { Items = new[] { item, } }; var originalRequestContext = new object(); @@ -116,7 +118,8 @@ public class DelegatedCompletionItemResolverTest : LanguageServerTestBase { // Arrange var server = TestDelegatedCompletionItemResolverServer.Create(); - var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), server); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); + var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), optionsMonitor, server); var expectedData = new object(); var item = new VSInternalCompletionItem() { @@ -138,7 +141,8 @@ public class DelegatedCompletionItemResolverTest : LanguageServerTestBase { // Arrange var server = TestDelegatedCompletionItemResolverServer.Create(); - var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), server); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); + var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), optionsMonitor, server); var item = new VSInternalCompletionItem(); var containingCompletionList = new VSInternalCompletionList() { Items = new[] { item, }, Data = new object() }; var expectedData = new object(); @@ -201,7 +205,8 @@ public class DelegatedCompletionItemResolverTest : LanguageServerTestBase // Arrange var expectedResolvedItem = new VSInternalCompletionItem(); var server = TestDelegatedCompletionItemResolverServer.Create(expectedResolvedItem); - var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), server); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); + var resolver = new DelegatedCompletionItemResolver(_documentContextFactory, _formattingService.GetValue(), optionsMonitor, server); var item = new VSInternalCompletionItem(); var containingCompletionList = new VSInternalCompletionList() { Items = new[] { item, } }; var originalRequestContext = new DelegatedCompletionResolutionContext(_htmlCompletionParams, new object()); @@ -224,7 +229,8 @@ public class DelegatedCompletionItemResolverTest : LanguageServerTestBase var server = TestDelegatedCompletionItemResolverServer.Create(csharpServer, DisposalToken); var documentContextFactory = new TestDocumentContextFactory("C:/path/to/file.razor", codeDocument, version: 123); - var resolver = new DelegatedCompletionItemResolver(documentContextFactory, _formattingService.GetValue(), server); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); + var resolver = new DelegatedCompletionItemResolver(documentContextFactory, _formattingService.GetValue(), optionsMonitor, server); var (containingCompletionList, csharpCompletionParams) = await GetCompletionListAndOriginalParamsAsync( cursorPosition, codeDocument, csharpServer); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs index a497f70c2b..0bb29ed5bb 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs @@ -28,7 +28,8 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) : formattingService, htmlFormatter, optionsMonitor); var @params = new DocumentRangeFormattingParams() { - TextDocument = new TextDocumentIdentifier { Uri = uri, } + TextDocument = new TextDocumentIdentifier { Uri = uri, }, + Options = new FormattingOptions() }; var requestContext = CreateRazorRequestContext(documentContext); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs index ba5973a36e..76ef36b22f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs @@ -126,7 +126,7 @@ public class Foo { } var path = "file:///path/to/document.razor"; var uri = new Uri(path); var (codeDocument, documentSnapshot) = CreateCodeDocumentAndSnapshot(source, uri.AbsolutePath, fileKind: fileKind); - var options = new FormattingOptions() + var options = new RazorFormattingOptions() { TabSize = tabSize, InsertSpaces = insertSpaces, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs index 60adc04a1f..9f6602f5bf 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs @@ -123,7 +123,7 @@ public class Foo { } var path = "file:///path/to/document.razor"; var uri = new Uri(path); var (codeDocument, documentSnapshot) = CreateCodeDocumentAndSnapshot(source, uri.AbsolutePath, fileKind: fileKind); - var options = new FormattingOptions() + var options = new RazorFormattingOptions() { TabSize = tabSize, InsertSpaces = insertSpaces, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs index e6771a34d0..843e4e07cd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs @@ -37,33 +37,33 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut { public bool Called { get; private set; } - public Task GetDocumentFormattingEditsAsync(VersionedDocumentContext documentContext, TextEdit[] htmlEdits, Range? range, FormattingOptions options, CancellationToken cancellationToken) + public Task GetDocumentFormattingEditsAsync(VersionedDocumentContext documentContext, TextEdit[] htmlEdits, Range? range, RazorFormattingOptions options, CancellationToken cancellationToken) { Called = true; return SpecializedTasks.EmptyArray(); } - public Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + public Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] formattedEdits, RazorFormattingOptions options, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken) + public Task GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, RazorFormattingOptions options, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) + public Task GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) { return Task.FromResult(htmlEdits); } - public Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit initialEdit, FormattingOptions options, CancellationToken cancellationToken) + public Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit initialEdit, RazorFormattingOptions options, CancellationToken cancellationToken) { throw new NotImplementedException(); } 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 559eba75bf..4dfaa96659 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 @@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; @@ -77,6 +78,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase TabSize = tabSize, InsertSpaces = insertSpaces, }; + var razorOptions = RazorFormattingOptions.From(options, codeBlockBraceOnNextLine: razorLSPOptions?.CodeBlockBraceOnNextLine ?? false); var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, razorLSPOptions); var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); @@ -92,7 +94,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken); // Act - var edits = await formattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range, options, DisposalToken); + var edits = await formattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range, razorOptions, DisposalToken); // Assert var edited = ApplyEdits(source, edits); @@ -138,13 +140,15 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase TabSize = tabSize, InsertSpaces = insertSpaces, }; + var razorOptions = RazorFormattingOptions.From(options, codeBlockBraceOnNextLine: razorLSPOptions?.CodeBlockBraceOnNextLine ?? false); + var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); // Act TextEdit[] edits; if (languageKind == RazorLanguageKind.CSharp) { - edits = await formattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); + edits = await formattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, razorOptions, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); } else { @@ -157,7 +161,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var htmlFormatter = new HtmlFormatter(client, versionCache); var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken); - edits = await formattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); + edits = await formattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, razorOptions, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); } // Assert @@ -218,11 +222,12 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase } var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument); - var options = new FormattingOptions() + var options = new RazorFormattingOptions() { TabSize = tabSize, InsertSpaces = insertSpaces, }; + var documentContext = new VersionedDocumentContext(uri, documentSnapshot, projectContext: null, version: 1); // Act diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs index c9efe60bea..971d844206 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs @@ -47,7 +47,7 @@ internal static class TestRazorFormattingService new HtmlFormattingPass(mappingService, loggerFactory), new CSharpFormattingPass(mappingService, loggerFactory), new CSharpOnTypeFormattingPass(mappingService, loggerFactory), - new LspRazorFormattingPass(mappingService, optionsMonitor), + new RazorFormattingPass(mappingService), new FormattingDiagnosticValidationPass(mappingService, loggerFactory), new FormattingContentValidationPass(mappingService, loggerFactory), }; From 2165fcb0c4f373691780f9db6622830ca1296427 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 10:48:10 +1000 Subject: [PATCH 12/26] One last minor cleanup --- .../Formatting/DocumentOnTypeFormattingEndpoint.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 bd8903326a..20488c36a1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -29,9 +29,9 @@ internal class DocumentOnTypeFormattingEndpoint( ILoggerFactory loggerFactory) : IRazorRequestHandler, ICapabilitiesProvider { - private readonly IRazorFormattingService _razorFormattingService = razorFormattingService ?? throw new ArgumentNullException(nameof(razorFormattingService)); - private readonly IDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); - private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); + private readonly IRazorFormattingService _razorFormattingService = razorFormattingService; + private readonly IDocumentMappingService _documentMappingService = documentMappingService; + private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; private readonly IHtmlFormatter _htmlFormatter = htmlFormatter; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); From d6357548f379aed64731b6001b8f549947e600aa Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 12:30:40 +1000 Subject: [PATCH 13/26] Be explicit about formatting passes Always felt like a huge potential bug farm. eg, if Html ended up not being first we'd have bugs, working out the Order property (which was weirdly backwards?) was a pain, and the entire formatting engine produces horrible results if the ordering changes anyway. --- .../IServiceCollectionExtensions.cs | 8 -- .../Formatting/CSharpFormattingPass.cs | 3 - .../Formatting/CSharpFormattingPassBase.cs | 2 - .../FormattingContentValidationPass.cs | 5 - .../FormattingDiagnosticValidationPass.cs | 5 - .../Formatting/FormattingPassBase.cs | 17 ---- .../Formatting/HtmlFormattingPass.cs | 5 - .../Formatting/IFormattingPass.cs | 4 - .../Formatting/RazorFormattingPass.cs | 5 - .../Formatting/RazorFormattingService.cs | 99 ++++++++++++++----- .../TestRazorFormattingService.cs | 15 +-- 11 files changed, 77 insertions(+), 91 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 90d8e8e368..cd986718dc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -62,14 +62,6 @@ internal static class IServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); - // Formatting Passes - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs index e061b305aa..c8da773ee7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs @@ -24,9 +24,6 @@ internal sealed class CSharpFormattingPass( { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - // Run after the HTML and Razor formatter pass. - public override int Order => DefaultOrder - 3; - public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { if (context.IsFormatOnType || result.Kind != RazorLanguageKind.Razor) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPassBase.cs index 27b38a0e31..5077ba59f9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPassBase.cs @@ -30,8 +30,6 @@ internal abstract class CSharpFormattingPassBase : FormattingPassBase protected CSharpFormatter CSharpFormatter { get; } - public override bool IsValidationPass => false; - protected async Task> AdjustIndentationAsync(FormattingContext context, CancellationToken cancellationToken, Range? range = null) { // In this method, the goal is to make final adjustments to the indentation of each line. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs index 7973e9d0df..c4fdd30fa7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs @@ -20,11 +20,6 @@ internal sealed class FormattingContentValidationPass( { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - // We want this to run at the very end. - public override int Order => DefaultOrder + 1000; - - public override bool IsValidationPass => true; - // Internal for testing. internal bool DebugAssertsEnabled { get; set; } = true; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs index 64bf765dd7..68485f670e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs @@ -22,11 +22,6 @@ internal sealed class FormattingDiagnosticValidationPass( { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - // We want this to run at the very end. - public override int Order => DefaultOrder + 1000; - - public override bool IsValidationPass => true; - // Internal for testing. internal bool DebugAssertsEnabled { get; set; } = true; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingPassBase.cs index 535649ec76..b97e536428 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingPassBase.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; @@ -13,28 +12,12 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal abstract class FormattingPassBase(IDocumentMappingService documentMappingService) : IFormattingPass { - protected static readonly int DefaultOrder = 1000; - - public abstract bool IsValidationPass { get; } - - public virtual int Order => DefaultOrder; - protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService; public abstract Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken); protected TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits, RazorLanguageKind projectedKind) { - if (codeDocument is null) - { - throw new ArgumentNullException(nameof(codeDocument)); - } - - if (projectedTextEdits is null) - { - throw new ArgumentNullException(nameof(projectedTextEdits)); - } - if (projectedKind != RazorLanguageKind.CSharp) { // Non C# projections map directly to Razor. No need to remap. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs index 0bedcf7b3f..bad83d3307 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs @@ -22,11 +22,6 @@ internal sealed class HtmlFormattingPass( { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - // We want this to run first because it uses the client HTML formatter. - public override int Order => DefaultOrder - 5; - - public override bool IsValidationPass => false; - public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { var originalText = context.SourceText; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingPass.cs index cebfed8ad8..074ccc19e7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingPass.cs @@ -8,9 +8,5 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal interface IFormattingPass { - int Order { get; } - - bool IsValidationPass { get; } - Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs index f19aa15394..273b072bbe 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs @@ -26,11 +26,6 @@ internal sealed class RazorFormattingPass( IDocumentMappingService documentMappingService) : FormattingPassBase(documentMappingService) { - // Run after the C# formatter pass. - public override int Order => DefaultOrder - 4; - - public override bool IsValidationPass => false; - public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { if (context.IsFormatOnType) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index de664da131..4e66595cea 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -3,12 +3,13 @@ using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -18,12 +19,32 @@ using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal class RazorFormattingService( - IEnumerable formattingPasses, - IAdhocWorkspaceFactory workspaceFactory) : IRazorFormattingService +internal class RazorFormattingService : IRazorFormattingService { - private readonly ImmutableArray _formattingPasses = formattingPasses.OrderByAsArray(f => f.Order); - private readonly IAdhocWorkspaceFactory _workspaceFactory = workspaceFactory; + private readonly IAdhocWorkspaceFactory _workspaceFactory; + + private readonly ImmutableArray _documentFormattingPasses; + private readonly ImmutableArray _validationPasses; + private readonly CSharpOnTypeFormattingPass _csharpOnTypeFormattingPass; + private readonly HtmlFormattingPass _htmlFormattingPass; + + public RazorFormattingService( + IDocumentMappingService documentMappingService, + IAdhocWorkspaceFactory workspaceFactory, + ILoggerFactory loggerFactory) + { + _workspaceFactory = workspaceFactory; + + var cSharpFormattingPass = new CSharpFormattingPass(documentMappingService, loggerFactory); + var razorFormattingPass = new RazorFormattingPass(documentMappingService); + var diagnosticValidationPass = new FormattingDiagnosticValidationPass(documentMappingService, loggerFactory); + var contentValidationPass = new FormattingContentValidationPass(documentMappingService, loggerFactory); + + _htmlFormattingPass = new HtmlFormattingPass(documentMappingService, loggerFactory); + _csharpOnTypeFormattingPass = new CSharpOnTypeFormattingPass(documentMappingService, loggerFactory); + _validationPasses = [diagnosticValidationPass, contentValidationPass]; + _documentFormattingPasses = [_htmlFormattingPass, razorFormattingPass, cSharpFormattingPass, .. _validationPasses]; + } public async Task GetDocumentFormattingEditsAsync( VersionedDocumentContext documentContext, @@ -32,8 +53,6 @@ internal class RazorFormattingService( RazorFormattingOptions options, CancellationToken cancellationToken) { - Debug.Assert(_formattingPasses[0] is HtmlFormattingPass, "Formatting requires the first pass to be Html"); - var codeDocument = await documentContext.Snapshot.GetFormatterCodeDocumentAsync().ConfigureAwait(false); // Range formatting happens on every paste, and if there are Razor diagnostics in the file @@ -66,7 +85,8 @@ internal class RazorFormattingService( var originalText = context.SourceText; var result = new FormattingResult(htmlEdits); - foreach (var pass in _formattingPasses) + + foreach (var pass in _documentFormattingPasses) { cancellationToken.ThrowIfCancellationRequested(); result = await pass.ExecuteAsync(context, result, cancellationToken).ConfigureAwait(false); @@ -80,20 +100,60 @@ internal class RazorFormattingService( } public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) - => ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, [], options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); + => ApplyFormattedEditsAsync( + documentContext, + RazorLanguageKind.CSharp, + formattedEdits: [], + options, + hostDocumentIndex, + triggerCharacter, + [_csharpOnTypeFormattingPass, .. _validationPasses], + collapseEdits: false, + automaticallyAddUsings: false, + cancellationToken: cancellationToken); public Task GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) - => ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.Html, htmlEdits, options, hostDocumentIndex, triggerCharacter, bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); + => ApplyFormattedEditsAsync( + documentContext, + RazorLanguageKind.Html, + htmlEdits, + options, + hostDocumentIndex, + triggerCharacter, + [_htmlFormattingPass, .. _validationPasses], + collapseEdits: false, + automaticallyAddUsings: false, + cancellationToken: cancellationToken); public async Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit edit, RazorFormattingOptions options, CancellationToken cancellationToken) { - var formattedEdits = await ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, [edit], options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: false, collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken).ConfigureAwait(false); + var formattedEdits = await ApplyFormattedEditsAsync( + documentContext, + RazorLanguageKind.CSharp, + [edit], + options, + hostDocumentIndex: 0, + triggerCharacter: '\0', + [_csharpOnTypeFormattingPass, .. _validationPasses], + collapseEdits: false, + automaticallyAddUsings: false, + cancellationToken: cancellationToken).ConfigureAwait(false); return formattedEdits.SingleOrDefault(); } public async Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] initialEdits, RazorFormattingOptions options, CancellationToken cancellationToken) { - var edits = await ApplyFormattedEditsAsync(documentContext, RazorLanguageKind.CSharp, initialEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', bypassValidationPasses: true, collapseEdits: true, automaticallyAddUsings: true, cancellationToken: cancellationToken).ConfigureAwait(false); + var edits = await ApplyFormattedEditsAsync( + documentContext, + RazorLanguageKind.CSharp, + initialEdits, + options, + hostDocumentIndex: 0, + triggerCharacter: '\0', + [_csharpOnTypeFormattingPass], + collapseEdits: true, + automaticallyAddUsings: true, + cancellationToken: cancellationToken).ConfigureAwait(false); return edits.SingleOrDefault(); } @@ -108,7 +168,7 @@ internal class RazorFormattingService( options, hostDocumentIndex: 0, triggerCharacter: '\0', - bypassValidationPasses: true, + [_csharpOnTypeFormattingPass], collapseEdits: true, automaticallyAddUsings: false, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -125,13 +185,11 @@ internal class RazorFormattingService( RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, - bool bypassValidationPasses, + ImmutableArray formattingPasses, bool collapseEdits, bool automaticallyAddUsings, CancellationToken cancellationToken) { - Debug.Assert(_formattingPasses[0] is HtmlFormattingPass, "Formatting requires the first pass to be Html"); - // If we only received a single edit, let's always return a single edit back. // Otherwise, merge only if explicitly asked. collapseEdits |= formattedEdits.Length == 1; @@ -142,13 +200,8 @@ internal class RazorFormattingService( using var context = FormattingContext.CreateForOnTypeFormatting(uri, documentSnapshot, codeDocument, options, _workspaceFactory, automaticallyAddUsings: automaticallyAddUsings, hostDocumentIndex, triggerCharacter); var result = new FormattingResult(formattedEdits, kind); - foreach (var pass in _formattingPasses) + foreach (var pass in formattingPasses) { - if (pass.IsValidationPass && bypassValidationPasses) - { - continue; - } - cancellationToken.ThrowIfCancellationRequested(); result = await pass.ExecuteAsync(context, result, cancellationToken).ConfigureAwait(false); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs index 971d844206..01152568ed 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs @@ -1,18 +1,15 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.LanguageServer.Test; -using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Moq; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; @@ -42,16 +39,6 @@ internal static class TestRazorFormattingService await optionsMonitor.UpdateAsync(CancellationToken.None); } - var passes = new List() - { - new HtmlFormattingPass(mappingService, loggerFactory), - new CSharpFormattingPass(mappingService, loggerFactory), - new CSharpOnTypeFormattingPass(mappingService, loggerFactory), - new RazorFormattingPass(mappingService), - new FormattingDiagnosticValidationPass(mappingService, loggerFactory), - new FormattingContentValidationPass(mappingService, loggerFactory), - }; - - return new RazorFormattingService(passes, TestAdhocWorkspaceFactory.Instance); + return new RazorFormattingService(mappingService, TestAdhocWorkspaceFactory.Instance, loggerFactory); } } From 4e1211718ae3b0d522b66f60707e24012242b188 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 12:31:10 +1000 Subject: [PATCH 14/26] Move files to their own folder --- .../Formatting/{ => Passes}/CSharpFormattingPass.cs | 0 .../Formatting/{ => Passes}/CSharpFormattingPassBase.cs | 0 .../Formatting/{ => Passes}/CSharpOnTypeFormattingPass.cs | 0 .../Formatting/{ => Passes}/FormattingContentValidationPass.cs | 0 .../Formatting/{ => Passes}/FormattingDiagnosticValidationPass.cs | 0 .../Formatting/{ => Passes}/FormattingPassBase.cs | 0 .../Formatting/{ => Passes}/HtmlFormattingPass.cs | 0 .../Formatting/{ => Passes}/RazorFormattingPass.cs | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{ => Passes}/CSharpFormattingPass.cs (100%) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{ => Passes}/CSharpFormattingPassBase.cs (100%) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{ => Passes}/CSharpOnTypeFormattingPass.cs (100%) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{ => Passes}/FormattingContentValidationPass.cs (100%) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{ => Passes}/FormattingDiagnosticValidationPass.cs (100%) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{ => Passes}/FormattingPassBase.cs (100%) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{ => Passes}/HtmlFormattingPass.cs (100%) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/{ => Passes}/RazorFormattingPass.cs (100%) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs similarity index 100% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPass.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs similarity index 100% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormattingPassBase.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs similarity index 100% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpOnTypeFormattingPass.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs similarity index 100% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContentValidationPass.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs similarity index 100% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingDiagnosticValidationPass.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingPassBase.cs similarity index 100% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingPassBase.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingPassBase.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs similarity index 100% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/HtmlFormattingPass.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPass.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs similarity index 100% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingPass.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs From 6f1c6ef3988cd6ed11ad9d5ee2f2ea09936a0b0d Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 14:43:59 +1000 Subject: [PATCH 15/26] Remove unnecessary base class --- .../Passes/CSharpFormattingPassBase.cs | 11 +++--- .../Passes/CSharpOnTypeFormattingPass.cs | 18 ++++++++++ .../Passes/FormattingContentValidationPass.cs | 8 ++--- .../FormattingDiagnosticValidationPass.cs | 8 ++--- .../Formatting/Passes/FormattingPassBase.cs | 36 ------------------- .../Formatting/Passes/HtmlFormattingPass.cs | 8 ++--- .../Formatting/Passes/RazorFormattingPass.cs | 9 ++--- .../Formatting/RazorFormattingService.cs | 8 ++--- .../FormattingContentValidationPassTest.cs | 4 +-- .../FormattingDiagnosticValidationPassTest.cs | 4 +-- 10 files changed, 37 insertions(+), 77 deletions(-) delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingPassBase.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs index 5077ba59f9..890a7a6e02 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs @@ -20,15 +20,12 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; -internal abstract class CSharpFormattingPassBase : FormattingPassBase +internal abstract class CSharpFormattingPassBase(IDocumentMappingService documentMappingService) : IFormattingPass { - protected CSharpFormattingPassBase(IDocumentMappingService documentMappingService) - : base(documentMappingService) - { - CSharpFormatter = new CSharpFormatter(documentMappingService); - } + protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService; + protected CSharpFormatter CSharpFormatter { get; } = new CSharpFormatter(documentMappingService); - protected CSharpFormatter CSharpFormatter { get; } + public abstract Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken); protected async Task> AdjustIndentationAsync(FormattingContext context, CancellationToken cancellationToken, Range? range = null) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs index 1d96525057..a86b3ae490 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs @@ -205,6 +205,24 @@ internal sealed class CSharpOnTypeFormattingPass( return new FormattingResult(finalEdits); } + private TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits, RazorLanguageKind projectedKind) + { + if (projectedKind != RazorLanguageKind.CSharp) + { + // Non C# projections map directly to Razor. No need to remap. + return projectedTextEdits; + } + + if (codeDocument.IsUnsupported()) + { + return []; + } + + var edits = DocumentMappingService.GetHostDocumentEdits(codeDocument.GetCSharpDocument(), projectedTextEdits); + + return edits; + } + private static async Task AddUsingStatementEditsIfNecessaryAsync(FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken) { if (context.AutomaticallyAddUsings) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs index c4fdd30fa7..76bd0e286f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; @@ -13,17 +12,14 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed class FormattingContentValidationPass( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - : FormattingPassBase(documentMappingService) +internal sealed class FormattingContentValidationPass(ILoggerFactory loggerFactory) : IFormattingPass { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); // Internal for testing. internal bool DebugAssertsEnabled { get; set; } = true; - public override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { if (result.Kind != RazorLanguageKind.Razor) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs index 68485f670e..a7824ea711 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs @@ -8,24 +8,20 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed class FormattingDiagnosticValidationPass( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - : FormattingPassBase(documentMappingService) +internal sealed class FormattingDiagnosticValidationPass(ILoggerFactory loggerFactory) : IFormattingPass { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); // Internal for testing. internal bool DebugAssertsEnabled { get; set; } = true; - public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { if (result.Kind != RazorLanguageKind.Razor) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingPassBase.cs deleted file mode 100644 index b97e536428..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingPassBase.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.CodeAnalysis.Razor.Formatting; - -internal abstract class FormattingPassBase(IDocumentMappingService documentMappingService) : IFormattingPass -{ - protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService; - - public abstract Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken); - - protected TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits, RazorLanguageKind projectedKind) - { - if (projectedKind != RazorLanguageKind.CSharp) - { - // Non C# projections map directly to Razor. No need to remap. - return projectedTextEdits; - } - - if (codeDocument.IsUnsupported()) - { - return []; - } - - var edits = DocumentMappingService.GetHostDocumentEdits(codeDocument.GetCSharpDocument(), projectedTextEdits); - - return edits; - } -} 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 bad83d3307..e8ec8227a9 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 @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; @@ -15,14 +14,11 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed class HtmlFormattingPass( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - : FormattingPassBase(documentMappingService) +internal sealed class HtmlFormattingPass(ILoggerFactory loggerFactory) : IFormattingPass { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { var originalText = context.SourceText; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs index 273b072bbe..7e16f09397 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -22,11 +21,9 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode; -internal sealed class RazorFormattingPass( - IDocumentMappingService documentMappingService) - : FormattingPassBase(documentMappingService) +internal sealed class RazorFormattingPass : IFormattingPass { - public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { if (context.IsFormatOnType) { @@ -244,7 +241,7 @@ internal sealed class RazorFormattingPass( return false; } - private void TryFormatCSharpBlockStructure(FormattingContext context, List edits, RazorSourceDocument source, SyntaxNode node) + private static void TryFormatCSharpBlockStructure(FormattingContext context, List edits, RazorSourceDocument source, SyntaxNode node) { // We're looking for a code block like this: // diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 4e66595cea..fb56921a88 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -36,11 +36,11 @@ internal class RazorFormattingService : IRazorFormattingService _workspaceFactory = workspaceFactory; var cSharpFormattingPass = new CSharpFormattingPass(documentMappingService, loggerFactory); - var razorFormattingPass = new RazorFormattingPass(documentMappingService); - var diagnosticValidationPass = new FormattingDiagnosticValidationPass(documentMappingService, loggerFactory); - var contentValidationPass = new FormattingContentValidationPass(documentMappingService, loggerFactory); + var razorFormattingPass = new RazorFormattingPass(); + var diagnosticValidationPass = new FormattingDiagnosticValidationPass(loggerFactory); + var contentValidationPass = new FormattingContentValidationPass(loggerFactory); - _htmlFormattingPass = new HtmlFormattingPass(documentMappingService, loggerFactory); + _htmlFormattingPass = new HtmlFormattingPass(loggerFactory); _csharpOnTypeFormattingPass = new CSharpOnTypeFormattingPass(documentMappingService, loggerFactory); _validationPasses = [diagnosticValidationPass, contentValidationPass]; _documentFormattingPasses = [_htmlFormattingPass, razorFormattingPass, cSharpFormattingPass, .. _validationPasses]; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs index 76ef36b22f..42688f3992 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs @@ -111,9 +111,7 @@ public class Foo { } private FormattingContentValidationPass GetPass() { - var mappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); - - var pass = new FormattingContentValidationPass(mappingService, LoggerFactory) + var pass = new FormattingContentValidationPass(LoggerFactory) { DebugAssertsEnabled = false }; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs index 9f6602f5bf..2865555416 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs @@ -108,9 +108,7 @@ public class Foo { } private FormattingDiagnosticValidationPass GetPass() { - var mappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); - - var pass = new FormattingDiagnosticValidationPass(mappingService, LoggerFactory) + var pass = new FormattingDiagnosticValidationPass(LoggerFactory) { DebugAssertsEnabled = false }; From f74150db382c95a676bd4ca1b21246eede907304 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 15:12:12 +1000 Subject: [PATCH 16/26] Separate html formatting passes, so we can remove the IsFormatOnType flag --- .../Formatting/CSharpFormatter.cs | 21 ---------- .../Formatting/FormattingContext.cs | 42 +++++++------------ .../Formatting/Passes/CSharpFormattingPass.cs | 12 +++--- .../Passes/CSharpFormattingPassBase.cs | 22 +++++----- .../Passes/CSharpOnTypeFormattingPass.cs | 8 +--- .../Formatting/Passes/HtmlFormattingPass.cs | 38 ++++++++++------- .../Formatting/Passes/RazorFormattingPass.cs | 42 ++++++++----------- .../Formatting/RazorFormattingService.cs | 25 ++++++----- 8 files changed, 89 insertions(+), 121 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs index fbef2ed9e2..defa18e346 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -27,16 +26,6 @@ internal sealed class CSharpFormatter(IDocumentMappingService documentMappingSer public async Task FormatAsync(FormattingContext context, Range rangeToFormat, CancellationToken cancellationToken) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (rangeToFormat is null) - { - throw new ArgumentNullException(nameof(rangeToFormat)); - } - if (!_documentMappingService.TryMapToGeneratedDocumentRange(context.CodeDocument.GetCSharpDocument(), rangeToFormat, out var projectedRange)) { return []; @@ -52,16 +41,6 @@ internal sealed class CSharpFormatter(IDocumentMappingService documentMappingSer IReadOnlyCollection projectedDocumentLocations, CancellationToken cancellationToken) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (projectedDocumentLocations is null) - { - throw new ArgumentNullException(nameof(projectedDocumentLocations)); - } - // Sorting ensures we count the marker offsets correctly. // We also want to ensure there are no duplicates to avoid duplicate markers. var filteredLocations = projectedDocumentLocations.Distinct().OrderBy(l => l).ToList(); 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 b4e5f825ad..328bfada28 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -29,14 +29,13 @@ internal sealed class FormattingContext : IDisposable private IReadOnlyDictionary? _indentations; private FormattingContext(IAdhocWorkspaceFactory workspaceFactory, Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, RazorFormattingOptions options, - bool isFormatOnType, bool automaticallyAddUsings, int hostDocumentIndex, char triggerCharacter) + bool automaticallyAddUsings, int hostDocumentIndex, char triggerCharacter) { _workspaceFactory = workspaceFactory; Uri = uri; OriginalSnapshot = originalSnapshot; CodeDocument = codeDocument; Options = options; - IsFormatOnType = isFormatOnType; AutomaticallyAddUsings = automaticallyAddUsings; HostDocumentIndex = hostDocumentIndex; TriggerCharacter = triggerCharacter; @@ -48,7 +47,6 @@ internal sealed class FormattingContext : IDisposable public IDocumentSnapshot OriginalSnapshot { get; } public RazorCodeDocument CodeDocument { get; } public RazorFormattingOptions Options { get; } - public bool IsFormatOnType { get; } public bool AutomaticallyAddUsings { get; } public int HostDocumentIndex { get; } public char TriggerCharacter { get; } @@ -267,7 +265,6 @@ internal sealed class FormattingContext : IDisposable OriginalSnapshot, codeDocument, Options, - IsFormatOnType, AutomaticallyAddUsings, HostDocumentIndex, TriggerCharacter); @@ -303,7 +300,16 @@ internal sealed class FormattingContext : IDisposable int hostDocumentIndex, char triggerCharacter) { - return CreateCore(uri, originalSnapshot, codeDocument, options, workspaceFactory, isFormatOnType: true, automaticallyAddUsings, hostDocumentIndex, triggerCharacter); + return new FormattingContext( + workspaceFactory, + uri, + originalSnapshot, + codeDocument, + options, + automaticallyAddUsings, + hostDocumentIndex, + triggerCharacter + ); } public static FormattingContext Create( @@ -313,33 +319,15 @@ internal sealed class FormattingContext : IDisposable RazorFormattingOptions options, IAdhocWorkspaceFactory workspaceFactory) { - return CreateCore(uri, originalSnapshot, codeDocument, options, workspaceFactory, isFormatOnType: false, automaticallyAddUsings: false, hostDocumentIndex: 0, triggerCharacter: '\0'); - } - - private static FormattingContext CreateCore( - Uri uri, - IDocumentSnapshot originalSnapshot, - RazorCodeDocument codeDocument, - RazorFormattingOptions options, - IAdhocWorkspaceFactory workspaceFactory, - bool isFormatOnType, - bool automaticallyAddUsings, - int hostDocumentIndex, - char triggerCharacter) - { - // hostDocumentIndex, triggerCharacter and automaticallyAddUsings are only supported in on type formatting - Debug.Assert(isFormatOnType || (hostDocumentIndex == 0 && triggerCharacter == '\0' && automaticallyAddUsings == false)); - return new FormattingContext( workspaceFactory, uri, originalSnapshot, codeDocument, options, - isFormatOnType, - automaticallyAddUsings, - hostDocumentIndex, - triggerCharacter - ); + automaticallyAddUsings: false, + hostDocumentIndex: 0, + triggerCharacter: '\0' + ); } } 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 c8da773ee7..03f4f3b79a 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 @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -20,17 +21,14 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal sealed class CSharpFormattingPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) - : CSharpFormattingPassBase(documentMappingService) + : CSharpFormattingPassBase(documentMappingService, isFormatOnType: false) { + private readonly CSharpFormatter _csharpFormatter = new CSharpFormatter(documentMappingService); private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { - if (context.IsFormatOnType || result.Kind != RazorLanguageKind.Razor) - { - // We don't want to handle OnTypeFormatting here. - return result; - } + Debug.Assert(result.Kind == RazorLanguageKind.Razor); // Apply previous edits if any. var originalText = context.SourceText; @@ -91,7 +89,7 @@ internal sealed class CSharpFormattingPass( // These should already be remapped. var range = sourceText.GetRange(span); - var edits = await CSharpFormatter.FormatAsync(context, range, cancellationToken).ConfigureAwait(false); + var edits = await _csharpFormatter.FormatAsync(context, range, cancellationToken).ConfigureAwait(false); csharpEdits.AddRange(edits.Where(e => range.Contains(e.Range))); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs index 890a7a6e02..6941dbf493 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs @@ -15,15 +15,15 @@ using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; +using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; namespace Microsoft.CodeAnalysis.Razor.Formatting; -using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; - -internal abstract class CSharpFormattingPassBase(IDocumentMappingService documentMappingService) : IFormattingPass +internal abstract class CSharpFormattingPassBase(IDocumentMappingService documentMappingService, bool isFormatOnType) : IFormattingPass { + private readonly bool _isFormatOnType = isFormatOnType; + protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService; - protected CSharpFormatter CSharpFormatter { get; } = new CSharpFormatter(documentMappingService); public abstract Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken); @@ -250,7 +250,7 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen if (indentations[i].StartsInHtmlContext) { // This is a non-C# line. - if (context.IsFormatOnType) + if (_isFormatOnType) { // HTML formatter doesn't run in the case of format on type. // Let's stick with our syntax understanding of HTML to figure out the desired indentation. @@ -291,13 +291,13 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen protected static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements) => ShouldFormat(context, mappingSpan, allowImplicitStatements, out _); - protected static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements, out SyntaxNode? foundOwner) + protected static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements, out RazorSyntaxNode? foundOwner) => ShouldFormat(context, mappingSpan, new ShouldFormatOptions(allowImplicitStatements, isLineRequest: false), out foundOwner); private static bool ShouldFormatLine(FormattingContext context, TextSpan mappingSpan, bool allowImplicitStatements) => ShouldFormat(context, mappingSpan, new ShouldFormatOptions(allowImplicitStatements, isLineRequest: true), out _); - private static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, ShouldFormatOptions options, out SyntaxNode? foundOwner) + private static bool ShouldFormat(FormattingContext context, TextSpan mappingSpan, ShouldFormatOptions options, out RazorSyntaxNode? foundOwner) { // We should be called with the range of various C# SourceMappings. @@ -437,10 +437,10 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen return owner is MarkupTextLiteralSyntax { - Parent: MarkupTagHelperAttributeSyntax { TagHelperAttributeInfo: { Bound: true } } or - MarkupTagHelperDirectiveAttributeSyntax { TagHelperAttributeInfo: { Bound: true } } or - MarkupMinimizedTagHelperAttributeSyntax { TagHelperAttributeInfo: { Bound: true } } or - MarkupMinimizedTagHelperDirectiveAttributeSyntax { TagHelperAttributeInfo: { Bound: true } } + Parent: MarkupTagHelperAttributeSyntax { TagHelperAttributeInfo.Bound: true } or + MarkupTagHelperDirectiveAttributeSyntax { TagHelperAttributeInfo.Bound: true } or + MarkupMinimizedTagHelperAttributeSyntax { TagHelperAttributeInfo.Bound: true } or + MarkupMinimizedTagHelperDirectiveAttributeSyntax { TagHelperAttributeInfo.Bound: true } } && !options.IsLineRequest; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs index a86b3ae490..77d319406d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs @@ -26,17 +26,13 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal sealed class CSharpOnTypeFormattingPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) - : CSharpFormattingPassBase(documentMappingService) + : CSharpFormattingPassBase(documentMappingService, isFormatOnType: true) { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { - if (!context.IsFormatOnType || result.Kind != RazorLanguageKind.CSharp) - { - // We don't want to handle regular formatting or non-C# on type formatting here. - return result; - } + Debug.Assert(result.Kind == RazorLanguageKind.CSharp); // Normalize and re-map the C# edits. var codeDocument = context.CodeDocument; 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 e8ec8227a9..c65fc754f1 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 @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,20 +15,34 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed class HtmlFormattingPass(ILoggerFactory loggerFactory) : IFormattingPass +internal sealed class HtmlFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger()) { - private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); +} - public async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) +internal sealed class HtmlOnTypeFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger()) +{ + public override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + { + Debug.Assert(result.Kind == RazorLanguageKind.Html); + + if (result.Edits.Length == 0) + { + // There are no HTML edits for us to apply. No op. + return Task.FromResult(new FormattingResult([])); + } + + return base.ExecuteAsync(context, result, cancellationToken); + } +} + +internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass +{ + private readonly ILogger _logger = logger; + + public virtual async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { var originalText = context.SourceText; - if (context.IsFormatOnType && result.Kind != RazorLanguageKind.Html) - { - // We don't want to handle on type formatting requests for other languages - return result; - } - var htmlEdits = result.Edits; var changedText = originalText; @@ -44,11 +59,6 @@ internal sealed class HtmlFormattingPass(ILoggerFactory loggerFactory) : IFormat _logger.LogTestOnly($"After normalizedEdits:\r\n{changedText}"); } - else if (context.IsFormatOnType) - { - // There are no HTML edits for us to apply. No op. - return new FormattingResult(htmlEdits); - } var indentationChanges = AdjustRazorIndentation(changedContext); if (indentationChanges.Count > 0) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs index 7e16f09397..520a720ee7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs @@ -13,24 +13,16 @@ using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; - using RazorRazorSyntaxNodeList = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxList; +using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; using RazorSyntaxNodeList = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxList; namespace Microsoft.CodeAnalysis.Razor.Formatting; -using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode; - internal sealed class RazorFormattingPass : IFormattingPass { public async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { - if (context.IsFormatOnType) - { - // We don't want to handle OnTypeFormatting here. - return result; - } - // Apply previous edits if any. var originalText = context.SourceText; var changedText = originalText; @@ -58,7 +50,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return new FormattingResult(finalEdits); } - private IEnumerable FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree) + private static IEnumerable FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree) { var edits = new List(); var source = syntaxTree.Source; @@ -75,7 +67,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return edits; } - private static void TryFormatBlocks(FormattingContext context, List edits, RazorSourceDocument source, SyntaxNode node) + private static void TryFormatBlocks(FormattingContext context, List edits, RazorSourceDocument source, RazorSyntaxNode node) { // We only want to run one of these _ = TryFormatFunctionsBlock(context, edits, source, node) || @@ -85,7 +77,7 @@ internal sealed class RazorFormattingPass : IFormattingPass TryFormatSectionBlock(context, edits, source, node); } - private static bool TryFormatSectionBlock(FormattingContext context, List edits, RazorSourceDocument source, SyntaxNode node) + private static bool TryFormatSectionBlock(FormattingContext context, List edits, RazorSourceDocument source, RazorSyntaxNode node) { // @section Goo { // } @@ -142,7 +134,7 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - private static bool TryFormatFunctionsBlock(FormattingContext context, IList edits, RazorSourceDocument source, SyntaxNode node) + private static bool TryFormatFunctionsBlock(FormattingContext context, IList edits, RazorSourceDocument source, RazorSyntaxNode node) { // @functions // { @@ -180,7 +172,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return false; } - private static bool TryFormatCSharpExplicitTransition(FormattingContext context, IList edits, RazorSourceDocument source, SyntaxNode node) + private static bool TryFormatCSharpExplicitTransition(FormattingContext context, IList edits, RazorSourceDocument source, RazorSyntaxNode node) { // We're looking for a code block like this: // @@ -201,7 +193,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return false; } - private static bool TryFormatComplexCSharpBlock(FormattingContext context, IList edits, RazorSourceDocument source, SyntaxNode node) + private static bool TryFormatComplexCSharpBlock(FormattingContext context, IList edits, RazorSourceDocument source, RazorSyntaxNode node) { // complex situations like // @{ @@ -223,7 +215,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return false; } - private static bool TryFormatHtmlInCSharp(FormattingContext context, IList edits, RazorSourceDocument source, SyntaxNode node) + private static bool TryFormatHtmlInCSharp(FormattingContext context, IList edits, RazorSourceDocument source, RazorSyntaxNode node) { // void Method() // { @@ -241,7 +233,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return false; } - private static void TryFormatCSharpBlockStructure(FormattingContext context, List edits, RazorSourceDocument source, SyntaxNode node) + private static void TryFormatCSharpBlockStructure(FormattingContext context, List edits, RazorSourceDocument source, RazorSyntaxNode node) { // We're looking for a code block like this: // @@ -301,7 +293,7 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - private static void TryFormatSingleLineDirective(List edits, RazorSourceDocument source, SyntaxNode node) + private static void TryFormatSingleLineDirective(List edits, RazorSourceDocument source, RazorSyntaxNode node) { // Looking for single line directives like // @@ -322,7 +314,7 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - static bool IsSingleLineDirective(SyntaxNode node, out RazorSyntaxNodeList children) + static bool IsSingleLineDirective(RazorSyntaxNode node, out RazorSyntaxNodeList children) { if (node is CSharpCodeBlockSyntax content && node.Parent?.Parent is RazorDirectiveSyntax directive && @@ -337,7 +329,7 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - private static void FormatWhitespaceBetweenDirectiveAndBrace(SyntaxNode node, RazorDirectiveSyntax directive, List edits, RazorSourceDocument source, FormattingContext context, bool forceNewLine) + private static void FormatWhitespaceBetweenDirectiveAndBrace(RazorSyntaxNode node, RazorDirectiveSyntax directive, List edits, RazorSourceDocument source, FormattingContext context, bool forceNewLine) { if (node.ContainsOnlyWhitespace(includingNewLines: false) && !forceNewLine) { @@ -356,7 +348,7 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - private static void ShrinkToSingleSpace(SyntaxNode node, List edits, RazorSourceDocument source) + private static void ShrinkToSingleSpace(RazorSyntaxNode node, List edits, RazorSourceDocument source) { // If there is anything other than one single space then we replace with one space between directive and brace. // @@ -365,7 +357,7 @@ internal sealed class RazorFormattingPass : IFormattingPass edits.Add(edit); } - private static bool FormatBlock(FormattingContext context, RazorSourceDocument source, SyntaxNode? directiveNode, SyntaxNode openBraceNode, SyntaxNode codeNode, SyntaxNode closeBraceNode, IList edits) + private static bool FormatBlock(FormattingContext context, RazorSourceDocument source, RazorSyntaxNode? directiveNode, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode, RazorSyntaxNode closeBraceNode, IList edits) { var didFormat = false; @@ -426,7 +418,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return hasBeenModified; } - static int GetAdditionalIndentationLevel(FormattingContext context, Range range, SyntaxNode openBraceNode, SyntaxNode codeNode) + static int GetAdditionalIndentationLevel(FormattingContext context, Range range, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode) { if (!context.TryGetIndentationLevel(codeNode.Position, out var desiredIndentationLevel)) { @@ -441,7 +433,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return desiredIndentationOffset - currentIndentationOffset; - static int GetLeadingWhitespaceLength(SyntaxNode node, FormattingContext context) + static int GetLeadingWhitespaceLength(RazorSyntaxNode node, FormattingContext context) { var tokens = node.GetTokens(); var whitespaceLength = 0; @@ -473,7 +465,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return whitespaceLength; } - static int GetTrailingWhitespaceLength(SyntaxNode node, FormattingContext context) + static int GetTrailingWhitespaceLength(RazorSyntaxNode node, FormattingContext context) { var tokens = node.GetTokens(); var whitespaceLength = 0; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index fb56921a88..a8f5e2542b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -26,7 +26,7 @@ internal class RazorFormattingService : IRazorFormattingService private readonly ImmutableArray _documentFormattingPasses; private readonly ImmutableArray _validationPasses; private readonly CSharpOnTypeFormattingPass _csharpOnTypeFormattingPass; - private readonly HtmlFormattingPass _htmlFormattingPass; + private readonly HtmlOnTypeFormattingPass _htmlOnTypeFormattingPass; public RazorFormattingService( IDocumentMappingService documentMappingService, @@ -35,15 +35,20 @@ internal class RazorFormattingService : IRazorFormattingService { _workspaceFactory = workspaceFactory; - var cSharpFormattingPass = new CSharpFormattingPass(documentMappingService, loggerFactory); - var razorFormattingPass = new RazorFormattingPass(); - var diagnosticValidationPass = new FormattingDiagnosticValidationPass(loggerFactory); - var contentValidationPass = new FormattingContentValidationPass(loggerFactory); - - _htmlFormattingPass = new HtmlFormattingPass(loggerFactory); + _htmlOnTypeFormattingPass = new HtmlOnTypeFormattingPass(loggerFactory); _csharpOnTypeFormattingPass = new CSharpOnTypeFormattingPass(documentMappingService, loggerFactory); - _validationPasses = [diagnosticValidationPass, contentValidationPass]; - _documentFormattingPasses = [_htmlFormattingPass, razorFormattingPass, cSharpFormattingPass, .. _validationPasses]; + _validationPasses = + [ + new FormattingDiagnosticValidationPass(loggerFactory), + new FormattingContentValidationPass(loggerFactory) + ]; + _documentFormattingPasses = + [ + new HtmlFormattingPass(loggerFactory), + new RazorFormattingPass(), + new CSharpFormattingPass(documentMappingService, loggerFactory), + .. _validationPasses + ]; } public async Task GetDocumentFormattingEditsAsync( @@ -120,7 +125,7 @@ internal class RazorFormattingService : IRazorFormattingService options, hostDocumentIndex, triggerCharacter, - [_htmlFormattingPass, .. _validationPasses], + [_htmlOnTypeFormattingPass, .. _validationPasses], collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken); From ef44f4fe61d22c31e3ceea649d51ed76b60187bf Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 15:12:55 +1000 Subject: [PATCH 17/26] Separate out the files in the Html formatter passes. Doing this separately, and purely mechanically, so make review easier --- .../Formatting/Passes/HtmlFormattingPass.cs | 201 ------------------ .../Passes/HtmlFormattingPassBase.cs | 189 ++++++++++++++++ .../Passes/HtmlOnTypeFormattingPass.cs | 26 +++ 3 files changed, 215 insertions(+), 201 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs 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 c65fc754f1..0a7800d816 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 @@ -1,211 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; internal sealed class HtmlFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger()) { } - -internal sealed class HtmlOnTypeFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger()) -{ - public override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) - { - Debug.Assert(result.Kind == RazorLanguageKind.Html); - - if (result.Edits.Length == 0) - { - // There are no HTML edits for us to apply. No op. - return Task.FromResult(new FormattingResult([])); - } - - return base.ExecuteAsync(context, result, cancellationToken); - } -} - -internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass -{ - private readonly ILogger _logger = logger; - - public virtual async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) - { - var originalText = context.SourceText; - - var htmlEdits = result.Edits; - - var changedText = originalText; - var changedContext = context; - - _logger.LogTestOnly($"Before HTML formatter:\r\n{changedText}"); - - if (htmlEdits.Length > 0) - { - var changes = htmlEdits.Select(originalText.GetTextChange); - changedText = originalText.WithChanges(changes); - // Create a new formatting context for the changed razor document. - changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); - - _logger.LogTestOnly($"After normalizedEdits:\r\n{changedText}"); - } - - var indentationChanges = AdjustRazorIndentation(changedContext); - if (indentationChanges.Count > 0) - { - // Apply the edits that adjust indentation. - changedText = changedText.WithChanges(indentationChanges); - _logger.LogTestOnly($"After AdjustRazorIndentation:\r\n{changedText}"); - } - - var finalChanges = changedText.GetTextChanges(originalText); - var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray(); - - return new FormattingResult(finalEdits); - } - - private static List AdjustRazorIndentation(FormattingContext context) - { - // Assume HTML formatter has already run at this point and HTML is relatively indented correctly. - // But HTML doesn't know about Razor blocks. - // Our goal here is to indent each line according to the surrounding Razor blocks. - var sourceText = context.SourceText; - var editsToApply = new List(); - var indentations = context.GetIndentations(); - - for (var i = 0; i < sourceText.Lines.Count; i++) - { - var line = sourceText.Lines[i]; - if (line.Span.Length == 0) - { - // Empty line. - continue; - } - - if (indentations[i].StartsInCSharpContext) - { - // Normally we don't do HTML things in C# contexts but there is one - // edge case when including render fragments in a C# code block, eg: - // - // @code { - // void Foo() - // { - // Render(@); - // { - // } - // - // This is popular in some libraries, like bUnit. The issue here is that - // the HTML formatter sees ~~~~~ and puts a newline before - // the tag, but obviously that breaks things. - // - // It's straight forward enough to just check for this situation and special case - // it by removing the newline again. - - // There needs to be at least one more line, and the current line needs to end with - // an @ sign, and have an open angle bracket at the start of the next line. - if (sourceText.Lines.Count >= i + 1 && - line.Text?.Length > 1 && - line.Text?[line.End - 1] == '@') - { - var nextLine = sourceText.Lines[i + 1]; - var firstChar = nextLine.GetFirstNonWhitespaceOffset().GetValueOrDefault(); - - // When the HTML formatter inserts the newline in this scenario, it doesn't - // indent the component tag, so we use that as another signal that this is - // the scenario we think it is. - if (firstChar == 0 && - nextLine.Text?[nextLine.Start] == '<') - { - var lineBreakLength = line.EndIncludingLineBreak - line.End; - var spanToReplace = new TextSpan(line.End, lineBreakLength); - var change = new TextChange(spanToReplace, string.Empty); - editsToApply.Add(change); - - // Skip the next line because we've essentially just removed it. - i++; - } - } - - continue; - } - - var razorDesiredIndentationLevel = indentations[i].RazorIndentationLevel; - if (razorDesiredIndentationLevel == 0) - { - // This line isn't under any Razor specific constructs. Trust the HTML formatter. - continue; - } - - var htmlDesiredIndentationLevel = indentations[i].HtmlIndentationLevel; - if (htmlDesiredIndentationLevel == 0 && !IsPartOfHtmlTag(context, indentations[i].FirstSpan.Span.Start)) - { - // This line is under some Razor specific constructs but not under any HTML tag. - // E.g, - // @{ - // @* comment *@ <---- - // } - // - // In this case, the HTML formatter wouldn't touch it but we should format it correctly. - // So, let's use our syntax understanding to rewrite the indentation. - // Note: This case doesn't apply for HTML tags (HTML formatter will touch it even if it is in the root). - // Hence the second part of the if condition. - // - var desiredIndentationLevel = indentations[i].IndentationLevel; - var desiredIndentationString = context.GetIndentationLevelString(desiredIndentationLevel); - var spanToReplace = new TextSpan(line.Start, indentations[i].ExistingIndentation); - var change = new TextChange(spanToReplace, desiredIndentationString); - editsToApply.Add(change); - } - else - { - // This line is under some Razor specific constructs and HTML tags. - // E.g, - // @{ - //
<---- - //
- // } - // - // In this case, the HTML formatter would've formatted it correctly. Let's not use our syntax understanding. - // Instead, we should just add to the existing indentation. - // - var razorDesiredIndentationString = context.GetIndentationLevelString(razorDesiredIndentationLevel); - var existingIndentationString = FormattingUtilities.GetIndentationString(indentations[i].ExistingIndentationSize, context.Options.InsertSpaces, context.Options.TabSize); - var desiredIndentationString = existingIndentationString + razorDesiredIndentationString; - var spanToReplace = new TextSpan(line.Start, indentations[i].ExistingIndentation); - var change = new TextChange(spanToReplace, desiredIndentationString); - editsToApply.Add(change); - } - } - - return editsToApply; - } - - private static bool IsPartOfHtmlTag(FormattingContext context, int position) - { - var syntaxTree = context.CodeDocument.GetSyntaxTree(); - var owner = syntaxTree.Root.FindInnermostNode(position, includeWhitespace: true); - if (owner is null) - { - // Can't determine owner of this position. - return false; - } - - // E.g, (| is position) - // - // `

` - true - // - return owner.AncestorsAndSelf().Any( - n => n is MarkupStartTagSyntax || n is MarkupTagHelperStartTagSyntax || n is MarkupEndTagSyntax || n is MarkupTagHelperEndTagSyntax); - } -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs new file mode 100644 index 0000000000..f08b23a0e2 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs @@ -0,0 +1,189 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Formatting; + +internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass +{ + private readonly ILogger _logger = logger; + + public virtual async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + { + var originalText = context.SourceText; + + var htmlEdits = result.Edits; + + var changedText = originalText; + var changedContext = context; + + _logger.LogTestOnly($"Before HTML formatter:\r\n{changedText}"); + + if (htmlEdits.Length > 0) + { + var changes = htmlEdits.Select(originalText.GetTextChange); + changedText = originalText.WithChanges(changes); + // Create a new formatting context for the changed razor document. + changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); + + _logger.LogTestOnly($"After normalizedEdits:\r\n{changedText}"); + } + + var indentationChanges = AdjustRazorIndentation(changedContext); + if (indentationChanges.Count > 0) + { + // Apply the edits that adjust indentation. + changedText = changedText.WithChanges(indentationChanges); + _logger.LogTestOnly($"After AdjustRazorIndentation:\r\n{changedText}"); + } + + var finalChanges = changedText.GetTextChanges(originalText); + var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray(); + + return new FormattingResult(finalEdits); + } + + private static List AdjustRazorIndentation(FormattingContext context) + { + // Assume HTML formatter has already run at this point and HTML is relatively indented correctly. + // But HTML doesn't know about Razor blocks. + // Our goal here is to indent each line according to the surrounding Razor blocks. + var sourceText = context.SourceText; + var editsToApply = new List(); + var indentations = context.GetIndentations(); + + for (var i = 0; i < sourceText.Lines.Count; i++) + { + var line = sourceText.Lines[i]; + if (line.Span.Length == 0) + { + // Empty line. + continue; + } + + if (indentations[i].StartsInCSharpContext) + { + // Normally we don't do HTML things in C# contexts but there is one + // edge case when including render fragments in a C# code block, eg: + // + // @code { + // void Foo() + // { + // Render(@); + // { + // } + // + // This is popular in some libraries, like bUnit. The issue here is that + // the HTML formatter sees ~~~~~ and puts a newline before + // the tag, but obviously that breaks things. + // + // It's straight forward enough to just check for this situation and special case + // it by removing the newline again. + + // There needs to be at least one more line, and the current line needs to end with + // an @ sign, and have an open angle bracket at the start of the next line. + if (sourceText.Lines.Count >= i + 1 && + line.Text?.Length > 1 && + line.Text?[line.End - 1] == '@') + { + var nextLine = sourceText.Lines[i + 1]; + var firstChar = nextLine.GetFirstNonWhitespaceOffset().GetValueOrDefault(); + + // When the HTML formatter inserts the newline in this scenario, it doesn't + // indent the component tag, so we use that as another signal that this is + // the scenario we think it is. + if (firstChar == 0 && + nextLine.Text?[nextLine.Start] == '<') + { + var lineBreakLength = line.EndIncludingLineBreak - line.End; + var spanToReplace = new TextSpan(line.End, lineBreakLength); + var change = new TextChange(spanToReplace, string.Empty); + editsToApply.Add(change); + + // Skip the next line because we've essentially just removed it. + i++; + } + } + + continue; + } + + var razorDesiredIndentationLevel = indentations[i].RazorIndentationLevel; + if (razorDesiredIndentationLevel == 0) + { + // This line isn't under any Razor specific constructs. Trust the HTML formatter. + continue; + } + + var htmlDesiredIndentationLevel = indentations[i].HtmlIndentationLevel; + if (htmlDesiredIndentationLevel == 0 && !IsPartOfHtmlTag(context, indentations[i].FirstSpan.Span.Start)) + { + // This line is under some Razor specific constructs but not under any HTML tag. + // E.g, + // @{ + // @* comment *@ <---- + // } + // + // In this case, the HTML formatter wouldn't touch it but we should format it correctly. + // So, let's use our syntax understanding to rewrite the indentation. + // Note: This case doesn't apply for HTML tags (HTML formatter will touch it even if it is in the root). + // Hence the second part of the if condition. + // + var desiredIndentationLevel = indentations[i].IndentationLevel; + var desiredIndentationString = context.GetIndentationLevelString(desiredIndentationLevel); + var spanToReplace = new TextSpan(line.Start, indentations[i].ExistingIndentation); + var change = new TextChange(spanToReplace, desiredIndentationString); + editsToApply.Add(change); + } + else + { + // This line is under some Razor specific constructs and HTML tags. + // E.g, + // @{ + //

<---- + //
+ // } + // + // In this case, the HTML formatter would've formatted it correctly. Let's not use our syntax understanding. + // Instead, we should just add to the existing indentation. + // + var razorDesiredIndentationString = context.GetIndentationLevelString(razorDesiredIndentationLevel); + var existingIndentationString = FormattingUtilities.GetIndentationString(indentations[i].ExistingIndentationSize, context.Options.InsertSpaces, context.Options.TabSize); + var desiredIndentationString = existingIndentationString + razorDesiredIndentationString; + var spanToReplace = new TextSpan(line.Start, indentations[i].ExistingIndentation); + var change = new TextChange(spanToReplace, desiredIndentationString); + editsToApply.Add(change); + } + } + + return editsToApply; + } + + private static bool IsPartOfHtmlTag(FormattingContext context, int position) + { + var syntaxTree = context.CodeDocument.GetSyntaxTree(); + var owner = syntaxTree.Root.FindInnermostNode(position, includeWhitespace: true); + if (owner is null) + { + // Can't determine owner of this position. + return false; + } + + // E.g, (| is position) + // + // `

` - true + // + return owner.AncestorsAndSelf().Any( + n => n is MarkupStartTagSyntax || n is MarkupTagHelperStartTagSyntax || n is MarkupEndTagSyntax || n is MarkupTagHelperEndTagSyntax); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs new file mode 100644 index 0000000000..fd5d5193cf --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Formatting; + +internal sealed class HtmlOnTypeFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger()) +{ + public override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + { + Debug.Assert(result.Kind == RazorLanguageKind.Html); + + if (result.Edits.Length == 0) + { + // There are no HTML edits for us to apply. No op. + return Task.FromResult(new FormattingResult([])); + } + + return base.ExecuteAsync(context, result, cancellationToken); + } +} From 45b8495ac80dd0d31cb35f360f3eb5b87fc9df3c Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 16:13:45 +1000 Subject: [PATCH 18/26] List -> PooledArrayBuilder --- .../Formatting/Passes/RazorFormattingPass.cs | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs index 520a720ee7..64fa3046b5 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using RazorRazorSyntaxNodeList = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxList; @@ -50,34 +52,34 @@ internal sealed class RazorFormattingPass : IFormattingPass return new FormattingResult(finalEdits); } - private static IEnumerable FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree) + private static ImmutableArray FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree) { - var edits = new List(); + using var edits = new PooledArrayBuilder(); var source = syntaxTree.Source; foreach (var node in syntaxTree.Root.DescendantNodes()) { // Disclaimer: CSharpCodeBlockSyntax is used a _lot_ in razor so these methods are probably // being overly careful to only try to format syntax forms they care about. - TryFormatCSharpBlockStructure(context, edits, source, node); // TODO - TryFormatSingleLineDirective(edits, source, node); - TryFormatBlocks(context, edits, source, node); + TryFormatCSharpBlockStructure(context, ref edits.AsRef(), source, node); // TODO + TryFormatSingleLineDirective(ref edits.AsRef(), source, node); + TryFormatBlocks(context, ref edits.AsRef(), source, node); } - return edits; + return edits.ToImmutable(); } - private static void TryFormatBlocks(FormattingContext context, List edits, RazorSourceDocument source, RazorSyntaxNode node) + private static void TryFormatBlocks(FormattingContext context, ref PooledArrayBuilder edits, RazorSourceDocument source, RazorSyntaxNode node) { // We only want to run one of these - _ = TryFormatFunctionsBlock(context, edits, source, node) || - TryFormatCSharpExplicitTransition(context, edits, source, node) || - TryFormatHtmlInCSharp(context, edits, source, node) || - TryFormatComplexCSharpBlock(context, edits, source, node) || - TryFormatSectionBlock(context, edits, source, node); + _ = TryFormatFunctionsBlock(context, ref edits, source, node) || + TryFormatCSharpExplicitTransition(context, ref edits, source, node) || + TryFormatHtmlInCSharp(context, ref edits, source, node) || + TryFormatComplexCSharpBlock(context, ref edits, source, node) || + TryFormatSectionBlock(context, ref edits, source, node); } - private static bool TryFormatSectionBlock(FormattingContext context, List edits, RazorSourceDocument source, RazorSyntaxNode node) + private static bool TryFormatSectionBlock(FormattingContext context, ref PooledArrayBuilder edits, RazorSourceDocument source, RazorSyntaxNode node) { // @section Goo { // } @@ -96,8 +98,8 @@ internal sealed class RazorFormattingPass : IFormattingPass if (TryGetWhitespace(children, out var whitespaceBeforeSectionName, out var whitespaceAfterSectionName)) { // For whitespace we normalize it differently depending on if its multi-line or not - FormatWhitespaceBetweenDirectiveAndBrace(whitespaceBeforeSectionName, directive, edits, source, context, forceNewLine: false); - FormatWhitespaceBetweenDirectiveAndBrace(whitespaceAfterSectionName, directive, edits, source, context, forceNewLine: false); + FormatWhitespaceBetweenDirectiveAndBrace(whitespaceBeforeSectionName, directive, ref edits, source, context, forceNewLine: false); + FormatWhitespaceBetweenDirectiveAndBrace(whitespaceAfterSectionName, directive, ref edits, source, context, forceNewLine: false); return true; } @@ -134,7 +136,7 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - private static bool TryFormatFunctionsBlock(FormattingContext context, IList edits, RazorSourceDocument source, RazorSyntaxNode node) + private static bool TryFormatFunctionsBlock(FormattingContext context, ref PooledArrayBuilder edits, RazorSourceDocument source, RazorSyntaxNode node) { // @functions // { @@ -166,13 +168,13 @@ internal sealed class RazorFormattingPass : IFormattingPass var codeNode = code.AssumeNotNull(); var closeBraceNode = closeBrace; - return FormatBlock(context, source, directive, openBraceNode, codeNode, closeBraceNode, edits); + return FormatBlock(context, source, directive, openBraceNode, codeNode, closeBraceNode, ref edits); } return false; } - private static bool TryFormatCSharpExplicitTransition(FormattingContext context, IList edits, RazorSourceDocument source, RazorSyntaxNode node) + private static bool TryFormatCSharpExplicitTransition(FormattingContext context, ref PooledArrayBuilder edits, RazorSourceDocument source, RazorSyntaxNode node) { // We're looking for a code block like this: // @@ -187,13 +189,13 @@ internal sealed class RazorFormattingPass : IFormattingPass var codeNode = csharpStatementBody.CSharpCode; var closeBraceNode = csharpStatementBody.CloseBrace; - return FormatBlock(context, source, directiveNode: null, openBraceNode, codeNode, closeBraceNode, edits); + return FormatBlock(context, source, directiveNode: null, openBraceNode, codeNode, closeBraceNode, ref edits); } return false; } - private static bool TryFormatComplexCSharpBlock(FormattingContext context, IList edits, RazorSourceDocument source, RazorSyntaxNode node) + private static bool TryFormatComplexCSharpBlock(FormattingContext context, ref PooledArrayBuilder edits, RazorSourceDocument source, RazorSyntaxNode node) { // complex situations like // @{ @@ -209,13 +211,13 @@ internal sealed class RazorFormattingPass : IFormattingPass var openBraceNode = outerCodeBlock.Children.PreviousSiblingOrSelf(innerCodeBlock); var closeBraceNode = outerCodeBlock.Children.NextSiblingOrSelf(innerCodeBlock); - return FormatBlock(context, source, directiveNode: null, openBraceNode, codeNode, closeBraceNode, edits); + return FormatBlock(context, source, directiveNode: null, openBraceNode, codeNode, closeBraceNode, ref edits); } return false; } - private static bool TryFormatHtmlInCSharp(FormattingContext context, IList edits, RazorSourceDocument source, RazorSyntaxNode node) + private static bool TryFormatHtmlInCSharp(FormattingContext context, ref PooledArrayBuilder edits, RazorSourceDocument source, RazorSyntaxNode node) { // void Method() // { @@ -227,13 +229,13 @@ internal sealed class RazorFormattingPass : IFormattingPass var openBraceNode = cSharpCodeBlock.Children.PreviousSiblingOrSelf(markupBlockNode); var closeBraceNode = cSharpCodeBlock.Children.NextSiblingOrSelf(markupBlockNode); - return FormatBlock(context, source, directiveNode: null, openBraceNode, markupBlockNode, closeBraceNode, edits); + return FormatBlock(context, source, directiveNode: null, openBraceNode, markupBlockNode, closeBraceNode, ref edits); } return false; } - private static void TryFormatCSharpBlockStructure(FormattingContext context, List edits, RazorSourceDocument source, RazorSyntaxNode node) + private static void TryFormatCSharpBlockStructure(FormattingContext context, ref PooledArrayBuilder edits, RazorSourceDocument source, RazorSyntaxNode node) { // We're looking for a code block like this: // @@ -261,7 +263,7 @@ internal sealed class RazorFormattingPass : IFormattingPass if (TryGetLeadingWhitespace(children, out var whitespace)) { // For whitespace we normalize it differently depending on if its multi-line or not - FormatWhitespaceBetweenDirectiveAndBrace(whitespace, directive, edits, source, context, forceNewLine); + FormatWhitespaceBetweenDirectiveAndBrace(whitespace, directive, ref edits, source, context, forceNewLine); } else if (children.TryGetOpenBraceToken(out var brace)) { @@ -293,7 +295,7 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - private static void TryFormatSingleLineDirective(List edits, RazorSourceDocument source, RazorSyntaxNode node) + private static void TryFormatSingleLineDirective(ref PooledArrayBuilder edits, RazorSourceDocument source, RazorSyntaxNode node) { // Looking for single line directives like // @@ -309,7 +311,7 @@ internal sealed class RazorFormattingPass : IFormattingPass { if (child.ContainsOnlyWhitespace(includingNewLines: false)) { - ShrinkToSingleSpace(child, edits, source); + ShrinkToSingleSpace(child, ref edits, source); } } } @@ -329,11 +331,11 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - private static void FormatWhitespaceBetweenDirectiveAndBrace(RazorSyntaxNode node, RazorDirectiveSyntax directive, List edits, RazorSourceDocument source, FormattingContext context, bool forceNewLine) + private static void FormatWhitespaceBetweenDirectiveAndBrace(RazorSyntaxNode node, RazorDirectiveSyntax directive, ref PooledArrayBuilder edits, RazorSourceDocument source, FormattingContext context, bool forceNewLine) { if (node.ContainsOnlyWhitespace(includingNewLines: false) && !forceNewLine) { - ShrinkToSingleSpace(node, edits, source); + ShrinkToSingleSpace(node, ref edits, source); } else { @@ -348,7 +350,7 @@ internal sealed class RazorFormattingPass : IFormattingPass } } - private static void ShrinkToSingleSpace(RazorSyntaxNode node, List edits, RazorSourceDocument source) + private static void ShrinkToSingleSpace(RazorSyntaxNode node, ref PooledArrayBuilder edits, RazorSourceDocument source) { // If there is anything other than one single space then we replace with one space between directive and brace. // @@ -357,7 +359,7 @@ internal sealed class RazorFormattingPass : IFormattingPass edits.Add(edit); } - private static bool FormatBlock(FormattingContext context, RazorSourceDocument source, RazorSyntaxNode? directiveNode, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode, RazorSyntaxNode closeBraceNode, IList edits) + private static bool FormatBlock(FormattingContext context, RazorSourceDocument source, RazorSyntaxNode? directiveNode, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode, RazorSyntaxNode closeBraceNode, ref PooledArrayBuilder edits) { var didFormat = false; @@ -367,7 +369,7 @@ internal sealed class RazorFormattingPass : IFormattingPass if (openBraceRange is not null && codeRange is not null && openBraceRange.End.Line == codeRange.Start.Line && - !RangeHasBeenModified(edits, codeRange)) + !RangeHasBeenModified(ref edits, codeRange)) { var additionalIndentationLevel = GetAdditionalIndentationLevel(context, openBraceRange, openBraceNode, codeNode); var newText = context.NewLineString; @@ -384,7 +386,7 @@ internal sealed class RazorFormattingPass : IFormattingPass var closeBraceRange = closeBraceNode.GetRangeWithoutWhitespace(source); if (codeRange is not null && closeBraceRange is not null && - !RangeHasBeenModified(edits, codeRange)) + !RangeHasBeenModified(ref edits, codeRange)) { if (directiveNode is not null && directiveNode.GetRange(source).Start.Character < closeBraceRange.Start.Character) @@ -409,7 +411,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return didFormat; - static bool RangeHasBeenModified(IList edits, Range range) + static bool RangeHasBeenModified(ref PooledArrayBuilder edits, Range range) { // Because we don't always know what kind of Razor object we're operating on we have to do this to avoid duplicate edits. // The other way to accomplish this would be to apply the edits after every node and function, but that's not in scope for my current work. From 5caa48f8e36900c3f9fd26fd2c563e466dcf0189 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 16:44:17 +1000 Subject: [PATCH 19/26] Remove Kind property from FormattingResult This was only used for validation as edits pass through the pipeline, but since we now tightly control the pipeline its unnecessary --- .../Formatting/FormattingResult.cs | 20 +----- .../Formatting/IRazorFormattingService.cs | 6 +- .../Formatting/Passes/CSharpFormattingPass.cs | 7 +-- .../Passes/CSharpOnTypeFormattingPass.cs | 18 ++---- .../Passes/FormattingContentValidationPass.cs | 7 --- .../FormattingDiagnosticValidationPass.cs | 7 --- .../Formatting/Passes/HtmlFormattingPass.cs | 3 + .../Passes/HtmlOnTypeFormattingPass.cs | 7 +-- .../Formatting/RazorFormattingService.cs | 61 ++++++++----------- .../FormattingContentValidationPassTest.cs | 45 +------------- .../FormattingDiagnosticValidationPassTest.cs | 47 +------------- 11 files changed, 50 insertions(+), 178 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingResult.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingResult.cs index e8db79e520..a5a2427177 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingResult.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingResult.cs @@ -1,26 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal readonly struct FormattingResult -{ - public FormattingResult(TextEdit[] edits, RazorLanguageKind kind = RazorLanguageKind.Razor) - { - if (edits is null) - { - throw new ArgumentNullException(nameof(edits)); - } - - Edits = edits; - Kind = kind; - } - - public TextEdit[] Edits { get; } - - public RazorLanguageKind Kind { get; } -} +internal readonly record struct FormattingResult(TextEdit[] Edits); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs index f2893792c9..63f4bef48f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IRazorFormattingService.cs @@ -34,19 +34,19 @@ internal interface IRazorFormattingService Task GetSingleCSharpEditAsync( DocumentContext documentContext, - TextEdit initialEdit, + TextEdit csharpEdit, RazorFormattingOptions options, CancellationToken cancellationToken); Task GetCSharpCodeActionEditAsync( DocumentContext documentContext, - TextEdit[] formattedEdits, + TextEdit[] csharpEdits, RazorFormattingOptions options, CancellationToken cancellationToken); Task GetCSharpSnippetFormattingEditAsync( DocumentContext documentContext, - TextEdit[] edits, + TextEdit[] csharpEdits, RazorFormattingOptions options, CancellationToken cancellationToken); } 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 03f4f3b79a..5674012404 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 @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,12 +11,14 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; +///

+/// Gets edits in Razor files, and returns edits to Razor files, with nicely formatted Html +/// internal sealed class CSharpFormattingPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) @@ -28,8 +29,6 @@ internal sealed class CSharpFormattingPass( public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { - Debug.Assert(result.Kind == RazorLanguageKind.Razor); - // Apply previous edits if any. var originalText = context.SourceText; var changedText = originalText; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs index 77d319406d..c5fef08081 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs @@ -16,13 +16,15 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.CodeAnalysis.Razor.Formatting; +/// +/// Gets edits in C# files, and returns edits to Razor files, with nicely formatted Html +/// internal sealed class CSharpOnTypeFormattingPass( IDocumentMappingService documentMappingService, ILoggerFactory loggerFactory) @@ -32,8 +34,6 @@ internal sealed class CSharpOnTypeFormattingPass( public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { - Debug.Assert(result.Kind == RazorLanguageKind.CSharp); - // Normalize and re-map the C# edits. var codeDocument = context.CodeDocument; var csharpText = codeDocument.GetCSharpSourceText(); @@ -85,9 +85,9 @@ internal sealed class CSharpOnTypeFormattingPass( return result; } } - + var normalizedEdits = csharpText.NormalizeTextEdits(textEdits, out var originalTextWithChanges); - var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits, result.Kind); + var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits); var filteredEdits = FilterCSharpTextEdits(context, mappedEdits); if (filteredEdits.Length == 0) { @@ -201,14 +201,8 @@ internal sealed class CSharpOnTypeFormattingPass( return new FormattingResult(finalEdits); } - private TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits, RazorLanguageKind projectedKind) + private TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits) { - if (projectedKind != RazorLanguageKind.CSharp) - { - // Non C# projections map directly to Razor. No need to remap. - return projectedTextEdits; - } - if (codeDocument.IsUnsupported()) { return []; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs index 76bd0e286f..733c6d05bf 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -21,12 +20,6 @@ internal sealed class FormattingContentValidationPass(ILoggerFactory loggerFacto public Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { - if (result.Kind != RazorLanguageKind.Razor) - { - // We don't care about changes to projected documents here. - return Task.FromResult(result); - } - var text = context.SourceText; var edits = result.Edits; var changes = edits.Select(text.GetTextChange); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs index a7824ea711..dbc058f4d1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; @@ -23,12 +22,6 @@ internal sealed class FormattingDiagnosticValidationPass(ILoggerFactory loggerFa public async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { - if (result.Kind != RazorLanguageKind.Razor) - { - // We don't care about changes to projected documents here. - return result; - } - var originalDiagnostics = context.CodeDocument.GetSyntaxTree().Diagnostics; var text = context.SourceText; 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 0a7800d816..4cb84022a6 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 @@ -5,6 +5,9 @@ using Microsoft.CodeAnalysis.Razor.Logging; namespace Microsoft.CodeAnalysis.Razor.Formatting; +/// +/// Gets edits in Razor files, and returns edits to Razor files, with nicely formatted Html +/// internal sealed class HtmlFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger()) { } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs index fd5d5193cf..5f138ca9c1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs @@ -1,20 +1,19 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Razor.Logging; -using Microsoft.CodeAnalysis.Razor.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; +/// +/// Gets edits in Html files, and returns edits to Razor files, with nicely formatted Html +/// internal sealed class HtmlOnTypeFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger()) { public override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) { - Debug.Assert(result.Kind == RazorLanguageKind.Html); - if (result.Edits.Length == 0) { // There are no HTML edits for us to apply. No op. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index a8f5e2542b..497c9c3f82 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -11,7 +11,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -107,8 +106,7 @@ internal class RazorFormattingService : IRazorFormattingService public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) => ApplyFormattedEditsAsync( documentContext, - RazorLanguageKind.CSharp, - formattedEdits: [], + generatedDocumentEdits: [], options, hostDocumentIndex, triggerCharacter, @@ -120,7 +118,6 @@ internal class RazorFormattingService : IRazorFormattingService public Task GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) => ApplyFormattedEditsAsync( documentContext, - RazorLanguageKind.Html, htmlEdits, options, hostDocumentIndex, @@ -130,12 +127,11 @@ internal class RazorFormattingService : IRazorFormattingService automaticallyAddUsings: false, cancellationToken: cancellationToken); - public async Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit edit, RazorFormattingOptions options, CancellationToken cancellationToken) + public async Task GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit csharpEdit, RazorFormattingOptions options, CancellationToken cancellationToken) { - var formattedEdits = await ApplyFormattedEditsAsync( + var razorEdits = await ApplyFormattedEditsAsync( documentContext, - RazorLanguageKind.CSharp, - [edit], + [csharpEdit], options, hostDocumentIndex: 0, triggerCharacter: '\0', @@ -143,15 +139,14 @@ internal class RazorFormattingService : IRazorFormattingService collapseEdits: false, automaticallyAddUsings: false, cancellationToken: cancellationToken).ConfigureAwait(false); - return formattedEdits.SingleOrDefault(); + return razorEdits.SingleOrDefault(); } - public async Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] initialEdits, RazorFormattingOptions options, CancellationToken cancellationToken) + public async Task GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] csharpEdits, RazorFormattingOptions options, CancellationToken cancellationToken) { - var edits = await ApplyFormattedEditsAsync( + var razorEdits = await ApplyFormattedEditsAsync( documentContext, - RazorLanguageKind.CSharp, - initialEdits, + csharpEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', @@ -159,17 +154,16 @@ internal class RazorFormattingService : IRazorFormattingService collapseEdits: true, automaticallyAddUsings: true, cancellationToken: cancellationToken).ConfigureAwait(false); - return edits.SingleOrDefault(); + return razorEdits.SingleOrDefault(); } - public async Task GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, RazorFormattingOptions options, CancellationToken cancellationToken) + public async Task GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] csharpEdits, RazorFormattingOptions options, CancellationToken cancellationToken) { - WrapCSharpSnippets(edits); + WrapCSharpSnippets(csharpEdits); - var formattedEdits = await ApplyFormattedEditsAsync( + var razorEdits = await ApplyFormattedEditsAsync( documentContext, - RazorLanguageKind.CSharp, - edits, + csharpEdits, options, hostDocumentIndex: 0, triggerCharacter: '\0', @@ -178,15 +172,14 @@ internal class RazorFormattingService : IRazorFormattingService automaticallyAddUsings: false, cancellationToken: cancellationToken).ConfigureAwait(false); - UnwrapCSharpSnippets(formattedEdits); + UnwrapCSharpSnippets(razorEdits); - return formattedEdits.SingleOrDefault(); + return razorEdits.SingleOrDefault(); } private async Task ApplyFormattedEditsAsync( DocumentContext documentContext, - RazorLanguageKind kind, - TextEdit[] formattedEdits, + TextEdit[] generatedDocumentEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, @@ -197,13 +190,13 @@ internal class RazorFormattingService : IRazorFormattingService { // If we only received a single edit, let's always return a single edit back. // Otherwise, merge only if explicitly asked. - collapseEdits |= formattedEdits.Length == 1; + collapseEdits |= generatedDocumentEdits.Length == 1; var documentSnapshot = documentContext.Snapshot; var uri = documentContext.Uri; var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); using var context = FormattingContext.CreateForOnTypeFormatting(uri, documentSnapshot, codeDocument, options, _workspaceFactory, automaticallyAddUsings: automaticallyAddUsings, hostDocumentIndex, triggerCharacter); - var result = new FormattingResult(formattedEdits, kind); + var result = new FormattingResult(generatedDocumentEdits); foreach (var pass in formattingPasses) { @@ -212,11 +205,11 @@ internal class RazorFormattingService : IRazorFormattingService } var originalText = context.SourceText; - var edits = originalText.NormalizeTextEdits(result.Edits); + var razorEdits = originalText.NormalizeTextEdits(result.Edits); if (collapseEdits) { - var collapsedEdit = MergeEdits(edits, originalText); + var collapsedEdit = MergeEdits(razorEdits, originalText); if (collapsedEdit.NewText.Length == 0 && collapsedEdit.Range.IsZeroWidth()) { @@ -226,7 +219,7 @@ internal class RazorFormattingService : IRazorFormattingService return [collapsedEdit]; } - return edits; + return razorEdits; } // Internal for testing @@ -248,25 +241,25 @@ internal class RazorFormattingService : IRazorFormattingService return sourceText.GetTextEdit(encompassingChange); } - private static void WrapCSharpSnippets(TextEdit[] snippetEdits) + private static void WrapCSharpSnippets(TextEdit[] csharpEdits) { // Currently this method only supports wrapping `$0`, any additional markers aren't formatted properly. - foreach (var snippetEdit in snippetEdits) + foreach (var edit in csharpEdits) { // Formatting doesn't work with syntax errors caused by the cursor marker ($0). // So, let's avoid the error by wrapping the cursor marker in a comment. - snippetEdit.NewText = snippetEdit.NewText.Replace("$0", "/*$0*/"); + edit.NewText = edit.NewText.Replace("$0", "/*$0*/"); } } - private static void UnwrapCSharpSnippets(TextEdit[] snippetEdits) + private static void UnwrapCSharpSnippets(TextEdit[] razorEdits) { - foreach (var snippetEdit in snippetEdits) + foreach (var edit in razorEdits) { // Formatting doesn't work with syntax errors caused by the cursor marker ($0). // So, let's avoid the error by wrapping the cursor marker in a comment. - snippetEdit.NewText = snippetEdit.NewText.Replace("/*$0*/", "$0"); + edit.NewText = edit.NewText.Replace("/*$0*/", "$0"); } } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs index 42688f3992..becc4807f3 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Moq; @@ -21,46 +20,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; public class FormattingContentValidationPassTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { - [Fact] - public async Task Execute_LanguageKindCSharp_Noops() - { - // Arrange - var source = SourceText.From(@" -@code { - public class Foo { } -} -"); - using var context = CreateFormattingContext(source); - var input = new FormattingResult([], RazorLanguageKind.CSharp); - var pass = GetPass(); - - // Act - var result = await pass.ExecuteAsync(context, input, DisposalToken); - - // Assert - Assert.Equal(input, result); - } - - [Fact] - public async Task Execute_LanguageKindHtml_Noops() - { - // Arrange - var source = SourceText.From(@" -@code { - public class Foo { } -} -"); - using var context = CreateFormattingContext(source); - var input = new FormattingResult([], RazorLanguageKind.Html); - var pass = GetPass(); - - // Act - var result = await pass.ExecuteAsync(context, input, DisposalToken); - - // Assert - Assert.Equal(input, result); - } - [Fact] public async Task Execute_NonDestructiveEdit_Allowed() { @@ -75,7 +34,7 @@ public class Foo { } { VsLspFactory.CreateTextEdit(2, 0, " ") }; - var input = new FormattingResult(edits, RazorLanguageKind.Razor); + var input = new FormattingResult(edits); var pass = GetPass(); // Act @@ -99,7 +58,7 @@ public class Foo { } { VsLspFactory.CreateTextEdit(2, 0, 3, 0, " ") // Nukes a line }; - var input = new FormattingResult(edits, RazorLanguageKind.Razor); + var input = new FormattingResult(edits); var pass = GetPass(); // Act diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs index 2865555416..82f92cfe49 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; @@ -19,48 +18,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; public class FormattingDiagnosticValidationPassTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { - [Fact] - public async Task ExecuteAsync_LanguageKindCSharp_Noops() - { - // Arrange - var source = SourceText.From(@" -@code { - public class Foo { } -} -"); - using var context = CreateFormattingContext(source); - var badEdit = VsLspFactory.CreateTextEdit(position: (0, 0), "@ "); - var input = new FormattingResult([badEdit], RazorLanguageKind.CSharp); - var pass = GetPass(); - - // Act - var result = await pass.ExecuteAsync(context, input, DisposalToken); - - // Assert - Assert.Equal(input, result); - } - - [Fact] - public async Task ExecuteAsync_LanguageKindHtml_Noops() - { - // Arrange - var source = SourceText.From(@" -@code { - public class Foo { } -} -"); - using var context = CreateFormattingContext(source); - var badEdit = VsLspFactory.CreateTextEdit(position: (0, 0), "@ "); - var input = new FormattingResult([badEdit], RazorLanguageKind.Html); - var pass = GetPass(); - - // Act - var result = await pass.ExecuteAsync(context, input, DisposalToken); - - // Assert - Assert.Equal(input, result); - } - [Fact] public async Task ExecuteAsync_NonDestructiveEdit_Allowed() { @@ -75,7 +32,7 @@ public class Foo { } { VsLspFactory.CreateTextEdit(2, 0, " ") }; - var input = new FormattingResult(edits, RazorLanguageKind.Razor); + var input = new FormattingResult(edits); var pass = GetPass(); // Act @@ -96,7 +53,7 @@ public class Foo { } "); using var context = CreateFormattingContext(source); var badEdit = VsLspFactory.CreateTextEdit(position: (0, 0), "@ "); // Creates a diagnostic - var input = new FormattingResult([badEdit], RazorLanguageKind.Razor); + var input = new FormattingResult([badEdit]); var pass = GetPass(); // Act From fa4e8efb54bb2c6f4bdaf3e64e64e26faf764c86 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 22 Aug 2024 17:02:20 +1000 Subject: [PATCH 20/26] Remove FormattingResult since it's now useless --- .../Formatting/FormattingResult.cs | 8 ------ .../Formatting/IFormattingPass.cs | 3 ++- .../Formatting/Passes/CSharpFormattingPass.cs | 8 +++--- .../Passes/CSharpFormattingPassBase.cs | 2 +- .../Passes/CSharpOnTypeFormattingPass.cs | 27 +++++++++---------- .../Passes/FormattingContentValidationPass.cs | 7 +++-- .../FormattingDiagnosticValidationPass.cs | 7 +++-- .../Passes/HtmlFormattingPassBase.cs | 10 +++---- .../Passes/HtmlOnTypeFormattingPass.cs | 9 ++++--- .../Formatting/Passes/RazorFormattingPass.cs | 12 ++++----- .../Formatting/RazorFormattingService.cs | 11 ++++---- .../FormattingContentValidationPassTest.cs | 10 +++---- .../FormattingDiagnosticValidationPassTest.cs | 9 +++---- 13 files changed, 55 insertions(+), 68 deletions(-) delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingResult.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingResult.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingResult.cs deleted file mode 100644 index a5a2427177..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingResult.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.CodeAnalysis.Razor.Formatting; - -internal readonly record struct FormattingResult(TextEdit[] Edits); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingPass.cs index 074ccc19e7..18e774ed89 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingPass.cs @@ -3,10 +3,11 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; internal interface IFormattingPass { - Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken); + Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken); } 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 5674012404..d1d5505c83 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 @@ -27,15 +27,15 @@ internal sealed class CSharpFormattingPass( private readonly CSharpFormatter _csharpFormatter = new CSharpFormatter(documentMappingService); private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public async override Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken) { // Apply previous edits if any. var originalText = context.SourceText; var changedText = originalText; var changedContext = context; - if (result.Edits.Length > 0) + if (edits.Length > 0) { - var changes = result.Edits.Select(originalText.GetTextChange).ToArray(); + var changes = edits.Select(originalText.GetTextChange); changedText = changedText.WithChanges(changes); changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); } @@ -69,7 +69,7 @@ internal sealed class CSharpFormattingPass( var finalChanges = changedText.GetTextChanges(originalText); var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray(); - return new FormattingResult(finalEdits); + return finalEdits; } private async Task> FormatCSharpAsync(FormattingContext context, CancellationToken cancellationToken) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs index 6941dbf493..4387ce3d7e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs @@ -25,7 +25,7 @@ internal abstract class CSharpFormattingPassBase(IDocumentMappingService documen protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService; - public abstract Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken); + public abstract Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken); protected async Task> AdjustIndentationAsync(FormattingContext context, CancellationToken cancellationToken, Range? range = null) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs index c5fef08081..077aa630c8 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs @@ -32,19 +32,18 @@ internal sealed class CSharpOnTypeFormattingPass( { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - public async override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public async override Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken) { // Normalize and re-map the C# edits. var codeDocument = context.CodeDocument; var csharpText = codeDocument.GetCSharpSourceText(); - var textEdits = result.Edits; - if (textEdits.Length == 0) + if (edits.Length == 0) { if (!DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), context.HostDocumentIndex, out _, out var projectedIndex)) { _logger.LogWarning($"Failed to map to projected position for document {context.Uri}."); - return result; + return edits; } // Ask C# for formatting changes. @@ -63,18 +62,18 @@ internal sealed class CSharpOnTypeFormattingPass( if (formattingChanges.IsEmpty) { _logger.LogInformation($"Received no results."); - return result; + return edits; } - textEdits = formattingChanges.Select(csharpText.GetTextEdit).ToArray(); - _logger.LogInformation($"Received {textEdits.Length} results from C#."); + edits = formattingChanges.Select(csharpText.GetTextEdit).ToArray(); + _logger.LogInformation($"Received {edits.Length} results from C#."); } // Sometimes the C# document is out of sync with our document, so Roslyn can return edits to us that will throw when we try // to normalize them. Instead of having this flow up and log a NFW, we just capture it here. Since this only happens when typing // very quickly, it is a safe assumption that we'll get another chance to do on type formatting, since we know the user is typing. // The proper fix for this is https://github.com/dotnet/razor-tooling/issues/6650 at which point this can be removed - foreach (var edit in textEdits) + foreach (var edit in edits) { var startLine = edit.Range.Start.Line; var endLine = edit.Range.End.Line; @@ -82,11 +81,11 @@ internal sealed class CSharpOnTypeFormattingPass( if (startLine >= count || endLine >= count) { _logger.LogWarning($"Got a bad edit that couldn't be applied. Edit is {startLine}-{endLine} but there are only {count} lines in C#."); - return result; + return edits; } } - var normalizedEdits = csharpText.NormalizeTextEdits(textEdits, out var originalTextWithChanges); + var normalizedEdits = csharpText.NormalizeTextEdits((TextEdit[])edits, out var originalTextWithChanges); var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits); var filteredEdits = FilterCSharpTextEdits(context, mappedEdits); if (filteredEdits.Length == 0) @@ -96,9 +95,9 @@ internal sealed class CSharpOnTypeFormattingPass( // // If there aren't any edits that are likely to contain using statement changes, this call will no-op. - filteredEdits = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, textEdits, originalTextWithChanges, filteredEdits, cancellationToken).ConfigureAwait(false); + filteredEdits = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, edits, originalTextWithChanges, filteredEdits, cancellationToken).ConfigureAwait(false); - return new FormattingResult(filteredEdits); + return filteredEdits; } // Find the lines that were affected by these edits. @@ -196,9 +195,9 @@ internal sealed class CSharpOnTypeFormattingPass( var finalChanges = cleanedText.GetTextChanges(originalText); var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray(); - finalEdits = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, textEdits, originalTextWithChanges, finalEdits, cancellationToken).ConfigureAwait(false); + finalEdits = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, edits, originalTextWithChanges, finalEdits, cancellationToken).ConfigureAwait(false); - return new FormattingResult(finalEdits); + return finalEdits; } private TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs index 733c6d05bf..938dc9dd91 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs @@ -18,10 +18,9 @@ internal sealed class FormattingContentValidationPass(ILoggerFactory loggerFacto // Internal for testing. internal bool DebugAssertsEnabled { get; set; } = true; - public Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken) { var text = context.SourceText; - var edits = result.Edits; var changes = edits.Select(text.GetTextChange); var changedText = text.WithChanges(changes); @@ -49,9 +48,9 @@ internal sealed class FormattingContentValidationPass(ILoggerFactory loggerFacto Debug.Fail("A formatting result was rejected because it was going to change non-whitespace content in the document."); } - return Task.FromResult(new FormattingResult([])); + return Task.FromResult([]); } - return Task.FromResult(result); + return Task.FromResult(edits); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs index dbc058f4d1..c223878de2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs @@ -20,12 +20,11 @@ internal sealed class FormattingDiagnosticValidationPass(ILoggerFactory loggerFa // Internal for testing. internal bool DebugAssertsEnabled { get; set; } = true; - public async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public async Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken) { var originalDiagnostics = context.CodeDocument.GetSyntaxTree().Diagnostics; var text = context.SourceText; - var edits = result.Edits; var changes = edits.Select(text.GetTextChange); var changedText = text.WithChanges(changes); var changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); @@ -56,10 +55,10 @@ internal sealed class FormattingDiagnosticValidationPass(ILoggerFactory loggerFa Debug.Fail("A formatting result was rejected because the formatted text produced different diagnostics compared to the original text."); } - return new FormattingResult([]); + return []; } - return result; + return edits; } private class LocationIgnoringDiagnosticComparer : IEqualityComparer diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs index f08b23a0e2..a6b073acc2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs @@ -17,20 +17,18 @@ internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass { private readonly ILogger _logger = logger; - public virtual async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public virtual async Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken) { var originalText = context.SourceText; - var htmlEdits = result.Edits; - var changedText = originalText; var changedContext = context; _logger.LogTestOnly($"Before HTML formatter:\r\n{changedText}"); - if (htmlEdits.Length > 0) + if (edits.Length > 0) { - var changes = htmlEdits.Select(originalText.GetTextChange); + var changes = edits.Select(originalText.GetTextChange); changedText = originalText.WithChanges(changes); // Create a new formatting context for the changed razor document. changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); @@ -49,7 +47,7 @@ internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass var finalChanges = changedText.GetTextChanges(originalText); var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray(); - return new FormattingResult(finalEdits); + return finalEdits; } private static List AdjustRazorIndentation(FormattingContext context) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs index 5f138ca9c1..e7cbf21680 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlOnTypeFormattingPass.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; @@ -12,14 +13,14 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; ///
internal sealed class HtmlOnTypeFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger()) { - public override Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public override Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken) { - if (result.Edits.Length == 0) + if (edits.Length == 0) { // There are no HTML edits for us to apply. No op. - return Task.FromResult(new FormattingResult([])); + return Task.FromResult([]); } - return base.ExecuteAsync(context, result, cancellationToken); + return base.ExecuteAsync(context, edits, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs index 64fa3046b5..66025be578 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs @@ -23,15 +23,15 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal sealed class RazorFormattingPass : IFormattingPass { - public async Task ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken) + public async Task ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken) { // Apply previous edits if any. var originalText = context.SourceText; var changedText = originalText; var changedContext = context; - if (result.Edits.Length > 0) + if (edits.Length > 0) { - var changes = result.Edits.Select(originalText.GetTextChange).ToArray(); + var changes = edits.Select(originalText.GetTextChange); changedText = changedText.WithChanges(changes); changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); @@ -40,16 +40,16 @@ internal sealed class RazorFormattingPass : IFormattingPass // Format the razor bits of the file var syntaxTree = changedContext.CodeDocument.GetSyntaxTree(); - var edits = FormatRazor(changedContext, syntaxTree); + var razorEdits = FormatRazor(changedContext, syntaxTree); // Compute the final combined set of edits - var formattingChanges = edits.Select(changedText.GetTextChange); + var formattingChanges = razorEdits.Select(changedText.GetTextChange); changedText = changedText.WithChanges(formattingChanges); var finalChanges = changedText.GetTextChanges(originalText); var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray(); - return new FormattingResult(finalEdits); + return finalEdits; } private static ImmutableArray FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 497c9c3f82..1808203447 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -88,8 +88,7 @@ internal class RazorFormattingService : IRazorFormattingService using var context = FormattingContext.Create(uri, documentSnapshot, codeDocument, options, _workspaceFactory); var originalText = context.SourceText; - var result = new FormattingResult(htmlEdits); - + var result = htmlEdits; foreach (var pass in _documentFormattingPasses) { cancellationToken.ThrowIfCancellationRequested(); @@ -97,8 +96,8 @@ internal class RazorFormattingService : IRazorFormattingService } var filteredEdits = range is null - ? result.Edits - : result.Edits.Where(e => range.LineOverlapsWith(e.Range)).ToArray(); + ? result + : result.Where(e => range.LineOverlapsWith(e.Range)).ToArray(); return originalText.NormalizeTextEdits(filteredEdits); } @@ -196,7 +195,7 @@ internal class RazorFormattingService : IRazorFormattingService var uri = documentContext.Uri; var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); using var context = FormattingContext.CreateForOnTypeFormatting(uri, documentSnapshot, codeDocument, options, _workspaceFactory, automaticallyAddUsings: automaticallyAddUsings, hostDocumentIndex, triggerCharacter); - var result = new FormattingResult(generatedDocumentEdits); + var result = generatedDocumentEdits; foreach (var pass in formattingPasses) { @@ -205,7 +204,7 @@ internal class RazorFormattingService : IRazorFormattingService } var originalText = context.SourceText; - var razorEdits = originalText.NormalizeTextEdits(result.Edits); + var razorEdits = originalText.NormalizeTextEdits(result); if (collapseEdits) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs index becc4807f3..9b46be8526 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs @@ -34,14 +34,14 @@ public class Foo { } { VsLspFactory.CreateTextEdit(2, 0, " ") }; - var input = new FormattingResult(edits); + var input = edits; var pass = GetPass(); // Act - var result = await pass.ExecuteAsync(context, input, DisposalToken); + var result = await pass.ExecuteAsync(context, edits, DisposalToken); // Assert - Assert.Equal(input, result); + Assert.Same(input, result); } [Fact] @@ -58,14 +58,14 @@ public class Foo { } { VsLspFactory.CreateTextEdit(2, 0, 3, 0, " ") // Nukes a line }; - var input = new FormattingResult(edits); + var input = edits; var pass = GetPass(); // Act var result = await pass.ExecuteAsync(context, input, DisposalToken); // Assert - Assert.Empty(result.Edits); + Assert.Empty(result); } private FormattingContentValidationPass GetPass() diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs index 82f92cfe49..b255405f91 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs @@ -32,14 +32,14 @@ public class Foo { } { VsLspFactory.CreateTextEdit(2, 0, " ") }; - var input = new FormattingResult(edits); + var input = edits; var pass = GetPass(); // Act var result = await pass.ExecuteAsync(context, input, DisposalToken); // Assert - Assert.Equal(input, result); + Assert.Same(input, result); } [Fact] @@ -53,14 +53,13 @@ public class Foo { } "); using var context = CreateFormattingContext(source); var badEdit = VsLspFactory.CreateTextEdit(position: (0, 0), "@ "); // Creates a diagnostic - var input = new FormattingResult([badEdit]); var pass = GetPass(); // Act - var result = await pass.ExecuteAsync(context, input, DisposalToken); + var result = await pass.ExecuteAsync(context, [badEdit], DisposalToken); // Assert - Assert.Empty(result.Edits); + Assert.Empty(result); } private FormattingDiagnosticValidationPass GetPass() From 587e72a2f2d172b1f7b44dd64842913686d5f956 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 23 Aug 2024 13:41:24 +1000 Subject: [PATCH 21/26] Simple PR feedback --- .../RazorCSharpFormattingBenchmark.cs | 2 +- .../DelegatedCompletionItemResolver.cs | 2 -- .../Formatting/HtmlFormatter.cs | 2 +- .../Formatting/CSharpFormatter.cs | 4 ++-- .../Passes/CSharpOnTypeFormattingPass.cs | 11 +++++------ .../Formatting/Passes/RazorFormattingPass.cs | 2 +- .../Formatting/RazorFormattingOptions.cs | 19 +++++++++---------- ...Microsoft.CodeAnalysis.Remote.Razor.csproj | 4 ---- .../FormattingDiagnosticValidationPassTest.cs | 2 +- 9 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs index 571eeb689c..0b687fc006 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs @@ -112,7 +112,7 @@ public class RazorCSharpFormattingBenchmark : RazorLanguageServerBenchmarkBase { var documentContext = new VersionedDocumentContext(DocumentUri, DocumentSnapshot, projectContext: null, version: 1); - var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, [], range: null, RazorFormattingOptions.Default, CancellationToken.None); + var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits: [], range: null, RazorFormattingOptions.Default, CancellationToken.None); #if DEBUG // For debugging purposes only. 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 df02fe2cfe..8fd531ccae 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 @@ -6,13 +6,11 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; -using static Nerdbank.Streams.MultiplexingStream; namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs index 0522c8360f..a20dde4541 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs @@ -89,7 +89,7 @@ internal sealed class HtmlFormatter( public static TextEdit[] FixHtmlTextEdits(SourceText htmlSourceText, TextEdit[] edits) { // Avoid computing a minimal diff if we don't need to - if (!edits.Any(e => e.NewText.Contains("~"))) + if (!edits.Any(static e => e.NewText.Contains("~"))) return edits; return htmlSourceText.NormalizeTextEdits(edits); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs index defa18e346..e0bd5a8721 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs @@ -63,7 +63,7 @@ internal sealed class CSharpFormatter(IDocumentMappingService documentMappingSer var root = await context.CSharpWorkspaceDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); Assumes.NotNull(root); - var changes = RazorCSharpFormattingInteractionService.GetFormattedTextChanges(context.CSharpWorkspace.Services, root, spanToFormat, context.Options.GetIndentationOptions(), cancellationToken); + var changes = RazorCSharpFormattingInteractionService.GetFormattedTextChanges(context.CSharpWorkspace.Services, root, spanToFormat, context.Options.ToIndentationOptions(), cancellationToken); var edits = changes.Select(csharpSourceText.GetTextEdit).ToArray(); return edits; @@ -85,7 +85,7 @@ internal sealed class CSharpFormatter(IDocumentMappingService documentMappingSer // At this point, we have added all the necessary markers and attached annotations. // Let's invoke the C# formatter and hope for the best. - var formattedRoot = RazorCSharpFormattingInteractionService.Format(context.CSharpWorkspace.Services, root, context.Options.GetIndentationOptions(), cancellationToken); + var formattedRoot = RazorCSharpFormattingInteractionService.Format(context.CSharpWorkspace.Services, root, context.Options.ToIndentationOptions(), cancellationToken); var formattedText = formattedRoot.GetText(); var desiredIndentationMap = new Dictionary(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs index 077aa630c8..5ce1e2b13b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs @@ -54,7 +54,7 @@ internal sealed class CSharpOnTypeFormattingPass( context.CSharpWorkspaceDocument, typedChar: context.TriggerCharacter, projectedIndex, - context.Options.GetIndentationOptions(), + context.Options.ToIndentationOptions(), autoFormattingOptions, indentStyle: CodeAnalysis.Formatting.FormattingOptions.IndentStyle.Smart, cancellationToken).ConfigureAwait(false); @@ -84,7 +84,7 @@ internal sealed class CSharpOnTypeFormattingPass( return edits; } } - + var normalizedEdits = csharpText.NormalizeTextEdits((TextEdit[])edits, out var originalTextWithChanges); var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits); var filteredEdits = FilterCSharpTextEdits(context, mappedEdits); @@ -94,7 +94,6 @@ internal sealed class CSharpOnTypeFormattingPass( // because they are non mappable, but might be the only thing changed (eg from the Add Using code action) // // If there aren't any edits that are likely to contain using statement changes, this call will no-op. - filteredEdits = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, edits, originalTextWithChanges, filteredEdits, cancellationToken).ConfigureAwait(false); return filteredEdits; @@ -217,7 +216,7 @@ internal sealed class CSharpOnTypeFormattingPass( if (context.AutomaticallyAddUsings) { // Because we need to parse the C# code twice for this operation, lets do a quick check to see if its even necessary - if (textEdits.Any(e => e.NewText.IndexOf("using") != -1)) + if (textEdits.Any(static e => e.NewText.IndexOf("using") != -1)) { var usingStatementEdits = await AddUsingsHelper.GetUsingStatementEditsAsync(codeDocument, csharpText, originalTextWithChanges, cancellationToken).ConfigureAwait(false); finalEdits = [.. usingStatementEdits, .. finalEdits]; @@ -340,7 +339,7 @@ internal sealed class CSharpOnTypeFormattingPass( if (owner is CSharpStatementLiteralSyntax && owner.TryGetPreviousSibling(out var prevNode) && - prevNode.FirstAncestorOrSelf(a => a is CSharpTemplateBlockSyntax) is { } template && + prevNode.FirstAncestorOrSelf(static a => a is CSharpTemplateBlockSyntax) is { } template && owner.SpanStart == template.Span.End && IsOnSingleLine(template, text)) { @@ -494,7 +493,7 @@ internal sealed class CSharpOnTypeFormattingPass( if (owner is CSharpStatementLiteralSyntax && owner.NextSpan() is { } nextNode && - nextNode.FirstAncestorOrSelf(a => a is CSharpTemplateBlockSyntax) is { } template && + nextNode.FirstAncestorOrSelf(static a => a is CSharpTemplateBlockSyntax) is { } template && template.SpanStart == owner.Span.End && IsOnSingleLine(template, text)) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs index 66025be578..82224f34c1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs @@ -411,7 +411,7 @@ internal sealed class RazorFormattingPass : IFormattingPass return didFormat; - static bool RangeHasBeenModified(ref PooledArrayBuilder edits, Range range) + static bool RangeHasBeenModified(ref readonly PooledArrayBuilder edits, Range range) { // Because we don't always know what kind of Razor object we're operating on we have to do this to avoid duplicate edits. // The other way to accomplish this would be to apply the edits after every node and function, but that's not in scope for my current work. 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 f830202a3b..fd7125c620 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs @@ -6,18 +6,17 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed record RazorFormattingOptions +internal record struct RazorFormattingOptions { - public bool InsertSpaces { get; init; } - public int TabSize { get; init; } - public bool CodeBlockBraceOnNextLine { get; init; } + public static readonly RazorFormattingOptions Default = new(); - public static RazorFormattingOptions Default => new RazorFormattingOptions() + public bool InsertSpaces { get; init; } = true; + public int TabSize { get; init; } = 4; + public bool CodeBlockBraceOnNextLine { get; init; } = false; + + public RazorFormattingOptions() { - InsertSpaces = true, - TabSize = 4, - CodeBlockBraceOnNextLine = false - }; + } public static RazorFormattingOptions From(FormattingOptions options, bool codeBlockBraceOnNextLine) { @@ -29,7 +28,7 @@ internal sealed record RazorFormattingOptions }; } - public RazorIndentationOptions GetIndentationOptions() + public RazorIndentationOptions ToIndentationOptions() => new( UseTabs: !InsertSpaces, TabSize: TabSize, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj index 60cd914522..3357fd1eb7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj @@ -57,8 +57,4 @@ - - - - diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs index b255405f91..d232aeffdb 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs @@ -95,7 +95,7 @@ public class Foo { } var projectEngine = RazorProjectEngine.Create(builder => builder.SetRootNamespace("Test")); var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, importSources: default, tagHelpers); - var documentSnapshot = FormattingTestBase.CreateDocumentSnapshot(path, tagHelpers, fileKind, [], [], projectEngine, codeDocument); + var documentSnapshot = FormattingTestBase.CreateDocumentSnapshot(path, tagHelpers, fileKind, importsDocuments: [], imports: [], projectEngine, codeDocument); return (codeDocument, documentSnapshot); } From 6622a0f979bea9aaa739a68b0dfa113cfe8a0895 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 23 Aug 2024 13:53:55 +1000 Subject: [PATCH 22/26] Missed the readonly bit --- .../Formatting/RazorFormattingOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fd7125c620..a0397ec80f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingOptions.cs @@ -6,7 +6,7 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal record struct RazorFormattingOptions +internal readonly record struct RazorFormattingOptions { public static readonly RazorFormattingOptions Default = new(); From 2157169d4d1d7d514503a8616a28745d589c4a9b Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 23 Aug 2024 16:24:43 +1000 Subject: [PATCH 23/26] More PR feedback --- .../DocumentOnTypeFormattingEndpoint.cs | 26 ++++++++++----- .../InlineCompletionEndPoint.cs | 5 +-- .../Formatting/FormattingContext.cs | 2 +- .../DocumentOnTypeFormattingEndpointTest.cs | 32 +++++++++++++++++++ .../FormattingLanguageServerTestBase.cs | 1 + 5 files changed, 55 insertions(+), 11 deletions(-) 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 20488c36a1..e72d79f83c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading; @@ -35,9 +36,11 @@ internal class DocumentOnTypeFormattingEndpoint( private readonly IHtmlFormatter _htmlFormatter = htmlFormatter; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - private static readonly FrozenSet s_csharpTriggerCharacters = FrozenSet.ToFrozenSet(["}", ";"]); - private static readonly FrozenSet s_htmlTriggerCharacters = FrozenSet.ToFrozenSet(["\n", "{", "}", ";"]); - private static readonly FrozenSet s_allTriggerCharacters = FrozenSet.ToFrozenSet(s_csharpTriggerCharacters.Concat(s_htmlTriggerCharacters)); + private static readonly ImmutableArray s_allTriggerCharacters = ["}", ";", "\n", "{"]; + + private static readonly FrozenSet s_csharpTriggerCharacterSet = FrozenSet.ToFrozenSet(["}", ";"], StringComparer.Ordinal); + private static readonly FrozenSet s_htmlTriggerCharacterSet = FrozenSet.ToFrozenSet(["\n", "{", "}", ";"], StringComparer.Ordinal); + private static readonly FrozenSet s_allTriggerCharacterSet = s_allTriggerCharacters.ToFrozenSet(StringComparer.Ordinal); public bool MutatesSolutionState => false; @@ -45,8 +48,8 @@ internal class DocumentOnTypeFormattingEndpoint( { serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions { - FirstTriggerCharacter = s_allTriggerCharacters.First(), - MoreTriggerCharacter = s_allTriggerCharacters.Skip(1).ToArray(), + FirstTriggerCharacter = s_allTriggerCharacters[0], + MoreTriggerCharacter = s_allTriggerCharacters.AsSpan()[1..].ToArray(), }; } @@ -71,7 +74,7 @@ internal class DocumentOnTypeFormattingEndpoint( return null; } - if (!s_allTriggerCharacters.Contains(request.Character, StringComparer.Ordinal)) + if (!s_allTriggerCharacterSet.Contains(request.Character)) { _logger.LogWarning($"Unexpected trigger character '{request.Character}'."); return null; @@ -149,14 +152,21 @@ internal class DocumentOnTypeFormattingEndpoint( { if (languageKind == RazorLanguageKind.CSharp) { - return s_csharpTriggerCharacters.Contains(triggerCharacter); + return s_csharpTriggerCharacterSet.Contains(triggerCharacter); } else if (languageKind == RazorLanguageKind.Html) { - return s_htmlTriggerCharacters.Contains(triggerCharacter); + return s_htmlTriggerCharacterSet.Contains(triggerCharacter); } // Unknown trigger character. return false; } + + internal static class TestAccessor + { + public static ImmutableArray GetAllTriggerCharacters() => s_allTriggerCharacters; + public static FrozenSet GetCSharpTriggerCharacterSet() => s_csharpTriggerCharacterSet; + public static FrozenSet GetHtmlTriggerCharacterSet() => s_htmlTriggerCharacterSet; + } } 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 779cc1eb04..1efda4e511 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Logging; @@ -113,7 +114,7 @@ internal sealed class InlineCompletionEndpoint( return null; } - var items = new List(); + using var items = new PooledArrayBuilder(list.Items.Length); foreach (var item in list.Items) { var containsSnippet = item.TextFormat == InsertTextFormat.Snippet; @@ -152,7 +153,7 @@ internal sealed class InlineCompletionEndpoint( _logger.LogInformation($"Returning {items.Count} items."); return new VSInternalInlineCompletionList { - Items = [.. items] + Items = items.ToArray() }; } 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 328bfada28..39d2bba962 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -221,7 +221,7 @@ internal sealed class FormattingContext : IDisposable { result = null; var formattingSpans = GetFormattingSpans(); - foreach (var formattingSpan in formattingSpans) + foreach (var formattingSpan in formattingSpans.AsEnumerable()) { var span = formattingSpan.Span; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs index 0dd606e64b..a84829c38c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; @@ -18,6 +19,37 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) : FormattingLanguageServerTestBase(testOutput) { + [Fact] + public void AllTriggerCharacters_IncludesCSharpTriggerCharacters() + { + var allChars = DocumentOnTypeFormattingEndpoint.TestAccessor.GetAllTriggerCharacters(); + + foreach (var character in DocumentOnTypeFormattingEndpoint.TestAccessor.GetCSharpTriggerCharacterSet()) + { + Assert.Contains(character, allChars); + } + } + + [Fact] + public void AllTriggerCharacters_IncludesHtmlTriggerCharacters() + { + var allChars = DocumentOnTypeFormattingEndpoint.TestAccessor.GetAllTriggerCharacters(); + + foreach (var character in DocumentOnTypeFormattingEndpoint.TestAccessor.GetHtmlTriggerCharacterSet()) + { + Assert.Contains(character, allChars); + } + } + + [Fact] + public void AllTriggerCharacters_ContainsUniqueCharacters() + { + var allChars = DocumentOnTypeFormattingEndpoint.TestAccessor.GetAllTriggerCharacters(); + var distinctChars = allChars.Distinct().ToArray(); + + Assert.Equal(distinctChars, allChars); + } + [Fact] public async Task Handle_OnTypeFormatting_FormattingDisabled_ReturnsNull() { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs index 0fdf80733d..4d9f3991a0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; From 19134c2ee6554ada37a97ab9752d95ffe46b17e5 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 27 Aug 2024 14:08:54 +1000 Subject: [PATCH 24/26] PR feedback --- .../Formatting/HtmlFormatter.cs | 2 +- .../Extensions/SourceTextExtensions.cs | 12 +++++++++--- .../Formatting/Passes/CSharpOnTypeFormattingPass.cs | 2 +- .../Formatting/RazorFormattingService.cs | 4 ++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs index a20dde4541..0123abd9b5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs @@ -92,6 +92,6 @@ internal sealed class HtmlFormatter( if (!edits.Any(static e => e.NewText.Contains("~"))) return edits; - return htmlSourceText.NormalizeTextEdits(edits); + return htmlSourceText.MinimizeTextEdits(edits); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SourceTextExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SourceTextExtensions.cs index cf8b62aa8c..77b8b9b1b8 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SourceTextExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/SourceTextExtensions.cs @@ -273,10 +273,16 @@ internal static class SourceTextExtensions return false; } - public static TextEdit[] NormalizeTextEdits(this SourceText text, TextEdit[] edits) - => NormalizeTextEdits(text, edits, out _); + /// + /// Applies the set of edits specified, and returns the minimal set needed to make the same changes + /// + public static TextEdit[] MinimizeTextEdits(this SourceText text, TextEdit[] edits) + => MinimizeTextEdits(text, edits, out _); - public static TextEdit[] NormalizeTextEdits(this SourceText text, TextEdit[] edits, out SourceText originalTextWithChanges) + /// + /// Applies the set of edits specified, and returns the minimal set needed to make the same changes + /// + public static TextEdit[] MinimizeTextEdits(this SourceText text, TextEdit[] edits, out SourceText originalTextWithChanges) { var changes = edits.Select(text.GetTextChange); originalTextWithChanges = text.WithChanges(changes); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs index 5ce1e2b13b..f8025b7f8d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs @@ -85,7 +85,7 @@ internal sealed class CSharpOnTypeFormattingPass( } } - var normalizedEdits = csharpText.NormalizeTextEdits((TextEdit[])edits, out var originalTextWithChanges); + var normalizedEdits = csharpText.MinimizeTextEdits(edits, out var originalTextWithChanges); var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits); var filteredEdits = FilterCSharpTextEdits(context, mappedEdits); if (filteredEdits.Length == 0) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 1808203447..8879e83232 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -99,7 +99,7 @@ internal class RazorFormattingService : IRazorFormattingService ? result : result.Where(e => range.LineOverlapsWith(e.Range)).ToArray(); - return originalText.NormalizeTextEdits(filteredEdits); + return originalText.MinimizeTextEdits(filteredEdits); } public Task GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) @@ -204,7 +204,7 @@ internal class RazorFormattingService : IRazorFormattingService } var originalText = context.SourceText; - var razorEdits = originalText.NormalizeTextEdits(result); + var razorEdits = originalText.MinimizeTextEdits(result); if (collapseEdits) { From 1de43a03745079e1f1f78af59cbbedace75d0f36 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 28 Aug 2024 16:07:08 +1000 Subject: [PATCH 25/26] Remove stale TODO --- .../Formatting/Passes/RazorFormattingPass.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs index 82224f34c1..6eb7e036b4 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs @@ -61,7 +61,7 @@ internal sealed class RazorFormattingPass : IFormattingPass { // Disclaimer: CSharpCodeBlockSyntax is used a _lot_ in razor so these methods are probably // being overly careful to only try to format syntax forms they care about. - TryFormatCSharpBlockStructure(context, ref edits.AsRef(), source, node); // TODO + TryFormatCSharpBlockStructure(context, ref edits.AsRef(), source, node); TryFormatSingleLineDirective(ref edits.AsRef(), source, node); TryFormatBlocks(context, ref edits.AsRef(), source, node); } From ee83a6b9bbfbb722e6e3ac2eeb3a0f4a996a2977 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 30 Aug 2024 07:38:43 +1000 Subject: [PATCH 26/26] Fixes after merge --- .../Formatting/FormattingContext.cs | 4 +-- .../FormattingDiagnosticValidationPass.cs | 2 +- .../AutoInsert/RemoteAutoInsertService.cs | 21 +++++-------- .../Formatting/RemoteFormattingPasses.cs | 30 ------------------- .../RemoteRazorFormattingService.cs | 13 +++----- .../Formatting_NetFx/FormattingTestBase.cs | 17 +++-------- .../TestRazorFormattingService.cs | 4 ++- 7 files changed, 22 insertions(+), 69 deletions(-) delete mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingPasses.cs 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 32233177ef..e0a16ef90d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -313,12 +313,12 @@ internal sealed class FormattingContext : IDisposable char triggerCharacter) { return new FormattingContext( + codeDocumentProvider, + workspaceFactory, uri, originalSnapshot, codeDocument, options, - codeDocumentProvider, - workspaceFactory, automaticallyAddUsings, hostDocumentIndex, triggerCharacter); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs index a402d6b6c9..c223878de2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs @@ -13,7 +13,7 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal class FormattingDiagnosticValidationPass(ILoggerFactory loggerFactory) : IFormattingPass +internal sealed class FormattingDiagnosticValidationPass(ILoggerFactory loggerFactory) : IFormattingPass { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs index 4c009c6b7a..9793c86b68 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs @@ -19,7 +19,6 @@ using Roslyn.LanguageServer.Protocol; using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; using RoslynFormattingOptions = Roslyn.LanguageServer.Protocol.FormattingOptions; using RoslynInsertTextFormat = Roslyn.LanguageServer.Protocol.InsertTextFormat; -using VsLspFormattingOptions = Microsoft.VisualStudio.LanguageServer.Protocol.FormattingOptions; namespace Microsoft.CodeAnalysis.Remote.Razor; @@ -171,7 +170,7 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args) return Response.NoFurtherHandling; } - var razorFormattingOptions = new VsLspFormattingOptions() + var razorFormattingOptions = new RazorFormattingOptions() { InsertSpaces = !indentWithTabs, TabSize = indentSize @@ -180,33 +179,29 @@ internal sealed class RemoteAutoInsertService(in ServiceArgs args) var vsLspTextEdit = VsLspFactory.CreateTextEdit( autoInsertResponseItem.TextEdit.Range.ToLinePositionSpan(), autoInsertResponseItem.TextEdit.NewText); - var mappedEdits = autoInsertResponseItem.TextEditFormat == RoslynInsertTextFormat.Snippet - ? await _razorFormattingService.FormatSnippetAsync( + var mappedEdit = autoInsertResponseItem.TextEditFormat == RoslynInsertTextFormat.Snippet + ? await _razorFormattingService.GetCSharpSnippetFormattingEditAsync( remoteDocumentContext, - RazorLanguageKind.CSharp, [vsLspTextEdit], razorFormattingOptions, cancellationToken) .ConfigureAwait(false) - : await _razorFormattingService.FormatOnTypeAsync( + : await _razorFormattingService.GetSingleCSharpEditAsync( remoteDocumentContext, - RazorLanguageKind.CSharp, - [vsLspTextEdit], + vsLspTextEdit, razorFormattingOptions, - hostDocumentIndex: 0, - triggerCharacter: '\0', cancellationToken) .ConfigureAwait(false); - if (mappedEdits is not [{ } edit]) + if (mappedEdit is null) { return Response.NoFurtherHandling; } return Response.Results( new RemoteAutoInsertTextEdit( - edit.Range.ToLinePositionSpan(), - edit.NewText, + mappedEdit.Range.ToLinePositionSpan(), + mappedEdit.NewText, autoInsertResponseItem.TextEditFormat)); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingPasses.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingPasses.cs deleted file mode 100644 index 2ca3bda8a9..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingPasses.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Composition; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Formatting; -using Microsoft.CodeAnalysis.Razor.Logging; - -namespace Microsoft.CodeAnalysis.Remote.Razor.Formatting; - -[Export(typeof(IFormattingPass)), Shared] -[method: ImportingConstructor] -internal sealed class RemoteCSharpFormattingPass( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - : CSharpFormattingPass(documentMappingService, loggerFactory); - -[Export(typeof(IFormattingPass)), Shared] -[method: ImportingConstructor] -internal sealed class RemoteFormattingContentValidationPass( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - : FormattingContentValidationPass(documentMappingService, loggerFactory); - -[Export(typeof(IFormattingPass)), Shared] -[method: ImportingConstructor] -internal sealed class RemoteFormattingDiagnosticValidationPass( - IDocumentMappingService documentMappingService, - ILoggerFactory loggerFactory) - : FormattingDiagnosticValidationPass(documentMappingService, loggerFactory); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs index bff121bd4c..7d9344fe3d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs @@ -1,22 +1,17 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Collections.Generic; using System.Composition; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Workspaces; namespace Microsoft.CodeAnalysis.Remote.Razor.Formatting; [Export(typeof(IRazorFormattingService)), Shared] [method: ImportingConstructor] -internal class RemoteRazorFormattingService( - [ImportMany] IEnumerable formattingPasses, - IFormattingCodeDocumentProvider codeDocumentProvider, - IAdhocWorkspaceFactory adhocWorkspaceFactory) - : RazorFormattingService( - formattingPasses, - codeDocumentProvider, - adhocWorkspaceFactory) +internal class RemoteRazorFormattingService(IFormattingCodeDocumentProvider codeDocumentProvider, IDocumentMappingService documentMappingService, IAdhocWorkspaceFactory adhocWorkspaceFactory, ILoggerFactory loggerFactory) + : RazorFormattingService(codeDocumentProvider, documentMappingService, adhocWorkspaceFactory, loggerFactory) { } 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 22d20e2284..8835b479ed 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 @@ -25,7 +25,6 @@ using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; -using Microsoft.VisualStudio.Settings.Internal; using Moq; using Roslyn.Test.Utilities; using Xunit; @@ -83,14 +82,10 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, razorLSPOptions); var documentContext = new DocumentContext(uri, documentSnapshot, projectContext: null); - var projectManager = StrictMock.Of(); - var versionCache = new DocumentVersionCache(projectManager); - versionCache.TrackDocumentVersion(documentSnapshot, version: 1); - var client = new FormattingLanguageServerClient(LoggerFactory); client.AddCodeDocument(codeDocument); - var htmlFormatter = new HtmlFormatter(client, versionCache); + var htmlFormatter = new HtmlFormatter(client); var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken); // Act @@ -152,14 +147,10 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase } else { - var projectManager = StrictMock.Of(); - var versionCache = new DocumentVersionCache(projectManager); - versionCache.TrackDocumentVersion(documentSnapshot, version: 1); - var client = new FormattingLanguageServerClient(LoggerFactory); client.AddCodeDocument(codeDocument); - var htmlFormatter = new HtmlFormatter(client, versionCache); + var htmlFormatter = new HtmlFormatter(client); var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken); edits = await formattingService.GetHtmlOnTypeFormattingEditsAsync(documentContext, htmlEdits, razorOptions, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken); } @@ -292,7 +283,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase ]); var projectEngine = RazorProjectEngine.Create( - new RazorConfiguration(RazorLanguageVersion.Latest, "TestConfiguration", ImmutableArray.Empty, new LanguageServerFlags(forceRuntimeCodeGeneration)), + new RazorConfiguration(RazorLanguageVersion.Latest, "TestConfiguration", Extensions: [], new LanguageServerFlags(forceRuntimeCodeGeneration)), projectFileSystem, builder => { @@ -301,7 +292,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase RazorExtensions.Register(builder); }); - var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, ImmutableArray.Create(importsDocument), tagHelpers); + var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, [importsDocument], tagHelpers); if (!allowDiagnostics) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs index 01152568ed..fc10917be7 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs @@ -39,6 +39,8 @@ internal static class TestRazorFormattingService await optionsMonitor.UpdateAsync(CancellationToken.None); } - return new RazorFormattingService(mappingService, TestAdhocWorkspaceFactory.Instance, loggerFactory); + var formattingCodeDocumentProvider = new LspFormattingCodeDocumentProvider(); + + return new RazorFormattingService(formattingCodeDocumentProvider, mappingService, TestAdhocWorkspaceFactory.Instance, loggerFactory); } }