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)); }
private Model FilterModelInBackgroundWorker( Model model, int id, SnapshotPoint caretPosition, bool recheckCaretPosition, bool dismissIfEmptyAllowed, CompletionFilterReason filterReason) { if (model == null) { return(null); } var filterState = model.FilterState; // If all the filters are on, or all the filters are off then we don't actually // need to filter. if (filterState != null) { if (filterState.Values.All(b => b) || filterState.Values.All(b => !b)) { filterState = null; } } // We want to dismiss the session if the caret ever moved outside our bounds. if (recheckCaretPosition && Controller.IsCaretOutsideAllItemBounds(model, caretPosition)) { return(null); } if (id != _filterId) { return(model); } var textSnapshot = caretPosition.Snapshot; var allFilteredItems = new List <PresentationItem>(); var textSpanToText = new Dictionary <TextSpan, string>(); var helper = this.Controller.GetCompletionHelper(); // isUnique tracks if there is a single bool? isUnique = null; PresentationItem bestFilterMatch = null; bool filterTextIsPotentialIdentifier = false; var recentItems = this.Controller.GetRecentItems(); var itemToFilterText = new Dictionary <CompletionItem, string>(); model = model.WithCompletionItemToFilterText(itemToFilterText); 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); } // We may have wrapped some items in the list in DescriptionModifying items, // but we should use the actual underlying items when filtering. That way // our rules can access the underlying item's provider. if (ItemIsFilteredOut(currentItem.Item, filterState)) { continue; } var filterText = model.GetCurrentTextInSnapshot(currentItem.Item.Span, textSnapshot, textSpanToText); var matchesFilterText = helper.MatchesFilterText(currentItem.Item, filterText, model.Trigger, filterReason, recentItems); itemToFilterText[currentItem.Item] = filterText; if (matchesFilterText) { allFilteredItems.Add(currentItem); // If we have no best match, or this match is better than the last match, // then the current item is the best filter match. if (bestFilterMatch == null || helper.IsBetterFilterMatch(currentItem.Item, bestFilterMatch.Item, filterText, model.Trigger, filterReason, recentItems)) { bestFilterMatch = currentItem; } // If isUnique is null, then this is the first time we've seen an item that // matches the filter text. That item is now considered unique. However, if // isUnique is non-null, then this is the second (or third, or fourth, etc.) // that a provider said to include. It's no longer unique. // // Note: We only want to do this if any filter text was actually provided. // This is so we can handle the following cases properly: // // Console.WriteLi$$ // // If they try to commit unique item there, we want to commit to // "WriteLine". However, if they just have: // // Console.$$ // // And they try to commit unique item, we won't commit something just // because it was in the MRU list. if (filterText != string.Empty) { isUnique = isUnique == null || false; } } else { if (filterText.Length <= 1) { // Even though the rule provider didn't match this, we'll still include it // since we want to allow a user typing a single character and seeing all // possibly completions. However, we don't consider it either unique or a // filter match, so we won't select it. allFilteredItems.Add(currentItem); } // We want to dismiss the list if the user is typing a # and nothing matches filterTextIsPotentialIdentifier = filterTextIsPotentialIdentifier || filterText.Length == 0 || (!char.IsDigit(filterText[0]) && filterText[0] != '-' && filterText[0] != '.'); } } if (!filterTextIsPotentialIdentifier && bestFilterMatch == null) { // We had no matches, and the user is typing a #, dismiss the list return(null); } if (allFilteredItems.Count == 0) { if (dismissIfEmptyAllowed && model.DismissIfEmpty && filterReason != CompletionFilterReason.BackspaceOrDelete) { return(null); } if (model.FilterState != null && model.FilterState.Values.Any(b => b)) { // 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. return(model.WithFilteredItems(allFilteredItems.ToImmutableArray()) .WithHardSelection(false) .WithIsUnique(false)); } else { // If we are going to filter everything out, then just preserve the existing // model, but switch over to soft selection. Also, nothing is unique at that // point. return(model.WithHardSelection(false) .WithIsUnique(false)); } } // If we have a best item, then select it. Otherwise just use the first item // in the list. var selectedItem = bestFilterMatch ?? allFilteredItems.First(); // If we have a best item, then we want to hard select it. Otherwise we want // soft selection. However, no hard selection if there's a builder. var hardSelection = IsHardSelection(model, bestFilterMatch, textSnapshot, helper, model.Trigger, filterReason); var result = model.WithFilteredItems(allFilteredItems.ToImmutableArray()) .WithSelectedItem(selectedItem) .WithHardSelection(hardSelection) .WithIsUnique(isUnique.HasValue && isUnique.Value); return(result); }
private Model FilterModelInBackgroundWorker( Model model, int id, SnapshotPoint caretPosition, bool recheckCaretPosition, bool dismissIfEmptyAllowed, CompletionFilterReason filterReason) { if (model == null) { return(null); } var filterState = model.FilterState; // If all the filters are on, or all the filters are off then we don't actually // need to filter. if (filterState != null) { if (filterState.Values.All(b => b) || filterState.Values.All(b => !b)) { filterState = null; } } // We want to dismiss the session if the caret ever moved outside our bounds. if (recheckCaretPosition && Controller.IsCaretOutsideAllItemBounds(model, caretPosition)) { return(null); } 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); } } 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 (ItemIsFilteredOut(currentItem, filterState)) { 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 { if (filterText.Length <= 1) { // Even though the rule provider didn't match this, we'll still include it // since we want to allow a user typing a single character and seeing all // possibly completions. 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, dismissIfEmptyAllowed)); } // If this was deletion, then we control the entire behavior of deletion // ourselves. if (model.Trigger.Kind == CompletionTriggerKind.Deletion) { return(HandleDeletionTrigger(model, filterResults)); } return(HandleNormalFiltering( model, document, filterReason, textSnapshot, helper, recentItems, filterText, filterResults)); }