Skip to content

Commit

Permalink
Cohost formatting (#10822)
Browse files Browse the repository at this point in the history
Fixes #10743
Part of #9519

Brings formatting to cohosting. Relatively simple because of previous
PRs. Have left sharing full test coverage of the formatting engine for
later
  • Loading branch information
davidwengier authored Sep 5, 2024
2 parents fbf8c8e + b7cd05e commit 9b339ba
Show file tree
Hide file tree
Showing 30 changed files with 1,386 additions and 224 deletions.
1 change: 1 addition & 0 deletions eng/targets/Services.props
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToDefinition" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToDefinitionService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Rename" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteRenameService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.AutoInsert" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteAutoInsertService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Formatting" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFormattingService+Factory" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public async Task RazorCSharpFormattingAsync()
{
var documentContext = new DocumentContext(DocumentUri, DocumentSnapshot, projectContext: null);

var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits: [], range: null, RazorFormattingOptions.Default, CancellationToken.None);
var edits = await RazorFormattingService.GetDocumentFormattingEditsAsync(documentContext, htmlEdits: [], range: null, new RazorFormattingOptions(), CancellationToken.None);

#if DEBUG
// For debugging purposes only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public async override Task<CodeAction> ResolveAsync(
var formattedEdit = await _razorFormattingService.GetCSharpCodeActionEditAsync(
documentContext,
csharpTextEdits,
RazorFormattingOptions.Default,
new RazorFormattingOptions(),
cancellationToken).ConfigureAwait(false);

cancellationToken.ThrowIfCancellationRequested();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,19 @@ public static void AddLifeCycleServices(this IServiceCollection services, RazorL
services.AddSingleton<IOnInitialized>(clientConnection);
}

public static void AddFormattingServices(this IServiceCollection services)
public static void AddFormattingServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions)
{
// Formatting
services.AddSingleton<IHtmlFormatter, HtmlFormatter>();
services.AddSingleton<IRazorFormattingService, RazorFormattingService>();

services.AddHandlerWithCapabilities<DocumentFormattingEndpoint>();
services.AddHandlerWithCapabilities<DocumentOnTypeFormattingEndpoint>();
services.AddHandlerWithCapabilities<DocumentRangeFormattingEndpoint>();
if (!featureOptions.UseRazorCohostServer)
{
services.AddSingleton<IHtmlFormatter, HtmlFormatter>();

services.AddHandlerWithCapabilities<DocumentFormattingEndpoint>();
services.AddHandlerWithCapabilities<DocumentOnTypeFormattingEndpoint>();
services.AddHandlerWithCapabilities<DocumentRangeFormattingEndpoint>();
}
}

public static void AddCompletionServices(this IServiceCollection services)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
Expand All @@ -25,32 +26,20 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting;
internal class DocumentOnTypeFormattingEndpoint(
IRazorFormattingService razorFormattingService,
IHtmlFormatter htmlFormatter,
IDocumentMappingService documentMappingService,
RazorLSPOptionsMonitor optionsMonitor,
ILoggerFactory loggerFactory)
: IRazorRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]?>, ICapabilitiesProvider
{
private readonly IRazorFormattingService _razorFormattingService = razorFormattingService;
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor;
private readonly IHtmlFormatter _htmlFormatter = htmlFormatter;
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<DocumentOnTypeFormattingEndpoint>();

private static readonly ImmutableArray<string> s_allTriggerCharacters = ["}", ";", "\n", "{"];

private static readonly FrozenSet<string> s_csharpTriggerCharacterSet = FrozenSet.ToFrozenSet(["}", ";"], StringComparer.Ordinal);
private static readonly FrozenSet<string> s_htmlTriggerCharacterSet = FrozenSet.ToFrozenSet(["\n", "{", "}", ";"], StringComparer.Ordinal);
private static readonly FrozenSet<string> s_allTriggerCharacterSet = s_allTriggerCharacters.ToFrozenSet(StringComparer.Ordinal);

public bool MutatesSolutionState => false;

public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
{
serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions
{
FirstTriggerCharacter = s_allTriggerCharacters[0],
MoreTriggerCharacter = s_allTriggerCharacters.AsSpan()[1..].ToArray(),
};
serverCapabilities.DocumentOnTypeFormattingProvider = new DocumentOnTypeFormattingOptions().EnableOnTypeFormattingTriggerCharacters();
}

public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormattingParams request)
Expand All @@ -74,7 +63,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormatting
return null;
}

if (!s_allTriggerCharacterSet.Contains(request.Character))
if (!RazorFormattingService.AllTriggerCharacterSet.Contains(request.Character))
{
_logger.LogWarning($"Unexpected trigger character '{request.Character}'.");
return null;
Expand Down Expand Up @@ -102,24 +91,13 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormatting
return null;
}

var triggerCharacterKind = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex, rightAssociative: false);
if (triggerCharacterKind is not (RazorLanguageKind.CSharp or RazorLanguageKind.Html))
{
_logger.LogInformation($"Unsupported trigger character language {triggerCharacterKind:G}.");
return null;
}

if (!IsApplicableTriggerCharacter(request.Character, triggerCharacterKind))
if (_razorFormattingService.TryGetOnTypeFormattingTriggerKind(codeDocument, hostDocumentIndex, request.Character, out var triggerCharacterKind))
{
// We were triggered but the trigger character doesn't make sense for the current cursor position. Bail.
_logger.LogInformation($"Unsupported trigger character location.");
return null;
}

cancellationToken.ThrowIfCancellationRequested();

Debug.Assert(request.Character.Length > 0);

var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine);

TextEdit[] formattedEdits;
Expand Down Expand Up @@ -147,26 +125,4 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormatting
_logger.LogInformation($"Returning {formattedEdits.Length} final formatted results.");
return formattedEdits;
}

private static bool IsApplicableTriggerCharacter(string triggerCharacter, RazorLanguageKind languageKind)
{
if (languageKind == RazorLanguageKind.CSharp)
{
return s_csharpTriggerCharacterSet.Contains(triggerCharacter);
}
else if (languageKind == RazorLanguageKind.Html)
{
return s_htmlTriggerCharacterSet.Contains(triggerCharacter);
}

// Unknown trigger character.
return false;
}

internal static class TestAccessor
{
public static ImmutableArray<string> GetAllTriggerCharacters() => s_allTriggerCharacters;
public static FrozenSet<string> GetCSharpTriggerCharacterSet() => s_csharpTriggerCharacterSet;
public static FrozenSet<string> GetHtmlTriggerCharacterSet() => s_htmlTriggerCharacterSet;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal sealed class LspFormattingCodeDocumentProvider : IFormattingCodeDocumen
{
public Task<RazorCodeDocument> GetCodeDocumentAsync(IDocumentSnapshot snapshot)
{
var useDesignTimeGeneratedOutput = snapshot.Project.Configuration.LanguageServerFlags?.ForceRuntimeCodeGeneration ?? false;
return snapshot.GetGeneratedOutputAsync(useDesignTimeGeneratedOutput);
// Formatting always uses design time
return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
using Microsoft.VisualStudio.LanguageServer.Protocol;

Expand Down Expand Up @@ -84,4 +85,12 @@ public static VSInternalDocumentOnAutoInsertOptions EnableOnAutoInsert(

return options;
}

public static DocumentOnTypeFormattingOptions EnableOnTypeFormattingTriggerCharacters(this DocumentOnTypeFormattingOptions options)
{
options.FirstTriggerCharacter = RazorFormattingService.FirstTriggerCharacter;
options.MoreTriggerCharacter = RazorFormattingService.MoreTriggerCharacters.ToArray();

return options;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ protected override ILspServices ConstructLspServices()
services.AddSemanticTokensServices(featureOptions);
services.AddDocumentManagementServices(featureOptions);
services.AddCompletionServices();
services.AddFormattingServices();
services.AddFormattingServices(featureOptions);
services.AddCodeActionsServices();
services.AddOptionsServices(_lspOptions);
services.AddHoverServices();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.Formatting;
Expand Down Expand Up @@ -49,4 +51,10 @@ Task<TextEdit[]> GetCSharpOnTypeFormattingEditsAsync(
TextEdit[] csharpEdits,
RazorFormattingOptions options,
CancellationToken cancellationToken);

bool TryGetOnTypeFormattingTriggerKind(
RazorCodeDocument codeDocument,
int hostDocumentIndex,
string triggerCharacter,
out RazorLanguageKind triggerCharacterKind);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Runtime.Serialization;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.Formatting;

[DataContract]
internal readonly record struct RazorFormattingOptions
{
public static readonly RazorFormattingOptions Default = new();

[DataMember(Order = 0)]
public bool InsertSpaces { get; init; } = true;
[DataMember(Order = 1)]
public int TabSize { get; init; } = 4;
[DataMember(Order = 2)]
public bool CodeBlockBraceOnNextLine { get; init; } = false;

public RazorFormattingOptions()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
Expand All @@ -11,6 +13,7 @@
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
Expand All @@ -20,7 +23,15 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting;

internal class RazorFormattingService : IRazorFormattingService
{
public static readonly string FirstTriggerCharacter = "}";
public static readonly ImmutableArray<string> MoreTriggerCharacters = [";", "\n", "{"];
public static readonly FrozenSet<string> AllTriggerCharacterSet = FrozenSet.ToFrozenSet([FirstTriggerCharacter, .. MoreTriggerCharacters], StringComparer.Ordinal);

private static readonly FrozenSet<string> s_csharpTriggerCharacterSet = FrozenSet.ToFrozenSet(["}", ";"], StringComparer.Ordinal);
private static readonly FrozenSet<string> s_htmlTriggerCharacterSet = FrozenSet.ToFrozenSet(["\n", "{", "}", ";"], StringComparer.Ordinal);

private readonly IFormattingCodeDocumentProvider _codeDocumentProvider;
private readonly IDocumentMappingService _documentMappingService;
private readonly IAdhocWorkspaceFactory _workspaceFactory;

private readonly ImmutableArray<IFormattingPass> _documentFormattingPasses;
Expand All @@ -35,6 +46,7 @@ public RazorFormattingService(
ILoggerFactory loggerFactory)
{
_codeDocumentProvider = codeDocumentProvider;
_documentMappingService = documentMappingService;
_workspaceFactory = workspaceFactory;

_htmlOnTypeFormattingPass = new HtmlOnTypeFormattingPass(loggerFactory);
Expand Down Expand Up @@ -186,6 +198,18 @@ public Task<TextEdit[]> GetHtmlOnTypeFormattingEditsAsync(DocumentContext docume
return razorEdits.SingleOrDefault();
}

public bool TryGetOnTypeFormattingTriggerKind(RazorCodeDocument codeDocument, int hostDocumentIndex, string triggerCharacter, out RazorLanguageKind triggerCharacterKind)
{
triggerCharacterKind = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex, rightAssociative: false);

return triggerCharacterKind switch
{
RazorLanguageKind.CSharp => s_csharpTriggerCharacterSet.Contains(triggerCharacter),
RazorLanguageKind.Html => s_htmlTriggerCharacterSet.Contains(triggerCharacter),
_ => false,
};
}

private async Task<TextEdit[]> ApplyFormattedEditsAsync(
DocumentContext documentContext,
TextEdit[] generatedDocumentEdits,
Expand All @@ -203,7 +227,7 @@ private async Task<TextEdit[]> ApplyFormattedEditsAsync(

var documentSnapshot = documentContext.Snapshot;
var uri = documentContext.Uri;
var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(documentSnapshot).ConfigureAwait(false);
using var context = FormattingContext.CreateForOnTypeFormatting(
uri,
documentSnapshot,
Expand Down Expand Up @@ -286,7 +310,7 @@ private static void UnwrapCSharpSnippets(TextEdit[] razorEdits)
/// If LF line endings are more prevalent, it removes any CR characters from the text edits
/// to ensure consistency with the LF style.
/// </summary>
private TextEdit[] NormalizeLineEndings(SourceText originalText, TextEdit[] edits)
private static TextEdit[] NormalizeLineEndings(SourceText originalText, TextEdit[] edits)
{
if (originalText.HasLFLineEndings())
{
Expand All @@ -298,4 +322,10 @@ private TextEdit[] NormalizeLineEndings(SourceText originalText, TextEdit[] edit

return edits;
}

internal static class TestAccessor
{
public static FrozenSet<string> GetCSharpTriggerCharacterSet() => s_csharpTriggerCharacterSet;
public static FrozenSet<string> GetHtmlTriggerCharacterSet() => s_htmlTriggerCharacterSet;
}
}
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal static class RazorServices
(typeof(IRemoteFoldingRangeService), null),
(typeof(IRemoteDocumentHighlightService), null),
(typeof(IRemoteAutoInsertService), null),
(typeof(IRemoteFormattingService), null),
];

// Internal for testing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal sealed class RemoteFormattingCodeDocumentProvider : IFormattingCodeDocu
{
public Task<RazorCodeDocument> GetCodeDocumentAsync(IDocumentSnapshot snapshot)
{
return snapshot.GetGeneratedOutputAsync();
// Formatting always uses design time
return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true);
}
}
Loading

0 comments on commit 9b339ba

Please sign in to comment.