зеркало из https://github.com/dotnet/razor.git
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:
Коммит
21c7674ac4
|
@ -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);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче