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:
David Wengier 2024-09-05 14:28:23 +10:00 коммит произвёл GitHub
Родитель fbf8c8ef4d b7cd05e453
Коммит 9b339ba27c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
30 изменённых файлов: 1383 добавлений и 221 удалений

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

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