|
// 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.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.CodeAnalysis.Options;
using Microsoft.VisualStudio.Text.Adornments;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[ExportCSharpVisualBasicStatelessLspService(typeof(SignatureHelpHandler)), Shared]
[Method(LSP.Methods.TextDocumentSignatureHelpName)]
internal class SignatureHelpHandler : ILspServiceDocumentRequestHandler<LSP.TextDocumentPositionParams, LSP.SignatureHelp?>
{
private readonly IEnumerable<Lazy<ISignatureHelpProvider, OrderableLanguageMetadata>> _allProviders;
private readonly IGlobalOptionService _globalOptions;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public SignatureHelpHandler(
[ImportMany] IEnumerable<Lazy<ISignatureHelpProvider, OrderableLanguageMetadata>> allProviders,
IGlobalOptionService globalOptions)
{
_allProviders = allProviders;
_globalOptions = globalOptions;
}
public bool MutatesSolutionState => false;
public bool RequiresLSPSolution => true;
public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.TextDocumentPositionParams request) => request.TextDocument;
public async Task<LSP.SignatureHelp?> HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
{
var clientCapabilities = context.GetRequiredClientCapabilities();
var document = context.Document;
if (document == null)
return null;
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);
var options = _globalOptions.GetSignatureHelpOptions(document.Project.Language);
foreach (var provider in providers)
{
var items = await provider.Value.GetItemsAsync(document, position, triggerInfo, options, cancellationToken).ConfigureAwait(false);
if (items != null)
{
var sigInfos = new ArrayBuilder<LSP.SignatureInformation>();
foreach (var item in items.Items)
{
LSP.SignatureInformation sigInfo;
if (clientCapabilities.HasVisualStudioLspCapability() == true)
{
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(items),
ActiveParameter = items.ArgumentIndex,
Signatures = sigInfos.ToArrayAndFree()
};
return sigHelp;
}
}
return null;
}
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;
}
/// <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)
{
var taggedTexts = new ArrayBuilder<TaggedText>();
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)));
}
}
}
|