зеркало из https://github.com/dotnet/razor.git
Cohost formatting (#10822)
Fixes https://github.com/dotnet/razor/issues/10743 Part of https://github.com/dotnet/razor/issues/9519 Brings formatting to cohosting. Relatively simple because of previous PRs. Have left sharing full test coverage of the formatting engine for later
This commit is contained in:
Коммит
9b339ba27c
|
@ -29,5 +29,6 @@
|
|||
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToDefinition" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToDefinitionService+Factory" />
|
||||
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Rename" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteRenameService+Factory" />
|
||||
<ServiceHubService Include="Microsoft.VisualStudio.Razor.AutoInsert" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteAutoInsertService+Factory" />
|
||||
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Formatting" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFormattingService+Factory" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -112,7 +112,7 @@ public class RazorCSharpFormattingBenchmark : RazorLanguageServerBenchmarkBase
|
|||
{
|
||||
var documentContext = new DocumentContext(DocumentUri, DocumentSnapshot, projectContext: null);
|
||||
|
||||
var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits: [], range: null, RazorFormattingOptions.Default, CancellationToken.None);
|
||||
var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits: [], range: null, new RazorFormattingOptions(), CancellationToken.None);
|
||||
|
||||
#if DEBUG
|
||||
// For debugging purposes only.
|
||||
|
|
|
@ -68,7 +68,7 @@ internal sealed class DefaultCSharpCodeActionResolver(
|
|||
var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync(
|
||||
documentContext,
|
||||
csharpTextEdits,
|
||||
RazorFormattingOptions.Default,
|
||||
new RazorFormattingOptions(),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
|
|
@ -56,15 +56,19 @@ internal static class IServiceCollectionExtensions
|
|||
services.AddSingleton<IOnInitialized>(clientConnection);
|
||||
}
|
||||
|
||||
public static void AddFormattingServices(this IServiceCollection services)
|
||||
public static void AddFormattingServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions)
|
||||
{
|
||||
// Formatting
|
||||
services.AddSingleton<IHtmlFormatter, HtmlFormatter>();
|
||||
services.AddSingleton<IRazorFormattingService, RazorFormattingService>();
|
||||
|
||||
services.AddHandlerWithCapabilities<DocumentFormattingEndpoint>();
|
||||
services.AddHandlerWithCapabilities<DocumentOnTypeFormattingEndpoint>();
|
||||
services.AddHandlerWithCapabilities<DocumentRangeFormattingEndpoint>();
|
||||
if (!featureOptions.UseRazorCohostServer)
|
||||
{
|
||||
services.AddSingleton<IHtmlFormatter, HtmlFormatter>();
|
||||
|
||||
services.AddHandlerWithCapabilities<DocumentFormattingEndpoint>();
|
||||
services.AddHandlerWithCapabilities<DocumentOnTypeFormattingEndpoint>();
|
||||
services.AddHandlerWithCapabilities<DocumentRangeFormattingEndpoint>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddCompletionServices(this IServiceCollection services)
|
||||
|
|
|
@ -15,6 +15,7 @@ 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.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
|
@ -25,32 +26,20 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
|
|||
internal class DocumentOnTypeFormattingEndpoint(
|
||||
IRazorFormattingService razorFormattingService,
|
||||
IHtmlFormatter htmlFormatter,
|
||||
IDocumentMappingService documentMappingService,
|
||||
RazorLSPOptionsMonitor optionsMonitor,
|
||||
ILoggerFactory loggerFactory)
|
||||
: IRazorRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]?>, ICapabilitiesProvider
|
||||
{
|
||||
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 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;
|
||||
|
||||
public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
|
||||
{
|
||||
serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions
|
||||
{
|
||||
FirstTriggerCharacter = s_allTriggerCharacters[0],
|
||||
MoreTriggerCharacter = s_allTriggerCharacters.AsSpan()[1..].ToArray(),
|
||||
};
|
||||
serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions().EnableOnTypeFormattingTriggerCharacters();
|
||||
}
|
||||
|
||||
public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormattingParams request)
|
||||
|
@ -74,7 +63,7 @@ internal class DocumentOnTypeFormattingEndpoint(
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!s_allTriggerCharacterSet.Contains(request.Character))
|
||||
if (!RazorFormattingService.AllTriggerCharacterSet.Contains(request.Character))
|
||||
{
|
||||
_logger.LogWarning($"Unexpected trigger character '{request.Character}'.");
|
||||
return null;
|
||||
|
@ -102,24 +91,13 @@ internal class DocumentOnTypeFormattingEndpoint(
|
|||
return null;
|
||||
}
|
||||
|
||||
var triggerCharacterKind = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex, rightAssociative: false);
|
||||
if (triggerCharacterKind is not (RazorLanguageKind.CSharp or RazorLanguageKind.Html))
|
||||
if (_razorFormattingService.TryGetOnTypeFormattingTriggerKind(codeDocument, hostDocumentIndex, request.Character, out var triggerCharacterKind))
|
||||
{
|
||||
_logger.LogInformation($"Unsupported trigger character language {triggerCharacterKind:G}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!IsApplicableTriggerCharacter(request.Character, triggerCharacterKind))
|
||||
{
|
||||
// We were triggered but the trigger character doesn't make sense for the current cursor position. Bail.
|
||||
_logger.LogInformation($"Unsupported trigger character location.");
|
||||
return null;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Debug.Assert(request.Character.Length > 0);
|
||||
|
||||
var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
|
||||
|
||||
TextEdit[] formattedEdits;
|
||||
|
@ -147,26 +125,4 @@ internal class DocumentOnTypeFormattingEndpoint(
|
|||
_logger.LogInformation($"Returning {formattedEdits.Length} final formatted results.");
|
||||
return formattedEdits;
|
||||
}
|
||||
|
||||
private static bool IsApplicableTriggerCharacter(string triggerCharacter, RazorLanguageKind languageKind)
|
||||
{
|
||||
if (languageKind == RazorLanguageKind.CSharp)
|
||||
{
|
||||
return s_csharpTriggerCharacterSet.Contains(triggerCharacter);
|
||||
}
|
||||
else if (languageKind == RazorLanguageKind.Html)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ internal sealed class LspFormattingCodeDocumentProvider : IFormattingCodeDocumen
|
|||
{
|
||||
public Task<RazorCodeDocument> GetCodeDocumentAsync(IDocumentSnapshot snapshot)
|
||||
{
|
||||
var useDesignTimeGeneratedOutput = snapshot.Project.Configuration.LanguageServerFlags?.ForceRuntimeCodeGeneration ?? false;
|
||||
return snapshot.GetGeneratedOutputAsync(useDesignTimeGeneratedOutput);
|
||||
// Formatting always uses design time
|
||||
return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
|
||||
|
@ -84,4 +85,12 @@ internal static class LspInitializationHelpers
|
|||
|
||||
return options;
|
||||
}
|
||||
|
||||
public static DocumentOnTypeFormattingOptions EnableOnTypeFormattingTriggerCharacters(this DocumentOnTypeFormattingOptions options)
|
||||
{
|
||||
options.FirstTriggerCharacter = RazorFormattingService.FirstTriggerCharacter;
|
||||
options.MoreTriggerCharacter = RazorFormattingService.MoreTriggerCharacters.ToArray();
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ internal partial class RazorLanguageServer : SystemTextJsonLanguageServer<RazorR
|
|||
services.AddSemanticTokensServices(featureOptions);
|
||||
services.AddDocumentManagementServices(featureOptions);
|
||||
services.AddCompletionServices();
|
||||
services.AddFormattingServices();
|
||||
services.AddFormattingServices(featureOptions);
|
||||
services.AddCodeActionsServices();
|
||||
services.AddOptionsServices(_lspOptions);
|
||||
services.AddHoverServices();
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
|
@ -49,4 +51,10 @@ internal interface IRazorFormattingService
|
|||
TextEdit[] csharpEdits,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
bool TryGetOnTypeFormattingTriggerKind(
|
||||
RazorCodeDocument codeDocument,
|
||||
int hostDocumentIndex,
|
||||
string triggerCharacter,
|
||||
out RazorLanguageKind triggerCharacterKind);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
|
||||
[DataContract]
|
||||
internal readonly record struct RazorFormattingOptions
|
||||
{
|
||||
public static readonly RazorFormattingOptions Default = new();
|
||||
|
||||
[DataMember(Order = 0)]
|
||||
public bool InsertSpaces { get; init; } = true;
|
||||
[DataMember(Order = 1)]
|
||||
public int TabSize { get; init; } = 4;
|
||||
[DataMember(Order = 2)]
|
||||
public bool CodeBlockBraceOnNextLine { get; init; } = false;
|
||||
|
||||
public RazorFormattingOptions()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
@ -11,6 +13,7 @@ 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,7 +23,15 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
|||
|
||||
internal class RazorFormattingService : IRazorFormattingService
|
||||
{
|
||||
public static readonly string FirstTriggerCharacter = "}";
|
||||
public static readonly ImmutableArray<string> MoreTriggerCharacters = [";", "\n", "{"];
|
||||
public static readonly FrozenSet<string> AllTriggerCharacterSet = FrozenSet.ToFrozenSet([FirstTriggerCharacter, .. MoreTriggerCharacters], StringComparer.Ordinal);
|
||||
|
||||
private static readonly FrozenSet<string> s_csharpTriggerCharacterSet = FrozenSet.ToFrozenSet(["}", ";"], StringComparer.Ordinal);
|
||||
private static readonly FrozenSet<string> s_htmlTriggerCharacterSet = FrozenSet.ToFrozenSet(["\n", "{", "}", ";"], StringComparer.Ordinal);
|
||||
|
||||
private readonly IFormattingCodeDocumentProvider _codeDocumentProvider;
|
||||
private readonly IDocumentMappingService _documentMappingService;
|
||||
private readonly IAdhocWorkspaceFactory _workspaceFactory;
|
||||
|
||||
private readonly ImmutableArray<IFormattingPass> _documentFormattingPasses;
|
||||
|
@ -35,6 +46,7 @@ internal class RazorFormattingService : IRazorFormattingService
|
|||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_codeDocumentProvider = codeDocumentProvider;
|
||||
_documentMappingService = documentMappingService;
|
||||
_workspaceFactory = workspaceFactory;
|
||||
|
||||
_htmlOnTypeFormattingPass = new HtmlOnTypeFormattingPass(loggerFactory);
|
||||
|
@ -186,6 +198,18 @@ internal class RazorFormattingService : IRazorFormattingService
|
|||
return razorEdits.SingleOrDefault();
|
||||
}
|
||||
|
||||
public bool TryGetOnTypeFormattingTriggerKind(RazorCodeDocument codeDocument, int hostDocumentIndex, string triggerCharacter, out RazorLanguageKind triggerCharacterKind)
|
||||
{
|
||||
triggerCharacterKind = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex, rightAssociative: false);
|
||||
|
||||
return triggerCharacterKind switch
|
||||
{
|
||||
RazorLanguageKind.CSharp => s_csharpTriggerCharacterSet.Contains(triggerCharacter),
|
||||
RazorLanguageKind.Html => s_htmlTriggerCharacterSet.Contains(triggerCharacter),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<TextEdit[]> ApplyFormattedEditsAsync(
|
||||
DocumentContext documentContext,
|
||||
TextEdit[] generatedDocumentEdits,
|
||||
|
@ -203,7 +227,7 @@ internal class RazorFormattingService : IRazorFormattingService
|
|||
|
||||
var documentSnapshot = documentContext.Snapshot;
|
||||
var uri = documentContext.Uri;
|
||||
var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
|
||||
var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(documentSnapshot).ConfigureAwait(false);
|
||||
using var context = FormattingContext.CreateForOnTypeFormatting(
|
||||
uri,
|
||||
documentSnapshot,
|
||||
|
@ -286,7 +310,7 @@ internal class RazorFormattingService : IRazorFormattingService
|
|||
/// If LF line endings are more prevalent, it removes any CR characters from the text edits
|
||||
/// to ensure consistency with the LF style.
|
||||
/// </summary>
|
||||
private TextEdit[] NormalizeLineEndings(SourceText originalText, TextEdit[] edits)
|
||||
private static TextEdit[] NormalizeLineEndings(SourceText originalText, TextEdit[] edits)
|
||||
{
|
||||
if (originalText.HasLFLineEndings())
|
||||
{
|
||||
|
@ -298,4 +322,10 @@ internal class RazorFormattingService : IRazorFormattingService
|
|||
|
||||
return edits;
|
||||
}
|
||||
|
||||
internal static class TestAccessor
|
||||
{
|
||||
public static FrozenSet<string> GetCSharpTriggerCharacterSet() => s_csharpTriggerCharacterSet;
|
||||
public static FrozenSet<string> GetHtmlTriggerCharacterSet() => s_htmlTriggerCharacterSet;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
// 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.Immutable;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Remote;
|
||||
|
||||
internal interface IRemoteFormattingService
|
||||
{
|
||||
ValueTask<ImmutableArray<TextChange>> GetDocumentFormattingEditsAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId documentId,
|
||||
ImmutableArray<TextChange> htmlChanges,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<ImmutableArray<TextChange>> GetRangeFormattingEditsAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId documentId,
|
||||
ImmutableArray<TextChange> htmlChanges,
|
||||
LinePositionSpan linePositionSpan,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<ImmutableArray<TextChange>> GetOnTypeFormattingEditsAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId documentId,
|
||||
ImmutableArray<TextChange> htmlChanges,
|
||||
LinePosition linePosition,
|
||||
string triggerCharacter,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<TriggerKind> GetOnTypeFormattingTriggerKindAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId documentId,
|
||||
LinePosition linePosition,
|
||||
string triggerCharacter,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
internal enum TriggerKind
|
||||
{
|
||||
Invalid,
|
||||
ValidHtml,
|
||||
ValidCSharp,
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ internal static class RazorServices
|
|||
(typeof(IRemoteFoldingRangeService), null),
|
||||
(typeof(IRemoteDocumentHighlightService), null),
|
||||
(typeof(IRemoteAutoInsertService), null),
|
||||
(typeof(IRemoteFormattingService), null),
|
||||
];
|
||||
|
||||
// Internal for testing
|
||||
|
|
|
@ -14,6 +14,7 @@ internal sealed class RemoteFormattingCodeDocumentProvider : IFormattingCodeDocu
|
|||
{
|
||||
public Task<RazorCodeDocument> GetCodeDocumentAsync(IDocumentSnapshot snapshot)
|
||||
{
|
||||
return snapshot.GetGeneratedOutputAsync();
|
||||
// Formatting always uses design time
|
||||
return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
// 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.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||
using Microsoft.CodeAnalysis.Razor.Remote;
|
||||
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Response = Microsoft.CodeAnalysis.Razor.Remote.IRemoteFormattingService.TriggerKind;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Remote.Razor;
|
||||
|
||||
internal sealed class RemoteFormattingService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteFormattingService
|
||||
{
|
||||
internal sealed class Factory : FactoryBase<IRemoteFormattingService>
|
||||
{
|
||||
protected override IRemoteFormattingService CreateService(in ServiceArgs args)
|
||||
=> new RemoteFormattingService(in args);
|
||||
}
|
||||
|
||||
private readonly IRazorFormattingService _formattingService = args.ExportProvider.GetExportedValue<IRazorFormattingService>();
|
||||
|
||||
public ValueTask<ImmutableArray<TextChange>> GetDocumentFormattingEditsAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId documentId,
|
||||
ImmutableArray<TextChange> htmlChanges,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
=> RunServiceAsync(
|
||||
solutionInfo,
|
||||
documentId,
|
||||
context => GetDocumentFormattingEditsAsync(context, htmlChanges, options, cancellationToken),
|
||||
cancellationToken);
|
||||
|
||||
private async ValueTask<ImmutableArray<TextChange>> GetDocumentFormattingEditsAsync(
|
||||
RemoteDocumentContext context,
|
||||
ImmutableArray<TextChange> htmlChanges,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
|
||||
var htmlEdits = htmlChanges.Select(sourceText.GetTextEdit).ToArray();
|
||||
|
||||
var edits = await _formattingService.GetDocumentFormattingEditsAsync(context, htmlEdits, range: null, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (edits is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return edits.SelectAsArray(sourceText.GetTextChange);
|
||||
}
|
||||
|
||||
public ValueTask<ImmutableArray<TextChange>> GetRangeFormattingEditsAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId documentId,
|
||||
ImmutableArray<TextChange> htmlChanges,
|
||||
LinePositionSpan linePositionSpan,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
=> RunServiceAsync(
|
||||
solutionInfo,
|
||||
documentId,
|
||||
context => GetRangeFormattingEditsAsync(context, htmlChanges, linePositionSpan, options, cancellationToken),
|
||||
cancellationToken);
|
||||
|
||||
private async ValueTask<ImmutableArray<TextChange>> GetRangeFormattingEditsAsync(
|
||||
RemoteDocumentContext context,
|
||||
ImmutableArray<TextChange> htmlChanges,
|
||||
LinePositionSpan linePositionSpan,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
|
||||
var htmlEdits = htmlChanges.Select(sourceText.GetTextEdit).ToArray();
|
||||
|
||||
var edits = await _formattingService.GetDocumentFormattingEditsAsync(context, htmlEdits, range: linePositionSpan.ToRange(), options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (edits is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return edits.SelectAsArray(sourceText.GetTextChange);
|
||||
}
|
||||
|
||||
public ValueTask<ImmutableArray<TextChange>> GetOnTypeFormattingEditsAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId documentId,
|
||||
ImmutableArray<TextChange> htmlChanges,
|
||||
LinePosition linePosition,
|
||||
string character,
|
||||
RazorFormattingOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
=> RunServiceAsync(
|
||||
solutionInfo,
|
||||
documentId,
|
||||
context => GetOnTypeFormattingEditsAsync(context, htmlChanges, linePosition, character, options, cancellationToken),
|
||||
cancellationToken);
|
||||
|
||||
private async ValueTask<ImmutableArray<TextChange>> GetOnTypeFormattingEditsAsync(RemoteDocumentContext context, ImmutableArray<TextChange> htmlChanges, LinePosition linePosition, string triggerCharacter, RazorFormattingOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!sourceText.TryGetAbsoluteIndex(linePosition, out var hostDocumentIndex))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!_formattingService.TryGetOnTypeFormattingTriggerKind(codeDocument, hostDocumentIndex, triggerCharacter, out var triggerCharacterKind))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
TextEdit[] result;
|
||||
if (triggerCharacterKind is RazorLanguageKind.Html)
|
||||
{
|
||||
var htmlEdits = htmlChanges.Select(sourceText.GetTextEdit).ToArray();
|
||||
result = await _formattingService.GetHtmlOnTypeFormattingEditsAsync(context, htmlEdits, options, hostDocumentIndex, triggerCharacter[0], cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (triggerCharacterKind is RazorLanguageKind.CSharp)
|
||||
{
|
||||
result = await _formattingService.GetCSharpOnTypeFormattingEditsAsync(context, options, hostDocumentIndex, triggerCharacter[0], cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Assumed.Unreachable<ImmutableArray<TextChange>>();
|
||||
}
|
||||
|
||||
return result.SelectAsArray(sourceText.GetTextChange);
|
||||
}
|
||||
|
||||
public ValueTask<Response> GetOnTypeFormattingTriggerKindAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId documentId,
|
||||
LinePosition linePosition,
|
||||
string triggerCharacter,
|
||||
CancellationToken cancellationToken)
|
||||
=> RunServiceAsync(
|
||||
solutionInfo,
|
||||
documentId,
|
||||
context => IsValidOnTypeFormattingTriggerAsync(context, linePosition, triggerCharacter, cancellationToken),
|
||||
cancellationToken);
|
||||
|
||||
private async ValueTask<Response> IsValidOnTypeFormattingTriggerAsync(RemoteDocumentContext context, LinePosition linePosition, string triggerCharacter, CancellationToken cancellationToken)
|
||||
{
|
||||
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sourceText = codeDocument.Source.Text;
|
||||
if (!sourceText.TryGetAbsoluteIndex(linePosition, out var hostDocumentIndex))
|
||||
{
|
||||
return Response.Invalid;
|
||||
}
|
||||
|
||||
if (!_formattingService.TryGetOnTypeFormattingTriggerKind(codeDocument, hostDocumentIndex, triggerCharacter, out var triggerCharacterKind))
|
||||
{
|
||||
return Response.Invalid;
|
||||
}
|
||||
|
||||
if (triggerCharacterKind is RazorLanguageKind.Html)
|
||||
{
|
||||
return Response.ValidHtml;
|
||||
}
|
||||
|
||||
// TryGetOnTypeFormattingTriggerKind only returns true for C# or Html
|
||||
Debug.Assert(triggerCharacterKind is RazorLanguageKind.CSharp);
|
||||
|
||||
return Response.ValidCSharp;
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.Formatting;
|
|||
|
||||
[Export(typeof(IRazorFormattingService)), Shared]
|
||||
[method: ImportingConstructor]
|
||||
internal class RemoteRazorFormattingService(IFormattingCodeDocumentProvider codeDocumentProvider, IDocumentMappingService documentMappingService, IAdhocWorkspaceFactory adhocWorkspaceFactory, ILoggerFactory loggerFactory)
|
||||
internal sealed class RemoteRazorFormattingService(IFormattingCodeDocumentProvider codeDocumentProvider, IDocumentMappingService documentMappingService, IAdhocWorkspaceFactory adhocWorkspaceFactory, ILoggerFactory loggerFactory)
|
||||
: RazorFormattingService(codeDocumentProvider, documentMappingService, adhocWorkspaceFactory, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
// 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.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.CodeAnalysis.Razor.Remote;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Razor.Settings;
|
||||
|
||||
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
|
||||
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
[Shared]
|
||||
[CohostEndpoint(Methods.TextDocumentFormattingName)]
|
||||
[Export(typeof(IDynamicRegistrationProvider))]
|
||||
[ExportCohostStatelessLspService(typeof(CohostDocumentFormattingEndpoint))]
|
||||
[method: ImportingConstructor]
|
||||
#pragma warning restore RS0030 // Do not use banned APIs
|
||||
internal sealed class CohostDocumentFormattingEndpoint(
|
||||
IRemoteServiceInvoker remoteServiceInvoker,
|
||||
IHtmlDocumentSynchronizer htmlDocumentSynchronizer,
|
||||
LSPRequestInvoker requestInvoker,
|
||||
IClientSettingsManager clientSettingsManager,
|
||||
ILoggerFactory loggerFactory)
|
||||
: AbstractRazorCohostDocumentRequestHandler<DocumentFormattingParams, TextEdit[]?>, IDynamicRegistrationProvider
|
||||
{
|
||||
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
|
||||
private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer;
|
||||
private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
|
||||
private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager;
|
||||
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostDocumentFormattingEndpoint>();
|
||||
|
||||
protected override bool MutatesSolutionState => false;
|
||||
|
||||
protected override bool RequiresLSPSolution => true;
|
||||
|
||||
public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext)
|
||||
{
|
||||
if (clientCapabilities.TextDocument?.Formatting?.DynamicRegistration is true)
|
||||
{
|
||||
return new Registration()
|
||||
{
|
||||
Method = Methods.TextDocumentFormattingName,
|
||||
RegisterOptions = new DocumentFormattingRegistrationOptions()
|
||||
{
|
||||
DocumentSelector = filter
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(DocumentFormattingParams request)
|
||||
=> request.TextDocument.ToRazorTextDocumentIdentifier();
|
||||
|
||||
protected override Task<TextEdit[]?> HandleRequestAsync(DocumentFormattingParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
|
||||
=> HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken);
|
||||
|
||||
private async Task<TextEdit[]?> HandleRequestAsync(DocumentFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug($"Getting Html formatting changes for {razorDocument.FilePath}");
|
||||
var htmlResult = await TryGetHtmlFormattingEditsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (htmlResult is not { } htmlEdits)
|
||||
{
|
||||
// We prefer to return null, so the client will try again
|
||||
_logger.LogDebug($"Didn't get any edits back from Html");
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
|
||||
var htmlChanges = htmlEdits.SelectAsArray(sourceText.GetTextChange);
|
||||
|
||||
var options = RazorFormattingOptions.From(request.Options, _clientSettingsManager.GetClientSettings().AdvancedSettings.CodeBlockBraceOnNextLine);
|
||||
|
||||
_logger.LogDebug($"Calling OOP with the {htmlChanges.Length} html edits, so it can fill in the rest");
|
||||
var remoteResult = await _remoteServiceInvoker.TryInvokeAsync<IRemoteFormattingService, ImmutableArray<TextChange>>(
|
||||
razorDocument.Project.Solution,
|
||||
(service, solutionInfo, cancellationToken) => service.GetDocumentFormattingEditsAsync(solutionInfo, razorDocument.Id, htmlChanges, options, cancellationToken),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (remoteResult.Length > 0)
|
||||
{
|
||||
_logger.LogDebug($"Got a total of {remoteResult.Length} ranges back from OOP");
|
||||
|
||||
return remoteResult.Select(sourceText.GetTextEdit).ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<TextEdit[]?> TryGetHtmlFormattingEditsAsync(DocumentFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
{
|
||||
var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
|
||||
if (htmlDocument is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
request.TextDocument = request.TextDocument.WithUri(htmlDocument.Uri);
|
||||
|
||||
_logger.LogDebug($"Requesting document formatting edits for {htmlDocument.Uri}");
|
||||
|
||||
var result = await _requestInvoker.ReinvokeRequestOnServerAsync<DocumentFormattingParams, TextEdit[]?>(
|
||||
htmlDocument.Buffer,
|
||||
Methods.TextDocumentFormattingName,
|
||||
RazorLSPConstants.HtmlLanguageServerName,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result?.Response is null)
|
||||
{
|
||||
_logger.LogDebug($"Didn't get any ranges back from Html. Returning null so we can abandon the whole thing");
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.Response;
|
||||
}
|
||||
|
||||
internal TestAccessor GetTestAccessor() => new(this);
|
||||
|
||||
internal readonly struct TestAccessor(CohostDocumentFormattingEndpoint instance)
|
||||
{
|
||||
public Task<TextEdit[]?> HandleRequestAsync(DocumentFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
=> instance.HandleRequestAsync(request, razorDocument, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
// 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.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.CodeAnalysis.Razor.Remote;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Razor.Settings;
|
||||
|
||||
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
|
||||
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
[Shared]
|
||||
[CohostEndpoint(Methods.TextDocumentOnTypeFormattingName)]
|
||||
[Export(typeof(IDynamicRegistrationProvider))]
|
||||
[ExportCohostStatelessLspService(typeof(CohostOnTypeFormattingEndpoint))]
|
||||
[method: ImportingConstructor]
|
||||
#pragma warning restore RS0030 // Do not use banned APIs
|
||||
internal sealed class CohostOnTypeFormattingEndpoint(
|
||||
IRemoteServiceInvoker remoteServiceInvoker,
|
||||
IHtmlDocumentSynchronizer htmlDocumentSynchronizer,
|
||||
LSPRequestInvoker requestInvoker,
|
||||
IClientSettingsManager clientSettingsManager,
|
||||
ILoggerFactory loggerFactory)
|
||||
: AbstractRazorCohostDocumentRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]?>, IDynamicRegistrationProvider
|
||||
{
|
||||
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
|
||||
private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer;
|
||||
private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
|
||||
private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager;
|
||||
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostOnTypeFormattingEndpoint>();
|
||||
|
||||
protected override bool MutatesSolutionState => false;
|
||||
|
||||
protected override bool RequiresLSPSolution => true;
|
||||
|
||||
public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext)
|
||||
{
|
||||
if (clientCapabilities.TextDocument?.Formatting?.DynamicRegistration is true)
|
||||
{
|
||||
return new Registration()
|
||||
{
|
||||
Method = Methods.TextDocumentOnTypeFormattingName,
|
||||
RegisterOptions = new DocumentOnTypeFormattingRegistrationOptions()
|
||||
{
|
||||
DocumentSelector = filter,
|
||||
}.EnableOnTypeFormattingTriggerCharacters()
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(DocumentOnTypeFormattingParams request)
|
||||
=> request.TextDocument.ToRazorTextDocumentIdentifier();
|
||||
|
||||
protected override Task<TextEdit[]?> HandleRequestAsync(DocumentOnTypeFormattingParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
|
||||
=> HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken);
|
||||
|
||||
private async Task<TextEdit[]?> HandleRequestAsync(DocumentOnTypeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
{
|
||||
var clientSettings = _clientSettingsManager.GetClientSettings();
|
||||
if (!clientSettings.AdvancedSettings.FormatOnType)
|
||||
{
|
||||
_logger.LogInformation($"Formatting on type disabled.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!RazorFormattingService.AllTriggerCharacterSet.Contains(request.Character))
|
||||
{
|
||||
_logger.LogWarning($"Unexpected trigger character '{request.Character}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// We have to go to OOP to find out if we want Html formatting for this request. This is a little unfortunate
|
||||
// but just asking Html for formatting, just in case, would be bad for a couple of reasons. Firstly, the Html
|
||||
// trigger characters are a superset of the C# triggers, so we can't use that as a sign. Secondly, whilst we
|
||||
// might be making one Html request, it could be then calling CSS or TypeScript servers, so our single request
|
||||
// to OOP could potentially save a few requests downstream. Lastly, our request to OOP is MessagePack which is
|
||||
// generally faster than Json anyway.
|
||||
var triggerKind = await _remoteServiceInvoker.TryInvokeAsync<IRemoteFormattingService, IRemoteFormattingService.TriggerKind>(
|
||||
razorDocument.Project.Solution,
|
||||
(service, solutionInfo, cancellationToken) => service.GetOnTypeFormattingTriggerKindAsync(solutionInfo, razorDocument.Id, request.Position.ToLinePosition(), request.Character, cancellationToken),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (triggerKind == IRemoteFormattingService.TriggerKind.Invalid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ImmutableArray<TextChange> htmlChanges = [];
|
||||
if (triggerKind == IRemoteFormattingService.TriggerKind.ValidHtml)
|
||||
{
|
||||
_logger.LogDebug($"Getting Html formatting changes for {razorDocument.FilePath}");
|
||||
var htmlResult = await GetHtmlFormattingEditsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (htmlResult is not { } htmlEdits)
|
||||
{
|
||||
// We prefer to return null, so the client will try again
|
||||
_logger.LogDebug($"Didn't get any edits back from Html");
|
||||
return null;
|
||||
}
|
||||
|
||||
htmlChanges = htmlEdits.SelectAsArray(sourceText.GetTextChange);
|
||||
}
|
||||
|
||||
var options = RazorFormattingOptions.From(request.Options, clientSettings.AdvancedSettings.CodeBlockBraceOnNextLine);
|
||||
|
||||
_logger.LogDebug($"Calling OOP with the {htmlChanges.Length} html edits, so it can fill in the rest");
|
||||
var remoteResult = await _remoteServiceInvoker.TryInvokeAsync<IRemoteFormattingService, ImmutableArray<TextChange>>(
|
||||
razorDocument.Project.Solution,
|
||||
(service, solutionInfo, cancellationToken) => service.GetOnTypeFormattingEditsAsync(solutionInfo, razorDocument.Id, htmlChanges, request.Position.ToLinePosition(), request.Character, options, cancellationToken),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (remoteResult.Length > 0)
|
||||
{
|
||||
_logger.LogDebug($"Got a total of {remoteResult.Length} ranges back from OOP");
|
||||
|
||||
return remoteResult.Select(sourceText.GetTextEdit).ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<TextEdit[]?> GetHtmlFormattingEditsAsync(DocumentOnTypeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
{
|
||||
var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
|
||||
if (htmlDocument is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
request.TextDocument = request.TextDocument.WithUri(htmlDocument.Uri);
|
||||
|
||||
_logger.LogDebug($"Requesting document formatting edits for {htmlDocument.Uri}");
|
||||
|
||||
var result = await _requestInvoker.ReinvokeRequestOnServerAsync<DocumentOnTypeFormattingParams, TextEdit[]?>(
|
||||
htmlDocument.Buffer,
|
||||
Methods.TextDocumentOnTypeFormattingName,
|
||||
RazorLSPConstants.HtmlLanguageServerName,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result?.Response is null)
|
||||
{
|
||||
_logger.LogDebug($"Didn't get any ranges back from Html. Returning null so we can abandon the whole thing");
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.Response;
|
||||
}
|
||||
|
||||
internal TestAccessor GetTestAccessor() => new(this);
|
||||
|
||||
internal readonly struct TestAccessor(CohostOnTypeFormattingEndpoint instance)
|
||||
{
|
||||
public Task<TextEdit[]?> HandleRequestAsync(DocumentOnTypeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
=> instance.HandleRequestAsync(request, razorDocument, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
// 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.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.CodeAnalysis.Razor.Remote;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Razor.Settings;
|
||||
|
||||
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
|
||||
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
[Shared]
|
||||
[CohostEndpoint(Methods.TextDocumentRangeFormattingName)]
|
||||
[Export(typeof(IDynamicRegistrationProvider))]
|
||||
[ExportCohostStatelessLspService(typeof(CohostRangeFormattingEndpoint))]
|
||||
[method: ImportingConstructor]
|
||||
#pragma warning restore RS0030 // Do not use banned APIs
|
||||
internal sealed class CohostRangeFormattingEndpoint(
|
||||
IRemoteServiceInvoker remoteServiceInvoker,
|
||||
IHtmlDocumentSynchronizer htmlDocumentSynchronizer,
|
||||
LSPRequestInvoker requestInvoker,
|
||||
IClientSettingsManager clientSettingsManager,
|
||||
ILoggerFactory loggerFactory)
|
||||
: AbstractRazorCohostDocumentRequestHandler<DocumentRangeFormattingParams, TextEdit[]?>, IDynamicRegistrationProvider
|
||||
{
|
||||
private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
|
||||
private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer;
|
||||
private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
|
||||
private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager;
|
||||
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostRangeFormattingEndpoint>();
|
||||
|
||||
protected override bool MutatesSolutionState => false;
|
||||
|
||||
protected override bool RequiresLSPSolution => true;
|
||||
|
||||
public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext)
|
||||
{
|
||||
if (clientCapabilities.TextDocument?.Formatting?.DynamicRegistration is true)
|
||||
{
|
||||
return new Registration()
|
||||
{
|
||||
Method = Methods.TextDocumentRangeFormattingName,
|
||||
RegisterOptions = new DocumentRangeFormattingRegistrationOptions()
|
||||
{
|
||||
DocumentSelector = filter
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(DocumentRangeFormattingParams request)
|
||||
=> request.TextDocument.ToRazorTextDocumentIdentifier();
|
||||
|
||||
protected override Task<TextEdit[]?> HandleRequestAsync(DocumentRangeFormattingParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
|
||||
=> HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken);
|
||||
|
||||
private async Task<TextEdit[]?> HandleRequestAsync(DocumentRangeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug($"Getting Html formatting changes for {razorDocument.FilePath}");
|
||||
var htmlResult = await GetHtmlFormattingEditsAsync(request, razorDocument, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (htmlResult is not { } htmlEdits)
|
||||
{
|
||||
// We prefer to return null, so the client will try again
|
||||
_logger.LogDebug($"Didn't get any edits back from Html");
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
|
||||
var htmlChanges = htmlEdits.SelectAsArray(sourceText.GetTextChange);
|
||||
|
||||
var options = RazorFormattingOptions.From(request.Options, _clientSettingsManager.GetClientSettings().AdvancedSettings.CodeBlockBraceOnNextLine);
|
||||
|
||||
_logger.LogDebug($"Calling OOP with the {htmlChanges.Length} html edits, so it can fill in the rest");
|
||||
var remoteResult = await _remoteServiceInvoker.TryInvokeAsync<IRemoteFormattingService, ImmutableArray<TextChange>>(
|
||||
razorDocument.Project.Solution,
|
||||
(service, solutionInfo, cancellationToken) => service.GetRangeFormattingEditsAsync(solutionInfo, razorDocument.Id, htmlChanges, request.Range.ToLinePositionSpan(), options, cancellationToken),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (remoteResult.Length > 0)
|
||||
{
|
||||
_logger.LogDebug($"Got a total of {remoteResult.Length} ranges back from OOP");
|
||||
|
||||
return remoteResult.Select(sourceText.GetTextEdit).ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<TextEdit[]?> GetHtmlFormattingEditsAsync(DocumentRangeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
{
|
||||
var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
|
||||
if (htmlDocument is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// We don't actually request range formatting results from Html, because our formatting engine can't deal with
|
||||
// relative formatting results. Instead we request full document formatting, and filter the edits inside the
|
||||
// formatting service to only the ones we care about.
|
||||
var formattingRequest = new DocumentFormattingParams
|
||||
{
|
||||
TextDocument = request.TextDocument.WithUri(htmlDocument.Uri),
|
||||
Options = request.Options
|
||||
};
|
||||
|
||||
_logger.LogDebug($"Requesting document formatting edits for {htmlDocument.Uri}");
|
||||
|
||||
var result = await _requestInvoker.ReinvokeRequestOnServerAsync<DocumentFormattingParams, TextEdit[]?>(
|
||||
htmlDocument.Buffer,
|
||||
Methods.TextDocumentFormattingName,
|
||||
RazorLSPConstants.HtmlLanguageServerName,
|
||||
formattingRequest,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result?.Response is null)
|
||||
{
|
||||
_logger.LogDebug($"Didn't get any ranges back from Html. Returning null so we can abandon the whole thing");
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.Response;
|
||||
}
|
||||
|
||||
internal TestAccessor GetTestAccessor() => new(this);
|
||||
|
||||
internal readonly struct TestAccessor(CohostRangeFormattingEndpoint instance)
|
||||
{
|
||||
public Task<TextEdit[]?> HandleRequestAsync(DocumentRangeFormattingParams request, TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
=> instance.HandleRequestAsync(request, razorDocument, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,16 +2,11 @@
|
|||
// 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.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
|
||||
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
|
||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
@ -19,49 +14,17 @@ 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()
|
||||
{
|
||||
// Arrange
|
||||
var uri = new Uri("file://path/test.razor");
|
||||
var formattingService = new DummyRazorFormattingService();
|
||||
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
|
||||
|
||||
var optionsMonitor = GetOptionsMonitor(enableFormatting: false);
|
||||
var htmlFormatter = new TestHtmlFormatter();
|
||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
|
||||
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||
var @params = new DocumentOnTypeFormattingParams { TextDocument = new TextDocumentIdentifier { Uri = uri, } };
|
||||
var requestContext = CreateRazorRequestContext(documentContext: null);
|
||||
|
||||
|
@ -85,12 +48,11 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
|||
|
||||
var documentContext = CreateDocumentContext(new Uri("file://path/testDifferentFile.razor"), codeDocument);
|
||||
var formattingService = new DummyRazorFormattingService();
|
||||
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
|
||||
|
||||
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
||||
var htmlFormatter = new TestHtmlFormatter();
|
||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
|
||||
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||
var @params = new DocumentOnTypeFormattingParams()
|
||||
{
|
||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||
|
@ -120,12 +82,11 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
|||
|
||||
var documentContext = CreateDocumentContext(uri, codeDocument);
|
||||
var formattingService = new DummyRazorFormattingService();
|
||||
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
|
||||
|
||||
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
||||
var htmlFormatter = new TestHtmlFormatter();
|
||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
|
||||
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||
var @params = new DocumentOnTypeFormattingParams()
|
||||
{
|
||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||
|
@ -154,14 +115,12 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
|||
var uri = new Uri("file://path/test.razor");
|
||||
|
||||
var documentContext = CreateDocumentContext(uri, codeDocument);
|
||||
var formattingService = new DummyRazorFormattingService();
|
||||
var formattingService = new DummyRazorFormattingService(RazorLanguageKind.Html);
|
||||
|
||||
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, htmlFormatter, documentMappingService.Object, optionsMonitor, LoggerFactory);
|
||||
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||
var @params = new DocumentOnTypeFormattingParams()
|
||||
{
|
||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||
|
@ -190,14 +149,12 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
|||
var uri = new Uri("file://path/test.razor");
|
||||
|
||||
var documentContext = CreateDocumentContext(uri, codeDocument);
|
||||
var formattingService = new DummyRazorFormattingService();
|
||||
var formattingService = new DummyRazorFormattingService(RazorLanguageKind.Razor);
|
||||
|
||||
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, htmlFormatter, documentMappingService.Object, optionsMonitor, LoggerFactory);
|
||||
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||
var @params = new DocumentOnTypeFormattingParams()
|
||||
{
|
||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||
|
@ -227,12 +184,11 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
|||
|
||||
var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument);
|
||||
var formattingService = new DummyRazorFormattingService();
|
||||
var documentMappingService = new LspDocumentMappingService(FilePathService, documentContextFactory, LoggerFactory);
|
||||
|
||||
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
||||
var htmlFormatter = new TestHtmlFormatter();
|
||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
|
||||
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||
var @params = new DocumentOnTypeFormattingParams()
|
||||
{
|
||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||
|
|
|
@ -3,25 +3,17 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
|
||||
using Microsoft.AspNetCore.Razor.PooledObjects;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
|
||||
using Microsoft.AspNetCore.Razor.Utilities;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||
using Microsoft.CodeAnalysis.Razor.Protocol.Formatting;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Text;
|
||||
using Microsoft.VisualStudio.Utilities;
|
||||
using Microsoft.WebTools.Languages.Shared.ContentTypes;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
|
||||
|
@ -40,52 +32,28 @@ internal class FormattingLanguageServerClient(ILoggerFactory loggerFactory) : IC
|
|||
_documents.Add("/" + path, codeDocument);
|
||||
}
|
||||
|
||||
private Task<RazorDocumentFormattingResponse> FormatAsync(DocumentOnTypeFormattingParams @params)
|
||||
private async Task<RazorDocumentFormattingResponse> FormatAsync(DocumentOnTypeFormattingParams @params)
|
||||
{
|
||||
var generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri);
|
||||
var generatedHtmlSource = SourceText.From(generatedHtml, Encoding.UTF8);
|
||||
var absoluteIndex = generatedHtmlSource.GetRequiredAbsoluteIndex(@params.Position);
|
||||
|
||||
var request = $$"""
|
||||
{
|
||||
"Options":
|
||||
{
|
||||
"UseSpaces": {{(@params.Options.InsertSpaces ? "true" : "false")}},
|
||||
"TabSize": {{@params.Options.TabSize}},
|
||||
"IndentSize": {{@params.Options.TabSize}}
|
||||
},
|
||||
"Uri": "{{@params.TextDocument.Uri}}",
|
||||
"GeneratedChanges": [],
|
||||
"OperationType": "FormatOnType",
|
||||
"SpanToFormat":
|
||||
{
|
||||
"Start": {{absoluteIndex}},
|
||||
"End": {{absoluteIndex}}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var edits = await HtmlFormatting.GetOnTypeFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Position, @params.Options.InsertSpaces, @params.Options.TabSize);
|
||||
|
||||
return CallWebToolsApplyFormattedEditsHandlerAsync(request, @params.TextDocument.Uri, generatedHtml);
|
||||
return new()
|
||||
{
|
||||
Edits = edits
|
||||
};
|
||||
}
|
||||
|
||||
private Task<RazorDocumentFormattingResponse> FormatAsync(DocumentFormattingParams @params)
|
||||
private async Task<RazorDocumentFormattingResponse> FormatAsync(DocumentFormattingParams @params)
|
||||
{
|
||||
var generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri);
|
||||
|
||||
var request = $$"""
|
||||
{
|
||||
"Options":
|
||||
{
|
||||
"UseSpaces": {{(@params.Options.InsertSpaces ? "true" : "false")}},
|
||||
"TabSize": {{@params.Options.TabSize}},
|
||||
"IndentSize": {{@params.Options.TabSize}}
|
||||
},
|
||||
"Uri": "{{@params.TextDocument.Uri}}",
|
||||
"GeneratedChanges": [],
|
||||
}
|
||||
""";
|
||||
var edits = await HtmlFormatting.GetDocumentFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Options.InsertSpaces, @params.Options.TabSize);
|
||||
|
||||
return CallWebToolsApplyFormattedEditsHandlerAsync(request, @params.TextDocument.Uri, generatedHtml);
|
||||
return new()
|
||||
{
|
||||
Edits = edits
|
||||
};
|
||||
}
|
||||
|
||||
private string GetGeneratedHtml(Uri uri)
|
||||
|
@ -95,51 +63,6 @@ internal class FormattingLanguageServerClient(ILoggerFactory loggerFactory) : IC
|
|||
return generatedHtml.Replace("\r", "").Replace("\n", "\r\n");
|
||||
}
|
||||
|
||||
private async Task<RazorDocumentFormattingResponse> CallWebToolsApplyFormattedEditsHandlerAsync(string serializedValue, Uri documentUri, string generatedHtml)
|
||||
{
|
||||
var exportProvider = TestComposition.Editor.ExportProviderFactory.CreateExportProvider();
|
||||
var contentTypeService = exportProvider.GetExportedValue<IContentTypeRegistryService>();
|
||||
|
||||
if (!contentTypeService.ContentTypes.Any(t => t.TypeName == HtmlContentTypeDefinition.HtmlContentType))
|
||||
{
|
||||
contentTypeService.AddContentType(HtmlContentTypeDefinition.HtmlContentType, [StandardContentTypeNames.Text]);
|
||||
}
|
||||
|
||||
var textBufferFactoryService = (ITextBufferFactoryService3)exportProvider.GetExportedValue<ITextBufferFactoryService>();
|
||||
var bufferManager = WebTools.BufferManager.New(contentTypeService, textBufferFactoryService, []);
|
||||
var logger = _loggerFactory.GetOrCreateLogger("ApplyFormattedEditsHandler");
|
||||
var applyFormatEditsHandler = WebTools.ApplyFormatEditsHandler.New(textBufferFactoryService, bufferManager, logger);
|
||||
|
||||
// Make sure the buffer manager knows about the source document
|
||||
var textSnapshot = bufferManager.CreateBuffer(
|
||||
documentUri: documentUri,
|
||||
contentTypeName: HtmlContentTypeDefinition.HtmlContentType,
|
||||
initialContent: generatedHtml,
|
||||
snapshotVersionFromLSP: 0);
|
||||
|
||||
var requestContext = WebTools.RequestContext.New(textSnapshot);
|
||||
|
||||
var request = WebTools.ApplyFormatEditsParam.DeserializeFrom(serializedValue);
|
||||
var response = await applyFormatEditsHandler.HandleRequestAsync(request, requestContext, CancellationToken.None);
|
||||
|
||||
var sourceText = SourceText.From(generatedHtml);
|
||||
|
||||
using var edits = new PooledArrayBuilder<TextEdit>();
|
||||
|
||||
foreach (var textChange in response.TextChanges)
|
||||
{
|
||||
var span = new TextSpan(textChange.Position, textChange.Length);
|
||||
var edit = VsLspFactory.CreateTextEdit(sourceText.GetRange(span), textChange.NewText);
|
||||
|
||||
edits.Add(edit);
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
Edits = edits.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<TResponse> SendRequestAsync<TParams, TResponse>(string method, TParams @params, CancellationToken cancellationToken)
|
||||
{
|
||||
if (@params is DocumentFormattingParams formattingParams &&
|
||||
|
|
|
@ -10,6 +10,7 @@ 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;
|
||||
|
@ -31,7 +32,7 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut
|
|||
return codeDocument;
|
||||
}
|
||||
|
||||
internal class DummyRazorFormattingService : IRazorFormattingService
|
||||
internal class DummyRazorFormattingService(RazorLanguageKind? languageKind = null) : IRazorFormattingService
|
||||
{
|
||||
public bool Called { get; private set; }
|
||||
|
||||
|
@ -65,5 +66,11 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut
|
|||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool TryGetOnTypeFormattingTriggerKind(RazorCodeDocument codeDocument, int hostDocumentIndex, string triggerCharacter, out RazorLanguageKind triggerCharacterKind)
|
||||
{
|
||||
triggerCharacterKind = languageKind.GetValueOrDefault();
|
||||
return languageKind is not null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,4 +40,22 @@ public class Foo{}
|
|||
|
||||
Assert.Equal(multiEditChange.ToString(), singleEditChange.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllTriggerCharacters_IncludesCSharpTriggerCharacters()
|
||||
{
|
||||
foreach (var character in RazorFormattingService.TestAccessor.GetCSharpTriggerCharacterSet())
|
||||
{
|
||||
Assert.Contains(character, RazorFormattingService.AllTriggerCharacterSet);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllTriggerCharacters_IncludesHtmlTriggerCharacters()
|
||||
{
|
||||
foreach (var character in RazorFormattingService.TestAccessor.GetHtmlTriggerCharacterSet())
|
||||
{
|
||||
Assert.Contains(character, RazorFormattingService.AllTriggerCharacterSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,19 +13,6 @@
|
|||
<Compile Include="..\OSSkipConditionFactAttribute.cs" LinkBase="Shared" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetFxTargetFramework)'">
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Html" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Html.Editor" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.LanguageServer.Server" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Shared" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Shared.Editor" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Shared.VS" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Shell.Framework" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web" />
|
||||
<PackageReference Include="Microsoft.WebTools.Shared" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Razor.LanguageServer\Microsoft.AspNetCore.Razor.LanguageServer.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Razor.Test.Common.Tooling\Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj" />
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.PooledObjects;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Text;
|
||||
using Microsoft.VisualStudio.Utilities;
|
||||
using Microsoft.WebTools.Languages.Shared.ContentTypes;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
|
||||
internal static class HtmlFormatting
|
||||
{
|
||||
public static Task<TextEdit[]?> GetDocumentFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, bool insertSpaces, int tabSize)
|
||||
{
|
||||
var request = $$"""
|
||||
{
|
||||
"Options":
|
||||
{
|
||||
"UseSpaces": {{(insertSpaces ? "true" : "false")}},
|
||||
"TabSize": {{tabSize}},
|
||||
"IndentSize": {{tabSize}}
|
||||
},
|
||||
"Uri": "{{uri}}",
|
||||
"GeneratedChanges": [],
|
||||
}
|
||||
""";
|
||||
|
||||
return CallWebToolsApplyFormattedEditsHandlerAsync(loggerFactory, request, uri, generatedHtml);
|
||||
}
|
||||
|
||||
public static Task<TextEdit[]?> GetOnTypeFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, Position position, bool insertSpaces, int tabSize)
|
||||
{
|
||||
var generatedHtmlSource = SourceText.From(generatedHtml, Encoding.UTF8);
|
||||
var absoluteIndex = generatedHtmlSource.GetRequiredAbsoluteIndex(position);
|
||||
|
||||
var request = $$"""
|
||||
{
|
||||
"Options":
|
||||
{
|
||||
"UseSpaces": {{(insertSpaces ? "true" : "false")}},
|
||||
"TabSize": {{tabSize}},
|
||||
"IndentSize": {{tabSize}}
|
||||
},
|
||||
"Uri": "{{uri}}",
|
||||
"GeneratedChanges": [],
|
||||
"OperationType": "FormatOnType",
|
||||
"SpanToFormat":
|
||||
{
|
||||
"Start": {{absoluteIndex}},
|
||||
"End": {{absoluteIndex}}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
return CallWebToolsApplyFormattedEditsHandlerAsync(loggerFactory, request, uri, generatedHtml);
|
||||
}
|
||||
|
||||
private static async Task<TextEdit[]?> CallWebToolsApplyFormattedEditsHandlerAsync(ILoggerFactory loggerFactory, string serializedValue, Uri documentUri, string generatedHtml)
|
||||
{
|
||||
var exportProvider = TestComposition.Editor.ExportProviderFactory.CreateExportProvider();
|
||||
var contentTypeService = exportProvider.GetExportedValue<IContentTypeRegistryService>();
|
||||
|
||||
lock (contentTypeService)
|
||||
{
|
||||
if (!contentTypeService.ContentTypes.Any(t => t.TypeName == HtmlContentTypeDefinition.HtmlContentType))
|
||||
{
|
||||
contentTypeService.AddContentType(HtmlContentTypeDefinition.HtmlContentType, [StandardContentTypeNames.Text]);
|
||||
}
|
||||
}
|
||||
|
||||
var textBufferFactoryService = (ITextBufferFactoryService3)exportProvider.GetExportedValue<ITextBufferFactoryService>();
|
||||
var bufferManager = WebTools.BufferManager.New(contentTypeService, textBufferFactoryService, []);
|
||||
var logger = loggerFactory.GetOrCreateLogger("ApplyFormattedEditsHandler");
|
||||
var applyFormatEditsHandler = WebTools.ApplyFormatEditsHandler.New(textBufferFactoryService, bufferManager, logger);
|
||||
|
||||
// Make sure the buffer manager knows about the source document
|
||||
var textSnapshot = bufferManager.CreateBuffer(
|
||||
documentUri: documentUri,
|
||||
contentTypeName: HtmlContentTypeDefinition.HtmlContentType,
|
||||
initialContent: generatedHtml,
|
||||
snapshotVersionFromLSP: 0);
|
||||
|
||||
var requestContext = WebTools.RequestContext.New(textSnapshot);
|
||||
|
||||
var request = WebTools.ApplyFormatEditsParam.DeserializeFrom(serializedValue);
|
||||
var response = await applyFormatEditsHandler.HandleRequestAsync(request, requestContext, CancellationToken.None);
|
||||
|
||||
var sourceText = SourceText.From(generatedHtml);
|
||||
|
||||
using var edits = new PooledArrayBuilder<TextEdit>();
|
||||
|
||||
foreach (var textChange in response.TextChanges)
|
||||
{
|
||||
var span = new TextSpan(textChange.Position, textChange.Length);
|
||||
var edit = VsLspFactory.CreateTextEdit(sourceText.GetRange(span), textChange.NewText);
|
||||
|
||||
edits.Add(edit);
|
||||
}
|
||||
|
||||
return edits.ToArray();
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ using System.Collections.Immutable;
|
|||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using Microsoft.AspNetCore.Razor.PooledObjects;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.VisualStudio.Settings.Internal;
|
||||
|
@ -16,7 +17,7 @@ using Microsoft.WebTools.Languages.Shared.Editor.Composition;
|
|||
using Microsoft.WebTools.Languages.Shared.Editor.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
|
||||
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides reflection-based access to the Web Tools LSP infrastructure needed for tests.
|
|
@ -35,6 +35,19 @@
|
|||
<ProjectReference Include="..\..\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetFxTargetFramework)'">
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Html" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Html.Editor" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.LanguageServer.Server" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Shared" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Shared.Editor" />
|
||||
<PackageReference Include="Microsoft.WebTools.Languages.Shared.VS" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Shell.Framework" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web" />
|
||||
<PackageReference Include="Microsoft.WebTools.Shared" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing" />
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Razor.Settings;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using Roslyn.Test.Utilities;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
|
||||
|
||||
[UseExportProvider]
|
||||
public class CohostDocumentFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
|
||||
{
|
||||
// All of the formatting tests in the language server exercise the formatting engine and cover various edge cases
|
||||
// and provide regression prevention. The tests here are not exhaustive, but they validate the the cohost endpoints
|
||||
// call into the formatting engine at least, and handles C#, Html and Razor formatting changes correctly.
|
||||
|
||||
[Fact]
|
||||
public Task Formatting()
|
||||
=> VerifyDocumentFormattingAsync(
|
||||
input: """
|
||||
@preservewhitespace true
|
||||
|
||||
<div></div>
|
||||
|
||||
@{
|
||||
<p>
|
||||
@{
|
||||
var t = 1;
|
||||
if (true)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
</p>
|
||||
<div>
|
||||
@{
|
||||
<div>
|
||||
<div>
|
||||
This is heavily nested
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private void M(string thisIsMyString)
|
||||
{
|
||||
var x = 5;
|
||||
|
||||
var y = "Hello";
|
||||
|
||||
M("Hello");
|
||||
}
|
||||
}
|
||||
|
||||
""",
|
||||
expected: """
|
||||
@preservewhitespace true
|
||||
|
||||
<div></div>
|
||||
|
||||
@{
|
||||
<p>
|
||||
@{
|
||||
var t = 1;
|
||||
if (true)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
</p>
|
||||
<div>
|
||||
@{
|
||||
<div>
|
||||
<div>
|
||||
This is heavily nested
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private void M(string thisIsMyString)
|
||||
{
|
||||
var x = 5;
|
||||
|
||||
var y = "Hello";
|
||||
|
||||
M("Hello");
|
||||
}
|
||||
}
|
||||
|
||||
""");
|
||||
|
||||
private async Task VerifyDocumentFormattingAsync(string input, string expected)
|
||||
{
|
||||
var document = CreateProjectAndRazorDocument(input);
|
||||
var inputText = await document.GetTextAsync(DisposalToken);
|
||||
|
||||
var htmlDocumentPublisher = new HtmlDocumentPublisher(RemoteServiceInvoker, StrictMock.Of<TrackingLSPDocumentManager>(), StrictMock.Of<JoinableTaskContext>(), LoggerFactory);
|
||||
var generatedHtml = await htmlDocumentPublisher.GetHtmlSourceFromOOPAsync(document, DisposalToken);
|
||||
Assert.NotNull(generatedHtml);
|
||||
|
||||
var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}");
|
||||
var htmlEdits = await HtmlFormatting.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4);
|
||||
|
||||
var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentFormattingName, htmlEdits)]);
|
||||
|
||||
var clientSettingsManager = new ClientSettingsManager(changeTriggers: []);
|
||||
|
||||
var endpoint = new CohostDocumentFormattingEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, clientSettingsManager, LoggerFactory);
|
||||
|
||||
var request = new DocumentFormattingParams()
|
||||
{
|
||||
TextDocument = new TextDocumentIdentifier() { Uri = document.CreateUri() },
|
||||
Options = new FormattingOptions()
|
||||
{
|
||||
TabSize = 4,
|
||||
InsertSpaces = true
|
||||
}
|
||||
};
|
||||
|
||||
var edits = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken);
|
||||
|
||||
var changes = edits.Select(inputText.GetTextChange);
|
||||
var finalText = inputText.WithChanges(changes);
|
||||
|
||||
AssertEx.EqualOrDiff(expected, finalText.ToString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Razor.Settings;
|
||||
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using Roslyn.Test.Utilities;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
|
||||
|
||||
[UseExportProvider]
|
||||
public class CohostOnTypeFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
|
||||
{
|
||||
[Fact]
|
||||
public async Task InvalidTrigger()
|
||||
{
|
||||
await VerifyOnTypeFormattingAsync(
|
||||
input: """
|
||||
@{
|
||||
if(true){}$$
|
||||
}
|
||||
""",
|
||||
expected: """
|
||||
@{
|
||||
if(true){}
|
||||
}
|
||||
""",
|
||||
triggerCharacter: 'h');
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CSharp_InvalidTrigger()
|
||||
{
|
||||
await VerifyOnTypeFormattingAsync(
|
||||
input: """
|
||||
@{
|
||||
if(true){}$$
|
||||
}
|
||||
""",
|
||||
expected: """
|
||||
@{
|
||||
if(true){}
|
||||
}
|
||||
""",
|
||||
triggerCharacter: '\n');
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CSharp()
|
||||
{
|
||||
await VerifyOnTypeFormattingAsync(
|
||||
input: """
|
||||
@{
|
||||
if(true){}$$
|
||||
}
|
||||
""",
|
||||
expected: """
|
||||
@{
|
||||
if (true) { }
|
||||
}
|
||||
""",
|
||||
triggerCharacter: '}');
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormatsSimpleHtmlTag_OnType()
|
||||
{
|
||||
await VerifyOnTypeFormattingAsync(
|
||||
input: """
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello</title>
|
||||
<script>
|
||||
var x = 2;$$
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
""",
|
||||
expected: """
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello</title>
|
||||
<script>
|
||||
var x = 2;
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
""",
|
||||
triggerCharacter: ';',
|
||||
html: true);
|
||||
}
|
||||
|
||||
private async Task VerifyOnTypeFormattingAsync(TestCode input, string expected, char triggerCharacter, bool html = false)
|
||||
{
|
||||
var document = CreateProjectAndRazorDocument(input.Text);
|
||||
var inputText = await document.GetTextAsync(DisposalToken);
|
||||
var position = inputText.GetPosition(input.Position);
|
||||
|
||||
LSPRequestInvoker requestInvoker;
|
||||
if (html)
|
||||
{
|
||||
var htmlDocumentPublisher = new HtmlDocumentPublisher(RemoteServiceInvoker, StrictMock.Of<TrackingLSPDocumentManager>(), StrictMock.Of<JoinableTaskContext>(), LoggerFactory);
|
||||
var generatedHtml = await htmlDocumentPublisher.GetHtmlSourceFromOOPAsync(document, DisposalToken);
|
||||
Assert.NotNull(generatedHtml);
|
||||
|
||||
var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}");
|
||||
var htmlEdits = await HtmlFormatting.GetOnTypeFormattingEditsAsync(LoggerFactory, uri, generatedHtml, position, insertSpaces: true, tabSize: 4);
|
||||
|
||||
requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentOnTypeFormattingName, htmlEdits)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We use a mock here so that it will throw if called
|
||||
requestInvoker = StrictMock.Of<LSPRequestInvoker>();
|
||||
}
|
||||
|
||||
var clientSettingsManager = new ClientSettingsManager(changeTriggers: []);
|
||||
|
||||
var endpoint = new CohostOnTypeFormattingEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, clientSettingsManager, LoggerFactory);
|
||||
|
||||
var request = new DocumentOnTypeFormattingParams()
|
||||
{
|
||||
TextDocument = new TextDocumentIdentifier() { Uri = document.CreateUri() },
|
||||
Options = new FormattingOptions()
|
||||
{
|
||||
TabSize = 4,
|
||||
InsertSpaces = true
|
||||
},
|
||||
Character = triggerCharacter.ToString(),
|
||||
Position = position
|
||||
};
|
||||
|
||||
var edits = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken);
|
||||
|
||||
if (edits is null)
|
||||
{
|
||||
Assert.Equal(expected, input.Text);
|
||||
return;
|
||||
}
|
||||
|
||||
var changes = edits.Select(inputText.GetTextChange);
|
||||
var finalText = inputText.WithChanges(changes);
|
||||
|
||||
AssertEx.EqualOrDiff(expected, finalText.ToString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
|
||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Razor.Settings;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using Roslyn.Test.Utilities;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
|
||||
|
||||
[UseExportProvider]
|
||||
public class CohostRangeFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper)
|
||||
{
|
||||
[Fact]
|
||||
public Task RangeFormatting()
|
||||
=> VerifyRangeFormattingAsync(
|
||||
input: """
|
||||
@preservewhitespace true
|
||||
|
||||
<div></div>
|
||||
|
||||
@{
|
||||
<p>
|
||||
@{
|
||||
var t = 1;
|
||||
if (true)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
</p>
|
||||
[|<div>
|
||||
@{
|
||||
<div>
|
||||
<div>
|
||||
This is heavily nested
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>|]
|
||||
}
|
||||
|
||||
@code {
|
||||
private void M(string thisIsMyString)
|
||||
{
|
||||
var x = 5;
|
||||
|
||||
var y = "Hello";
|
||||
|
||||
M("Hello");
|
||||
}
|
||||
}
|
||||
""",
|
||||
expected: """
|
||||
@preservewhitespace true
|
||||
|
||||
<div></div>
|
||||
|
||||
@{
|
||||
<p>
|
||||
@{
|
||||
var t = 1;
|
||||
if (true)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
</p>
|
||||
<div>
|
||||
@{
|
||||
<div>
|
||||
<div>
|
||||
This is heavily nested
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private void M(string thisIsMyString)
|
||||
{
|
||||
var x = 5;
|
||||
|
||||
var y = "Hello";
|
||||
|
||||
M("Hello");
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
private async Task VerifyRangeFormattingAsync(TestCode input, string expected)
|
||||
{
|
||||
var document = CreateProjectAndRazorDocument(input.Text);
|
||||
var inputText = await document.GetTextAsync(DisposalToken);
|
||||
|
||||
var htmlDocumentPublisher = new HtmlDocumentPublisher(RemoteServiceInvoker, StrictMock.Of<TrackingLSPDocumentManager>(), StrictMock.Of<JoinableTaskContext>(), LoggerFactory);
|
||||
var generatedHtml = await htmlDocumentPublisher.GetHtmlSourceFromOOPAsync(document, DisposalToken);
|
||||
Assert.NotNull(generatedHtml);
|
||||
|
||||
var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}");
|
||||
var htmlEdits = await HtmlFormatting.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4);
|
||||
|
||||
var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentFormattingName, htmlEdits)]);
|
||||
|
||||
var clientSettingsManager = new ClientSettingsManager(changeTriggers: []);
|
||||
|
||||
var endpoint = new CohostRangeFormattingEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, clientSettingsManager, LoggerFactory);
|
||||
|
||||
var request = new DocumentRangeFormattingParams()
|
||||
{
|
||||
TextDocument = new TextDocumentIdentifier() { Uri = document.CreateUri() },
|
||||
Options = new FormattingOptions()
|
||||
{
|
||||
TabSize = 4,
|
||||
InsertSpaces = true
|
||||
},
|
||||
Range = inputText.GetRange(input.Span)
|
||||
};
|
||||
|
||||
var edits = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken);
|
||||
|
||||
var changes = edits.Select(inputText.GetTextChange);
|
||||
var finalText = inputText.WithChanges(changes);
|
||||
|
||||
AssertEx.EqualOrDiff(expected, finalText.ToString());
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче