зеркало из 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.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.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.AutoInsert" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteAutoInsertService+Factory" />
|
||||||
|
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Formatting" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFormattingService+Factory" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -112,7 +112,7 @@ public class RazorCSharpFormattingBenchmark : RazorLanguageServerBenchmarkBase
|
||||||
{
|
{
|
||||||
var documentContext = new DocumentContext(DocumentUri, DocumentSnapshot, projectContext: null);
|
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
|
#if DEBUG
|
||||||
// For debugging purposes only.
|
// For debugging purposes only.
|
||||||
|
|
|
@ -68,7 +68,7 @@ internal sealed class DefaultCSharpCodeActionResolver(
|
||||||
var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync(
|
var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync(
|
||||||
documentContext,
|
documentContext,
|
||||||
csharpTextEdits,
|
csharpTextEdits,
|
||||||
RazorFormattingOptions.Default,
|
new RazorFormattingOptions(),
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
|
@ -56,15 +56,19 @@ internal static class IServiceCollectionExtensions
|
||||||
services.AddSingleton<IOnInitialized>(clientConnection);
|
services.AddSingleton<IOnInitialized>(clientConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddFormattingServices(this IServiceCollection services)
|
public static void AddFormattingServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions)
|
||||||
{
|
{
|
||||||
// Formatting
|
// Formatting
|
||||||
services.AddSingleton<IHtmlFormatter, HtmlFormatter>();
|
|
||||||
services.AddSingleton<IRazorFormattingService, RazorFormattingService>();
|
services.AddSingleton<IRazorFormattingService, RazorFormattingService>();
|
||||||
|
|
||||||
services.AddHandlerWithCapabilities<DocumentFormattingEndpoint>();
|
if (!featureOptions.UseRazorCohostServer)
|
||||||
services.AddHandlerWithCapabilities<DocumentOnTypeFormattingEndpoint>();
|
{
|
||||||
services.AddHandlerWithCapabilities<DocumentRangeFormattingEndpoint>();
|
services.AddSingleton<IHtmlFormatter, HtmlFormatter>();
|
||||||
|
|
||||||
|
services.AddHandlerWithCapabilities<DocumentFormattingEndpoint>();
|
||||||
|
services.AddHandlerWithCapabilities<DocumentOnTypeFormattingEndpoint>();
|
||||||
|
services.AddHandlerWithCapabilities<DocumentRangeFormattingEndpoint>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddCompletionServices(this IServiceCollection services)
|
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.DocumentMapping;
|
||||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||||
using Microsoft.CodeAnalysis.Text;
|
using Microsoft.CodeAnalysis.Text;
|
||||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||||
|
@ -25,32 +26,20 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
|
||||||
internal class DocumentOnTypeFormattingEndpoint(
|
internal class DocumentOnTypeFormattingEndpoint(
|
||||||
IRazorFormattingService razorFormattingService,
|
IRazorFormattingService razorFormattingService,
|
||||||
IHtmlFormatter htmlFormatter,
|
IHtmlFormatter htmlFormatter,
|
||||||
IDocumentMappingService documentMappingService,
|
|
||||||
RazorLSPOptionsMonitor optionsMonitor,
|
RazorLSPOptionsMonitor optionsMonitor,
|
||||||
ILoggerFactory loggerFactory)
|
ILoggerFactory loggerFactory)
|
||||||
: IRazorRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]?>, ICapabilitiesProvider
|
: IRazorRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]?>, ICapabilitiesProvider
|
||||||
{
|
{
|
||||||
private readonly IRazorFormattingService _razorFormattingService = razorFormattingService;
|
private readonly IRazorFormattingService _razorFormattingService = razorFormattingService;
|
||||||
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
|
|
||||||
private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor;
|
private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor;
|
||||||
private readonly IHtmlFormatter _htmlFormatter = htmlFormatter;
|
private readonly IHtmlFormatter _htmlFormatter = htmlFormatter;
|
||||||
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<DocumentOnTypeFormattingEndpoint>();
|
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 bool MutatesSolutionState => false;
|
||||||
|
|
||||||
public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
|
public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
|
||||||
{
|
{
|
||||||
serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions
|
serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions().EnableOnTypeFormattingTriggerCharacters();
|
||||||
{
|
|
||||||
FirstTriggerCharacter = s_allTriggerCharacters[0],
|
|
||||||
MoreTriggerCharacter = s_allTriggerCharacters.AsSpan()[1..].ToArray(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormattingParams request)
|
public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormattingParams request)
|
||||||
|
@ -74,7 +63,7 @@ internal class DocumentOnTypeFormattingEndpoint(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!s_allTriggerCharacterSet.Contains(request.Character))
|
if (!RazorFormattingService.AllTriggerCharacterSet.Contains(request.Character))
|
||||||
{
|
{
|
||||||
_logger.LogWarning($"Unexpected trigger character '{request.Character}'.");
|
_logger.LogWarning($"Unexpected trigger character '{request.Character}'.");
|
||||||
return null;
|
return null;
|
||||||
|
@ -102,24 +91,13 @@ internal class DocumentOnTypeFormattingEndpoint(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var triggerCharacterKind = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex, rightAssociative: false);
|
if (_razorFormattingService.TryGetOnTypeFormattingTriggerKind(codeDocument, hostDocumentIndex, request.Character, out var triggerCharacterKind))
|
||||||
if (triggerCharacterKind is not (RazorLanguageKind.CSharp or RazorLanguageKind.Html))
|
|
||||||
{
|
{
|
||||||
_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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Debug.Assert(request.Character.Length > 0);
|
|
||||||
|
|
||||||
var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
|
var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);
|
||||||
|
|
||||||
TextEdit[] formattedEdits;
|
TextEdit[] formattedEdits;
|
||||||
|
@ -147,26 +125,4 @@ internal class DocumentOnTypeFormattingEndpoint(
|
||||||
_logger.LogInformation($"Returning {formattedEdits.Length} final formatted results.");
|
_logger.LogInformation($"Returning {formattedEdits.Length} final formatted results.");
|
||||||
return formattedEdits;
|
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)
|
public Task<RazorCodeDocument> GetCodeDocumentAsync(IDocumentSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var useDesignTimeGeneratedOutput = snapshot.Project.Configuration.LanguageServerFlags?.ForceRuntimeCodeGeneration ?? false;
|
// Formatting always uses design time
|
||||||
return snapshot.GetGeneratedOutputAsync(useDesignTimeGeneratedOutput);
|
return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||||
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
|
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
|
||||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||||
|
|
||||||
|
@ -84,4 +85,12 @@ internal static class LspInitializationHelpers
|
||||||
|
|
||||||
return options;
|
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.AddSemanticTokensServices(featureOptions);
|
||||||
services.AddDocumentManagementServices(featureOptions);
|
services.AddDocumentManagementServices(featureOptions);
|
||||||
services.AddCompletionServices();
|
services.AddCompletionServices();
|
||||||
services.AddFormattingServices();
|
services.AddFormattingServices(featureOptions);
|
||||||
services.AddCodeActionsServices();
|
services.AddCodeActionsServices();
|
||||||
services.AddOptionsServices(_lspOptions);
|
services.AddOptionsServices(_lspOptions);
|
||||||
services.AddHoverServices();
|
services.AddHoverServices();
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
||||||
|
@ -49,4 +51,10 @@ internal interface IRazorFormattingService
|
||||||
TextEdit[] csharpEdits,
|
TextEdit[] csharpEdits,
|
||||||
RazorFormattingOptions options,
|
RazorFormattingOptions options,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
bool TryGetOnTypeFormattingTriggerKind(
|
||||||
|
RazorCodeDocument codeDocument,
|
||||||
|
int hostDocumentIndex,
|
||||||
|
string triggerCharacter,
|
||||||
|
out RazorLanguageKind triggerCharacterKind);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
// 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.CodeAnalysis.ExternalAccess.Razor;
|
||||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
internal readonly record struct RazorFormattingOptions
|
internal readonly record struct RazorFormattingOptions
|
||||||
{
|
{
|
||||||
public static readonly RazorFormattingOptions Default = new();
|
[DataMember(Order = 0)]
|
||||||
|
|
||||||
public bool InsertSpaces { get; init; } = true;
|
public bool InsertSpaces { get; init; } = true;
|
||||||
|
[DataMember(Order = 1)]
|
||||||
public int TabSize { get; init; } = 4;
|
public int TabSize { get; init; } = 4;
|
||||||
|
[DataMember(Order = 2)]
|
||||||
public bool CodeBlockBraceOnNextLine { get; init; } = false;
|
public bool CodeBlockBraceOnNextLine { get; init; } = false;
|
||||||
|
|
||||||
public RazorFormattingOptions()
|
public RazorFormattingOptions()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
// 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.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -11,6 +13,7 @@ using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
|
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
|
||||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||||
using Microsoft.CodeAnalysis.Razor.Workspaces;
|
using Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||||
using Microsoft.CodeAnalysis.Text;
|
using Microsoft.CodeAnalysis.Text;
|
||||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||||
|
@ -20,7 +23,15 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
||||||
|
|
||||||
internal class RazorFormattingService : IRazorFormattingService
|
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 IFormattingCodeDocumentProvider _codeDocumentProvider;
|
||||||
|
private readonly IDocumentMappingService _documentMappingService;
|
||||||
private readonly IAdhocWorkspaceFactory _workspaceFactory;
|
private readonly IAdhocWorkspaceFactory _workspaceFactory;
|
||||||
|
|
||||||
private readonly ImmutableArray<IFormattingPass> _documentFormattingPasses;
|
private readonly ImmutableArray<IFormattingPass> _documentFormattingPasses;
|
||||||
|
@ -35,6 +46,7 @@ internal class RazorFormattingService : IRazorFormattingService
|
||||||
ILoggerFactory loggerFactory)
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_codeDocumentProvider = codeDocumentProvider;
|
_codeDocumentProvider = codeDocumentProvider;
|
||||||
|
_documentMappingService = documentMappingService;
|
||||||
_workspaceFactory = workspaceFactory;
|
_workspaceFactory = workspaceFactory;
|
||||||
|
|
||||||
_htmlOnTypeFormattingPass = new HtmlOnTypeFormattingPass(loggerFactory);
|
_htmlOnTypeFormattingPass = new HtmlOnTypeFormattingPass(loggerFactory);
|
||||||
|
@ -186,6 +198,18 @@ internal class RazorFormattingService : IRazorFormattingService
|
||||||
return razorEdits.SingleOrDefault();
|
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(
|
private async Task<TextEdit[]> ApplyFormattedEditsAsync(
|
||||||
DocumentContext documentContext,
|
DocumentContext documentContext,
|
||||||
TextEdit[] generatedDocumentEdits,
|
TextEdit[] generatedDocumentEdits,
|
||||||
|
@ -203,7 +227,7 @@ internal class RazorFormattingService : IRazorFormattingService
|
||||||
|
|
||||||
var documentSnapshot = documentContext.Snapshot;
|
var documentSnapshot = documentContext.Snapshot;
|
||||||
var uri = documentContext.Uri;
|
var uri = documentContext.Uri;
|
||||||
var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
|
var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(documentSnapshot).ConfigureAwait(false);
|
||||||
using var context = FormattingContext.CreateForOnTypeFormatting(
|
using var context = FormattingContext.CreateForOnTypeFormatting(
|
||||||
uri,
|
uri,
|
||||||
documentSnapshot,
|
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
|
/// If LF line endings are more prevalent, it removes any CR characters from the text edits
|
||||||
/// to ensure consistency with the LF style.
|
/// to ensure consistency with the LF style.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private TextEdit[] NormalizeLineEndings(SourceText originalText, TextEdit[] edits)
|
private static TextEdit[] NormalizeLineEndings(SourceText originalText, TextEdit[] edits)
|
||||||
{
|
{
|
||||||
if (originalText.HasLFLineEndings())
|
if (originalText.HasLFLineEndings())
|
||||||
{
|
{
|
||||||
|
@ -298,4 +322,10 @@ internal class RazorFormattingService : IRazorFormattingService
|
||||||
|
|
||||||
return edits;
|
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(IRemoteFoldingRangeService), null),
|
||||||
(typeof(IRemoteDocumentHighlightService), null),
|
(typeof(IRemoteDocumentHighlightService), null),
|
||||||
(typeof(IRemoteAutoInsertService), null),
|
(typeof(IRemoteAutoInsertService), null),
|
||||||
|
(typeof(IRemoteFormattingService), null),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Internal for testing
|
// Internal for testing
|
||||||
|
|
|
@ -14,6 +14,7 @@ internal sealed class RemoteFormattingCodeDocumentProvider : IFormattingCodeDocu
|
||||||
{
|
{
|
||||||
public Task<RazorCodeDocument> GetCodeDocumentAsync(IDocumentSnapshot snapshot)
|
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]
|
[Export(typeof(IRazorFormattingService)), Shared]
|
||||||
[method: ImportingConstructor]
|
[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)
|
: 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.
|
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Razor.Language;
|
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.ProjectSystem;
|
||||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||||
using Moq;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
@ -19,49 +14,17 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
|
||||||
|
|
||||||
public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput) : FormattingLanguageServerTestBase(testOutput)
|
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]
|
[Fact]
|
||||||
public async Task Handle_OnTypeFormatting_FormattingDisabled_ReturnsNull()
|
public async Task Handle_OnTypeFormatting_FormattingDisabled_ReturnsNull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var uri = new Uri("file://path/test.razor");
|
var uri = new Uri("file://path/test.razor");
|
||||||
var formattingService = new DummyRazorFormattingService();
|
var formattingService = new DummyRazorFormattingService();
|
||||||
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
|
|
||||||
|
|
||||||
var optionsMonitor = GetOptionsMonitor(enableFormatting: false);
|
var optionsMonitor = GetOptionsMonitor(enableFormatting: false);
|
||||||
var htmlFormatter = new TestHtmlFormatter();
|
var htmlFormatter = new TestHtmlFormatter();
|
||||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||||
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
|
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||||
var @params = new DocumentOnTypeFormattingParams { TextDocument = new TextDocumentIdentifier { Uri = uri, } };
|
var @params = new DocumentOnTypeFormattingParams { TextDocument = new TextDocumentIdentifier { Uri = uri, } };
|
||||||
var requestContext = CreateRazorRequestContext(documentContext: null);
|
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 documentContext = CreateDocumentContext(new Uri("file://path/testDifferentFile.razor"), codeDocument);
|
||||||
var formattingService = new DummyRazorFormattingService();
|
var formattingService = new DummyRazorFormattingService();
|
||||||
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
|
|
||||||
|
|
||||||
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
||||||
var htmlFormatter = new TestHtmlFormatter();
|
var htmlFormatter = new TestHtmlFormatter();
|
||||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||||
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
|
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||||
var @params = new DocumentOnTypeFormattingParams()
|
var @params = new DocumentOnTypeFormattingParams()
|
||||||
{
|
{
|
||||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||||
|
@ -120,12 +82,11 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
||||||
|
|
||||||
var documentContext = CreateDocumentContext(uri, codeDocument);
|
var documentContext = CreateDocumentContext(uri, codeDocument);
|
||||||
var formattingService = new DummyRazorFormattingService();
|
var formattingService = new DummyRazorFormattingService();
|
||||||
var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory);
|
|
||||||
|
|
||||||
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
||||||
var htmlFormatter = new TestHtmlFormatter();
|
var htmlFormatter = new TestHtmlFormatter();
|
||||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||||
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
|
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||||
var @params = new DocumentOnTypeFormattingParams()
|
var @params = new DocumentOnTypeFormattingParams()
|
||||||
{
|
{
|
||||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||||
|
@ -154,14 +115,12 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
||||||
var uri = new Uri("file://path/test.razor");
|
var uri = new Uri("file://path/test.razor");
|
||||||
|
|
||||||
var documentContext = CreateDocumentContext(uri, codeDocument);
|
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 optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
||||||
var htmlFormatter = new TestHtmlFormatter();
|
var htmlFormatter = new TestHtmlFormatter();
|
||||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||||
formattingService, htmlFormatter, documentMappingService.Object, optionsMonitor, LoggerFactory);
|
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||||
var @params = new DocumentOnTypeFormattingParams()
|
var @params = new DocumentOnTypeFormattingParams()
|
||||||
{
|
{
|
||||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||||
|
@ -190,14 +149,12 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
||||||
var uri = new Uri("file://path/test.razor");
|
var uri = new Uri("file://path/test.razor");
|
||||||
|
|
||||||
var documentContext = CreateDocumentContext(uri, codeDocument);
|
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 optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
||||||
var htmlFormatter = new TestHtmlFormatter();
|
var htmlFormatter = new TestHtmlFormatter();
|
||||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||||
formattingService, htmlFormatter, documentMappingService.Object, optionsMonitor, LoggerFactory);
|
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||||
var @params = new DocumentOnTypeFormattingParams()
|
var @params = new DocumentOnTypeFormattingParams()
|
||||||
{
|
{
|
||||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||||
|
@ -227,12 +184,11 @@ public class DocumentOnTypeFormattingEndpointTest(ITestOutputHelper testOutput)
|
||||||
|
|
||||||
var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument);
|
var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument);
|
||||||
var formattingService = new DummyRazorFormattingService();
|
var formattingService = new DummyRazorFormattingService();
|
||||||
var documentMappingService = new LspDocumentMappingService(FilePathService, documentContextFactory, LoggerFactory);
|
|
||||||
|
|
||||||
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
var optionsMonitor = GetOptionsMonitor(enableFormatting: true);
|
||||||
var htmlFormatter = new TestHtmlFormatter();
|
var htmlFormatter = new TestHtmlFormatter();
|
||||||
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
var endpoint = new DocumentOnTypeFormattingEndpoint(
|
||||||
formattingService, htmlFormatter, documentMappingService, optionsMonitor, LoggerFactory);
|
formattingService, htmlFormatter, optionsMonitor, LoggerFactory);
|
||||||
var @params = new DocumentOnTypeFormattingParams()
|
var @params = new DocumentOnTypeFormattingParams()
|
||||||
{
|
{
|
||||||
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
TextDocument = new TextDocumentIdentifier { Uri = uri, },
|
||||||
|
|
|
@ -3,25 +3,17 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Razor.Language;
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
|
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.AspNetCore.Razor.Utilities;
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.Razor;
|
using Microsoft.CodeAnalysis.Razor;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||||
using Microsoft.CodeAnalysis.Razor.Protocol;
|
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||||
using Microsoft.CodeAnalysis.Razor.Protocol.Formatting;
|
using Microsoft.CodeAnalysis.Razor.Protocol.Formatting;
|
||||||
using Microsoft.CodeAnalysis.Text;
|
|
||||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||||
using Microsoft.VisualStudio.Text;
|
|
||||||
using Microsoft.VisualStudio.Utilities;
|
|
||||||
using Microsoft.WebTools.Languages.Shared.ContentTypes;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
|
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
|
||||||
|
@ -40,52 +32,28 @@ internal class FormattingLanguageServerClient(ILoggerFactory loggerFactory) : IC
|
||||||
_documents.Add("/" + path, codeDocument);
|
_documents.Add("/" + path, codeDocument);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<RazorDocumentFormattingResponse> FormatAsync(DocumentOnTypeFormattingParams @params)
|
private async Task<RazorDocumentFormattingResponse> FormatAsync(DocumentOnTypeFormattingParams @params)
|
||||||
{
|
{
|
||||||
var generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri);
|
var generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri);
|
||||||
var generatedHtmlSource = SourceText.From(generatedHtml, Encoding.UTF8);
|
|
||||||
var absoluteIndex = generatedHtmlSource.GetRequiredAbsoluteIndex(@params.Position);
|
|
||||||
|
|
||||||
var request = $$"""
|
var edits = await HtmlFormatting.GetOnTypeFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Position, @params.Options.InsertSpaces, @params.Options.TabSize);
|
||||||
{
|
|
||||||
"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}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
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 generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri);
|
||||||
|
|
||||||
var request = $$"""
|
var edits = await HtmlFormatting.GetDocumentFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Options.InsertSpaces, @params.Options.TabSize);
|
||||||
{
|
|
||||||
"Options":
|
|
||||||
{
|
|
||||||
"UseSpaces": {{(@params.Options.InsertSpaces ? "true" : "false")}},
|
|
||||||
"TabSize": {{@params.Options.TabSize}},
|
|
||||||
"IndentSize": {{@params.Options.TabSize}}
|
|
||||||
},
|
|
||||||
"Uri": "{{@params.TextDocument.Uri}}",
|
|
||||||
"GeneratedChanges": [],
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
return CallWebToolsApplyFormattedEditsHandlerAsync(request, @params.TextDocument.Uri, generatedHtml);
|
return new()
|
||||||
|
{
|
||||||
|
Edits = edits
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetGeneratedHtml(Uri uri)
|
private string GetGeneratedHtml(Uri uri)
|
||||||
|
@ -95,51 +63,6 @@ internal class FormattingLanguageServerClient(ILoggerFactory loggerFactory) : IC
|
||||||
return generatedHtml.Replace("\r", "").Replace("\n", "\r\n");
|
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)
|
public async Task<TResponse> SendRequestAsync<TParams, TResponse>(string method, TParams @params, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (@params is DocumentFormattingParams formattingParams &&
|
if (@params is DocumentFormattingParams formattingParams &&
|
||||||
|
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
|
||||||
using Microsoft.AspNetCore.Razor.Threading;
|
using Microsoft.AspNetCore.Razor.Threading;
|
||||||
using Microsoft.CodeAnalysis.Razor.Formatting;
|
using Microsoft.CodeAnalysis.Razor.Formatting;
|
||||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||||
|
using Microsoft.CodeAnalysis.Razor.Protocol;
|
||||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
||||||
|
@ -31,7 +32,7 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut
|
||||||
return codeDocument;
|
return codeDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DummyRazorFormattingService : IRazorFormattingService
|
internal class DummyRazorFormattingService(RazorLanguageKind? languageKind = null) : IRazorFormattingService
|
||||||
{
|
{
|
||||||
public bool Called { get; private set; }
|
public bool Called { get; private set; }
|
||||||
|
|
||||||
|
@ -65,5 +66,11 @@ public abstract class FormattingLanguageServerTestBase(ITestOutputHelper testOut
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
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());
|
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" />
|
<Compile Include="..\OSSkipConditionFactAttribute.cs" LinkBase="Shared" />
|
||||||
</ItemGroup>
|
</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>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Razor.LanguageServer\Microsoft.AspNetCore.Razor.LanguageServer.csproj" />
|
<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" />
|
<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.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Razor;
|
||||||
using Microsoft.AspNetCore.Razor.PooledObjects;
|
using Microsoft.AspNetCore.Razor.PooledObjects;
|
||||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||||
using Microsoft.VisualStudio.Settings.Internal;
|
using Microsoft.VisualStudio.Settings.Internal;
|
||||||
|
@ -16,7 +17,7 @@ using Microsoft.WebTools.Languages.Shared.Editor.Composition;
|
||||||
using Microsoft.WebTools.Languages.Shared.Editor.Text;
|
using Microsoft.WebTools.Languages.Shared.Editor.Text;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
|
namespace Microsoft.CodeAnalysis.Razor.Formatting;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides reflection-based access to the Web Tools LSP infrastructure needed for tests.
|
/// 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" />
|
<ProjectReference Include="..\..\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj" />
|
||||||
</ItemGroup>
|
</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>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" />
|
<PackageReference Include="Newtonsoft.Json" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing" />
|
<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());
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче