private Model FilterModelInBackgroundWorker( Model model, int id, SnapshotPoint caretPosition, CompletionFilterReason filterReason, ImmutableDictionary <CompletionItemFilter, bool> filterState) { if (model == null) { return(null); } // We want to dismiss the session if the caret ever moved outside our bounds. // Do this before we check the _filterId. We don't want this work to not happen // just because the user typed more text and added more filter items. if (filterReason == CompletionFilterReason.CaretPositionChanged && Controller.IsCaretOutsideAllItemBounds(model, caretPosition)) { return(null); } // If the UI specified an updated filter state, then incorporate that // into our model. Do this before we check the _filterId. We don't // want this work to not happen just because the user typed more text // and added more filter items. if (filterState != null) { model = model.WithFilterState(filterState); } // If there's another request in the queue to filter items, then just // bail out immediately. No point in doing extra work that's just // going to be overridden by the next filter task. if (id != _filterId) { return(model); } var textSnapshot = caretPosition.Snapshot; var textSpanToText = new Dictionary <TextSpan, string>(); var document = this.Controller.GetDocument(); var helper = this.Controller.GetCompletionHelper(); var recentItems = this.Controller.GetRecentItems(); var filterResults = new List <FilterResult>(); var filterText = model.GetCurrentTextInSnapshot( model.OriginalList.Span, textSnapshot, textSpanToText); // 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. var filterTextStartsWithANumber = filterText.Length > 0 && char.IsNumber(filterText[0]); if (filterTextStartsWithANumber) { if (!IsAfterDot(model, textSnapshot, textSpanToText)) { return(null); } } var effectiveFilterItemState = ComputeEffectiveFilterItemState(model); foreach (var currentItem in model.TotalItems) { // Check if something new has happened and there's a later on filter operation // in the chain. If so, there's no need for us to do any more work (as it will // just be superceded by the later work). if (id != _filterId) { return(model); } if (CompletionItemFilter.ShouldBeFilteredOutOfCompletionList( currentItem, effectiveFilterItemState)) { continue; } // Check if the item matches the filter text typed so far. var matchesFilterText = MatchesFilterText(helper, currentItem, filterText, model.Trigger, filterReason, recentItems); if (matchesFilterText) { filterResults.Add(new FilterResult( currentItem, filterText, matchedFilterText: true)); } else { // The item didn't match the filter text. We'll still keep it in the list // if one of two things is true: // // 1. The user has 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. // // 2. They brough up completion with ctrl-j or through deletion. In these // cases we just always keep all the items in the list. var wasTriggeredByDeleteOrSimpleInvoke = model.Trigger.Kind == CompletionTriggerKind.Deletion || model.Trigger.Kind == CompletionTriggerKind.Invoke; var shouldKeepItem = filterText.Length <= 1 || wasTriggeredByDeleteOrSimpleInvoke; if (shouldKeepItem) { filterResults.Add(new FilterResult( currentItem, filterText, matchedFilterText: false)); } } } model = model.WithFilterText(filterText); // If no items matched the filter text then determine what we should do. if (filterResults.Count == 0) { return(HandleAllItemsFilteredOut(model, filterReason)); } // If this was deletion, then we control the entire behavior of deletion // ourselves. if (model.Trigger.Kind == CompletionTriggerKind.Deletion) { return(HandleDeletionTrigger(model, filterReason, filterResults)); } return(HandleNormalFiltering( model, document, filterReason, caretPosition, helper, recentItems, filterText, filterResults)); }