ItemManager.cs 36.1 KB
Newer Older
J
Jonathon Marolf 已提交
1 2 3
// 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.
4 5 6 7 8 9 10 11 12

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
13 14
using Microsoft.CodeAnalysis.PatternMatching;
using Microsoft.CodeAnalysis.PooledObjects;
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem;
using VSCompletionItem = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionItem;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion
{
    internal class ItemManager : IAsyncCompletionItemManager
    {
        /// <summary>
        /// Used for filtering non-Roslyn data only. 
        /// </summary>
        private readonly CompletionHelper _defaultCompletionHelper;

        private readonly RecentItemsManager _recentItemsManager;

D
David Poeschl 已提交
36 37 38
        /// <summary>
        /// For telemetry.
        /// </summary>
D
David Poeschl 已提交
39 40
        private readonly object _targetTypeCompletionFilterChosenMarker = new object();

41
        internal ItemManager(RecentItemsManager recentItemsManager)
42
        {
43 44 45 46 47 48 49 50 51 52
            // Let us make the completion Helper used for non-Roslyn items case-sensitive.
            // We can change this if get requests from partner teams.
            _defaultCompletionHelper = new CompletionHelper(isCaseSensitive: true);
            _recentItemsManager = recentItemsManager;
        }

        public Task<ImmutableArray<VSCompletionItem>> SortCompletionListAsync(
            IAsyncCompletionSession session,
            AsyncCompletionSessionInitialDataSnapshot data,
            CancellationToken cancellationToken)
D
David Poeschl 已提交
53
        {
54
            if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isTargetTypeFilterEnabled) && isTargetTypeFilterEnabled)
D
David Poeschl 已提交
55
            {
56
                AsyncCompletionLogger.LogSessionHasTargetTypeFilterEnabled();
D
David Poeschl 已提交
57

D
David Poeschl 已提交
58 59 60
                // This method is called exactly once, so use the opportunity to set a baseline for telemetry.
                if (data.InitialList.Any(i => i.Filters.Any(f => f.DisplayText == FeaturesResources.Target_type_matches)))
                {
D
David Poeschl 已提交
61
                    AsyncCompletionLogger.LogSessionContainsTargetTypeFilter();
D
David Poeschl 已提交
62
                }
D
David Poeschl 已提交
63 64
            }

65 66 67 68 69
            if (session.TextView.Properties.TryGetProperty(CompletionSource.TypeImportCompletionEnabled, out bool isTypeImportCompletionEnabled) && isTypeImportCompletionEnabled)
            {
                AsyncCompletionLogger.LogSessionWithTypeImportCompletionEnabled();
            }

G
Gen Lu 已提交
70
            return Task.FromResult(data.InitialList.OrderBy(i => i.SortText, StringComparer.Ordinal).ToImmutableArray());
D
David Poeschl 已提交
71
        }
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

        public Task<FilteredCompletionModel> UpdateCompletionListAsync(
            IAsyncCompletionSession session,
            AsyncCompletionSessionDataSnapshot data,
            CancellationToken cancellationToken)
            => Task.FromResult(UpdateCompletionList(session, data, cancellationToken));

        private FilteredCompletionModel UpdateCompletionList(
            IAsyncCompletionSession session,
            AsyncCompletionSessionDataSnapshot data,
            CancellationToken cancellationToken)
        {
            if (!session.Properties.TryGetProperty(CompletionSource.HasSuggestionItemOptions, out bool hasSuggestedItemOptions))
            {
                // This is the scenario when the session is created out of Roslyn, in some other provider, e.g. in Debugger.
                // For now, the default hasSuggestedItemOptions is false.
                hasSuggestedItemOptions = false;
            }

91 92
            hasSuggestedItemOptions |= data.DisplaySuggestionItem;

93 94
            var filterText = session.ApplicableToSpan.GetText(data.Snapshot);
            var reason = data.Trigger.Reason;
95
            var initialRoslynTriggerKind = Helpers.GetRoslynTriggerKind(data.InitialTrigger);
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116

            // Check if the user is typing a number. If so, only proceed if it's a number
            // directly after a <dot>. That's because it is actually reasonable for completion
            // to be brought up after a <dot> and for the user to want to filter completion
            // items based on a number that exists in the name of the item. However, when
            // we are not after a dot (i.e. we're being brought up after <space> is typed)
            // then we don't want to filter things. Consider the user writing:
            //
            //      dim i =<space>
            //
            // We'll bring up the completion list here (as VB has completion on <space>).
            // If the user then types '3', we don't want to match against Int32.
            if (filterText.Length > 0 && char.IsNumber(filterText[0]))
            {
                if (!IsAfterDot(data.Snapshot, session.ApplicableToSpan))
                {
                    // Dismiss the session.
                    return null;
                }
            }

G
Gen Lu 已提交
117 118
            // We need to filter if 
            // 1. a non-empty strict subset of filters are selected
G
Gen Lu 已提交
119
            // 2. a non-empty set of expanders are unselected
G
Gen Lu 已提交
120
            var nonExpanderFilterStates = data.SelectedFilters.WhereAsArray(f => !(f.Filter is CompletionExpander));
G
Gen Lu 已提交
121

G
Gen Lu 已提交
122
            var selectedNonExpanderFilters = nonExpanderFilterStates.Where(f => f.IsSelected).SelectAsArray(f => f.Filter);
G
Gen Lu 已提交
123 124
            var needToFilter = selectedNonExpanderFilters.Length > 0 && selectedNonExpanderFilters.Length < nonExpanderFilterStates.Length;

G
Gen Lu 已提交
125
            var unselectedExpanders = data.SelectedFilters.Where(f => !f.IsSelected && f.Filter is CompletionExpander).SelectAsArray(f => f.Filter);
G
Gen Lu 已提交
126
            var needToFilterExpanded = unselectedExpanders.Length > 0;
D
David Poeschl 已提交
127

D
David Poeschl 已提交
128
            if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isExperimentEnabled) && isExperimentEnabled)
D
David Poeschl 已提交
129
            {
D
David Poeschl 已提交
130
                // Telemetry: Want to know % of sessions with the "Target type matches" filter where that filter is actually enabled
D
David Poeschl 已提交
131 132
                if (needToFilter &&
                    !session.Properties.ContainsProperty(_targetTypeCompletionFilterChosenMarker) &&
G
Gen Lu 已提交
133
                    selectedNonExpanderFilters.Any(f => f.DisplayText == FeaturesResources.Target_type_matches))
D
David Poeschl 已提交
134
                {
D
David Poeschl 已提交
135
                    AsyncCompletionLogger.LogTargetTypeFilterChosenInSession();
D
David Poeschl 已提交
136

D
David Poeschl 已提交
137 138 139 140
                    // Make sure we only record one enabling of the filter per session
                    session.Properties.AddProperty(_targetTypeCompletionFilterChosenMarker, _targetTypeCompletionFilterChosenMarker);
                }
            }
D
David Poeschl 已提交
141

142 143 144 145
            var filterReason = Helpers.GetFilterReason(data.Trigger);

            // If the session was created/maintained out of Roslyn, e.g. in debugger; no properties are set and we should use data.Snapshot.
            // However, we prefer using the original snapshot in some projection scenarios.
G
Gen Lu 已提交
146
            var snapshotForDocument = Helpers.TryGetInitialTriggerLocation(session, out var triggerLocation)
147 148
                ? triggerLocation.Snapshot
                : data.Snapshot;
149 150 151 152 153 154

            var document = snapshotForDocument.TextBuffer.AsTextContainer().GetOpenDocumentInCurrentContext();
            var completionService = document?.GetLanguageService<CompletionService>();
            var completionRules = completionService?.GetRules() ?? CompletionRules.Default;
            var completionHelper = document != null ? CompletionHelper.GetHelper(document) : _defaultCompletionHelper;

G
Gen Lu 已提交
155
            // DismissIfLastCharacterDeleted should be applied only when started with Insertion, and then Deleted all characters typed.
G
Gen Lu 已提交
156
            // This conforms with the original VS 2010 behavior.
G
Gen Lu 已提交
157 158 159 160 161 162 163 164 165 166
            if (initialRoslynTriggerKind == CompletionTriggerKind.Insertion &&
                data.Trigger.Reason == CompletionTriggerReason.Backspace &&
                completionRules.DismissIfLastCharacterDeleted &&
                session.ApplicableToSpan.GetText(data.Snapshot).Length == 0)
            {
                // Dismiss the session
                return null;
            }

            var options = document?.Project.Solution.Options;
G
Gen Lu 已提交
167
            var highlightMatchingPortions = options?.GetOption(CompletionOptions.HighlightMatchingPortionsOfCompletionListItems, document.Project.Language) ?? false;
G
Gen Lu 已提交
168 169
            // Nothing to highlight if user hasn't typed anything yet.
            highlightMatchingPortions = highlightMatchingPortions && filterText.Length > 0;
G
Gen Lu 已提交
170

171 172
            // Use a monotonically increasing integer to keep track the original alphabetical order of each item.
            var currentIndex = 0;
173
            var builder = ArrayBuilder<MatchResult>.GetInstance();
174

175 176 177 178
            foreach (var item in data.InitialSortedList)
            {
                cancellationToken.ThrowIfCancellationRequested();

G
Gen Lu 已提交
179 180 181 182 183
                if (needToFilter && ShouldBeFilteredOutOfCompletionList(item, selectedNonExpanderFilters))
                {
                    continue;
                }

G
Gen Lu 已提交
184
                if (needToFilterExpanded && ShouldBeFilteredOutOfExpandedCompletionList(item, unselectedExpanders))
185 186 187 188
                {
                    continue;
                }

189
                if (TryCreateMatchResult(
G
Gen Lu 已提交
190
                    completionHelper,
191
                    item,
G
Gen Lu 已提交
192 193 194 195
                    filterText,
                    initialRoslynTriggerKind,
                    filterReason,
                    _recentItemsManager.RecentItems,
196 197 198
                    highlightMatchingPortions: highlightMatchingPortions,
                    ref currentIndex,
                    out var matchResult))
199
                {
200
                    builder.Add(matchResult);
201 202 203
                }
            }

204
            if (builder.Count == 0)
205 206 207 208
            {
                return HandleAllItemsFilteredOut(reason, data.SelectedFilters, completionRules);
            }

209 210 211
            // Sort the items by pattern matching results.
            // Note that we want to preserve the original alphabetical order for items with same pattern match score,
            // but `ArrayBuilder.Sort` isn't stable. Therefore we have to add a monotonically increasing integer
G
Gen Lu 已提交
212
            // to `MatchResult` to achieve this.
213
            builder.Sort(MatchResult.SortingComparer);
G
Gen Lu 已提交
214

215
            var initialListOfItemsToBeIncluded = builder.ToImmutableAndFree();
216

217 218 219
            var showCompletionItemFilters = options?.GetOption(CompletionOptions.ShowCompletionItemFilters, document.Project.Language) ?? true;

            var updatedFilters = showCompletionItemFilters
220
                ? GetUpdatedFilters(initialListOfItemsToBeIncluded, data.SelectedFilters)
221 222 223 224 225
                : ImmutableArray<CompletionFilterWithState>.Empty;

            // If this was deletion, then we control the entire behavior of deletion ourselves.
            if (initialRoslynTriggerKind == CompletionTriggerKind.Deletion)
            {
226
                return HandleDeletionTrigger(data.Trigger.Reason, initialListOfItemsToBeIncluded, filterText, updatedFilters);
227 228
            }

229
            Func<ImmutableArray<(RoslynCompletionItem, PatternMatch?)>, string, ImmutableArray<RoslynCompletionItem>> filterMethod;
230 231
            if (completionService == null)
            {
232
                filterMethod = (itemsWithPatternMatches, text) => CompletionService.FilterItems(completionHelper, itemsWithPatternMatches);
233 234 235
            }
            else
            {
236
                filterMethod = (itemsWithPatternMatches, text) => completionService.FilterItems(document, itemsWithPatternMatches, text);
237 238 239 240 241 242 243 244
            }

            return HandleNormalFiltering(
                filterMethod,
                filterText,
                updatedFilters,
                filterReason,
                data.Trigger.Character,
245
                initialListOfItemsToBeIncluded,
246
                hasSuggestedItemOptions);
G
Gen Lu 已提交
247 248 249

            static bool ShouldBeFilteredOutOfCompletionList(VSCompletionItem item, ImmutableArray<CompletionFilter> activeNonExpanderFilters)
            {
G
Gen Lu 已提交
250
                if (item.Filters.Any(filter => activeNonExpanderFilters.Contains(filter)))
G
Gen Lu 已提交
251
                {
G
Gen Lu 已提交
252
                    return false;
G
Gen Lu 已提交
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
                }

                return true;
            }

            static bool ShouldBeFilteredOutOfExpandedCompletionList(VSCompletionItem item, ImmutableArray<CompletionFilter> unselectedExpanders)
            {
                var associatedWithUnselectedExpander = false;
                foreach (var itemFilter in item.Filters)
                {
                    if (itemFilter is CompletionExpander)
                    {
                        if (!unselectedExpanders.Contains(itemFilter))
                        {
                            // If any of the associated expander is selected, the item should be included in the expanded list.
                            return false;
                        }

                        associatedWithUnselectedExpander = true;
                    }
                }

                // at this point, the item either:
                // 1. has no expander filter, therefore should be included
G
Gen Lu 已提交
277
                // 2. or, all associated expanders are unselected, therefore should be excluded
G
Gen Lu 已提交
278 279
                return associatedWithUnselectedExpander;
            }
280 281 282 283 284 285 286 287 288
        }

        private static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableToSpan)
        {
            var position = applicableToSpan.GetStartPoint(snapshot).Position;
            return position > 0 && snapshot[position - 1] == '.';
        }

        private FilteredCompletionModel HandleNormalFiltering(
289
            Func<ImmutableArray<(RoslynCompletionItem, PatternMatch?)>, string, ImmutableArray<RoslynCompletionItem>> filterMethod,
290 291 292 293
            string filterText,
            ImmutableArray<CompletionFilterWithState> filters,
            CompletionFilterReason filterReason,
            char typeChar,
294
            ImmutableArray<MatchResult> itemsInList,
295 296 297 298 299 300
            bool hasSuggestedItemOptions)
        {
            // Not deletion.  Defer to the language to decide which item it thinks best
            // matches the text typed so far.

            // Ask the language to determine which of the *matched* items it wants to select.
301 302
            var matchingItems = itemsInList.Where(r => r.MatchedFilterText)
                                           .SelectAsArray(t => (t.RoslynCompletionItem, t.PatternMatch));
303 304 305

            var chosenItems = filterMethod(matchingItems, filterText);

C
Use var  
Cyrus Najmabadi 已提交
306
            var selectedItemIndex = 0;
307 308
            VSCompletionItem uniqueItem = null;
            MatchResult bestOrFirstMatchResult;
309

310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
            if (chosenItems.Length == 0)
            {
                // We do not have matches: pick the first item from the list.
                bestOrFirstMatchResult = itemsInList.FirstOrDefault();
            }
            else
            {
                var recentItems = _recentItemsManager.RecentItems;

                // Of the items the service returned, pick the one most recently committed
                var bestItem = GetBestCompletionItemBasedOnMRU(chosenItems, recentItems);

                // Determine if we should consider this item 'unique' or not.  A unique item
                // will be automatically committed if the user hits the 'invoke completion' 
                // without bringing up the completion list.  An item is unique if it was the
                // only item to match the text typed so far, and there was at least some text
                // typed.  i.e.  if we have "Console.$$" we don't want to commit something
                // like "WriteLine" since no filter text has actually been provided.  HOwever,
                // if "Console.WriteL$$" is typed, then we do want "WriteLine" to be committed.
                selectedItemIndex = itemsInList.IndexOf(i => Equals(i.RoslynCompletionItem, bestItem));
                bestOrFirstMatchResult = itemsInList[selectedItemIndex];
                var deduplicatedListCount = matchingItems.Count(r => !r.RoslynCompletionItem.IsPreferredItem());
                if (deduplicatedListCount == 1 &&
333
                    filterText.Length > 0)
334
                {
335
                    uniqueItem = itemsInList[selectedItemIndex].VSCompletionItem;
336 337 338
                }
            }

339
            // Check that it is a filter symbol. We can be called for a non-filter symbol.
340 341 342
            // If inserting a non-filter character (neither IsPotentialFilterCharacter, nor Helpers.IsFilterCharacter), we should dismiss completion  
            // except cases where this is the first symbol typed for the completion session (string.IsNullOrEmpty(filterText) or string.Equals(filterText, typeChar.ToString(), StringComparison.OrdinalIgnoreCase)).
            // In the latter case, we should keep the completion because it was confirmed just before in InitializeCompletion.
343 344
            if (filterReason == CompletionFilterReason.Insertion &&
                !string.IsNullOrEmpty(filterText) &&
345 346
                !string.Equals(filterText, typeChar.ToString(), StringComparison.OrdinalIgnoreCase) &&
                !IsPotentialFilterCharacter(typeChar) &&
347
                !Helpers.IsFilterCharacter(bestOrFirstMatchResult.RoslynCompletionItem, typeChar, filterText))
348 349 350 351
            {
                return null;
            }

C
Use var  
Cyrus Najmabadi 已提交
352
            var isHardSelection = IsHardSelection(
353
                filterText, bestOrFirstMatchResult.RoslynCompletionItem, bestOrFirstMatchResult.MatchedFilterText, hasSuggestedItemOptions);
354 355 356 357 358 359 360 361 362 363

            var updateSelectionHint = isHardSelection ? UpdateSelectionHint.Selected : UpdateSelectionHint.SoftSelected;

            // If no items found above, select the first item.
            if (selectedItemIndex == -1)
            {
                selectedItemIndex = 0;
            }

            return new FilteredCompletionModel(
364
                GetHighlightedList(itemsInList), selectedItemIndex, filters,
365 366 367 368
                updateSelectionHint, centerSelection: true, uniqueItem);
        }

        private FilteredCompletionModel HandleDeletionTrigger(
369
            CompletionTriggerReason filterTriggerKind,
370
            ImmutableArray<MatchResult> matchResults,
371
            string filterText,
372
            ImmutableArray<CompletionFilterWithState> filters)
373
        {
374
            var matchingItems = matchResults.WhereAsArray(r => r.MatchedFilterText);
375
            if (filterTriggerKind == CompletionTriggerReason.Insertion &&
376
                !matchingItems.Any())
377 378 379
            {
                // The user has typed something, but nothing in the actual list matched what
                // they were typing.  In this case, we want to dismiss completion entirely.
G
Gen Lu 已提交
380
                // The thought process is as follows: we aggressively brought up completion
381 382 383 384 385 386
                // to help them when they typed delete (in case they wanted to pick another
                // item).  However, they're typing something that doesn't seem to match at all
                // The completion list is just distracting at this point.
                return null;
            }

387
            MatchResult? bestMatchResult = null;
388
            bool moreThanOneMatchWithSamePriority = false;
389
            foreach (var currentMatchResult in matchingItems)
390
            {
391
                if (bestMatchResult == null)
392 393
                {
                    // We had no best result yet, so this is now our best result.
394
                    bestMatchResult = currentMatchResult;
395
                }
396 397
                else
                {
398
                    var match = currentMatchResult.CompareTo(bestMatchResult.Value, filterText);
399 400 401
                    if (match > 0)
                    {
                        moreThanOneMatchWithSamePriority = false;
402
                        bestMatchResult = currentMatchResult;
403 404 405 406 407 408
                    }
                    else if (match == 0)
                    {
                        moreThanOneMatchWithSamePriority = true;
                    }
                }
409 410 411 412 413 414 415 416 417
            }

            int index;
            bool hardSelect;

            // If we had a matching item, then pick the best of the matching items and
            // choose that one to be hard selected.  If we had no actual matching items
            // (which can happen if the user deletes down to a single character and we
            // include everything), then we just soft select the first item.
418
            if (bestMatchResult != null)
419 420 421 422 423 424 425
            {
                // Only hard select this result if it's a prefix match
                // We need to do this so that
                // * deleting and retyping a dot in a member access does not change the
                //   text that originally appeared before the dot
                // * deleting through a word from the end keeps that word selected
                // This also preserves the behavior the VB had through Dev12.
426 427
                hardSelect = bestMatchResult.Value.VSCompletionItem.FilterText.StartsWith(filterText, StringComparison.CurrentCultureIgnoreCase);
                index = matchResults.IndexOf(bestMatchResult.Value);
428 429 430 431 432 433 434 435
            }
            else
            {
                index = 0;
                hardSelect = false;
            }

            return new FilteredCompletionModel(
436
                GetHighlightedList(matchResults), index, filters,
437
                hardSelect ? UpdateSelectionHint.Selected : UpdateSelectionHint.SoftSelected,
438
                centerSelection: true,
439
                uniqueItem: moreThanOneMatchWithSamePriority ? default : bestMatchResult.GetValueOrDefault().VSCompletionItem);
440 441
        }

442 443 444 445
        private static ImmutableArray<CompletionItemWithHighlight> GetHighlightedList(ImmutableArray<MatchResult> matchResults)
            => matchResults.SelectAsArray(matchResult =>
            new CompletionItemWithHighlight(matchResult.VSCompletionItem, matchResult.HighlightedSpans));

446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
        private FilteredCompletionModel HandleAllItemsFilteredOut(
            CompletionTriggerReason triggerReason,
            ImmutableArray<CompletionFilterWithState> filters,
            CompletionRules completionRules)
        {
            if (triggerReason == CompletionTriggerReason.Insertion)
            {
                // If the user was just typing, and the list went to empty *and* this is a 
                // language that wants to dismiss on empty, then just return a null model
                // to stop the completion session.
                if (completionRules.DismissIfEmpty)
                {
                    return null;
                }
            }

            // If the user has turned on some filtering states, and we filtered down to
            // nothing, then we do want the UI to show that to them.  That way the user
            // can turn off filters they don't want and get the right set of items.

            // If we are going to filter everything out, then just preserve the existing
            // model (and all the previously filtered items), but switch over to soft
            // selection.
            var selection = UpdateSelectionHint.SoftSelected;

            return new FilteredCompletionModel(
                ImmutableArray<CompletionItemWithHighlight>.Empty, selectedItemIndex: 0,
                filters, selection, centerSelection: true, uniqueItem: default);
        }

        private static ImmutableArray<CompletionFilterWithState> GetUpdatedFilters(
477
            ImmutableArray<MatchResult> filteredList,
478 479 480 481 482
            ImmutableArray<CompletionFilterWithState> filters)
        {
            // See which filters might be enabled based on the typed code
            var textFilteredFilters = filteredList.SelectMany(n => n.VSCompletionItem.Filters).ToImmutableHashSet();

G
Gen Lu 已提交
483 484
            // When no items are available for a given filter, it becomes unavailable.
            // Expanders always appear available as long as it's presented.
G
Gen Lu 已提交
485
            return filters.SelectAsArray(n => n.WithAvailability(n.Filter is CompletionExpander ? true : textFilteredFilters.Contains(n.Filter)));
486 487 488 489 490 491
        }

        /// <summary>
        /// Given multiple possible chosen completion items, pick the one that has the
        /// best MRU index.
        /// </summary>
492
        private static RoslynCompletionItem GetBestCompletionItemBasedOnMRU(
493 494 495 496 497 498 499 500 501 502 503
            ImmutableArray<RoslynCompletionItem> chosenItems, ImmutableArray<string> recentItems)
        {
            // Try to find the chosen item has been most
            // recently used.
            var bestItem = chosenItems.FirstOrDefault();
            for (int i = 0, n = chosenItems.Length; i < n; i++)
            {
                var chosenItem = chosenItems[i];
                var mruIndex1 = GetRecentItemIndex(recentItems, bestItem);
                var mruIndex2 = GetRecentItemIndex(recentItems, chosenItem);

504 505
                if ((mruIndex2 < mruIndex1) ||
                    (mruIndex2 == mruIndex1 && !bestItem.IsPreferredItem() && chosenItem.IsPreferredItem()))
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
                {
                    bestItem = chosenItem;
                }
            }

            // If our best item appeared in the MRU list, use it
            if (GetRecentItemIndex(recentItems, bestItem) <= 0)
            {
                return bestItem;
            }

            // Otherwise use the chosen item that has the highest
            // matchPriority.
            for (int i = 1, n = chosenItems.Length; i < n; i++)
            {
                var chosenItem = chosenItems[i];
                var bestItemPriority = bestItem.Rules.MatchPriority;
                var currentItemPriority = chosenItem.Rules.MatchPriority;

I
Ivan Basov 已提交
525
                if ((currentItemPriority > bestItemPriority) ||
526
                    ((currentItemPriority == bestItemPriority) && !bestItem.IsPreferredItem() && chosenItem.IsPreferredItem()))
527 528 529 530 531 532 533 534
                {
                    bestItem = chosenItem;
                }
            }

            return bestItem;
        }

535
        private static int GetRecentItemIndex(ImmutableArray<string> recentItems, RoslynCompletionItem item)
536
        {
537
            var index = recentItems.IndexOf(item.FilterText);
538 539 540
            return -index;
        }

541 542 543
        private static bool TryCreateMatchResult(
            CompletionHelper completionHelper,
            VSCompletionItem item,
544 545 546 547
            string filterText,
            CompletionTriggerKind initialTriggerKind,
            CompletionFilterReason filterReason,
            ImmutableArray<string> recentItems,
548 549 550
            bool highlightMatchingPortions,
            ref int currentIndex,
            out MatchResult matchResult)
551
        {
552 553 554 555 556 557 558 559 560
            if (!item.Properties.TryGetProperty(CompletionSource.RoslynItem, out RoslynCompletionItem roslynItem))
            {
                roslynItem = RoslynCompletionItem.Create(
                    displayText: item.DisplayText,
                    filterText: item.FilterText,
                    sortText: item.SortText,
                    displayTextSuffix: item.Suffix);
            }

561 562 563
            // 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 
G
Gen Lu 已提交
564 565 566
            // against terms like "IList" and not IList<>.
            // Note that the check on filter text length is purely for efficiency, we should 
            // get the same result with or without it.
567 568
            var patternMatch = filterText.Length > 0
                ? completionHelper.GetMatch(item.FilterText, filterText, includeMatchSpans: highlightMatchingPortions, CultureInfo.CurrentCulture)
G
Gen Lu 已提交
569
                : null;
570

571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
            var matchedFilterText = MatchesFilterText(
                roslynItem,
                filterText,
                initialTriggerKind,
                filterReason,
                recentItems,
                patternMatch);

            // If the item didn't match the filter text, we still keep it in the list
            // if one of two things is true:
            //
            //  1. The user has typed nothing or only typed a single character.  In this case they might
            //     have just typed the character to get completion.  Filtering out items
            //     here is not desirable.
            //
G
Gen Lu 已提交
586
            //  2. They brought up completion with ctrl-j or through deletion.  In these
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
            //     cases we just always keep all the items in the list.
            if (matchedFilterText ||
                initialTriggerKind == CompletionTriggerKind.Deletion ||
                initialTriggerKind == CompletionTriggerKind.Invoke ||
                filterText.Length <= 1)
            {
                matchResult = new MatchResult(
                    roslynItem, item, matchedFilterText: matchedFilterText,
                    patternMatch: patternMatch, index: currentIndex++, GetHighlightedSpans());

                return true;
            }

            matchResult = default;
            return false;

            ImmutableArray<Span> GetHighlightedSpans()
            {
                if (!highlightMatchingPortions)
                {
                    return ImmutableArray<Span>.Empty;
                }

                if (roslynItem.HasDifferentFilterText)
                {
                    // The PatternMatch in MatchResult 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 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.
                    return completionHelper.GetHighlightedSpans(
                        item.DisplayText, filterText, CultureInfo.CurrentCulture).SelectAsArray(s => s.ToSpan());
                }

                if (patternMatch.HasValue)
                {
                    // Since VS item's display text is created as Prefix + DisplayText + Suffix, 
                    // we can calculate the highlighted span by adding an offset that is the length of the Prefix.
                    return patternMatch.Value.MatchedSpans
                        .SelectAsArray(s => s.MoveTo(roslynItem.DisplayTextPrefix?.Length ?? 0).ToSpan());
                }

                // If there's no match for Roslyn item's filter text which is identical to its display text,
                // then we can safely assume there'd be no matching to VS item's display text.
                return ImmutableArray<Span>.Empty;
            }
        }

        private static bool MatchesFilterText(
            RoslynCompletionItem item,
            string filterText,
            CompletionTriggerKind initialTriggerKind,
            CompletionFilterReason filterReason,
            ImmutableArray<string> recentItems,
            PatternMatch? patternMatch)
        {
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
            // 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.

            // Specifically, to avoid being too aggressive when matching an item during 
            // completion, we require that the current filter text be a prefix of the 
            // item in the list.
            if (filterReason == CompletionFilterReason.Deletion &&
                initialTriggerKind == CompletionTriggerKind.Deletion)
            {
                return item.FilterText.GetCaseInsensitivePrefixLength(filterText) > 0;
            }

            // If the user hasn't typed anything, and this item was preselected, or was in the
            // MRU list, then we definitely want to include it.
            if (filterText.Length == 0)
            {
                if (item.Rules.MatchPriority > MatchPriority.Default)
                {
                    return true;
                }

664
                if (!recentItems.IsDefault && GetRecentItemIndex(recentItems, item) <= 0)
665 666 667 668 669
                {
                    return true;
                }
            }

670 671
            // Otherwise, the item matches filter text if a pattern match is returned.
            return patternMatch != null;
672 673
        }

674 675 676 677
        private static bool IsHardSelection(
            string filterText,
            RoslynCompletionItem item,
            bool matchedFilterText,
678 679
            bool useSuggestionMode)
        {
680
            if (item == null || useSuggestionMode)
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696
            {
                return false;
            }

            // We don't have a builder and we have a best match.  Normally this will be hard
            // selected, except for a few cases.  Specifically, if no filter text has been
            // provided, and this is not a preselect match then we will soft select it.  This
            // happens when the completion list comes up implicitly and there is something in
            // the MRU list.  In this case we do want to select it, but not with a hard
            // selection.  Otherwise you can end up with the following problem:
            //
            //  dim i as integer =<space>
            //
            // Completion will comes up after = with 'integer' selected (Because of MRU).  We do
            // not want 'space' to commit this.

G
Gen Lu 已提交
697
            // If all that has been typed is punctuation, then don't hard select anything.
698 699 700 701 702
            // It's possible the user is just typing language punctuation and selecting
            // anything in the list will interfere.  We only allow this if the filter text
            // exactly matches something in the list already. 
            if (filterText.Length > 0 && IsAllPunctuation(filterText) && filterText != item.DisplayText)
            {
703
                return false;
704 705 706 707 708 709 710 711 712 713 714
            }

            // If the user hasn't actually typed anything, then don't hard select any item.
            // The only exception to this is if the completion provider has requested the
            // item be preselected.
            if (filterText.Length == 0)
            {
                // Item didn't want to be hard selected with no filter text.
                // So definitely soft select it.
                if (item.Rules.SelectionBehavior != CompletionItemSelectionBehavior.HardSelection)
                {
715
                    return false;
716 717 718 719 720
                }

                // Item did not ask to be preselected.  So definitely soft select it.
                if (item.Rules.MatchPriority == MatchPriority.Default)
                {
721
                    return false;
722 723 724 725 726 727
                }
            }

            // The user typed something, or the item asked to be preselected.  In 
            // either case, don't soft select this.
            Debug.Assert(filterText.Length > 0 || item.Rules.MatchPriority != MatchPriority.Default);
728 729 730 731 732 733 734 735 736 737 738

            // 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 (!matchedFilterText)
            {
                return false;
            }

            // There was either filter text, or this was a preselect match.  In either case, we
            // can hard select this.
            return true;
739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757
        }

        private static bool IsAllPunctuation(string filterText)
        {
            foreach (var ch in filterText)
            {
                if (!char.IsPunctuation(ch))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// A potential filter character is something that can filter a completion lists and is
        /// *guaranteed* to not be a commit character.
        /// </summary>
758
        private static bool IsPotentialFilterCharacter(char c)
759
        {
G
Gen Lu 已提交
760
            // TODO(cyrusn): Actually use the right Unicode categories here.
761 762 763 764 765 766
            return char.IsLetter(c)
                || char.IsNumber(c)
                || c == '_';
        }
    }
}