File: InlayHints\RemoteInlayHintService.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.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
using Microsoft.CodeAnalysis.Razor.Protocol.InlayHints;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Remote.Razor;
 
internal sealed class RemoteInlayHintService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteInlayHintService
{
    internal sealed class Factory : FactoryBase<IRemoteInlayHintService>
    {
        protected override IRemoteInlayHintService CreateService(in ServiceArgs args)
            => new RemoteInlayHintService(in args);
    }
 
    private readonly InlayHintCacheWrapperProvider _cacheWrapperProvider = args.ExportProvider.GetExportedValue<InlayHintCacheWrapperProvider>();
    private readonly IRazorEditService _razorEditService = args.ExportProvider.GetExportedValue<IRazorEditService>();
 
    public ValueTask<InlayHint[]?> GetInlayHintsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHintParams inlayHintParams, bool displayAllOverride, CancellationToken cancellationToken)
        => RunServiceAsync(
            solutionInfo,
            razorDocumentId,
            context => GetInlayHintsAsync(context, inlayHintParams, displayAllOverride, cancellationToken),
            cancellationToken);
 
    private async ValueTask<InlayHint[]?> GetInlayHintsAsync(RemoteDocumentContext context, InlayHintParams inlayHintParams, bool displayAllOverride, CancellationToken cancellationToken)
    {
        var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
        var csharpDocument = codeDocument.GetRequiredCSharpDocument();
 
        var span = inlayHintParams.Range.ToLinePositionSpan();
 
        cancellationToken.ThrowIfCancellationRequested();
 
        var overlappingSpans = DocumentMappingService.GetCSharpSpansOverlappingRazorSpan(csharpDocument, span);
 
        if (overlappingSpans.IsEmpty)
        {
            // There's no C# in the range.
            return null;
        }
 
        var generatedDocument = await context.Snapshot
            .GetGeneratedDocumentAsync(cancellationToken)
            .ConfigureAwait(false);
 
        var textDocument = inlayHintParams.TextDocument.WithUri(generatedDocument.CreateUri());
 
        using var inlayHintsBuilder = new PooledArrayBuilder<InlayHint>();
        var razorSourceText = codeDocument.Source.Text;
        var csharpSourceText = codeDocument.GetCSharpSourceText();
        var root = codeDocument.GetRequiredSyntaxRoot();
 
        foreach (var csharpSpan in overlappingSpans)
        {
            var range = csharpSpan.ToRange();
            var hints = await InlayHints.GetInlayHintsAsync(generatedDocument, textDocument, range, displayAllOverride, _cacheWrapperProvider.GetCache(), cancellationToken).ConfigureAwait(false);
            if (hints is null)
            {
                continue;
            }
 
            foreach (var hint in hints)
            {
                if (csharpSourceText.TryGetAbsoluteIndex(hint.Position.ToLinePosition(), out var absoluteIndex) &&
                    DocumentMappingService.TryMapToRazorDocumentPosition(csharpDocument, absoluteIndex, out var hostDocumentPosition, out var hostDocumentIndex))
                {
                    // We know this C# maps to Razor, but does it map to Razor that we like?
 
                    // We don't want inlay hints in tag helper attributes
                    var node = root.FindInnermostNode(hostDocumentIndex);
                    if (node?.FirstAncestorOrSelf<MarkupTagHelperAttributeValueSyntax>() is not null)
                    {
                        continue;
                    }
 
                    // Inlay hints in directives are okay, eg '@attribute [Description(description: "Desc")]', but if the hint is going to be
                    // at the very start of the directive, we want to strip any TextEdit as it would make for an invalid document. eg: '// @page template: "/"'
                    if (node?.SpanStart == hostDocumentIndex &&
                        node.FirstAncestorOrSelf<RazorDirectiveSyntax>(static n => n.IsDirectiveKind(DirectiveKind.SingleLine)) is not null)
                    {
                        hint.TextEdits = null;
                    }
 
                    if (hint.TextEdits is not null)
                    {
                        var changes = hint.TextEdits.SelectAsArray(csharpSourceText.GetTextChange);
                        var textChanges = await _razorEditService.MapCSharpEditsAsync(changes, context.Snapshot, cancellationToken).ConfigureAwait(false);
 
                        var textEdits = textChanges.SelectAsArray(razorSourceText.GetTextEdit);
 
                        hint.TextEdits = ImmutableCollectionsMarshal.AsArray(textEdits);
                    }
 
                    hint.Data = new InlayHintDataWrapper(inlayHintParams.TextDocument, hint.Data, hint.Position);
                    hint.Position = hostDocumentPosition.ToPosition();
 
                    inlayHintsBuilder.Add(hint);
                }
            }
        }
 
        return inlayHintsBuilder.ToArray();
    }
 
    public ValueTask<InlayHint> ResolveHintAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHint inlayHint, CancellationToken cancellationToken)
       => RunServiceAsync(
            solutionInfo,
            razorDocumentId,
            context => ResolveInlayHintAsync(context, inlayHint, cancellationToken),
            cancellationToken);
 
    private async ValueTask<InlayHint> ResolveInlayHintAsync(RemoteDocumentContext context, InlayHint inlayHint, CancellationToken cancellationToken)
    {
        var generatedDocument = await context.Snapshot
            .GetGeneratedDocumentAsync(cancellationToken)
            .ConfigureAwait(false);
 
        return await InlayHints.ResolveInlayHintAsync(generatedDocument, inlayHint, _cacheWrapperProvider.GetCache(), cancellationToken).ConfigureAwait(false);
    }
}