Re-architect formatting to prepare for cohosting (and for fun!) (#10778)

I nerd sniped myself thinking about how to get formatting into
cohosting, given the limitations Alex ran into doing the relayering for
auto insert, and this is the result. I was going to go further and port
the actual formatting endpoint to cohosting, but that would have ran
into the same issue that Alex did with auto insert, so I figured I'd
wait for that to merge, and put this up in the meantime.

This unblocks the formatting, code action and completion end points from
being ported.
Part of https://github.com/dotnet/razor/issues/10743
Part of https://github.com/dotnet/razor/issues/9519

I **strongly** recommend reviewing commit-at-a-time, as I did this
deliberately in an order, and in order to (hopefully) make reviewing
easier. Though granted, there are a lot of commits.
This commit is contained in:
David Wengier 2024-08-30 10:33:50 +10:00 коммит произвёл GitHub
Родитель e1f6fbaad7 ee83a6b9bb
Коммит 21c7674ac4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
59 изменённых файлов: 1189 добавлений и 1476 удалений

Просмотреть файл

@ -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 DocumentContext(DocumentUri, DocumentSnapshot, projectContext: null);
var edits = await RazorFormattingService.FormatAsync(documentContext, range: null, options, CancellationToken.None);
var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits: [], range: null, RazorFormattingOptions.Default, CancellationToken.None);
#if DEBUG
// For debugging purposes only.

Просмотреть файл

@ -4,7 +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;
using System.Threading.Tasks;
@ -185,19 +185,21 @@ 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.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);
if (mappedEdits is not [{ } edit])
var options = RazorFormattingOptions.From(originalRequest.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
var mappedEdit = delegatedResponse.TextEditFormat == InsertTextFormat.Snippet
? 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;
}
return new VSInternalDocumentOnAutoInsertResponseItem()
{
TextEdit = edit,
TextEdit = mappedEdit,
TextEditFormat = delegatedResponse.TextEditFormat,
};
}

Просмотреть файл

@ -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<TextEdit[]> 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<TextEdit>();
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<IEnumerable<string>> 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<UsingDirectiveSyntax>()
// 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;
}
/// <summary>
/// Extracts the namespace from a C# add using statement provided by Visual Studio
/// </summary>
/// <param name="csharpAddUsing">Add using statement of the form `using System.X;`</param>
/// <param name="namespace">Extract namespace `System.X`</param>
/// <param name="prefix">The prefix to show, before the namespace, if any</param>
/// <returns></returns>
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<char> fullTypeName, out ReadOnlySpan<char> @namespace, out ReadOnlySpan<char> 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;
}
}

Просмотреть файл

@ -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<string, object>
{
{ "trimTrailingWhitespace", true },
{ "insertFinalNewline", true },
{ "trimFinalNewlines", true },
},
};
private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory;
private readonly IRazorFormattingService _razorFormattingService = razorFormattingService;
@ -80,11 +65,10 @@ 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 formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync(
documentContext,
RazorLanguageKind.CSharp,
csharpTextEdits,
s_defaultFormattingOptions,
RazorFormattingOptions.Default,
cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
@ -99,7 +83,7 @@ internal sealed class DefaultCSharpCodeActionResolver(
new TextDocumentEdit()
{
TextDocument = codeDocumentIdentifier,
Edits = formattedEdits,
Edits = formattedEdit is null ? [] : [formattedEdit],
}
}
};

Просмотреть файл

@ -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));

Просмотреть файл

@ -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

Просмотреть файл

@ -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<TextDocumentEdit>();
// 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<RazorUsingDirective>();
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<RazorUsingDirective> existingUsingDirectives)
// Internal for testing
internal static string GetNamespaceFromFQN(string fullyQualifiedName)
{
Debug.Assert(existingUsingDirectives.Count > 0);
using var edits = new PooledArrayBuilder<TextEdit>();
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<char> fullTypeName, out ReadOnlySpan<char> @namespace, out ReadOnlySpan<char> 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<RazorUsingDirective> 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;
}
}

Просмотреть файл

@ -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);

Просмотреть файл

@ -204,20 +204,20 @@ 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 formattedEdits = await _razorFormattingService.FormatCodeActionAsync(
var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync(
documentContext,
RazorLanguageKind.CSharp,
result,
formattingOptions,
cancellationToken).ConfigureAwait(false);
edits = formattedEdits;
edits = formattedEdit is null ? [] : [formattedEdit];
}
}

Просмотреть файл

@ -6,7 +6,6 @@ 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;
@ -15,21 +14,16 @@ using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation;
internal class DelegatedCompletionItemResolver : CompletionItemResolver
internal class DelegatedCompletionItemResolver(
IDocumentContextFactory documentContextFactory,
IRazorFormattingService formattingService,
RazorLSPOptionsMonitor optionsMonitor,
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 RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor;
private readonly IClientConnection _clientConnection = clientConnection;
public override async Task<VSInternalCompletionItem?> ResolveAsync(
VSInternalCompletionItem item,
@ -118,18 +112,19 @@ internal class DelegatedCompletionItemResolver : CompletionItemResolver
return resolvedCompletionItem;
}
var options = RazorFormattingOptions.From(formattingOptions, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
if (resolvedCompletionItem.TextEdit is not null)
{
if (resolvedCompletionItem.TextEdit.Value.TryGetFirst(out var textEdit))
{
var formattedTextEdit = await _formattingService.FormatSnippetAsync(
var formattedTextEdit = await _formattingService.GetCSharpSnippetFormattingEditAsync(
documentContext,
RazorLanguageKind.CSharp,
new[] { textEdit },
formattingOptions,
[textEdit],
options,
cancellationToken).ConfigureAwait(false);
resolvedCompletionItem.TextEdit = formattedTextEdit.FirstOrDefault();
resolvedCompletionItem.TextEdit = formattedTextEdit;
}
else
{
@ -141,14 +136,13 @@ internal class DelegatedCompletionItemResolver : CompletionItemResolver
if (resolvedCompletionItem.AdditionalTextEdits is not null)
{
var formattedTextEdits = await _formattingService.FormatSnippetAsync(
var formattedTextEdit = await _formattingService.GetCSharpSnippetFormattingEditAsync(
documentContext,
RazorLanguageKind.CSharp,
resolvedCompletionItem.AdditionalTextEdits,
formattingOptions,
options,
cancellationToken).ConfigureAwait(false);
resolvedCompletionItem.AdditionalTextEdits = formattedTextEdits;
resolvedCompletionItem.AdditionalTextEdits = formattedTextEdit is null ? null : [formattedTextEdit];
}
return resolvedCompletionItem;

Просмотреть файл

@ -59,16 +59,9 @@ internal static class IServiceCollectionExtensions
public static void AddFormattingServices(this IServiceCollection services)
{
// Formatting
services.AddSingleton<IHtmlFormatter, HtmlFormatter>();
services.AddSingleton<IRazorFormattingService, RazorFormattingService>();
// Formatting Passes
services.AddSingleton<IFormattingPass, HtmlFormattingPass>();
services.AddSingleton<IFormattingPass, CSharpFormattingPass>();
services.AddSingleton<IFormattingPass, LspCSharpOnTypeFormattingPass>();
services.AddSingleton<IFormattingPass, FormattingDiagnosticValidationPass>();
services.AddSingleton<IFormattingPass, FormattingContentValidationPass>();
services.AddSingleton<IFormattingPass, LspRazorFormattingPass>();
services.AddHandlerWithCapabilities<DocumentFormattingEndpoint>();
services.AddHandlerWithCapabilities<DocumentOnTypeFormattingEndpoint>();
services.AddHandlerWithCapabilities<DocumentRangeFormattingEndpoint>();

Просмотреть файл

@ -1,29 +1,25 @@
// 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.TextDocumentFormattingName)]
internal class DocumentFormattingEndpoint : IRazorRequestHandler<DocumentFormattingParams, TextEdit[]?>, ICapabilitiesProvider
internal class DocumentFormattingEndpoint(
IRazorFormattingService razorFormattingService,
IHtmlFormatter htmlFormatter,
RazorLSPOptionsMonitor optionsMonitor) : IRazorRequestHandler<DocumentFormattingParams, TextEdit[]?>, 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;
private readonly IHtmlFormatter _htmlFormatter = htmlFormatter;
public bool MutatesSolutionState => false;
@ -56,7 +52,10 @@ internal class DocumentFormattingEndpoint : IRazorRequestHandler<DocumentFormatt
return null;
}
var edits = await _razorFormattingService.FormatAsync(documentContext, range: null, request.Options, cancellationToken).ConfigureAwait(false);
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, options, cancellationToken).ConfigureAwait(false);
return edits;
}
}

Просмотреть файл

@ -2,13 +2,16 @@
// 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.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
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,19 +24,23 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
[RazorLanguageServerEndpoint(Methods.TextDocumentOnTypeFormattingName)]
internal class DocumentOnTypeFormattingEndpoint(
IRazorFormattingService razorFormattingService,
IHtmlFormatter htmlFormatter,
IDocumentMappingService documentMappingService,
RazorLSPOptionsMonitor optionsMonitor,
ILoggerFactory loggerFactory)
: IRazorRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]?>, 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<DocumentOnTypeFormattingEndpoint>();
private static readonly IReadOnlyList<string> s_csharpTriggerCharacters = new[] { "}", ";" };
private static readonly IReadOnlyList<string> s_htmlTriggerCharacters = new[] { "\n", "{", "}", ";" };
private static readonly IReadOnlyList<string> s_allTriggerCharacters = s_csharpTriggerCharacters.Concat(s_htmlTriggerCharacters).ToArray();
private static readonly ImmutableArray<string> s_allTriggerCharacters = ["}", ";", "\n", "{"];
private static readonly FrozenSet<string> s_csharpTriggerCharacterSet = FrozenSet.ToFrozenSet(["}", ";"], StringComparer.Ordinal);
private static readonly FrozenSet<string> s_htmlTriggerCharacterSet = FrozenSet.ToFrozenSet(["\n", "{", "}", ";"], StringComparer.Ordinal);
private static readonly FrozenSet<string> s_allTriggerCharacterSet = s_allTriggerCharacters.ToFrozenSet(StringComparer.Ordinal);
public bool MutatesSolutionState => false;
@ -42,7 +49,7 @@ internal class DocumentOnTypeFormattingEndpoint(
serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions
{
FirstTriggerCharacter = s_allTriggerCharacters[0],
MoreTriggerCharacter = s_allTriggerCharacters.Skip(1).ToArray(),
MoreTriggerCharacter = s_allTriggerCharacters.AsSpan()[1..].ToArray(),
};
}
@ -67,14 +74,13 @@ 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;
}
var documentContext = requestContext.DocumentContext;
if (documentContext is null)
{
_logger.LogWarning($"Failed to find document {request.TextDocument.Uri}.");
@ -114,7 +120,24 @@ internal class DocumentOnTypeFormattingEndpoint(
Debug.Assert(request.Character.Length > 0);
var formattedEdits = await _razorFormattingService.FormatOnTypeAsync(documentContext, triggerCharacterKind, Array.Empty<TextEdit>(), request.Options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false);
var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
TextEdit[] formattedEdits;
if (triggerCharacterKind == RazorLanguageKind.CSharp)
{
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, options, hostDocumentIndex, request.Character[0], cancellationToken).ConfigureAwait(false);
}
else
{
Assumed.Unreachable();
return null;
}
if (formattedEdits.Length == 0)
{
_logger.LogInformation($"No formatting changes were necessary");
@ -129,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<string> GetAllTriggerCharacters() => s_allTriggerCharacters;
public static FrozenSet<string> GetCSharpTriggerCharacterSet() => s_csharpTriggerCharacterSet;
public static FrozenSet<string> GetHtmlTriggerCharacterSet() => s_htmlTriggerCharacterSet;
}
}

Просмотреть файл

@ -1,29 +1,25 @@
// 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<DocumentRangeFormattingParams, TextEdit[]?>, ICapabilitiesProvider
internal class DocumentRangeFormattingEndpoint(
IRazorFormattingService razorFormattingService,
IHtmlFormatter htmlFormatter,
RazorLSPOptionsMonitor optionsMonitor) : IRazorRequestHandler<DocumentRangeFormattingParams, TextEdit[]?>, 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 IHtmlFormatter _htmlFormatter = htmlFormatter;
public bool MutatesSolutionState => false;
@ -56,7 +52,10 @@ internal class DocumentRangeFormattingEndpoint : IRazorRequestHandler<DocumentRa
return null;
}
var edits = await _razorFormattingService.FormatAsync(documentContext, request.Range, request.Options, cancellationToken).ConfigureAwait(false);
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, options, cancellationToken).ConfigureAwait(false);
return edits;
}

Просмотреть файл

@ -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;
@ -15,34 +14,25 @@ using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
internal class HtmlFormatter
internal sealed class HtmlFormatter(
IClientConnection clientConnection) : IHtmlFormatter
{
private readonly IClientConnection _clientConnection;
private readonly IClientConnection _clientConnection = clientConnection;
public HtmlFormatter(IClientConnection clientConnection)
{
_clientConnection = clientConnection;
}
public async Task<TextEdit[]> FormatAsync(
FormattingContext context,
public async Task<TextEdit[]> GetDocumentFormattingEditsAsync(
IDocumentSnapshot documentSnapshot,
Uri uri,
FormattingOptions options,
CancellationToken cancellationToken)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var documentVersion = context.OriginalSnapshot.Version;
var @params = new RazorDocumentFormattingParams()
{
TextDocument = new TextDocumentIdentifier
{
Uri = context.Uri,
Uri = uri,
},
HostDocumentVersion = documentVersion,
Options = context.Options
HostDocumentVersion = documentSnapshot.Version,
Options = options
};
var result = await _clientConnection.SendRequestAsync<DocumentFormattingParams, RazorDocumentFormattingResponse?>(
@ -50,22 +40,24 @@ internal class HtmlFormatter
@params,
cancellationToken).ConfigureAwait(false);
return result?.Edits ?? Array.Empty<TextEdit>();
return result?.Edits ?? [];
}
public async Task<TextEdit[]> FormatOnTypeAsync(
FormattingContext context,
CancellationToken cancellationToken)
public async Task<TextEdit[]> GetOnTypeFormattingEditsAsync(
IDocumentSnapshot documentSnapshot,
Uri uri,
Position position,
string triggerCharacter,
FormattingOptions options,
CancellationToken cancellationToken)
{
var documentVersion = context.OriginalSnapshot.Version;
var @params = new RazorDocumentOnTypeFormattingParams()
{
Position = context.SourceText.GetPosition(context.HostDocumentIndex),
Character = context.TriggerCharacter.ToString(),
TextDocument = new TextDocumentIdentifier { Uri = context.Uri },
Options = context.Options,
HostDocumentVersion = documentVersion,
Position = position,
Character = triggerCharacter.ToString(),
TextDocument = new TextDocumentIdentifier { Uri = uri },
Options = options,
HostDocumentVersion = documentSnapshot.Version,
};
var result = await _clientConnection.SendRequestAsync<RazorDocumentOnTypeFormattingParams, RazorDocumentFormattingResponse?>(
@ -73,7 +65,7 @@ internal class HtmlFormatter
@params,
cancellationToken).ConfigureAwait(false);
return result?.Edits ?? Array.Empty<TextEdit>();
return result?.Edits ?? [];
}
/// <summary>
@ -82,20 +74,12 @@ internal class HtmlFormatter
/// minimal text edits
/// </summary>
// 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("~")))
if (!edits.Any(static 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.MinimizeTextEdits(edits);
}
}

Просмотреть файл

@ -0,0 +1,16 @@
// 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<TextEdit[]> GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken);
Task<TextEdit[]> GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken);
}

Просмотреть файл

@ -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<TextEdit[]> 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 AddUsingsCodeActionProviderHelper.GetUsingStatementEditsAsync(codeDocument, csharpText, originalTextWithChanges, cancellationToken).ConfigureAwait(false);
finalEdits = [.. usingStatementEdits, .. finalEdits];
}
}
return finalEdits;
}
}

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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;
@ -30,6 +31,7 @@ internal sealed class InlineCompletionEndpoint(
IClientConnection clientConnection,
IFormattingCodeDocumentProvider formattingCodeDocumentProvider,
IAdhocWorkspaceFactory adhocWorkspaceFactory,
RazorLSPOptionsMonitor optionsMonitor,
ILoggerFactory loggerFactory)
: IRazorRequestHandler<VSInternalInlineCompletionRequest, VSInternalInlineCompletionList?>, ICapabilitiesProvider
{
@ -42,6 +44,7 @@ internal sealed class InlineCompletionEndpoint(
private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection));
private readonly IFormattingCodeDocumentProvider _formattingCodeDocumentProvider = formattingCodeDocumentProvider;
private readonly IAdhocWorkspaceFactory _adhocWorkspaceFactory = adhocWorkspaceFactory ?? throw new ArgumentNullException(nameof(adhocWorkspaceFactory));
private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<InlineCompletionEndpoint>();
public bool MutatesSolutionState => false;
@ -113,7 +116,7 @@ internal sealed class InlineCompletionEndpoint(
return null;
}
var items = new List<VSInternalInlineCompletionItem>();
using var items = new PooledArrayBuilder<VSInternalInlineCompletionItem>(list.Items.Length);
foreach (var item in list.Items)
{
var containsSnippet = item.TextFormat == InsertTextFormat.Snippet;
@ -125,11 +128,12 @@ internal sealed class InlineCompletionEndpoint(
continue;
}
var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
using var formattingContext = FormattingContext.Create(
request.TextDocument.Uri,
documentContext.Snapshot,
codeDocument,
request.Options,
options,
_formattingCodeDocumentProvider,
_adhocWorkspaceFactory);
if (!TryGetSnippetWithAdjustedIndentation(formattingContext, item.Text, hostDocumentIndex, out var newSnippetText))

Просмотреть файл

@ -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;

Просмотреть файл

@ -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,28 @@ internal static class SourceTextExtensions
location = default;
return false;
}
/// <summary>
/// Applies the set of edits specified, and returns the minimal set needed to make the same changes
/// </summary>
public static TextEdit[] MinimizeTextEdits(this SourceText text, TextEdit[] edits)
=> MinimizeTextEdits(text, edits, out _);
/// <summary>
/// Applies the set of edits specified, and returns the minimal set needed to make the same changes
/// </summary>
public static TextEdit[] MinimizeTextEdits(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;
}
}

Просмотреть файл

@ -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<TextEdit[]> 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<TextEdit>();
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();
}
/// <summary>
/// Extracts the namespace from a C# add using statement provided by Visual Studio
/// </summary>
/// <param name="csharpAddUsing">Add using statement of the form `using System.X;`</param>
/// <param name="namespace">Extract namespace `System.X`</param>
/// <param name="prefix">The prefix to show, before the namespace, if any</param>
/// <returns></returns>
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<TextDocumentEdit>();
// 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<RazorUsingDirective>();
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<IEnumerable<string>> 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<UsingDirectiveSyntax>()
// 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<RazorUsingDirective> existingUsingDirectives)
{
Debug.Assert(existingUsingDirectives.Count > 0);
using var edits = new PooledArrayBuilder<TextEdit>();
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<RazorUsingDirective> 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;
}
}

Просмотреть файл

@ -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<TextEdit[]> 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<int> 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();
@ -84,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;
@ -106,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<int, int>();

Просмотреть файл

@ -35,8 +35,7 @@ internal sealed class FormattingContext : IDisposable
Uri uri,
IDocumentSnapshot originalSnapshot,
RazorCodeDocument codeDocument,
FormattingOptions options,
bool isFormatOnType,
RazorFormattingOptions options,
bool automaticallyAddUsings,
int hostDocumentIndex,
char triggerCharacter)
@ -47,7 +46,6 @@ internal sealed class FormattingContext : IDisposable
OriginalSnapshot = originalSnapshot;
CodeDocument = codeDocument;
Options = options;
IsFormatOnType = isFormatOnType;
AutomaticallyAddUsings = automaticallyAddUsings;
HostDocumentIndex = hostDocumentIndex;
TriggerCharacter = triggerCharacter;
@ -58,8 +56,7 @@ internal sealed class FormattingContext : IDisposable
public Uri Uri { get; }
public IDocumentSnapshot OriginalSnapshot { get; }
public RazorCodeDocument CodeDocument { get; }
public FormattingOptions Options { get; }
public bool IsFormatOnType { get; }
public RazorFormattingOptions Options { get; }
public bool AutomaticallyAddUsings { get; }
public int HostDocumentIndex { get; }
public char TriggerCharacter { get; }
@ -97,7 +94,7 @@ internal sealed class FormattingContext : IDisposable
{
if (_indentations is null)
{
var sourceText = this.SourceText;
var sourceText = SourceText;
var indentations = new Dictionary<int, IndentationContext>();
var previousIndentationLevel = 0;
@ -111,7 +108,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)
@ -190,11 +187,6 @@ internal sealed class FormattingContext : IDisposable
private static IReadOnlyList<FormattingSpan> GetFormattingSpans(RazorSyntaxTree syntaxTree, bool inGlobalNamespace)
{
if (syntaxTree is null)
{
throw new ArgumentNullException(nameof(syntaxTree));
}
var visitor = new FormattingVisitor(inGlobalNamespace: inGlobalNamespace);
visitor.Visit(syntaxTree.Root);
@ -238,11 +230,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.AsEnumerable())
{
var formattingspan = formattingspans[i];
var span = formattingspan.Span;
var span = formattingSpan.Span;
if (span.Start <= absoluteIndex && span.End >= absoluteIndex)
{
@ -253,7 +244,7 @@ internal sealed class FormattingContext : IDisposable
continue;
}
result = formattingspan;
result = formattingSpan;
return true;
}
}
@ -272,11 +263,6 @@ internal sealed class FormattingContext : IDisposable
public async Task<FormattingContext> WithTextAsync(SourceText changedText)
{
if (changedText is null)
{
throw new ArgumentNullException(nameof(changedText));
}
var changedSnapshot = OriginalSnapshot.WithText(changedText);
var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(changedSnapshot).ConfigureAwait(false);
@ -290,7 +276,6 @@ internal sealed class FormattingContext : IDisposable
OriginalSnapshot,
codeDocument,
Options,
IsFormatOnType,
AutomaticallyAddUsings,
HostDocumentIndex,
TriggerCharacter);
@ -320,21 +305,20 @@ internal sealed class FormattingContext : IDisposable
Uri uri,
IDocumentSnapshot originalSnapshot,
RazorCodeDocument codeDocument,
FormattingOptions options,
RazorFormattingOptions options,
IFormattingCodeDocumentProvider codeDocumentProvider,
IAdhocWorkspaceFactory workspaceFactory,
bool automaticallyAddUsings,
int hostDocumentIndex,
char triggerCharacter)
{
return CreateCore(
return new FormattingContext(
codeDocumentProvider,
workspaceFactory,
uri,
originalSnapshot,
codeDocument,
options,
codeDocumentProvider,
workspaceFactory,
isFormatOnType: true,
automaticallyAddUsings,
hostDocumentIndex,
triggerCharacter);
@ -344,76 +328,20 @@ internal sealed class FormattingContext : IDisposable
Uri uri,
IDocumentSnapshot originalSnapshot,
RazorCodeDocument codeDocument,
FormattingOptions options,
RazorFormattingOptions options,
IFormattingCodeDocumentProvider codeDocumentProvider,
IAdhocWorkspaceFactory workspaceFactory)
{
return CreateCore(
return new FormattingContext(
codeDocumentProvider,
workspaceFactory,
uri,
originalSnapshot,
codeDocument,
options,
codeDocumentProvider,
workspaceFactory,
isFormatOnType: false,
automaticallyAddUsings: false,
hostDocumentIndex: 0,
triggerCharacter: '\0');
}
private static FormattingContext CreateCore(
Uri uri,
IDocumentSnapshot originalSnapshot,
RazorCodeDocument codeDocument,
FormattingOptions options,
IFormattingCodeDocumentProvider codeDocumentProvider,
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(
codeDocumentProvider,
workspaceFactory,
uri,
originalSnapshot,
codeDocument,
options,
isFormatOnType,
automaticallyAddUsings,
hostDocumentIndex,
triggerCharacter
);
return result;
triggerCharacter: '\0'
);
}
}

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -1,53 +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.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 static readonly int DefaultOrder = 1000;
public abstract bool IsValidationPass { get; }
public virtual int Order => DefaultOrder;
protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService;
public abstract Task<FormattingResult> 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.
return projectedTextEdits;
}
if (codeDocument.IsUnsupported())
{
return [];
}
var edits = DocumentMappingService.GetHostDocumentEdits(codeDocument.GetCSharpDocument(), projectedTextEdits);
return edits;
}
}

Просмотреть файл

@ -1,26 +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.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; }
}

Просмотреть файл

@ -3,14 +3,11 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
internal interface IFormattingPass
{
int Order { get; }
bool IsValidationPass { get; }
Task<FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken);
Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken);
}

Просмотреть файл

@ -4,39 +4,49 @@
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;
internal interface IRazorFormattingService
{
Task<TextEdit[]> FormatAsync(
Task<TextEdit[]> GetDocumentFormattingEditsAsync(
DocumentContext documentContext,
TextEdit[] htmlEdits,
Range? range,
FormattingOptions options,
RazorFormattingOptions options,
CancellationToken cancellationToken);
Task<TextEdit[]> FormatOnTypeAsync(
Task<TextEdit[]> GetHtmlOnTypeFormattingEditsAsync(
DocumentContext documentContext,
RazorLanguageKind kind,
TextEdit[] formattedEdits,
FormattingOptions options,
TextEdit[] htmlEdits,
RazorFormattingOptions options,
int hostDocumentIndex,
char triggerCharacter,
CancellationToken cancellationToken);
Task<TextEdit[]> FormatCodeActionAsync(
Task<TextEdit[]> GetCSharpOnTypeFormattingEditsAsync(
DocumentContext documentContext,
RazorFormattingOptions options,
int hostDocumentIndex,
char triggerCharacter,
CancellationToken cancellationToken);
Task<TextEdit?> GetSingleCSharpEditAsync(
DocumentContext documentContext,
TextEdit csharpEdit,
RazorFormattingOptions options,
CancellationToken cancellationToken);
Task<TextEdit?> GetCSharpCodeActionEditAsync(
DocumentContext documentContext,
RazorLanguageKind kind,
TextEdit[] formattedEdits,
FormattingOptions options,
TextEdit[] csharpEdits,
RazorFormattingOptions options,
CancellationToken cancellationToken);
Task<TextEdit[]> FormatSnippetAsync(
Task<TextEdit?> GetCSharpSnippetFormattingEditAsync(
DocumentContext documentContext,
RazorLanguageKind kind,
TextEdit[] edits,
FormattingOptions options,
TextEdit[] csharpEdits,
RazorFormattingOptions options,
CancellationToken cancellationToken);
}

Просмотреть файл

@ -11,37 +11,31 @@ 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;
internal class CSharpFormattingPass(
/// <summary>
/// Gets edits in Razor files, and returns edits to Razor files, with nicely formatted Html
/// </summary>
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<CSharpFormattingPass>();
// Run after the HTML and Razor formatter pass.
public override int Order => DefaultOrder - 3;
public async override Task<FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken)
public async override Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
{
if (context.IsFormatOnType || result.Kind != RazorLanguageKind.Razor)
{
// We don't want to handle OnTypeFormatting here.
return result;
}
// 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);
}
@ -75,7 +69,7 @@ internal class CSharpFormattingPass(
var finalChanges = changedText.GetTextChanges(originalText);
var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray();
return new FormattingResult(finalEdits);
return finalEdits;
}
private async Task<ImmutableArray<TextEdit>> FormatCSharpAsync(FormattingContext context, CancellationToken cancellationToken)
@ -94,7 +88,7 @@ internal 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)));
}

Просмотреть файл

@ -15,22 +15,17 @@ 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 : FormattingPassBase
internal abstract class CSharpFormattingPassBase(IDocumentMappingService documentMappingService, bool isFormatOnType) : IFormattingPass
{
protected CSharpFormattingPassBase(IDocumentMappingService documentMappingService)
: base(documentMappingService)
{
CSharpFormatter = new CSharpFormatter(documentMappingService);
}
private readonly bool _isFormatOnType = isFormatOnType;
protected CSharpFormatter CSharpFormatter { get; }
protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService;
public override bool IsValidationPass => false;
public abstract Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken);
protected async Task<List<TextChange>> AdjustIndentationAsync(FormattingContext context, CancellationToken cancellationToken, Range? range = null)
{
@ -255,7 +250,7 @@ internal abstract class CSharpFormattingPassBase : FormattingPassBase
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.
@ -296,13 +291,13 @@ internal abstract class CSharpFormattingPassBase : FormattingPassBase
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.
@ -442,10 +437,10 @@ internal abstract class CSharpFormattingPassBase : FormattingPassBase
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;
}

Просмотреть файл

@ -12,46 +12,38 @@ 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;
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;
using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
internal abstract class CSharpOnTypeFormattingPassBase(
/// <summary>
/// Gets edits in C# files, and returns edits to Razor files, with nicely formatted Html
/// </summary>
internal sealed class CSharpOnTypeFormattingPass(
IDocumentMappingService documentMappingService,
ILoggerFactory loggerFactory)
: CSharpFormattingPassBase(documentMappingService)
: CSharpFormattingPassBase(documentMappingService, isFormatOnType: true)
{
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CSharpOnTypeFormattingPassBase>();
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CSharpOnTypeFormattingPass>();
public async override Task<FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken)
public async override Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, 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;
}
// 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.
@ -62,7 +54,7 @@ internal abstract class CSharpOnTypeFormattingPassBase(
context.CSharpWorkspaceDocument,
typedChar: context.TriggerCharacter,
projectedIndex,
context.Options.GetIndentationOptions(),
context.Options.ToIndentationOptions(),
autoFormattingOptions,
indentStyle: CodeAnalysis.Formatting.FormattingOptions.IndentStyle.Smart,
cancellationToken).ConfigureAwait(false);
@ -70,18 +62,18 @@ internal abstract class CSharpOnTypeFormattingPassBase(
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;
@ -89,12 +81,12 @@ internal abstract class CSharpOnTypeFormattingPassBase(
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 = NormalizeTextEdits(csharpText, textEdits, out var originalTextWithChanges);
var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits, result.Kind);
var normalizedEdits = csharpText.MinimizeTextEdits(edits, out var originalTextWithChanges);
var mappedEdits = RemapTextEdits(codeDocument, normalizedEdits);
var filteredEdits = FilterCSharpTextEdits(context, mappedEdits);
if (filteredEdits.Length == 0)
{
@ -102,10 +94,9 @@ internal abstract class CSharpOnTypeFormattingPassBase(
// 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);
filteredEdits = await AddUsingStatementEditsIfNecessaryAsync(context, codeDocument, csharpText, textEdits, originalTextWithChanges, filteredEdits, cancellationToken).ConfigureAwait(false);
return new FormattingResult(filteredEdits);
return filteredEdits;
}
// Find the lines that were affected by these edits.
@ -203,12 +194,37 @@ internal abstract class CSharpOnTypeFormattingPassBase(
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;
}
protected abstract Task<TextEdit[]> AddUsingStatementEditsIfNecessaryAsync(FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken);
private TextEdit[] RemapTextEdits(RazorCodeDocument codeDocument, TextEdit[] projectedTextEdits)
{
if (codeDocument.IsUnsupported())
{
return [];
}
var edits = DocumentMappingService.GetHostDocumentEdits(codeDocument.GetCSharpDocument(), projectedTextEdits);
return edits;
}
private static async Task<TextEdit[]> AddUsingStatementEditsIfNecessaryAsync(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(static 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<TextChange> changes, out TextSpan spanBeforeChange, out TextSpan spanAfterChange)
@ -323,7 +339,7 @@ internal abstract class CSharpOnTypeFormattingPassBase(
if (owner is CSharpStatementLiteralSyntax &&
owner.TryGetPreviousSibling(out var prevNode) &&
prevNode.FirstAncestorOrSelf<SyntaxNode>(a => a is CSharpTemplateBlockSyntax) is { } template &&
prevNode.FirstAncestorOrSelf<RazorSyntaxNode>(static a => a is CSharpTemplateBlockSyntax) is { } template &&
owner.SpanStart == template.Span.End &&
IsOnSingleLine(template, text))
{
@ -477,7 +493,7 @@ internal abstract class CSharpOnTypeFormattingPassBase(
if (owner is CSharpStatementLiteralSyntax &&
owner.NextSpan() is { } nextNode &&
nextNode.FirstAncestorOrSelf<SyntaxNode>(a => a is CSharpTemplateBlockSyntax) is { } template &&
nextNode.FirstAncestorOrSelf<RazorSyntaxNode>(static a => a is CSharpTemplateBlockSyntax) is { } template &&
template.SpanStart == owner.Span.End &&
IsOnSingleLine(template, text))
{
@ -523,19 +539,10 @@ 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);
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;
}
}

Просмотреть файл

@ -5,39 +5,22 @@ 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;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
internal class FormattingContentValidationPass(
IDocumentMappingService documentMappingService,
ILoggerFactory loggerFactory)
: FormattingPassBase(documentMappingService)
internal sealed class FormattingContentValidationPass(ILoggerFactory loggerFactory) : IFormattingPass
{
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<FormattingContentValidationPass>();
// 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;
public override Task<FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken)
public Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, 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);
var changedText = text.WithChanges(changes);
@ -65,9 +48,9 @@ internal class FormattingContentValidationPass(
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<TextEdit[]>([]);
}
return Task.FromResult(result);
return Task.FromResult(edits);
}
}

Просмотреть файл

@ -8,40 +8,23 @@ 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 class FormattingDiagnosticValidationPass(
IDocumentMappingService documentMappingService,
ILoggerFactory loggerFactory)
: FormattingPassBase(documentMappingService)
internal sealed class FormattingDiagnosticValidationPass(ILoggerFactory loggerFactory) : IFormattingPass
{
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<FormattingDiagnosticValidationPass>();
// 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;
public async override Task<FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken)
public async Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, 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;
var edits = result.Edits;
var changes = edits.Select(text.GetTextChange);
var changedText = text.WithChanges(changes);
var changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false);
@ -72,10 +55,10 @@ internal class FormattingDiagnosticValidationPass(
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<RazorDiagnostic>

Просмотреть файл

@ -0,0 +1,13 @@
// 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.Logging;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
/// <summary>
/// Gets edits in Razor files, and returns edits to Razor files, with nicely formatted Html
/// </summary>
internal sealed class HtmlFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger<HtmlFormattingPass>())
{
}

Просмотреть файл

@ -7,69 +7,34 @@ 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;
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,
IClientConnection clientConnection,
ILoggerFactory loggerFactory)
: FormattingPassBase(documentMappingService)
internal abstract class HtmlFormattingPassBase(ILogger logger) : IFormattingPass
{
private readonly HtmlFormatter _htmlFormatter = new HtmlFormatter(clientConnection);
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<HtmlFormattingPass>();
private readonly ILogger _logger = logger;
// 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<FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken)
public virtual async Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
{
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
{
// We don't want to handle on type formatting requests for other languages
return result;
}
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);
_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)
@ -82,7 +47,7 @@ internal sealed class HtmlFormattingPass(
var finalChanges = changedText.GetTextChanges(originalText);
var finalEdits = finalChanges.Select(originalText.GetTextEdit).ToArray();
return new FormattingResult(finalEdits);
return finalEdits;
}
private static List<TextChange> AdjustRazorIndentation(FormattingContext context)

Просмотреть файл

@ -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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
/// <summary>
/// Gets edits in Html files, and returns edits to Razor files, with nicely formatted Html
/// </summary>
internal sealed class HtmlOnTypeFormattingPass(ILoggerFactory loggerFactory) : HtmlFormattingPassBase(loggerFactory.GetOrCreateLogger<HtmlOnTypeFormattingPass>())
{
public override Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, CancellationToken cancellationToken)
{
if (edits.Length == 0)
{
// There are no HTML edits for us to apply. No op.
return Task.FromResult<TextEdit[]>([]);
}
return base.ExecuteAsync(context, edits, cancellationToken);
}
}

Просмотреть файл

@ -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,41 +12,26 @@ 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.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using RazorRazorSyntaxNodeList = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxList<Microsoft.AspNetCore.Razor.Language.Syntax.RazorSyntaxNode>;
using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
using RazorSyntaxNodeList = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxList<Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode>;
namespace Microsoft.CodeAnalysis.Razor.Formatting;
using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode;
internal abstract class RazorFormattingPassBase(
IDocumentMappingService documentMappingService)
: FormattingPassBase(documentMappingService)
internal sealed class RazorFormattingPass : IFormattingPass
{
// Run after the C# formatter pass.
public override int Order => DefaultOrder - 4;
public override bool IsValidationPass => false;
public async override Task<FormattingResult> ExecuteAsync(FormattingContext context, FormattingResult result, CancellationToken cancellationToken)
public async Task<TextEdit[]> ExecuteAsync(FormattingContext context, TextEdit[] edits, 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;
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);
@ -54,48 +40,46 @@ internal abstract class RazorFormattingPassBase(
// 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;
}
protected abstract bool CodeBlockBraceOnNextLine { get; }
private IEnumerable<TextEdit> FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree)
private static ImmutableArray<TextEdit> FormatRazor(FormattingContext context, RazorSyntaxTree syntaxTree)
{
var edits = new List<TextEdit>();
using var edits = new PooledArrayBuilder<TextEdit>();
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);
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<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
private static void TryFormatBlocks(FormattingContext context, ref PooledArrayBuilder<TextEdit> 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<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
private static bool TryFormatSectionBlock(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
{
// @section Goo {
// }
@ -114,8 +98,8 @@ internal abstract class RazorFormattingPassBase(
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;
}
@ -152,7 +136,7 @@ internal abstract class RazorFormattingPassBase(
}
}
private static bool TryFormatFunctionsBlock(FormattingContext context, IList<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
private static bool TryFormatFunctionsBlock(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
{
// @functions
// {
@ -184,13 +168,13 @@ internal abstract class RazorFormattingPassBase(
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<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
private static bool TryFormatCSharpExplicitTransition(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
{
// We're looking for a code block like this:
//
@ -205,13 +189,13 @@ internal abstract class RazorFormattingPassBase(
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<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
private static bool TryFormatComplexCSharpBlock(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
{
// complex situations like
// @{
@ -227,13 +211,13 @@ internal abstract class RazorFormattingPassBase(
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<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
private static bool TryFormatHtmlInCSharp(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
{
// void Method()
// {
@ -245,13 +229,13 @@ internal abstract class RazorFormattingPassBase(
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 void TryFormatCSharpBlockStructure(FormattingContext context, List<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
private static void TryFormatCSharpBlockStructure(FormattingContext context, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
{
// We're looking for a code block like this:
//
@ -271,7 +255,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);
@ -279,7 +263,7 @@ internal abstract class RazorFormattingPassBase(
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))
{
@ -311,7 +295,7 @@ internal abstract class RazorFormattingPassBase(
}
}
private static void TryFormatSingleLineDirective(List<TextEdit> edits, RazorSourceDocument source, SyntaxNode node)
private static void TryFormatSingleLineDirective(ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, RazorSyntaxNode node)
{
// Looking for single line directives like
//
@ -327,12 +311,12 @@ internal abstract class RazorFormattingPassBase(
{
if (child.ContainsOnlyWhitespace(includingNewLines: false))
{
ShrinkToSingleSpace(child, edits, source);
ShrinkToSingleSpace(child, ref edits, source);
}
}
}
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 &&
@ -347,11 +331,11 @@ internal abstract class RazorFormattingPassBase(
}
}
private static void FormatWhitespaceBetweenDirectiveAndBrace(SyntaxNode node, RazorDirectiveSyntax directive, List<TextEdit> edits, RazorSourceDocument source, FormattingContext context, bool forceNewLine)
private static void FormatWhitespaceBetweenDirectiveAndBrace(RazorSyntaxNode node, RazorDirectiveSyntax directive, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source, FormattingContext context, bool forceNewLine)
{
if (node.ContainsOnlyWhitespace(includingNewLines: false) && !forceNewLine)
{
ShrinkToSingleSpace(node, edits, source);
ShrinkToSingleSpace(node, ref edits, source);
}
else
{
@ -366,7 +350,7 @@ internal abstract class RazorFormattingPassBase(
}
}
private static void ShrinkToSingleSpace(SyntaxNode node, List<TextEdit> edits, RazorSourceDocument source)
private static void ShrinkToSingleSpace(RazorSyntaxNode node, ref PooledArrayBuilder<TextEdit> edits, RazorSourceDocument source)
{
// If there is anything other than one single space then we replace with one space between directive and brace.
//
@ -375,7 +359,7 @@ internal abstract class RazorFormattingPassBase(
edits.Add(edit);
}
private static bool FormatBlock(FormattingContext context, RazorSourceDocument source, SyntaxNode? directiveNode, SyntaxNode openBraceNode, SyntaxNode codeNode, SyntaxNode closeBraceNode, IList<TextEdit> edits)
private static bool FormatBlock(FormattingContext context, RazorSourceDocument source, RazorSyntaxNode? directiveNode, RazorSyntaxNode openBraceNode, RazorSyntaxNode codeNode, RazorSyntaxNode closeBraceNode, ref PooledArrayBuilder<TextEdit> edits)
{
var didFormat = false;
@ -385,7 +369,7 @@ internal abstract class RazorFormattingPassBase(
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;
@ -402,7 +386,7 @@ internal abstract class RazorFormattingPassBase(
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)
@ -427,7 +411,7 @@ internal abstract class RazorFormattingPassBase(
return didFormat;
static bool RangeHasBeenModified(IList<TextEdit> edits, Range range)
static bool RangeHasBeenModified(ref readonly PooledArrayBuilder<TextEdit> 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.
@ -436,7 +420,7 @@ internal abstract class RazorFormattingPassBase(
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))
{
@ -451,7 +435,7 @@ internal abstract class RazorFormattingPassBase(
return desiredIndentationOffset - currentIndentationOffset;
static int GetLeadingWhitespaceLength(SyntaxNode node, FormattingContext context)
static int GetLeadingWhitespaceLength(RazorSyntaxNode node, FormattingContext context)
{
var tokens = node.GetTokens();
var whitespaceLength = 0;
@ -483,7 +467,7 @@ internal abstract class RazorFormattingPassBase(
return whitespaceLength;
}
static int GetTrailingWhitespaceLength(SyntaxNode node, FormattingContext context)
static int GetTrailingWhitespaceLength(RazorSyntaxNode node, FormattingContext context)
{
var tokens = node.GetTokens();
var whitespaceLength = 0;

Просмотреть файл

@ -0,0 +1,36 @@
// 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 readonly record struct RazorFormattingOptions
{
public static readonly RazorFormattingOptions Default = new();
public bool InsertSpaces { get; init; } = true;
public int TabSize { get; init; } = 4;
public bool CodeBlockBraceOnNextLine { get; init; } = false;
public RazorFormattingOptions()
{
}
public static RazorFormattingOptions From(FormattingOptions options, bool codeBlockBraceOnNextLine)
{
return new RazorFormattingOptions()
{
InsertSpaces = options.InsertSpaces,
TabSize = options.TabSize,
CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine
};
}
public RazorIndentationOptions ToIndentationOptions()
=> new(
UseTabs: !InsertSpaces,
TabSize: TabSize,
IndentationSize: TabSize);
}

Просмотреть файл

@ -1,16 +1,16 @@
// 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.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;
@ -20,29 +20,44 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting;
internal class RazorFormattingService : IRazorFormattingService
{
private readonly List<IFormattingPass> _formattingPasses;
private readonly IFormattingCodeDocumentProvider _codeDocumentProvider;
private readonly IAdhocWorkspaceFactory _workspaceFactory;
public RazorFormattingService(
IEnumerable<IFormattingPass> formattingPasses,
IFormattingCodeDocumentProvider codeDocumentProvider,
IAdhocWorkspaceFactory workspaceFactory)
{
if (formattingPasses is null)
{
throw new ArgumentNullException(nameof(formattingPasses));
}
private readonly ImmutableArray<IFormattingPass> _documentFormattingPasses;
private readonly ImmutableArray<IFormattingPass> _validationPasses;
private readonly CSharpOnTypeFormattingPass _csharpOnTypeFormattingPass;
private readonly HtmlOnTypeFormattingPass _htmlOnTypeFormattingPass;
_formattingPasses = formattingPasses.OrderBy(f => f.Order).ToList();
_codeDocumentProvider = codeDocumentProvider ?? throw new ArgumentNullException(nameof(codeDocumentProvider));
_workspaceFactory = workspaceFactory ?? throw new ArgumentNullException(nameof(workspaceFactory));
public RazorFormattingService(
IFormattingCodeDocumentProvider codeDocumentProvider,
IDocumentMappingService documentMappingService,
IAdhocWorkspaceFactory workspaceFactory,
ILoggerFactory loggerFactory)
{
_codeDocumentProvider = codeDocumentProvider;
_workspaceFactory = workspaceFactory;
_htmlOnTypeFormattingPass = new HtmlOnTypeFormattingPass(loggerFactory);
_csharpOnTypeFormattingPass = new CSharpOnTypeFormattingPass(documentMappingService, loggerFactory);
_validationPasses =
[
new FormattingDiagnosticValidationPass(loggerFactory),
new FormattingContentValidationPass(loggerFactory)
];
_documentFormattingPasses =
[
new HtmlFormattingPass(loggerFactory),
new RazorFormattingPass(),
new CSharpFormattingPass(documentMappingService, loggerFactory),
.. _validationPasses
];
}
public async Task<TextEdit[]> FormatAsync(
public async Task<TextEdit[]> GetDocumentFormattingEditsAsync(
DocumentContext documentContext,
TextEdit[] htmlEdits,
Range? range,
FormattingOptions options,
RazorFormattingOptions options,
CancellationToken cancellationToken)
{
var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(documentContext.Snapshot).ConfigureAwait(false);
@ -82,85 +97,108 @@ internal class RazorFormattingService : IRazorFormattingService
_workspaceFactory);
var originalText = context.SourceText;
var result = new FormattingResult([]);
foreach (var pass in _formattingPasses)
var result = htmlEdits;
foreach (var pass in _documentFormattingPasses)
{
cancellationToken.ThrowIfCancellationRequested();
result = await pass.ExecuteAsync(context, result, cancellationToken).ConfigureAwait(false);
}
var filteredEdits = range is null
? result.Edits
: result.Edits.Where(e => range.LineOverlapsWith(e.Range));
? result
: result.Where(e => range.LineOverlapsWith(e.Range)).ToArray();
return GetMinimalEdits(originalText, filteredEdits);
return originalText.MinimizeTextEdits(filteredEdits);
}
private static TextEdit[] GetMinimalEdits(SourceText originalText, IEnumerable<TextEdit> 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<TextEdit>();
}
// Only send back the minimum edits
var minimalChanges = SourceTextDiffer.GetMinimalTextChanges(originalText, changedText, DiffKind.Char);
var finalEdits = minimalChanges.Select(originalText.GetTextEdit).ToArray();
return finalEdits;
}
public Task<TextEdit[]> FormatOnTypeAsync(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<TextEdit[]> FormatCodeActionAsync(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<TextEdit[]> FormatSnippetAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] edits, FormattingOptions options, CancellationToken cancellationToken)
{
if (kind == RazorLanguageKind.CSharp)
{
WrapCSharpSnippets(edits);
}
var formattedEdits = await ApplyFormattedEditsAsync(
public Task<TextEdit[]> GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
=> ApplyFormattedEditsAsync(
documentContext,
kind,
edits,
generatedDocumentEdits: [],
options,
hostDocumentIndex,
triggerCharacter,
[_csharpOnTypeFormattingPass, .. _validationPasses],
collapseEdits: false,
automaticallyAddUsings: false,
cancellationToken: cancellationToken);
public Task<TextEdit[]> GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
=> ApplyFormattedEditsAsync(
documentContext,
htmlEdits,
options,
hostDocumentIndex,
triggerCharacter,
[_htmlOnTypeFormattingPass, .. _validationPasses],
collapseEdits: false,
automaticallyAddUsings: false,
cancellationToken: cancellationToken);
public async Task<TextEdit?> GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit csharpEdit, RazorFormattingOptions options, CancellationToken cancellationToken)
{
var razorEdits = await ApplyFormattedEditsAsync(
documentContext,
[csharpEdit],
options,
hostDocumentIndex: 0,
triggerCharacter: '\0',
bypassValidationPasses: true,
[_csharpOnTypeFormattingPass, .. _validationPasses],
collapseEdits: false,
automaticallyAddUsings: false,
cancellationToken: cancellationToken).ConfigureAwait(false);
return razorEdits.SingleOrDefault();
}
public async Task<TextEdit?> GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] csharpEdits, RazorFormattingOptions options, CancellationToken cancellationToken)
{
var razorEdits = await ApplyFormattedEditsAsync(
documentContext,
csharpEdits,
options,
hostDocumentIndex: 0,
triggerCharacter: '\0',
[_csharpOnTypeFormattingPass],
collapseEdits: true,
automaticallyAddUsings: true,
cancellationToken: cancellationToken).ConfigureAwait(false);
return razorEdits.SingleOrDefault();
}
public async Task<TextEdit?> GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] csharpEdits, RazorFormattingOptions options, CancellationToken cancellationToken)
{
WrapCSharpSnippets(csharpEdits);
var razorEdits = await ApplyFormattedEditsAsync(
documentContext,
csharpEdits,
options,
hostDocumentIndex: 0,
triggerCharacter: '\0',
[_csharpOnTypeFormattingPass],
collapseEdits: true,
automaticallyAddUsings: false,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (kind == RazorLanguageKind.CSharp)
{
UnwrapCSharpSnippets(formattedEdits);
}
UnwrapCSharpSnippets(razorEdits);
return formattedEdits;
return razorEdits.SingleOrDefault();
}
private async Task<TextEdit[]> ApplyFormattedEditsAsync(
DocumentContext documentContext,
RazorLanguageKind kind,
TextEdit[] formattedEdits,
FormattingOptions options,
TextEdit[] generatedDocumentEdits,
RazorFormattingOptions options,
int hostDocumentIndex,
char triggerCharacter,
bool bypassValidationPasses,
ImmutableArray<IFormattingPass> formattingPasses,
bool collapseEdits,
bool automaticallyAddUsings,
CancellationToken cancellationToken)
{
// 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;
@ -175,35 +213,30 @@ internal class RazorFormattingService : IRazorFormattingService
automaticallyAddUsings: automaticallyAddUsings,
hostDocumentIndex,
triggerCharacter);
var result = new FormattingResult(formattedEdits, kind);
var result = generatedDocumentEdits;
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);
}
var originalText = context.SourceText;
var edits = GetMinimalEdits(originalText, result.Edits);
var razorEdits = originalText.MinimizeTextEdits(result);
if (collapseEdits)
{
var collapsedEdit = MergeEdits(edits, originalText);
var collapsedEdit = MergeEdits(razorEdits, originalText);
if (collapsedEdit.NewText.Length == 0 &&
collapsedEdit.Range.IsZeroWidth())
{
return Array.Empty<TextEdit>();
return [];
}
return new[] { collapsedEdit };
return [collapsedEdit];
}
return edits;
return razorEdits;
}
// Internal for testing
@ -214,14 +247,7 @@ internal class RazorFormattingService : IRazorFormattingService
return edits[0];
}
var textChanges = new List<TextChange>();
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);
@ -232,30 +258,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.
for (var i = 0; i < snippetEdits.Length; i++)
foreach (var edit in csharpEdits)
{
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;
edit.NewText = edit.NewText.Replace("$0", "/*$0*/");
}
}
private static void UnwrapCSharpSnippets(TextEdit[] snippetEdits)
private static void UnwrapCSharpSnippets(TextEdit[] razorEdits)
{
for (var i = 0; i < snippetEdits.Length; i++)
foreach (var edit in razorEdits)
{
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.
edit.NewText = edit.NewText.Replace("/*$0*/", "$0");
}
}
}

Просмотреть файл

@ -19,7 +19,6 @@ using Roslyn.LanguageServer.Protocol;
using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Microsoft.CodeAnalysis.Razor.Protocol.AutoInsert.RemoteAutoInsertTextEdit?>;
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));
}
}

Просмотреть файл

@ -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 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;
[Export(typeof(IFormattingPass)), Shared]
[method: ImportingConstructor]
internal sealed class RemoteCSharpOnTypeFormattingPass(
IDocumentMappingService documentMappingService,
ILoggerFactory loggerFactory)
: CSharpOnTypeFormattingPassBase(documentMappingService, loggerFactory)
{
protected override Task<TextEdit[]> AddUsingStatementEditsIfNecessaryAsync(CodeAnalysis.Razor.Formatting.FormattingContext context, RazorCodeDocument codeDocument, SourceText csharpText, TextEdit[] textEdits, SourceText originalTextWithChanges, TextEdit[] finalEdits, CancellationToken cancellationToken)
{
// Implement this when code actions are migrated to cohosting,
// probably will be able to move it back into base class and make that non-abstract.
return Task.FromResult(finalEdits);
}
}

Просмотреть файл

@ -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);

Просмотреть файл

@ -1,18 +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;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
[Export(typeof(IFormattingPass)), Shared]
[method: ImportingConstructor]
internal sealed class RemoteRazorFormattingPass(
IDocumentMappingService documentMappingService)
: RazorFormattingPassBase(documentMappingService)
{
// TODO: properly plumb this through
protected override bool CodeBlockBraceOnNextLine => false;
}

Просмотреть файл

@ -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<IFormattingPass> 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)
{
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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()
{

Просмотреть файл

@ -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,12 +188,11 @@ public class DefaultCSharpCodeActionResolverTest(ITestOutputHelper testOutput) :
private static IRazorFormattingService CreateRazorFormattingService(Uri documentUri)
{
var razorFormattingService = Mock.Of<IRazorFormattingService>(
rfs => rfs.FormatCodeActionAsync(
rfs => rfs.GetCSharpCodeActionEditAsync(
It.Is<DocumentContext>(c => c.Uri == documentUri),
RazorLanguageKind.CSharp,
It.IsAny<TextEdit[]>(),
It.IsAny<FormattingOptions>(),
It.IsAny<CancellationToken>()) == Task.FromResult(s_defaultFormattedEdits), MockBehavior.Strict);
It.IsAny<RazorFormattingOptions>(),
It.IsAny<CancellationToken>()) == Task.FromResult(s_defaultFormattedEdit), MockBehavior.Strict);
return razorFormattingService;
}

Просмотреть файл

@ -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);
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);

Просмотреть файл

@ -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()
{
@ -27,8 +59,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
var optionsMonitor = GetOptionsMonitor(enableFormatting: false);
var htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentOnTypeFormattingEndpoint(
formattingService, documentMappingService, optionsMonitor, LoggerFactory);
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
var @params = new DocumentOnTypeFormattingParams { TextDocument = new TextDocumentIdentifier { Uri = uri, } };
var requestContext = CreateRazorRequestContext(documentContext: null);
@ -55,8 +88,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
var htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentOnTypeFormattingEndpoint(
formattingService, documentMappingService, optionsMonitor, LoggerFactory);
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
var @params = new DocumentOnTypeFormattingParams()
{
TextDocument = new TextDocumentIdentifier { Uri = uri, },
@ -89,8 +123,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
var htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentOnTypeFormattingEndpoint(
formattingService, documentMappingService, optionsMonitor, LoggerFactory);
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
var @params = new DocumentOnTypeFormattingParams()
{
TextDocument = new TextDocumentIdentifier { Uri = uri, },
@ -124,8 +159,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
var documentMappingService = new Mock<IDocumentMappingService>(MockBehavior.Strict);
documentMappingService.Setup(s => s.GetLanguageKind(codeDocument, 17, false)).Returns(RazorLanguageKind.Html);
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
var htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentOnTypeFormattingEndpoint(
formattingService, documentMappingService.Object, optionsMonitor, LoggerFactory);
formattingService, htmlFormatter, documentMappingService.Object, optionsMonitor, LoggerFactory);
var @params = new DocumentOnTypeFormattingParams()
{
TextDocument = new TextDocumentIdentifier { Uri = uri, },
@ -159,8 +195,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
var documentMappingService = new Mock<IDocumentMappingService>(MockBehavior.Strict);
documentMappingService.Setup(s => s.GetLanguageKind(codeDocument, 17, false)).Returns(RazorLanguageKind.Razor);
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
var htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentOnTypeFormattingEndpoint(
formattingService, documentMappingService.Object, optionsMonitor, LoggerFactory);
formattingService, htmlFormatter, documentMappingService.Object, optionsMonitor, LoggerFactory);
var @params = new DocumentOnTypeFormattingParams()
{
TextDocument = new TextDocumentIdentifier { Uri = uri, },
@ -193,8 +230,9 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
var documentMappingService = new LspDocumentMappingService(FilePathService, documentContextFactory, LoggerFactory);
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
var htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentOnTypeFormattingEndpoint(
formattingService, documentMappingService, optionsMonitor, LoggerFactory);
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
var @params = new DocumentOnTypeFormattingParams()
{
TextDocument = new TextDocumentIdentifier { Uri = uri, },

Просмотреть файл

@ -22,12 +22,14 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) :
var documentContext = CreateDocumentContext(uri, codeDocument);
var formattingService = new DummyRazorFormattingService();
var htmlFormatter = new TestHtmlFormatter();
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
var endpoint = new DocumentRangeFormattingEndpoint(
formattingService, optionsMonitor);
formattingService, htmlFormatter, optionsMonitor);
var @params = new DocumentRangeFormattingParams()
{
TextDocument = new TextDocumentIdentifier { Uri = uri, }
TextDocument = new TextDocumentIdentifier { Uri = uri, },
Options = new FormattingOptions()
};
var requestContext = CreateRazorRequestContext(documentContext);
@ -45,7 +47,8 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) :
// Arrange
var formattingService = new DummyRazorFormattingService();
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
var endpoint = new DocumentRangeFormattingEndpoint(formattingService, optionsMonitor);
var htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentRangeFormattingEndpoint(formattingService, htmlFormatter, optionsMonitor);
var uri = new Uri("file://path/test.razor");
var @params = new DocumentRangeFormattingParams()
{
@ -71,7 +74,8 @@ 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 htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentRangeFormattingEndpoint(formattingService, htmlFormatter, optionsMonitor);
var @params = new DocumentRangeFormattingParams()
{
TextDocument = new TextDocumentIdentifier { Uri = uri, }
@ -91,7 +95,8 @@ public class DocumentRangeFormattingEndpointTest(ITestOutputHelper testOutput) :
// Arrange
var formattingService = new DummyRazorFormattingService();
var optionsMonitor = GetOptionsMonitor(enableFormatting: false);
var endpoint = new DocumentRangeFormattingEndpoint(formattingService, optionsMonitor);
var htmlFormatter = new TestHtmlFormatter();
var endpoint = new DocumentRangeFormattingEndpoint(formattingService, htmlFormatter, optionsMonitor);
var @params = new DocumentRangeFormattingParams();
var requestContext = CreateRazorRequestContext(documentContext: null);

Просмотреть файл

@ -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,14 +34,14 @@ public class Foo { }
{
VsLspFactory.CreateTextEdit(2, 0, " ")
};
var input = new FormattingResult(edits, RazorLanguageKind.Razor);
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]
@ -99,21 +58,19 @@ public class Foo { }
{
VsLspFactory.CreateTextEdit(2, 0, 3, 0, " ") // Nukes a line
};
var input = new FormattingResult(edits, RazorLanguageKind.Razor);
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()
{
var mappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
var pass = new FormattingContentValidationPass(mappingService, LoggerFactory)
var pass = new FormattingContentValidationPass(LoggerFactory)
{
DebugAssertsEnabled = false
};
@ -126,7 +83,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,

Просмотреть файл

@ -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,14 +32,14 @@ public class Foo { }
{
VsLspFactory.CreateTextEdit(2, 0, " ")
};
var input = new FormattingResult(edits, RazorLanguageKind.Razor);
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]
@ -96,21 +53,18 @@ 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 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()
{
var mappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
var pass = new FormattingDiagnosticValidationPass(mappingService, LoggerFactory)
var pass = new FormattingDiagnosticValidationPass(LoggerFactory)
{
DebugAssertsEnabled = false
};
@ -123,7 +77,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,
@ -147,7 +101,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);
}

Просмотреть файл

@ -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;
@ -9,7 +10,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;
@ -35,25 +35,35 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut
{
public bool Called { get; private set; }
public Task<TextEdit[]> FormatAsync(DocumentContext documentContext, Range? range, FormattingOptions options, CancellationToken cancellationToken)
public Task<TextEdit[]> GetDocumentFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, Range? range, RazorFormattingOptions options, CancellationToken cancellationToken)
{
Called = true;
return SpecializedTasks.EmptyArray<TextEdit>();
}
public Task<TextEdit[]> FormatCodeActionAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken)
public Task<TextEdit?> GetCSharpCodeActionEditAsync(DocumentContext documentContext, TextEdit[] formattedEdits, RazorFormattingOptions options, CancellationToken cancellationToken)
{
return Task.FromResult(formattedEdits);
throw new NotImplementedException();
}
public Task<TextEdit[]> FormatOnTypeAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
public Task<TextEdit[]> GetCSharpOnTypeFormattingEditsAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
{
return Task.FromResult(formattedEdits);
throw new NotImplementedException();
}
public Task<TextEdit[]> FormatSnippetAsync(DocumentContext documentContext, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken)
public Task<TextEdit?> GetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, TextEdit[] edits, RazorFormattingOptions options, CancellationToken cancellationToken)
{
return Task.FromResult(formattedEdits);
throw new NotImplementedException();
}
public Task<TextEdit[]> GetHtmlOnTypeFormattingEditsAsync(DocumentContext documentContext, TextEdit[] htmlEdits, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken)
{
return Task.FromResult(htmlEdits);
}
public Task<TextEdit?> GetSingleCSharpEditAsync(DocumentContext documentContext, TextEdit initialEdit, RazorFormattingOptions options, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -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;
@ -76,12 +77,19 @@ 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 DocumentContext(uri, documentSnapshot, projectContext: null);
var client = new FormattingLanguageServerClient(LoggerFactory);
client.AddCodeDocument(codeDocument);
var htmlFormatter = new HtmlFormatter(client);
var htmlEdits = await htmlFormatter.GetDocumentFormattingEditsAsync(documentSnapshot, uri, options, DisposalToken);
// Act
var edits = await formattingService.FormatAsync(documentContext, range, options, DisposalToken);
var edits = await formattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits, range, razorOptions, DisposalToken);
// Assert
var edited = ApplyEdits(source, edits);
@ -127,10 +135,25 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
TabSize = tabSize,
InsertSpaces = insertSpaces,
};
var razorOptions = RazorFormattingOptions.From(options, codeBlockBraceOnNextLine: razorLSPOptions?.CodeBlockBraceOnNextLine ?? false);
var documentContext = new DocumentContext(uri, documentSnapshot, projectContext: null);
// Act
var edits = await formattingService.FormatOnTypeAsync(documentContext, languageKind, Array.Empty<TextEdit>(), options, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken);
TextEdit[] edits;
if (languageKind == RazorLanguageKind.CSharp)
{
edits = await formattingService.GetCSharpOnTypeFormattingEditsAsync(documentContext, razorOptions, hostDocumentIndex: positionAfterTrigger, triggerCharacter: triggerCharacter, DisposalToken);
}
else
{
var client = new FormattingLanguageServerClient(LoggerFactory);
client.AddCodeDocument(codeDocument);
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);
}
// Assert
var edited = ApplyEdits(razorSourceText, edits);
@ -190,7 +213,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
}
var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument);
var options = new FormattingOptions()
var options = new RazorFormattingOptions()
{
TabSize = tabSize,
InsertSpaces = insertSpaces,
@ -198,10 +221,10 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
var documentContext = new DocumentContext(uri, documentSnapshot, projectContext: null);
// Act
var edits = await formattingService.FormatCodeActionAsync(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);
@ -260,7 +283,7 @@ public class FormattingTestBase : RazorToolingIntegrationTestBase
]);
var projectEngine = RazorProjectEngine.Create(
new RazorConfiguration(RazorLanguageVersion.Latest, "TestConfiguration", ImmutableArray<RazorExtension>.Empty, new LanguageServerFlags(forceRuntimeCodeGeneration)),
new RazorConfiguration(RazorLanguageVersion.Latest, "TestConfiguration", Extensions: [], new LanguageServerFlags(forceRuntimeCodeGeneration)),
projectFileSystem,
builder =>
{
@ -269,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)
{

Просмотреть файл

@ -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<TextEdit[]> GetDocumentFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, FormattingOptions options, CancellationToken cancellationToken)
{
return SpecializedTasks.EmptyArray<TextEdit>();
}
public Task<TextEdit[]> GetOnTypeFormattingEditsAsync(IDocumentSnapshot documentSnapshot, Uri uri, Position position, string triggerCharacter, FormattingOptions options, CancellationToken cancellationToken)
{
return SpecializedTasks.EmptyArray<TextEdit>();
}
}

Просмотреть файл

@ -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;
@ -29,11 +26,6 @@ internal static class TestRazorFormattingService
var filePathService = new LSPFilePathService(TestLanguageServerFeatureOptions.Instance);
var mappingService = new LspDocumentMappingService(filePathService, new TestDocumentContextFactory(), loggerFactory);
var projectManager = StrictMock.Of<IProjectSnapshotManager>();
var client = new FormattingLanguageServerClient(loggerFactory);
client.AddCodeDocument(codeDocument);
var configurationSyncService = new Mock<IConfigurationSyncService>(MockBehavior.Strict);
configurationSyncService
.Setup(c => c.GetLatestOptionsAsync(It.IsAny<CancellationToken>()))
@ -47,19 +39,8 @@ internal static class TestRazorFormattingService
await optionsMonitor.UpdateAsync(CancellationToken.None);
}
var passes = new List<IFormattingPass>()
{
new HtmlFormattingPass(mappingService, client, loggerFactory),
new CSharpFormattingPass(mappingService, loggerFactory),
new LspCSharpOnTypeFormattingPass(mappingService, loggerFactory),
new LspRazorFormattingPass(mappingService, optionsMonitor),
new FormattingDiagnosticValidationPass(mappingService, loggerFactory),
new FormattingContentValidationPass(mappingService, loggerFactory),
};
var formattingCodeDocumentProvider = new LspFormattingCodeDocumentProvider();
return new RazorFormattingService(
passes,
new LspFormattingCodeDocumentProvider(),
TestAdhocWorkspaceFactory.Instance);
return new RazorFormattingService(formattingCodeDocumentProvider, mappingService, TestAdhocWorkspaceFactory.Instance, loggerFactory);
}
}

Просмотреть файл

@ -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));

Просмотреть файл

@ -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);
}
}