Dnspy/dnSpy/Roslyn/dnSpy.Roslyn.Internal/SignatureHelp/SignatureHelpService.Roslyn.cs

257 lines
9.7 KiB
C#
Raw Permalink Normal View History

2021-09-20 18:20:01 +02:00
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
/*
Original Roslyn files:
Controller.Session_ComputeModel.cs
Controller.Session_UpdateModel.cs
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.CodeAnalysis.Text;
using SIGHLP = Microsoft.CodeAnalysis.SignatureHelp;
namespace dnSpy.Roslyn.Internal.SignatureHelp {
partial class SignatureHelpService {
/// <summary>
/// Returns <code>null</code> if our work was preempted and we want to return the
/// previous model we've computed.
/// </summary>
private async Task<(ISignatureHelpProvider provider, SignatureHelpItems items)> ComputeItemsAsync(
ISignatureHelpProvider[] providers,
int caretPosition,
SIGHLP.SignatureHelpTriggerInfo triggerInfo,
Document document,
CancellationToken cancellationToken) {
ISignatureHelpProvider bestProvider = null;
SignatureHelpItems bestItems = null;
// TODO(cyrusn): We're calling into extensions, we need to make ourselves resilient
// to the extension crashing.
foreach (var provider in providers) {
// If this is a retrigger command, and another retrigger command has already
// been issued then we can bail out immediately.
//if (IsNonTypeCharRetrigger(triggerInfo) &&
// localRetriggerId != _retriggerId) {
// return null;
//}
cancellationToken.ThrowIfCancellationRequested();
var currentItems = await provider.GetItemsAsync(document, caretPosition, triggerInfo, cancellationToken).ConfigureAwait(false);
if (currentItems != null && currentItems.ApplicableSpan.IntersectsWith(caretPosition)) {
// If another provider provides sig help items, then only take them if they
// start after the last batch of items. i.e. we want the set of items that
// conceptually are closer to where the caret position is. This way if you have:
//
// Goo(new Bar($$
//
// Then invoking sig help will only show the items for "new Bar(" and not also
// the items for "Goo(..."
if (IsBetter(bestItems, currentItems.ApplicableSpan)) {
bestItems = new SignatureHelpItems(currentItems);
bestProvider = provider;
}
}
}
return (bestProvider, bestItems);
}
private bool IsBetter(SignatureHelpItems bestItems, TextSpan currentTextSpan) {
// If we have no best text span, then this span is definitely better.
if (bestItems == null) {
return true;
}
// Otherwise we want the one that is conceptually the innermost signature. So it's
// only better if the distance from it to the caret position is less than the best
// one so far.
return currentTextSpan.Start > bestItems.ApplicableSpan.Start;
}
SignatureHelpResult GetSignatureHelpResult((ISignatureHelpProvider provider, SignatureHelpItems items) res, Document document) {
// Code is from the end of ComputeModelInBackgroundAsync()
var items = res.items;
if (items == null)
return null;
var selectedItem = GetSelectedItem(items, res.provider);
var syntaxFactsService = document?.Project?.LanguageServices?.GetService<Microsoft.CodeAnalysis.LanguageServices.ISyntaxFactsService>();
var isCaseSensitive = syntaxFactsService == null || syntaxFactsService.IsCaseSensitive;
var selection = DefaultSignatureHelpSelector.GetSelection(items.Items,
selectedItem, items.ArgumentIndex, items.ArgumentCount, items.ArgumentName, isCaseSensitive);
return new SignatureHelpResult(items, selection.SelectedItem, selection.SelectedParameter);
}
private static SignatureHelpItem GetSelectedItem(SignatureHelpItems items, ISignatureHelpProvider provider) {
// Try to find the most appropriate item in the list to select by default.
// If the provider specified one a selected item, then always stick with that one.
if (items.SelectedItemIndex.HasValue) {
return items.Items[items.SelectedItemIndex.Value];
}
// If the provider did not pick a default, and it's the same provider as the previous
// model we have, then try to return the same item that we had before.
//if (currentModel != null && currentModel.Provider == provider) {
// return items.Items.FirstOrDefault(i => DisplayPartsMatch(i, currentModel.SelectedItem)) ?? items.Items.First();
//}
// Otherwise, just pick the first item we have.
return items.Items.First();
}
private static bool DisplayPartsMatch(SignatureHelpItem i1, SignatureHelpItem i2)
=> i1.GetAllParts().SequenceEqual(i2.GetAllParts(), CompareParts);
private static bool CompareParts(TaggedText p1, TaggedText p2)
=> p1.ToString() == p2.ToString();
internal struct SignatureHelpSelection {
private readonly SignatureHelpItem _selectedItem;
private readonly int? _selectedParameter;
public SignatureHelpSelection(SignatureHelpItem selectedItem, int? selectedParameter) : this() {
_selectedItem = selectedItem;
_selectedParameter = selectedParameter;
}
public int? SelectedParameter => _selectedParameter;
public SignatureHelpItem SelectedItem => _selectedItem;
}
internal static class DefaultSignatureHelpSelector {
public static SignatureHelpSelection GetSelection(
IList<SignatureHelpItem> items,
SignatureHelpItem selectedItem,
int argumentIndex,
int argumentCount,
string argumentName,
bool isCaseSensitive) {
var bestItem = GetBestItem(selectedItem, items, argumentIndex, argumentCount, argumentName, isCaseSensitive);
var selectedParameter = GetSelectedParameter(bestItem, argumentIndex, argumentName, isCaseSensitive);
return new SignatureHelpSelection(bestItem, selectedParameter);
}
private static int GetSelectedParameter(SignatureHelpItem bestItem, int parameterIndex, string parameterName, bool isCaseSensitive) {
if (!string.IsNullOrEmpty(parameterName)) {
var comparer = isCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
var index = bestItem.Parameters.IndexOf(p => comparer.Equals(p.Name, parameterName));
if (index >= 0) {
return index;
}
}
return parameterIndex;
}
private static SignatureHelpItem GetBestItem(
SignatureHelpItem currentItem, IList<SignatureHelpItem> filteredItems, int selectedParameter, int argumentCount, string name, bool isCaseSensitive) {
// If the current item is still applicable, then just keep it.
if (filteredItems.Contains(currentItem) &&
IsApplicable(currentItem, argumentCount, name, isCaseSensitive)) {
return currentItem;
}
// Try to find the first applicable item. If there is none, then that means the
// selected parameter was outside the bounds of all methods. i.e. all methods only
// went up to 3 parameters, and selected parameter is 3 or higher. In that case,
// just pick the very last item as it is closest in parameter count.
var result = filteredItems.FirstOrDefault(i => IsApplicable(i, argumentCount, name, isCaseSensitive));
if (result != null) {
return result;
}
// if we couldn't find a best item, and they provided a name, then try again without
// a name.
if (name != null) {
return GetBestItem(currentItem, filteredItems, selectedParameter, argumentCount, null, isCaseSensitive);
}
// If we don't have an item that can take that number of parameters, then just pick
// the last item. Or stick with the current item if the last item isn't any better.
var lastItem = filteredItems.Last();
if (currentItem.IsVariadic || currentItem.Parameters.Length == lastItem.Parameters.Length) {
return currentItem;
}
return lastItem;
}
private static bool IsApplicable(SignatureHelpItem item, int argumentCount, string name, bool isCaseSensitive) {
// If they provided a name, then the item is only valid if it has a parameter that
// matches that name.
if (name != null) {
var comparer = isCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
return item.Parameters.Any(p => comparer.Equals(p.Name, name));
}
// An item is applicable if it has at least as many parameters as the selected
// parameter index. i.e. if it has 2 parameters and we're at index 0 or 1 then it's
// applicable. However, if it has 2 parameters and we're at index 2, then it's not
// applicable.
if (item.Parameters.Length >= argumentCount) {
return true;
}
// However, if it is variadic then it is applicable as it can take any number of
// items.
if (item.IsVariadic) {
return true;
}
// Also, we special case 0. that's because if the user has "Goo(" and goo takes no
// arguments, then we'll see that it's arg count is 0. We still want to consider
// any item applicable here though.
return argumentCount == 0;
}
}
}
static class IListExtensions {
public static int IndexOf<T>(this IList<T> list, Func<T, bool> predicate) {
for (var i = 0; i < list.Count; i++) {
if (predicate(list[i])) {
return i;
}
}
return -1;
}
public static bool SequenceEqual<T>(this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer) {
Debug.Assert(comparer != null);
if (first == second) {
return true;
}
if (first == null || second == null) {
return false;
}
using (var enumerator = first.GetEnumerator())
using (var enumerator2 = second.GetEnumerator()) {
while (enumerator.MoveNext()) {
if (!enumerator2.MoveNext() || !comparer(enumerator.Current, enumerator2.Current)) {
return false;
}
}
if (enumerator2.MoveNext()) {
return false;
}
}
return true;
}
}
}