File: LanguageClient\Cohost\CohostWrapWithTagEndpoint.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.VisualStudio.LanguageServices.Razor\Microsoft.VisualStudio.LanguageServices.Razor.csproj (Microsoft.VisualStudio.LanguageServices.Razor)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
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;
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
using Microsoft.CodeAnalysis.Razor.Cohost;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
using Microsoft.VisualStudio.Razor.LanguageClient.WrapWithTag;
 
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
 
#pragma warning disable RS0030 // Do not use banned APIs
[Shared]
[CohostEndpoint(LanguageServerConstants.RazorWrapWithTagEndpoint)]
[ExportCohostStatelessLspService(typeof(CohostWrapWithTagEndpoint))]
[method: ImportingConstructor]
#pragma warning restore RS0030 // Do not use banned APIs
internal sealed class CohostWrapWithTagEndpoint(
    IRemoteServiceInvoker remoteServiceInvoker,
    IHtmlRequestInvoker requestInvoker,
    LSPDocumentManager documentManager,
    IIncompatibleProjectService incompatibleProjectService,
    ILoggerFactory loggerFactory)
    : AbstractCohostDocumentEndpoint<VSInternalWrapWithTagParams, VSInternalWrapWithTagResponse?>(incompatibleProjectService)
{
    private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker;
    private readonly IHtmlRequestInvoker _requestInvoker = requestInvoker;
    private readonly LSPDocumentManager _documentManager = documentManager;
    private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostWrapWithTagEndpoint>();
 
    protected override bool MutatesSolutionState => false;
 
    protected override bool RequiresLSPSolution => true;
 
    protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSInternalWrapWithTagParams request)
        => request.TextDocument.ToRazorTextDocumentIdentifier();
 
    protected override async Task<VSInternalWrapWithTagResponse?> HandleRequestAsync(VSInternalWrapWithTagParams request, TextDocument razorDocument, CancellationToken cancellationToken)
    {
        // First, check if the position is valid for wrap with tag operation through the remote service
        var range = request.Range.ToLinePositionSpan();
        var result = await _remoteServiceInvoker.TryInvokeAsync<IRemoteWrapWithTagService, RemoteResponse<LinePositionSpan>>(
            razorDocument.Project.Solution,
            (service, solutionInfo, cancellationToken) => service.GetValidWrappingRangeAsync(solutionInfo, razorDocument.Id, range, cancellationToken),
            cancellationToken).ConfigureAwait(false);
 
        // If the remote service says it's not a valid location or we should stop handling, return null
        if (result.StopHandling)
        {
            return null;
        }
 
        request.Range = result.Result.ToRange();
 
        // The location is valid, so delegate to the HTML server
        var htmlResponse = await _requestInvoker.MakeHtmlLspRequestAsync<VSInternalWrapWithTagParams, VSInternalWrapWithTagResponse>(
            razorDocument,
            LanguageServerConstants.RazorWrapWithTagEndpoint,
            request,
            cancellationToken).ConfigureAwait(false);
 
        // If the Html response has ~s in it, then we need to clean them up.
        if (htmlResponse?.TextEdits is { } edits &&
            edits.Any(static e => e.NewText.Contains('~')))
        {
            // To do this we don't actually need to go to OOP, we just need a SourceText with the Html document,
            // and we already have that in a virtual buffer, because it's what the above request was made against.
            // So we can write a little bit of code to grab that, and avoid the extra OOP call.
 
            if (!_documentManager.TryGetDocument(razorDocument.CreateUri(), out var snapshot))
            {
                _logger.LogError($"Couldn't find document in LSPDocumentManager for {razorDocument.FilePath}");
                return null;
            }
 
            if (!snapshot.TryGetVirtualDocument<HtmlVirtualDocumentSnapshot>(out var htmlDocument))
            {
                _logger.LogError($"Couldn't find virtual document snapshot for {snapshot.Uri}");
                return null;
            }
 
            var htmlSourceText = htmlDocument.Snapshot.AsText();
            htmlResponse.TextEdits = FormattingUtilities.FixHtmlTextEdits(htmlSourceText, edits);
        }
 
        return htmlResponse;
    }
 
    internal TestAccessor GetTestAccessor() => new(this);
 
    internal readonly struct TestAccessor(CohostWrapWithTagEndpoint instance)
    {
        public Task<VSInternalWrapWithTagResponse?> HandleRequestAsync(VSInternalWrapWithTagParams request, TextDocument razorDocument, CancellationToken cancellationToken)
            => instance.HandleRequestAsync(request, razorDocument, cancellationToken);
    }
}