File: Hover\RemoteHoverService.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Remote.Razor\Microsoft.CodeAnalysis.Remote.Razor.csproj (Microsoft.CodeAnalysis.Remote.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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Hover;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse<Roslyn.LanguageServer.Protocol.Hover?>;
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
 
namespace Microsoft.CodeAnalysis.Remote.Razor;
 
internal sealed class RemoteHoverService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteHoverService
{
    internal sealed class Factory : FactoryBase<IRemoteHoverService>
    {
        protected override IRemoteHoverService CreateService(in ServiceArgs args)
            => new RemoteHoverService(in args);
    }
 
    private readonly IClientCapabilitiesService _clientCapabilitiesService = args.ExportProvider.GetExportedValue<IClientCapabilitiesService>();
 
    protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance;
 
    public ValueTask<RemoteResponse<Hover?>> GetHoverAsync(
        JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
        JsonSerializableDocumentId documentId,
        Position position,
        CancellationToken cancellationToken)
        => RunServiceAsync(
            solutionInfo,
            documentId,
            context => GetHoverAsync(context, position, cancellationToken),
            cancellationToken);
 
    private async ValueTask<RemoteResponse<Hover?>> GetHoverAsync(
        RemoteDocumentContext context,
        Position position,
        CancellationToken cancellationToken)
    {
        var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
 
        var sourceText = codeDocument.Source.Text;
        if (!sourceText.TryGetAbsoluteIndex(position, out var hostDocumentIndex))
        {
            return NoFurtherHandling;
        }
 
        var originalHostDocumentIndex = hostDocumentIndex;
 
        // Adjust position if on a component end tag to use the start tag position
        hostDocumentIndex = codeDocument.AdjustPositionForComponentEndTag(hostDocumentIndex);
 
        var clientCapabilities = _clientCapabilitiesService.ClientCapabilities;
        var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true);
 
        if (positionInfo.LanguageKind == RazorLanguageKind.CSharp)
        {
            var generatedDocument = await context.Snapshot
                .GetGeneratedDocumentAsync(cancellationToken)
                .ConfigureAwait(false);
 
            var csharpHover = await ExternalHandlers.Hover
                .GetHoverAsync(
                    generatedDocument,
                    positionInfo.Position.ToLinePosition(),
                    clientCapabilities.SupportsVisualStudioExtensions(),
                    clientCapabilities.SupportsMarkdown(),
                    cancellationToken)
                .ConfigureAwait(false);
 
            // Roslyn couldn't provide a hover, so we're done.
            if (csharpHover is null)
            {
                return NoFurtherHandling;
            }
 
            // Map the hover range back to the host document
            if (csharpHover.Range is { } range &&
                DocumentMappingService.TryMapToRazorDocumentRange(codeDocument.GetRequiredCSharpDocument(), range.ToLinePositionSpan(), out var hostDocumentSpan))
            {
                csharpHover.Range = LspFactory.CreateRange(hostDocumentSpan);
            }
 
            // If we adjusted from an end tag to a start tag, we need to make sure the range covers the end tag,
            // not just the start tag, or VS won't show the hover info
            if (originalHostDocumentIndex > hostDocumentIndex &&
                csharpHover.Range is not null)
            {
                // We were originally on the end tag somewhere, then redirected to the start tag to get the hover.
                // We now need to translate the range we got back, and mapped, over to the end tag again. This is as
                // easy as just offsetting the range by the difference between our original and adjusted index.
                if (sourceText.TryGetAbsoluteIndex(csharpHover.Range.Start, out var adjustedStart) &&
                    sourceText.TryGetAbsoluteIndex(csharpHover.Range.End, out var adjustedEnd))
                {
                    var offset = originalHostDocumentIndex - hostDocumentIndex;
 
                    // Make sure we don't fall off the end of the document, though it should be impossible
                    adjustedStart = Math.Min(adjustedStart + offset, sourceText.Length - 1);
                    adjustedEnd = Math.Min(adjustedEnd + offset, sourceText.Length - 1);
 
                    csharpHover.Range = sourceText.GetRange(adjustedStart, adjustedEnd);
                }
            }
 
            // As there is a C# hover, stop further handling.
            return new RemoteResponse<Hover?>(StopHandling: true, Result: csharpHover);
        }
 
        if (positionInfo.LanguageKind is not (RazorLanguageKind.Html or RazorLanguageKind.Razor))
        {
            Debug.Fail($"Encountered an unexpected {nameof(RazorLanguageKind)}: {positionInfo.LanguageKind}");
            return NoFurtherHandling;
        }
 
        // If this is Html or Razor, try to retrieve a hover from Razor.
        var options = HoverDisplayOptions.From(clientCapabilities);
 
        // In co-hosting, there isn't a singleton IComponentAvailabilityService in the MEF composition.
        // So, we construct one using the current solution snapshot.
        // All of this will change when solution snapshots are available in the core Razor project model.
 
        // TODO: Remove this when solution snapshots are available in the core Razor project model.
        var componentAvailabilityService = new ComponentAvailabilityService(context.Snapshot.ProjectSnapshot.SolutionSnapshot);
 
        var razorHover = await HoverFactory
            .GetHoverAsync(codeDocument, hostDocumentIndex, options, componentAvailabilityService, cancellationToken)
            .ConfigureAwait(false);
 
        // Roslyn couldn't provide a hover, so we're done.
        if (razorHover is null)
        {
            return CallHtml;
        }
 
        // Ensure that we convert our Hover to a Roslyn Hover.
        var resultHover = ConvertHover(razorHover);
 
        return Results(resultHover);
    }
 
    /// <summary>
    ///  Converts a <see cref="Hover"/> to a <see cref="Hover"/>.
    /// </summary>
    /// <remarks>
    ///  Once Razor moves wholly over to Roslyn.LanguageServer.Protocol, this method can be removed.
    /// </remarks>
    private static Hover ConvertHover(Hover hover)
    {
        // Note: Razor only ever produces a Hover with MarkupContent or a VSInternalHover with RawContents.
        // Both variants return a Range.
 
        return hover switch
        {
            VSInternalHover { Range: var range, RawContent: { } rawContent } => new VSInternalHover()
            {
                Range = range,
                Contents = string.Empty,
                RawContent = rawContent
            },
            Hover { Range: var range, Contents.Fourth: MarkupContent contents } => new Hover()
            {
                Range = range,
                Contents = contents
            },
            _ => Assumed.Unreachable<Hover>(),
        };
    }
}