提交 96e66888 编写于 作者: G Gen Lu

Sort completion list by pattern matching results

上级 8cf35ae6
......@@ -3,6 +3,7 @@
using System;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.PatternMatching;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion
......@@ -12,12 +13,14 @@ internal struct FilterResult : IComparable<FilterResult>
public readonly CompletionItem CompletionItem;
public readonly bool MatchedFilterText;
public readonly string FilterText;
public readonly PatternMatch? PatternMatch;
public FilterResult(CompletionItem completionItem, string filterText, bool matchedFilterText)
public FilterResult(CompletionItem completionItem, string filterText, bool matchedFilterText, PatternMatch? patternMatch)
{
CompletionItem = completionItem;
MatchedFilterText = matchedFilterText;
FilterText = filterText;
PatternMatch = patternMatch;
}
public int CompareTo(FilterResult other)
......
......@@ -9,6 +9,8 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.PatternMatching;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
......@@ -174,9 +176,9 @@ internal ItemManager(RecentItemsManager recentItemsManager)
displayTextSuffix: item.Suffix);
}
if (MatchesFilterText(completionHelper, roslynItem, filterText, initialRoslynTriggerKind, filterReason, _recentItemsManager.RecentItems))
if (MatchesFilterText(completionHelper, roslynItem, filterText, initialRoslynTriggerKind, filterReason, _recentItemsManager.RecentItems, out var patternMatch))
{
initialListOfItemsToBeIncluded.Add(new ExtendedFilterResult(item, new FilterResult(roslynItem, filterText, matchedFilterText: true)));
initialListOfItemsToBeIncluded.Add(new ExtendedFilterResult(item, new FilterResult(roslynItem, filterText, matchedFilterText: true, patternMatch)));
}
else
{
......@@ -193,7 +195,7 @@ internal ItemManager(RecentItemsManager recentItemsManager)
initialRoslynTriggerKind == CompletionTriggerKind.Invoke ||
filterText.Length <= 1)
{
initialListOfItemsToBeIncluded.Add(new ExtendedFilterResult(item, new FilterResult(roslynItem, filterText, matchedFilterText: false)));
initialListOfItemsToBeIncluded.Add(new ExtendedFilterResult(item, new FilterResult(roslynItem, filterText, matchedFilterText: false, patternMatch)));
}
}
}
......@@ -214,30 +216,33 @@ internal ItemManager(RecentItemsManager recentItemsManager)
return HandleAllItemsFilteredOut(reason, data.SelectedFilters, completionRules);
}
// todo: Add comment
var itemsToBeIncludedSortedByMatch = initialListOfItemsToBeIncluded.OrderBy(result => result.FilterResult.PatternMatch).ToImmutableArray();
var options = document?.Project.Solution.Options;
var highlightMatchingPortions = options?.GetOption(CompletionOptions.HighlightMatchingPortionsOfCompletionListItems, document.Project.Language) ?? true;
var showCompletionItemFilters = options?.GetOption(CompletionOptions.ShowCompletionItemFilters, document.Project.Language) ?? true;
var updatedFilters = showCompletionItemFilters
? GetUpdatedFilters(initialListOfItemsToBeIncluded, data.SelectedFilters)
? GetUpdatedFilters(itemsToBeIncludedSortedByMatch, data.SelectedFilters)
: ImmutableArray<CompletionFilterWithState>.Empty;
var highlightedList = GetHighlightedList(initialListOfItemsToBeIncluded, filterText, completionHelper, highlightMatchingPortions).ToImmutableArray();
var highlightedList = GetHighlightedList(itemsToBeIncludedSortedByMatch, filterText, completionHelper, highlightMatchingPortions);
// If this was deletion, then we control the entire behavior of deletion ourselves.
if (initialRoslynTriggerKind == CompletionTriggerKind.Deletion)
{
return HandleDeletionTrigger(data.Trigger.Reason, initialListOfItemsToBeIncluded, filterText, updatedFilters, highlightedList);
return HandleDeletionTrigger(data.Trigger.Reason, itemsToBeIncludedSortedByMatch, filterText, updatedFilters, highlightedList);
}
Func<ImmutableArray<RoslynCompletionItem>, string, ImmutableArray<RoslynCompletionItem>> filterMethod;
Func<ImmutableArray<(RoslynCompletionItem, PatternMatch?)>, string, ImmutableArray<RoslynCompletionItem>> filterMethod;
if (completionService == null)
{
filterMethod = (items, text) => CompletionService.FilterItems(completionHelper, items, text);
filterMethod = (itemsWithPatternMatches, text) => CompletionService.FilterItems(completionHelper, itemsWithPatternMatches);
}
else
{
filterMethod = (items, text) => completionService.FilterItems(document, items, text);
filterMethod = (itemsWithPatternMatches, text) => completionService.FilterItems(document, itemsWithPatternMatches, text);
}
return HandleNormalFiltering(
......@@ -247,7 +252,7 @@ internal ItemManager(RecentItemsManager recentItemsManager)
initialRoslynTriggerKind,
filterReason,
data.Trigger.Character,
initialListOfItemsToBeIncluded,
itemsToBeIncludedSortedByMatch,
highlightedList,
completionHelper,
hasSuggestedItemOptions);
......@@ -284,6 +289,35 @@ static bool ShouldBeFilteredOutOfExpandedCompletionList(VSCompletionItem item, I
// 2. or, all associated expanders are unselected, therefore should be excluded
return associatedWithUnselectedExpander;
}
static ImmutableArray<CompletionItemWithHighlight> GetHighlightedList(
ImmutableArray<ExtendedFilterResult> filterResults,
string filterText,
CompletionHelper completionHelper,
bool highlightMatchingPortions)
{
if (!highlightMatchingPortions)
{
return filterResults.SelectAsArray(r => new CompletionItemWithHighlight(r.VSCompletionItem, ImmutableArray<Span>.Empty));
}
var highlightedItems = ArrayBuilder<CompletionItemWithHighlight>.GetInstance(filterResults.Length);
foreach (var extendedResult in filterResults)
{
// The PatternMatch in FilterResult is calculated based on Roslyn item's FilterText,
// which can be used to calculate highlighted span for VSCompletion item's DisplayText w/o doing the matching again.
// However, if the PatternMatch is null or the Roslyn item's FilterText is different from its DisplayText,
// we need to do the match against the display text of the VS item directly to get the highlighted spans.
var filterResult = extendedResult.FilterResult;
var highlightedSpans = filterResult.PatternMatch.HasValue && !filterResult.CompletionItem.HasDifferentFilterText
? filterResult.PatternMatch.Value.MatchedSpans.SelectAsArray(s => s.ToSpan(filterResult.CompletionItem.DisplayTextPrefix?.Length ?? 0))
: completionHelper.GetHighlightedSpans(extendedResult.VSCompletionItem.DisplayText, filterText, CultureInfo.CurrentCulture).SelectAsArray(s => s.ToSpan());
highlightedItems.Add(new CompletionItemWithHighlight(extendedResult.VSCompletionItem, highlightedSpans));
}
return highlightedItems.ToImmutableAndFree();
}
}
private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableToSpan)
......@@ -293,13 +327,13 @@ private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableT
}
private FilteredCompletionModel HandleNormalFiltering(
Func<ImmutableArray<RoslynCompletionItem>, string, ImmutableArray<RoslynCompletionItem>> filterMethod,
Func<ImmutableArray<(RoslynCompletionItem, PatternMatch?)>, string, ImmutableArray<RoslynCompletionItem>> filterMethod,
string filterText,
ImmutableArray<CompletionFilterWithState> filters,
CompletionTriggerKind initialRoslynTriggerKind,
CompletionFilterReason filterReason,
char typeChar,
List<ExtendedFilterResult> itemsInList,
ImmutableArray<ExtendedFilterResult> itemsInList,
ImmutableArray<CompletionItemWithHighlight> highlightedList,
CompletionHelper completionHelper,
bool hasSuggestedItemOptions)
......@@ -309,8 +343,7 @@ private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableT
// Ask the language to determine which of the *matched* items it wants to select.
var matchingItems = itemsInList.Where(r => r.FilterResult.MatchedFilterText)
.Select(t => t.FilterResult.CompletionItem)
.AsImmutable();
.SelectAsArray(t => (t.FilterResult.CompletionItem, t.FilterResult.PatternMatch));
var chosenItems = filterMethod(matchingItems, filterText);
......@@ -331,7 +364,7 @@ private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableT
if (bestItem != null)
{
selectedItemIndex = itemsInList.IndexOf(i => Equals(i.FilterResult.CompletionItem, bestItem));
var deduplicatedListCount = matchingItems.Where(r => !r.IsPreferredItem()).Count();
var deduplicatedListCount = matchingItems.Where(r => !r.CompletionItem.IsPreferredItem()).Count();
if (selectedItemIndex > -1 &&
deduplicatedListCount == 1 &&
filterText.Length > 0)
......@@ -376,12 +409,12 @@ private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableT
private FilteredCompletionModel HandleDeletionTrigger(
CompletionTriggerReason filterTriggerKind,
List<ExtendedFilterResult> filterResults,
ImmutableArray<ExtendedFilterResult> filterResults,
string filterText,
ImmutableArray<CompletionFilterWithState> filters,
ImmutableArray<CompletionItemWithHighlight> highlightedList)
{
var matchingItems = filterResults.Where(r => r.FilterResult.MatchedFilterText).AsImmutable();
var matchingItems = filterResults.WhereAsArray(r => r.FilterResult.MatchedFilterText);
if (filterTriggerKind == CompletionTriggerReason.Insertion &&
!matchingItems.Any())
{
......@@ -479,26 +512,8 @@ private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableT
filters, selection, centerSelection: true, uniqueItem: default);
}
private static IEnumerable<CompletionItemWithHighlight> GetHighlightedList(
IEnumerable<ExtendedFilterResult> filterResults,
string filterText,
CompletionHelper completionHelper,
bool highlightMatchingPortions)
{
var highlightedList = new List<CompletionItemWithHighlight>();
foreach (var item in filterResults)
{
var highlightedSpans = highlightMatchingPortions
? completionHelper.GetHighlightedSpans(item.VSCompletionItem.DisplayText, filterText, CultureInfo.CurrentCulture)
: ImmutableArray<TextSpan>.Empty;
highlightedList.Add(new CompletionItemWithHighlight(item.VSCompletionItem, highlightedSpans.Select(s => s.ToSpan()).ToImmutableArray()));
}
return highlightedList;
}
private static ImmutableArray<CompletionFilterWithState> GetUpdatedFilters(
List<ExtendedFilterResult> filteredList,
ImmutableArray<ExtendedFilterResult> filteredList,
ImmutableArray<CompletionFilterWithState> filters)
{
// See which filters might be enabled based on the typed code
......@@ -571,10 +586,20 @@ internal static bool IsBetterDeletionMatch(FilterResult result1, FilterResult re
=> result1.CompareTo(result2) > 0;
internal static bool MatchesFilterText(
CompletionHelper helper, RoslynCompletionItem item,
string filterText, CompletionTriggerKind initialTriggerKind,
CompletionFilterReason filterReason, ImmutableArray<string> recentItems)
CompletionHelper helper,
RoslynCompletionItem item,
string filterText,
CompletionTriggerKind initialTriggerKind,
CompletionFilterReason filterReason,
ImmutableArray<string> recentItems,
out PatternMatch? patternMatch)
{
// Get the match of the given completion item for the pattern provided so far.
// A completion item is checked against the pattern by see if it's
// CompletionItem.FilterText matches the item. That way, the pattern it checked
// against terms like "IList" and not IList<>
patternMatch = helper.GetMatch(item.FilterText, filterText, includeMatchSpans: true, CultureInfo.CurrentCulture);
// For the deletion we bake in the core logic for how matching should work.
// This way deletion feels the same across all languages that opt into deletion
// as a completion trigger.
......@@ -603,11 +628,8 @@ internal static bool IsBetterDeletionMatch(FilterResult result1, FilterResult re
}
}
// Checks if the given completion item matches the pattern provided so far.
// A completion item is checked against the pattern by see if it's
// CompletionItem.FilterText matches the item. That way, the pattern it checked
// against terms like "IList" and not IList<>
return helper.MatchesPattern(item.FilterText, filterText, CultureInfo.CurrentCulture);
// Otherwise, the item matches filter text if a pattern match is returned.
return patternMatch != null;
}
internal static bool IsHardSelection(
......@@ -644,7 +666,7 @@ internal static bool IsBetterDeletionMatch(FilterResult result1, FilterResult re
// If the user moved the caret left after they started typing, the 'best' match may not match at all
// against the full text span that this item would be replacing.
if (!ItemManager.MatchesFilterText(completionHelper, bestFilterMatch, fullFilterText, initialTriggerKind, filterReason, recentItems))
if (!ItemManager.MatchesFilterText(completionHelper, bestFilterMatch, fullFilterText, initialTriggerKind, filterReason, recentItems, out _))
{
return false;
}
......
......@@ -140,12 +140,12 @@ internal partial class Session
}
// Check if the item matches the filter text typed so far.
var matchesFilterText = ItemManager.MatchesFilterText(helper, currentItem, filterText, model.Trigger.Kind, filterReason, recentItems);
var matchesFilterText = ItemManager.MatchesFilterText(helper, currentItem, filterText, model.Trigger.Kind, filterReason, recentItems, out var patternMatch);
if (matchesFilterText)
{
filterResults.Add(new FilterResult(
currentItem, filterText, matchedFilterText: true));
currentItem, filterText, matchedFilterText: true, patternMatch));
}
else
{
......@@ -167,7 +167,7 @@ internal partial class Session
if (shouldKeepItem)
{
filterResults.Add(new FilterResult(
currentItem, filterText, matchedFilterText: false));
currentItem, filterText, matchedFilterText: false, patternMatch));
}
}
}
......@@ -250,8 +250,7 @@ private bool IsAfterDot(Model model)
}
var matchingCompletionItems = filterResults.Where(r => r.MatchedFilterText)
.Select(t => t.CompletionItem)
.AsImmutable();
.SelectAsArray(t => (t.CompletionItem, t.PatternMatch));
var chosenItems = service.FilterItems(
document, matchingCompletionItems, filterText);
......
......@@ -2,7 +2,6 @@
using System.Diagnostics;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Text.Shared.Extensions
{
......@@ -16,6 +15,14 @@ public static Span ToSpan(this TextSpan textSpan)
return new Span(textSpan.Start, textSpan.Length);
}
/// <summary>
/// Convert a <see cref="TextSpan"/> instance to a <see cref="TextSpan"/> with additional offset.
/// </summary>
public static Span ToSpan(this TextSpan textSpan, int offset)
{
return new Span(textSpan.Start + offset, textSpan.Length);
}
/// <summary>
/// Convert a <see cref="TextSpan"/> to a <see cref="SnapshotSpan"/> on the given <see cref="ITextSnapshot"/> instance
/// </summary>
......
......@@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PatternMatching;
using Microsoft.CodeAnalysis.Tags;
namespace Microsoft.CodeAnalysis.Completion
......@@ -55,5 +56,11 @@ protected static bool IsSnippetItem(CompletionItem item)
{
return item.Tags.Contains(WellKnownTags.Snippet);
}
internal override ImmutableArray<CompletionItem> FilterItems(Document document, ImmutableArray<(CompletionItem, PatternMatch?)> itemsWithPatternMatch, string filterText)
{
var helper = CompletionHelper.GetHelper(document);
return CompletionService.FilterItems(helper, itemsWithPatternMatch);
}
}
}
......@@ -43,14 +43,13 @@ public static CompletionHelper GetHelper(Document document)
/// results, or false if it should not be.
/// </summary>
public bool MatchesPattern(string text, string pattern, CultureInfo culture)
=> GetMatch(text, pattern, culture) != null;
=> GetMatch(text, pattern, includeMatchSpans: false, culture) != null;
private PatternMatch? GetMatch(string text, string pattern, CultureInfo culture)
=> GetMatch(text, pattern, includeMatchSpans: false, culture: culture);
private PatternMatch? GetMatch(
string completionItemText, string pattern,
bool includeMatchSpans, CultureInfo culture)
public PatternMatch? GetMatch(
string completionItemText,
string pattern,
bool includeMatchSpans,
CultureInfo culture)
{
// If the item has a dot in it (i.e. for something like enum completion), then attempt
// to match what the user wrote against the last portion of the name. That way if they
......@@ -136,9 +135,14 @@ private PatternMatcher GetPatternMatcher(string pattern, CultureInfo culture, bo
/// </summary>
public int CompareItems(CompletionItem item1, CompletionItem item2, string pattern, CultureInfo culture)
{
var match1 = GetMatch(item1.FilterText, pattern, culture);
var match2 = GetMatch(item2.FilterText, pattern, culture);
var match1 = GetMatch(item1.FilterText, pattern, includeMatchSpans: false, culture);
var match2 = GetMatch(item2.FilterText, pattern, includeMatchSpans: false, culture);
return CompareItems(item1, match1, item2, match2);
}
public int CompareItems(CompletionItem item1, PatternMatch? match1, CompletionItem item2, PatternMatch? match2)
{
if (match1 != null && match2 != null)
{
var result = CompareMatches(match1.Value, match2.Value, item1, item2);
......
......@@ -14,6 +14,8 @@ namespace Microsoft.CodeAnalysis.Completion
[DebuggerDisplay("{DisplayText}")]
public sealed class CompletionItem : IComparable<CompletionItem>
{
private readonly string _filterText;
/// <summary>
/// The text that is displayed to the user.
/// </summary>
......@@ -37,7 +39,9 @@ public sealed class CompletionItem : IComparable<CompletionItem>
/// The text used to determine if the item matches the filter and is show in the list.
/// This is often the same as <see cref="DisplayText"/> but may be different in certain circumstances.
/// </summary>
public string FilterText { get; }
public string FilterText => _filterText ?? DisplayText;
internal bool HasDifferentFilterText => _filterText != null;
/// <summary>
/// The text used to determine the order that the item appears in the list.
......@@ -107,13 +111,17 @@ public sealed class CompletionItem : IComparable<CompletionItem>
DisplayText = displayText ?? "";
DisplayTextPrefix = displayTextPrefix ?? "";
DisplayTextSuffix = displayTextSuffix ?? "";
FilterText = filterText ?? DisplayText;
SortText = sortText ?? DisplayText;
InlineDescription = inlineDescription ?? "";
Span = span;
Properties = properties ?? ImmutableDictionary<string, string>.Empty;
Tags = tags.NullToEmpty();
Rules = rules ?? CompletionItemRules.Default;
if (!DisplayText.Equals(filterText, StringComparison.Ordinal))
{
_filterText = filterText;
}
}
// binary back compat overload
......
......@@ -2,11 +2,13 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PatternMatching;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
......@@ -175,41 +177,62 @@ public virtual TextSpan GetDefaultCompletionListSpan(SourceText text, int caretP
return FilterItems(helper, items, filterText);
}
internal virtual ImmutableArray<CompletionItem> FilterItems(
Document document,
ImmutableArray<(CompletionItem, PatternMatch?)> itemsWithPatternMatch,
string filterText)
{
// Default implementation just drops the pattern matches and
// calls the public overload of FilterItems for compatibility.
return FilterItems(document, itemsWithPatternMatch.SelectAsArray(item => item.Item1), filterText);
}
internal static ImmutableArray<CompletionItem> FilterItems(
CompletionHelper completionHelper,
ImmutableArray<CompletionItem> items,
string filterText)
{
var bestItems = ArrayBuilder<CompletionItem>.GetInstance();
foreach (var item in items)
var itemsWithPatternMatch = items.SelectAsArray(
item => (item, completionHelper.GetMatch(item.FilterText, filterText, includeMatchSpans: false, CultureInfo.CurrentCulture)));
return FilterItems(completionHelper, itemsWithPatternMatch);
}
internal static ImmutableArray<CompletionItem> FilterItems(
CompletionHelper completionHelper,
ImmutableArray<(CompletionItem item, PatternMatch? match)> itemsWithPatternMatch)
{
var bestItems = ArrayBuilder<(CompletionItem, PatternMatch?)>.GetInstance();
foreach (var pair in itemsWithPatternMatch)
{
if (bestItems.Count == 0)
{
// We've found no good items yet. So this is the best item currently.
bestItems.Add(item);
bestItems.Add(pair);
}
else
{
var comparison = completionHelper.CompareItems(item, bestItems.First(), filterText, CultureInfo.CurrentCulture);
var (bestItem, bestItemMatch) = bestItems.First();
var comparison = completionHelper.CompareItems(pair.item, pair.match, bestItem, bestItemMatch);
if (comparison < 0)
{
// This item is strictly better than the best items we've found so far.
bestItems.Clear();
bestItems.Add(item);
bestItems.Add(pair);
}
else if (comparison == 0)
{
// This item is as good as the items we've been collecting. We'll return
// it and let the controller decide what to do. (For example, it will
// pick the one that has the best MRU index).
bestItems.Add(item);
bestItems.Add(pair);
}
// otherwise, this item is strictly worse than the ones we've been collecting.
// We can just ignore it.
}
}
return bestItems.ToImmutableAndFree();
return bestItems.ToImmutableAndFree().SelectAsArray(itemWithPatternMatch => itemWithPatternMatch.Item1);
}
}
}
......@@ -34,7 +34,7 @@ public static CompletionItem Create(INamedTypeSymbol typeSymbol, string containi
// it also makes sure type with shorter name shows first, e.g. 'SomeType` before 'SomeTypeWithLongerName'.
var sortTextBuilder = PooledStringBuilder.GetInstance();
sortTextBuilder.Builder.AppendFormat(SortTextFormat, typeSymbol.Name, containingNamespace);
var item = CompletionItem.Create(
displayText: typeSymbol.Name,
filterText: typeSymbol.Name,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册