// 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. #nullable enable using System; using System.Collections.Generic; using System.Composition; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.SignatureHelp; using Microsoft.VisualStudio.Text.Adornments; using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler { [Shared] [ExportLspMethod(LSP.Methods.TextDocumentSignatureHelpName)] internal class SignatureHelpHandler : AbstractRequestHandler { private readonly IEnumerable> _allProviders; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public SignatureHelpHandler([ImportMany] IEnumerable> allProviders, ILspSolutionProvider solutionProvider) : base(solutionProvider) => _allProviders = allProviders; public override async Task HandleRequestAsync(LSP.TextDocumentPositionParams request, LSP.ClientCapabilities clientCapabilities, string? clientName, CancellationToken cancellationToken) { var document = SolutionProvider.GetDocument(request.TextDocument, clientName); if (document == null) { return new LSP.SignatureHelp(); } var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); var providers = _allProviders.Where(p => p.Metadata.Language == document.Project.Language); var triggerInfo = new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.InvokeSignatureHelpCommand); foreach (var provider in providers) { var items = await provider.Value.GetItemsAsync(document, position, triggerInfo, cancellationToken).ConfigureAwait(false); if (items != null) { var sigInfos = new ArrayBuilder(); foreach (var item in items.Items) { LSP.SignatureInformation sigInfo; if (clientCapabilities?.HasVisualStudioLspCapability() == true) { sigInfo = new LSP.VSSignatureInformation { 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(items), ActiveParameter = items.ArgumentIndex, Signatures = sigInfos.ToArrayAndFree() }; return sigHelp; } } return new LSP.SignatureHelp(); } 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.ArgumentIndex); return matchingSignature != null ? items.Items.IndexOf(matchingSignature) : 0; } /// /// The 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. /// 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) { var taggedTexts = new ArrayBuilder(); 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.ToArrayAndFree().Select(part => new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text))); } } }