File: Handler\SignatureHelp\SignatureHelpHandler.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.Composition;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Text.Adornments;
using Roslyn.Utilities;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
    [ExportCSharpVisualBasicStatelessLspService(typeof(SignatureHelpHandler)), Shared]
    [Method(LSP.Methods.TextDocumentSignatureHelpName)]
    [method: ImportingConstructor]
    [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    internal class SignatureHelpHandler(SignatureHelpService signatureHelpService) : ILspServiceDocumentRequestHandler<LSP.TextDocumentPositionParams, LSP.SignatureHelp?>
    {
        public bool MutatesSolutionState => false;
        public bool RequiresLSPSolution => true;
 
        public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.TextDocumentPositionParams request) => request.TextDocument;
 
        public Task<LSP.SignatureHelp?> HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
        {
            var document = context.Document;
            if (document == null)
                return SpecializedTasks.Null<LSP.SignatureHelp>();
 
            var supportsVisualStudioExtensions = context.GetRequiredClientCapabilities().HasVisualStudioLspCapability();
            var linePosition = ProtocolConversions.PositionToLinePosition(request.Position);
            return GetSignatureHelpAsync(signatureHelpService, document, linePosition, supportsVisualStudioExtensions, cancellationToken);
        }
 
        internal static async Task<LSP.SignatureHelp?> GetSignatureHelpAsync(SignatureHelpService signatureHelpService, Document document, LinePosition linePosition, bool supportsVisualStudioExtensions, CancellationToken cancellationToken)
        {
            var position = await document.GetPositionFromLinePositionAsync(linePosition, cancellationToken).ConfigureAwait(false);
            var triggerInfo = new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.InvokeSignatureHelpCommand);
 
            var (_, sigItems) = await signatureHelpService.GetSignatureHelpAsync(document, position, triggerInfo, cancellationToken).ConfigureAwait(false);
            if (sigItems is null)
            {
                return null;
            }
            using var _ = ArrayBuilder<LSP.SignatureInformation>.GetInstance(out var sigInfos);
            foreach (var item in sigItems.Items)
            {
                LSP.SignatureInformation sigInfo;
                if (supportsVisualStudioExtensions)
                {
                    sigInfo = new LSP.VSInternalSignatureInformation
                    {
                        ColorizedLabel = GetSignatureClassifiedText(item)
                    };
                }
                else
                {
                    sigInfo = new LSP.SignatureInformation();
                }
 
                sigInfo.Label = GetSignatureText(item);
                sigInfo.Documentation = new LSP.MarkupContent { Kind = LSP.MarkupKind.PlainText, Value = item.DocumentationFactory(cancellationToken).GetFullText() };
                sigInfo.Parameters = item.Parameters.Select(p => new LSP.ParameterInformation
                {
                    Label = p.Name,
                    Documentation = new LSP.MarkupContent { Kind = LSP.MarkupKind.PlainText, Value = p.DocumentationFactory(cancellationToken).GetFullText() }
                }).ToArray();
                sigInfos.Add(sigInfo);
            }
 
            var sigHelp = new LSP.SignatureHelp
            {
                ActiveSignature = GetActiveSignature(sigItems),
                ActiveParameter = sigItems.SemanticParameterIndex,
                Signatures = sigInfos.ToArray()
            };
 
            return sigHelp;
        }
 
        private static int GetActiveSignature(SignatureHelpItems items)
        {
            if (items.SelectedItemIndex.HasValue)
            {
                return items.SelectedItemIndex.Value;
            }
 
            // From Roslyn's doc comments for SelectedItemIndex:
            //   If this is null, then the controller will pick the first item that has enough arguments
            //   to be viable based on what argument position the user is currently inside of.
            // However, the LSP spec expects the language server to make this decision.
            // So implement the logic of picking a signature that has enough arguments here.
 
            var matchingSignature = items.Items.FirstOrDefault(
                sig => sig.Parameters.Length > items.SemanticParameterIndex);
            return matchingSignature != null ? items.Items.IndexOf(matchingSignature) : 0;
        }
 
        /// <summary>
        /// The <see cref="SignatureHelpItem"/> contains a prefix, parameters separated by a
        /// separator and a suffix. Parameters themselves have a prefix, display and suffix.
        /// Concatenate them all to get the text.
        /// </summary>
        private static string GetSignatureText(SignatureHelpItem item)
        {
            var sb = new StringBuilder();
 
            sb.Append(item.PrefixDisplayParts.GetFullText());
 
            var separators = item.SeparatorDisplayParts.GetFullText();
            for (var i = 0; i < item.Parameters.Length; i++)
            {
                var param = item.Parameters[i];
 
                if (i > 0)
                {
                    sb.Append(separators);
                }
 
                sb.Append(param.PrefixDisplayParts.GetFullText());
                sb.Append(param.DisplayParts.GetFullText());
                sb.Append(param.SuffixDisplayParts.GetFullText());
            }
 
            sb.Append(item.SuffixDisplayParts.GetFullText());
            sb.Append(item.DescriptionParts.GetFullText());
 
            return sb.ToString();
        }
 
        private static ClassifiedTextElement GetSignatureClassifiedText(SignatureHelpItem item)
        {
            using var _ = ArrayBuilder<TaggedText>.GetInstance(out var taggedTexts);
 
            taggedTexts.AddRange(item.PrefixDisplayParts);
 
            var separators = item.SeparatorDisplayParts;
            for (var i = 0; i < item.Parameters.Length; i++)
            {
                var param = item.Parameters[i];
 
                if (i > 0)
                {
                    taggedTexts.AddRange(separators);
                }
 
                taggedTexts.AddRange(param.PrefixDisplayParts);
                taggedTexts.AddRange(param.DisplayParts);
                taggedTexts.AddRange(param.SuffixDisplayParts);
            }
 
            taggedTexts.AddRange(item.SuffixDisplayParts);
            taggedTexts.AddRange(item.DescriptionParts);
 
            return new ClassifiedTextElement(taggedTexts.ToArray().Select(part => new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text)));
        }
    }
}