File: LanguageClient\Cohost\HtmlRequestInvoker.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.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Cohost;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
using Microsoft.CodeAnalysis.Razor.Telemetry;
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
 
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
 
[Export(typeof(IHtmlRequestInvoker))]
[method: ImportingConstructor]
internal sealed class HtmlRequestInvoker(
    LSPRequestInvoker requestInvoker,
    LSPDocumentManager documentManager,
    IHtmlDocumentSynchronizer htmlDocumentSynchronizer,
    ITelemetryReporter telemetryReporter,
    ILoggerFactory loggerFactory) : IHtmlRequestInvoker
{
    private readonly LSPRequestInvoker _requestInvoker = requestInvoker;
    private readonly TrackingLSPDocumentManager _documentManager = documentManager as TrackingLSPDocumentManager ?? throw new InvalidOperationException("Expected TrackingLSPDocumentManager");
    private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer;
    private readonly ITelemetryReporter _telemetryReporter = telemetryReporter;
    private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<HtmlRequestInvoker>();
 
    public async Task<TResponse?> MakeHtmlLspRequestAsync<TRequest, TResponse>(TextDocument razorDocument, string method, TRequest request, TimeSpan threshold, Guid correlationId, CancellationToken cancellationToken) where TRequest : notnull
    {
        var syncResult = await _htmlDocumentSynchronizer.TrySynchronizeAsync(razorDocument, cancellationToken).ConfigureAwait(false);
        if (!syncResult.Synchronized)
        {
            _logger.LogDebug($"Couldn't synchronize for {razorDocument.FilePath}");
            return default;
        }
 
        if (!_documentManager.TryGetDocument(razorDocument.CreateUri(), out var snapshot))
        {
            _logger.LogError($"Couldn't find document in LSPDocumentManager for {razorDocument.FilePath}");
            return default;
        }
 
        if (!snapshot.TryGetVirtualDocument<HtmlVirtualDocumentSnapshot>(out var htmlDocument))
        {
            _logger.LogError($"Couldn't find virtual document snapshot for {snapshot.Uri}");
            return default;
        }
 
        var existingChecksum = (ChecksumWrapper)htmlDocument.State.AssumeNotNull();
        if (!existingChecksum.Equals(syncResult.Checksum))
        {
            _logger.LogError($"Checksum for {snapshot.Uri}, {htmlDocument.State} doesn't match {syncResult.Checksum}.");
            return default;
        }
 
        // If the request is for a text document, we need to update the Uri to point to the Html document,
        // and most importantly set it back again before leaving the method in case a caller uses it.
        UpdateTextDocumentUri(request, new(htmlDocument.Uri), out var originalUri);
 
        try
        {
            _logger.LogDebug($"Making LSP request for {method} from {htmlDocument.Uri}{(request is ITextDocumentPositionParams positionParams ? $" at {positionParams.Position}" : "")}, checksum {syncResult.Checksum}.");
 
            // Passing Guid.Empty to this method will mean no tracking
            using var _ = _telemetryReporter.TrackLspRequest(method, RazorLSPConstants.HtmlLanguageServerName, threshold, correlationId);
 
            var result = await _requestInvoker.ReinvokeRequestOnServerAsync<TRequest, TResponse?>(
                htmlDocument.Snapshot.TextBuffer,
                method,
                RazorLSPConstants.HtmlLanguageServerName,
                request,
                cancellationToken).ConfigureAwait(false);
 
            if (result is null)
            {
                return default;
            }
 
            return result.Response;
        }
        finally
        {
            if (originalUri is not null)
            {
                UpdateTextDocumentUri(request, originalUri, out _);
            }
        }
    }
 
    private void UpdateTextDocumentUri<TRequest>(TRequest request, DocumentUri uri, out DocumentUri? originalUri) where TRequest : notnull
    {
        var textDocument = request switch
        {
            ITextDocumentParams textDocumentParams => textDocumentParams.TextDocument,
            // VSInternalDiagnosticParams doesn't implement the interface because the TextDocument property is nullable
            VSInternalDiagnosticParams vsInternalDiagnosticParams => vsInternalDiagnosticParams.TextDocument,
            VSCodeActionParams vsCodeActionParams => vsCodeActionParams.TextDocument,
            _ => null
        };
 
        originalUri = textDocument?.DocumentUri;
        textDocument?.DocumentUri = uri;
    }
}