File: Handler\InlayHint\InlayHintHandler.cs
Web Access
Project: src\src\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.InlineHints;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.LanguageServer.Protocol;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint
{
    [ExportCSharpVisualBasicStatelessLspService(typeof(InlayHintHandler)), Shared]
    [Method(Methods.TextDocumentInlayHintName)]
    internal sealed class InlayHintHandler : ILspServiceDocumentRequestHandler<InlayHintParams, LSP.InlayHint[]?>
    {
        private readonly IGlobalOptionService _optionsService;
 
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public InlayHintHandler(IGlobalOptionService optionsService)
        {
            _optionsService = optionsService;
        }
 
        public bool MutatesSolutionState => false;
 
        public bool RequiresLSPSolution => true;
 
        public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request)
            => request.TextDocument;
 
        public Task<LSP.InlayHint[]?> HandleRequestAsync(InlayHintParams request, RequestContext context, CancellationToken cancellationToken)
        {
            var document = context.GetRequiredDocument();
            var inlayHintCache = context.GetRequiredLspService<InlayHintCache>();
            var options = _optionsService.GetInlineHintsOptions(document.Project.Language);
 
            return GetInlayHintsAsync(document, request.TextDocument, request.Range, options, displayAllOverride: false, inlayHintCache, cancellationToken);
        }
 
        internal static async Task<LSP.InlayHint[]?> GetInlayHintsAsync(Document document, TextDocumentIdentifier textDocumentIdentifier, LSP.Range range, InlineHintsOptions options, bool displayAllOverride, InlayHintCache inlayHintCache, CancellationToken cancellationToken)
        {
            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            var hints = await CalculateInlayHintsAsync(document, range, options, displayAllOverride, cancellationToken).ConfigureAwait(false);
            var syntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false);
 
            // Store the members in the resolve cache so that when we get a resolve request for a particular
            // member we can re-use the inline hint.
            var resultId = inlayHintCache.UpdateCache(new InlayHintCache.InlayHintCacheEntry(hints));
 
            var inlayHints = new LSP.InlayHint[hints.Length];
            for (var i = 0; i < hints.Length; i++)
            {
                var hint = hints[i];
                var (label, leftPadding, rightPadding) = Trim(hint.DisplayParts);
                var linePosition = text.Lines.GetLinePosition(hint.Span.Start);
                var kind = hint.Ranking == InlineHintsConstants.ParameterRanking
                    ? InlayHintKind.Parameter
                    : InlayHintKind.Type;
 
                // TextChange is calculated at the same time as the InlineHint,
                // so it should not need to be resolved.
                TextEdit[]? textEdits = null;
                if (hint.ReplacementTextChange.HasValue)
                {
                    var textEdit = ProtocolConversions.TextChangeToTextEdit(hint.ReplacementTextChange.Value, text);
                    textEdits = [textEdit];
                }
 
                var inlayHint = new LSP.InlayHint
                {
                    Position = ProtocolConversions.LinePositionToPosition(linePosition),
                    Label = label,
                    Kind = kind,
                    TextEdits = textEdits,
                    ToolTip = null,
                    PaddingLeft = leftPadding,
                    PaddingRight = rightPadding,
                    Data = new InlayHintResolveData(resultId, i, textDocumentIdentifier, syntaxVersion.ToString(), range, displayAllOverride)
                };
 
                inlayHints[i] = inlayHint;
            }
 
            return inlayHints;
        }
 
        internal static async Task<ImmutableArray<InlineHint>> CalculateInlayHintsAsync(Document document, LSP.Range range, InlineHintsOptions options, bool displayAllOverride, CancellationToken cancellationToken)
        {
            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            var textSpan = ProtocolConversions.RangeToTextSpan(range, text);
 
            var inlineHintService = document.GetRequiredLanguageService<IInlineHintsService>();
            var hints = await inlineHintService.GetInlineHintsAsync(document, textSpan, options, displayAllOverride, cancellationToken).ConfigureAwait(false);
            return hints;
        }
 
        /// <summary>
        /// Goes through the tagged text of the hint and trims off leading and trailing spaces. 
        /// If there is leading or trailing space, then we want to add padding to the left and right accordingly.
        /// </summary>
        private static (string label, bool leftPadding, bool rightPadding) Trim(ImmutableArray<TaggedText> taggedTexts)
        {
            using var _ = ArrayBuilder<TaggedText>.GetInstance(out var result);
            var leftPadding = false;
            var rightPadding = false;
 
            if (taggedTexts.Length == 1)
            {
                var first = taggedTexts.First();
 
                var trimStart = first.Text.TrimStart();
                var trimBoth = trimStart.TrimEnd();
                result.Add(new TaggedText(first.Tag, trimBoth));
                leftPadding = first.Text.Length - trimStart.Length != 0;
                rightPadding = trimStart.Length - trimBoth.Length != 0;
            }
            else if (taggedTexts.Length >= 2)
            {
                var first = taggedTexts.First();
                var trimStart = first.Text.TrimStart();
                result.Add(new TaggedText(first.Tag, trimStart));
                leftPadding = first.Text.Length - trimStart.Length != 0;
 
                for (var i = 1; i < taggedTexts.Length - 1; i++)
                    result.Add(taggedTexts[i]);
 
                var last = taggedTexts.Last();
                var trimEnd = last.Text.TrimEnd();
                result.Add(new TaggedText(last.Tag, trimEnd));
                rightPadding = last.Text.Length - trimEnd.Length != 0;
            }
 
            return (result.ToImmutable().JoinText(), leftPadding, rightPadding);
        }
    }
}