private static Model HandleAllItemsFilteredOut( Model model, CompletionFilterReason filterReason) { if (model.DismissIfEmpty && filterReason == CompletionFilterReason.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. return(null); } if (model.FilterState?.Values.Any(b => b) == true) { // 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. return(model.WithFilteredItems(ImmutableArray <CompletionItem> .Empty) .WithFilterText("") .WithHardSelection(false) .WithIsUnique(false)); } else { // 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. return(model.WithHardSelection(false) .WithIsUnique(false)); } }
private static bool MatchesFilterText( CompletionHelper helper, CompletionItem item, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray <string> recentItems) { // 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 && trigger.Kind == 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); } if (!recentItems.IsDefault && GetRecentItemIndex(recentItems, item) <= 0) { return(true); } } return(helper.MatchesFilterText(item, filterText, CultureInfo.CurrentCulture)); }
public void FilterModel( CompletionFilterReason filterReason, bool recheckCaretPosition = false, bool dismissIfEmptyAllowed = true, ImmutableDictionary<CompletionItemFilter, bool> filterState = null) { AssertIsForeground(); var caretPosition = GetCaretPointInViewBuffer(); // Use an interlocked increment so that reads by existing filter tasks will see the // change. Interlocked.Increment(ref _filterId); var localId = _filterId; Computation.ChainTaskAndNotifyControllerWhenFinished( model => { if (model != null && filterState != null) { // If the UI specified an updated filter state, then incorporate that // into our model. model = model.WithFilterState(filterState); } return FilterModelInBackground( model, localId, caretPosition, recheckCaretPosition, dismissIfEmptyAllowed, filterReason); }); }
private bool IsHardSelection( Model model, CompletionItem bestFilterMatch, SnapshotPoint caretPosition, CompletionHelper completionHelper, CompletionFilterReason filterReason) { var itemViewSpan = model.GetViewBufferSpan(bestFilterMatch.Span); var fullFilterText = model.GetCurrentTextInSnapshot(itemViewSpan, caretPosition.Snapshot, endPoint: null); var textSpan = itemViewSpan.TextSpan; // Switch to soft selection, if user moved caret to the start of a non-empty filter span. // This prevents commiting if user types a commit character at this position later, but // still has the list if user types filter character // i.e. blah| -> |blah -> !|blah // We want the filter span non-empty because we still want hard selection in the following case: // // A a = new | if (caretPosition == textSpan.Start && textSpan.Length > 0) { return(false); } return(ItemManager.IsHardSelection(fullFilterText, model.Trigger.Kind, bestFilterMatch, completionHelper, filterReason, this.Controller.GetRecentItems(), model.UseSuggestionMode)); }
public void FilterModel( CompletionFilterReason filterReason, bool recheckCaretPosition = false, bool dismissIfEmptyAllowed = true, ImmutableDictionary <CompletionItemFilter, bool> filterState = null) { AssertIsForeground(); var caretPosition = GetCaretPointInViewBuffer(); // Use an interlocked increment so that reads by existing filter tasks will see the // change. Interlocked.Increment(ref _filterId); var localId = _filterId; Computation.ChainTaskAndNotifyControllerWhenFinished( model => { if (model != null && filterState != null) { // If the UI specified an updated filter state, then incorporate that // into our model. model = model.WithFilterState(filterState); } return(FilterModelInBackground( model, localId, caretPosition, recheckCaretPosition, dismissIfEmptyAllowed, filterReason)); }); }
private bool IsHardSelection( Model model, CompletionItem bestFilterMatch, SnapshotPoint caretPosition, CompletionHelper completionHelper, CompletionFilterReason reason) { if (bestFilterMatch == null || model.UseSuggestionMode) { return(false); } var textSnapshot = caretPosition.Snapshot; // 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. var itemViewSpan = model.GetViewBufferSpan(bestFilterMatch.Span); var fullFilterText = model.GetCurrentTextInSnapshot(itemViewSpan, textSnapshot, endPoint: null); var trigger = model.Trigger; var shouldSoftSelect = ShouldSoftSelectItem(bestFilterMatch, fullFilterText, trigger); if (shouldSoftSelect) { return(false); } // 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 (!MatchesFilterText(completionHelper, bestFilterMatch, fullFilterText, trigger, reason, this.Controller.GetRecentItems())) { return(false); } // Switch to soft selection, if user moved caret to the start of a non-empty filter span. // This prevents commiting if user types a commit character at this position later, but // still has the list if user types filter character // i.e. blah| -> |blah -> !|blah // We want the filter span non-empty because we still want hard selection in the following case: // // A a = new | if (caretPosition == itemViewSpan.TextSpan.Start && itemViewSpan.TextSpan.Length > 0) { return(false); } // There was either filter text, or this was a preselect match. In either case, we // can hard select this. return(true); }
private Model HandleDeletionTrigger( Model model, CompletionFilterReason filterReason, List <FilterResult> filterResults) { if (filterReason == CompletionFilterReason.Insertion && !filterResults.Any(r => r.MatchedFilterText)) { // 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. // The thought process is as follows: we aggressively brough up completion // 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); } FilterResult?bestFilterResult = null; int matchCount = 0; foreach (var currentFilterResult in filterResults.Where(r => r.MatchedFilterText)) { if (bestFilterResult == null || IsBetterDeletionMatch(currentFilterResult, bestFilterResult.Value)) { // We had no best result yet, so this is now our best result. bestFilterResult = currentFilterResult; matchCount++; } } // 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. var filteredItems = filterResults.Select(r => r.CompletionItem).AsImmutable(); model = model.WithFilteredItems(filteredItems); if (bestFilterResult != null) { // 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. var hardSelect = bestFilterResult.Value.CompletionItem.FilterText.StartsWith(model.FilterText, StringComparison.CurrentCultureIgnoreCase); return(model.WithSelectedItem(bestFilterResult.Value.CompletionItem) .WithHardSelection(hardSelect) .WithIsUnique(matchCount == 1)); } else { return(model.WithHardSelection(false) .WithIsUnique(false)); } }
private Model HandleNormalFiltering( Model model, Document document, CompletionFilterReason filterReason, ITextSnapshot textSnapshot, CompletionHelper helper, ImmutableArray <string> recentItems, string filterText, List <FilterResult> filterResults) { // 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. var service = this.Controller.GetCompletionService(); if (service == null) { return(null); } var matchingCompletionItems = filterResults.Where(r => r.MatchedFilterText) .Select(t => t.PresentationItem.Item) .AsImmutable(); var chosenItems = service.FilterItems( document, matchingCompletionItems, filterText); // Of the items the service returned, pick the one most recently committed var bestCompletionItem = GetBestCompletionItemBasedOnMRU(chosenItems, recentItems); // If we don't have a best completion item yet, then pick the first item from the list. var bestOrFirstCompletionItem = bestCompletionItem ?? filterResults.First().PresentationItem.Item; var bestOrFirstPresentationItem = filterResults.Where( r => r.PresentationItem.Item == bestOrFirstCompletionItem).First().PresentationItem; var hardSelection = IsHardSelection( model, bestOrFirstPresentationItem, textSnapshot, helper, filterReason); // 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. var matchingItemCount = matchingCompletionItems.Length; var isUnique = bestCompletionItem != null && matchingItemCount == 1 && filterText.Length > 0; var result = model.WithFilteredItems(filterResults.Select(r => r.PresentationItem).AsImmutable()) .WithSelectedItem(bestOrFirstPresentationItem) .WithHardSelection(hardSelection) .WithIsUnique(isUnique); return(result); }
private bool MatchesFilterText( CompletionItem item, string filterText, ICompletionRules completionRules, CompletionTriggerInfo triggerInfo, CompletionFilterReason reason) { return(completionRules.MatchesFilterText(item, filterText, triggerInfo, reason) ?? false); }
private bool IsBetterFilterMatch( CompletionItem item, CompletionItem bestItem, string filterText, ICompletionRules completionRules, CompletionTriggerInfo triggerInfo, CompletionFilterReason filterReason) { return(completionRules.IsBetterFilterMatch(item, bestItem, filterText, triggerInfo, filterReason) ?? false); }
public void FilterModel(CompletionFilterReason filterReason, bool recheckCaretPosition = false, bool dismissIfEmptyAllowed = true) { AssertIsForeground(); var caretPosition = GetCaretPointInViewBuffer(); // Use an interlocked increment so that reads by existing filter tasks will see the // change. Interlocked.Increment(ref _filterId); var localId = _filterId; Computation.ChainTaskAndNotifyControllerWhenFinished(model => FilterModelInBackground(model, localId, caretPosition, recheckCaretPosition, dismissIfEmptyAllowed, filterReason)); }
private bool IsHardSelection( Model model, CompletionItem bestFilterMatch, ITextSnapshot textSnapshot, IList <ICompletionRules> completionRulesList, CompletionTriggerInfo triggerInfo, CompletionFilterReason reason) { if (model.Builder != null) { return(bestFilterMatch != null && bestFilterMatch.DisplayText == model.Builder.DisplayText); } if (bestFilterMatch == null || model.UseSuggestionCompletionMode) { 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. var viewSpan = model.GetSubjectBufferFilterSpanInViewBuffer(bestFilterMatch.FilterSpan); var fullFilterText = model.GetCurrentTextInSnapshot(viewSpan, textSnapshot, endPoint: null); foreach (var completionRules in completionRulesList) { var shouldSoftSelect = completionRules.ShouldSoftSelectItem(GetExternallyUsableCompletionItem(bestFilterMatch), fullFilterText, triggerInfo); if (shouldSoftSelect == true) { return(false); } } // 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 (!MatchesFilterText(bestFilterMatch, fullFilterText, completionRulesList, triggerInfo, reason)) { return(false); } // There was either filter text, or this was a preselect match. In either case, we // can hard select this. return(true); }
private Model FilterModelInBackground( Model model, int id, SnapshotPoint caretPosition, bool recheckCaretPosition, bool dismissIfEmptyAllowed, CompletionFilterReason filterReason) { using (Logger.LogBlock(FunctionId.Completion_ModelComputation_FilterModelInBackground, CancellationToken.None)) { return FilterModelInBackgroundWorker(model, id, caretPosition, recheckCaretPosition, dismissIfEmptyAllowed, filterReason); } }
private Model FilterModelInBackground( Model model, int id, SnapshotPoint caretPosition, bool recheckCaretPosition, bool dismissIfEmptyAllowed, CompletionFilterReason filterReason) { using (Logger.LogBlock(FunctionId.Completion_ModelComputation_FilterModelInBackground, CancellationToken.None)) { return(FilterModelInBackgroundWorker(model, id, caretPosition, recheckCaretPosition, dismissIfEmptyAllowed, filterReason)); } }
private Model FilterModelInBackground( Model model, int id, SnapshotPoint caretPosition, CompletionFilterReason filterReason, ImmutableDictionary <CompletionItemFilter, bool> filterState) { using (Logger.LogBlock(FunctionId.Completion_ModelComputation_FilterModelInBackground, CancellationToken.None)) { return(FilterModelInBackgroundWorker( model, id, caretPosition, filterReason, filterState)); } }
private Model HandleDeletionTrigger( Model model, CompletionFilterReason filterReason, List <FilterResult> filterResults) { if (filterReason == CompletionFilterReason.Insertion && !filterResults.Any(r => r.MatchedFilterText)) { // 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. // The thought process is as follows: we aggressively brough up completion // 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); } FilterResult?bestFilterResult = null; int matchCount = 0; foreach (var currentFilterResult in filterResults.Where(r => r.MatchedFilterText)) { if (bestFilterResult == null || IsBetterDeletionMatch(currentFilterResult, bestFilterResult.Value)) { // We had no best result yet, so this is now our best result. bestFilterResult = currentFilterResult; matchCount++; } } // 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. var filteredItems = filterResults.Select(r => r.CompletionItem).AsImmutable(); model = model.WithFilteredItems(filteredItems); if (bestFilterResult != null) { return(model.WithSelectedItem(bestFilterResult.Value.CompletionItem) .WithHardSelection(true) .WithIsUnique(matchCount == 1)); } else { return(model.WithHardSelection(false) .WithIsUnique(false)); } }
public bool? IsBetterFilterMatch(CompletionItem item1, CompletionItem item2, string filterText, CompletionTriggerInfo triggerInfo, CompletionFilterReason filterReason) { var match1 = _patternMatcher.MatchPatternFirstOrNullable( _completionService.GetCultureSpecificQuirks(item1.FilterText), _completionService.GetCultureSpecificQuirks(filterText)); var match2 = _patternMatcher.MatchPatternFirstOrNullable( _completionService.GetCultureSpecificQuirks(item2.FilterText), _completionService.GetCultureSpecificQuirks(filterText)); if (match1 != null && match2 != null) { var result = match1.Value.CompareTo(match2.Value); if (result != 0) { return result < 0; } } else if (match1 != null) { return true; } else if (match2 != null) { return false; } // If they both seemed just as good, but they differ on preselection, then // item1 is better if it is preselected, otherwise it it worse. if (item1.Preselect != item2.Preselect) { return item1.Preselect; } // Prefer things with a keyword glyph, if the filter texts are the same. if (item1.Glyph != item2.Glyph && item1.FilterText == item2.FilterText) { return item1.Glyph == Glyph.Keyword; } // They matched on everything, including preselection values. Item1 is better if it // has a lower MRU index. var item1MRUIndex = _completionService.GetMRUIndex(item1); var item2MRUIndex = _completionService.GetMRUIndex(item2); // The one with the lower index is the better one. return item1MRUIndex < item2MRUIndex; }
public void FilterModel( CompletionFilterReason filterReason, ImmutableDictionary <CompletionItemFilter, bool> filterState) { AssertIsForeground(); var caretPosition = GetCaretPointInViewBuffer(); // Use an interlocked increment so that reads by existing filter tasks will see the // change. Interlocked.Increment(ref _filterId); var localId = _filterId; Computation.ChainTaskAndNotifyControllerWhenFinished( model => FilterModelInBackground( model, localId, caretPosition, filterReason, filterState)); }
public void IdentifyBestMatchAndFilterToAllItems(CompletionFilterReason filterReason, bool recheckCaretPosition = false, bool dismissIfEmptyAllowed = true) { AssertIsForeground(); var caretPosition = GetCaretPointInViewBuffer(); // Use an interlocked increment so that reads by existing filter tasks will see the // change. Interlocked.Increment(ref _filterId); var localId = _filterId; Computation.ChainTaskAndNotifyControllerWhenFinished(model => { var filteredModel = FilterModelInBackground(model, localId, caretPosition, recheckCaretPosition, dismissIfEmptyAllowed, filterReason); return filteredModel != null ? filteredModel.WithFilteredItems(filteredModel.TotalItems).WithSelectedItem(filteredModel.SelectedItem) : null; }); }
private bool MatchesFilterText( CompletionItem item, string filterText, IList <ICompletionRules> completionRulesList, CompletionTriggerInfo triggerInfo, CompletionFilterReason reason) { foreach (var completionRule in completionRulesList) { var result = completionRule.MatchesFilterText(item, filterText, triggerInfo, reason); if (result.HasValue) { return(result.Value); } } return(false); }
internal static bool IsHardSelection( string fullFilterText, CompletionTriggerKind initialTriggerKind, RoslynCompletionItem bestFilterMatch, CompletionHelper completionHelper, CompletionFilterReason filterReason, ImmutableArray <string> recentItems, bool useSuggestionMode) { if (bestFilterMatch == null || useSuggestionMode) { 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. var shouldSoftSelect = ShouldSoftSelectItem(bestFilterMatch, fullFilterText); if (shouldSoftSelect) { return(false); } // 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)) { return(false); } // There was either filter text, or this was a preselect match. In either case, we // can hard select this. return(true); }
public void IdentifyBestMatchAndFilterToAllItems(CompletionFilterReason filterReason, bool recheckCaretPosition = false, bool dismissIfEmptyAllowed = true) { AssertIsForeground(); var caretPosition = GetCaretPointInViewBuffer(); // Use an interlocked increment so that reads by existing filter tasks will see the // change. Interlocked.Increment(ref _filterId); var localId = _filterId; Computation.ChainTaskAndNotifyControllerWhenFinished(model => { var filteredModel = FilterModelInBackground(model, localId, caretPosition, recheckCaretPosition, dismissIfEmptyAllowed, filterReason); return(filteredModel != null ? filteredModel.WithFilteredItems(filteredModel.TotalItems).WithSelectedItem(filteredModel.SelectedItem) : null); }); }
private static bool MatchesFilterText( CompletionHelper helper, CompletionItem item, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray <string> recentItems) { // 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.BackspaceOrDelete && trigger.Kind == CompletionTriggerKind.Deletion) { return(item.FilterText.GetCaseInsensitivePrefixLength(filterText) > 0); } return(helper.MatchesFilterText(item, filterText, trigger, recentItems)); }
/// <summary> /// Returns true if the completion item matches the filter text typed so far. Returns 'true' /// iff the completion item matches and should be included in the filtered completion /// results, or false if it should not be. /// </summary> public virtual bool MatchesFilterText(CompletionItem item, string filterText, CompletionTriggerInfo triggerInfo, CompletionFilterReason filterReason) { // 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.Preselect || _completionService.GetMRUIndex(item) < 0) { return true; } } if (IsAllDigits(filterText)) { // The user is just typing a number. We never want this to match against // anything we would put in a completion list. return false; } return GetMatch(item, filterText) != null; }
/// <summary> /// Returns true if the completion item matches the filter text typed so far. Returns 'true' /// iff the completion item matches and should be included in the filtered completion /// results, or false if it should not be. /// </summary> public virtual bool MatchesFilterText(CompletionItem item, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray<string> recentItems = default(ImmutableArray<string>)) { // 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.Preselect || (!recentItems.IsDefault && GetRecentItemIndex(recentItems, item) < 0)) { return true; } } if (IsAllDigits(filterText)) { // The user is just typing a number. We never want this to match against // anything we would put in a completion list. return false; } return GetMatch(item, filterText) != null; }
internal static bool MatchesFilterText( CompletionHelper helper, RoslynCompletionItem item, string filterText, CompletionTriggerKind initialTriggerKind, CompletionFilterReason filterReason, ImmutableArray <string> recentItems) { // 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); } if (!recentItems.IsDefault && ItemManager.GetRecentItemIndex(recentItems, item) <= 0) { return(true); } } // 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)); }
private static bool IsBetterFilterMatch( CompletionHelper helper, CompletionItem item1, CompletionItem item2, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray <string> recentItems) { // For the deletion we bake in the core logic for how betterness should work. // This way deletion feels the same across all languages that opt into deletion // as a completion trigger. if (filterReason == CompletionFilterReason.BackspaceOrDelete) { var prefixLength1 = item1.FilterText.GetCaseInsensitivePrefixLength(filterText); var prefixLength2 = item2.FilterText.GetCaseInsensitivePrefixLength(filterText); // Prefer the item that matches a longer prefix of the filter text. if (prefixLength1 > prefixLength2) { return(true); } // If the lengths are the same, prefer the one with the higher match priority. // But only if it's an item that would have been hard selected. We don't want // to aggressively select an item that was only going to be softly offered. var item1Priority = item1.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection ? item1.Rules.MatchPriority : MatchPriority.Default; var item2Priority = item2.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection ? item2.Rules.MatchPriority : MatchPriority.Default; if (item1Priority > item2Priority) { return(true); } return(false); } return(helper.IsBetterFilterMatch(item1, item2, filterText, trigger, recentItems)); }
public bool? MatchesFilterText(CompletionItem item, string filterText, CompletionTriggerInfo triggerInfo, CompletionFilterReason filterReason) { // 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.Preselect || _completionService.GetMRUIndex(item) < 0) { return true; } } if (IsAllDigits(filterText)) { // The user is just typing a number. We never want this to match against // anything we would put in a completion list. return false; } var match = _patternMatcher.MatchPatternFirstOrNullable( _completionService.GetCultureSpecificQuirks(item.FilterText), _completionService.GetCultureSpecificQuirks(filterText)); return match != null; }
private bool MatchesFilterText( CompletionItem item, string filterText, IList<ICompletionRules> completionRulesList, CompletionTriggerInfo triggerInfo, CompletionFilterReason reason) { foreach (var completionRule in completionRulesList) { var result = completionRule.MatchesFilterText(item, filterText, triggerInfo, reason); if (result.HasValue) { return result.Value; } } return false; }
/// <summary> /// Returns true if item1 is a better completion item than item2 given the provided filter /// text, or false if it is not better. /// </summary> public virtual bool IsBetterFilterMatch(CompletionItem item1, CompletionItem item2, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray <string> recentItems = default(ImmutableArray <string>)) { var match1 = GetMatch(item1, filterText); var match2 = GetMatch(item2, filterText); if (match1 != null && match2 != null) { var result = CompareMatches(match1.Value, match2.Value, item1, item2); if (result != 0) { return(result < 0); } } else if (match1 != null) { return(true); } else if (match2 != null) { return(false); } // If they both seemed just as good, but they differ on preselection, then // item1 is better if it is preselected, otherwise it is worse. if (item1.Rules.Preselect != item2.Rules.Preselect) { return(item1.Rules.Preselect); } // Prefer things with a keyword tag, if the filter texts are the same. if (!TagsEqual(item1, item2) && item1.FilterText == item2.FilterText) { return(IsKeywordItem(item1)); } // They matched on everything, including preselection values. Item1 is better if it // has a lower MRU index. if (!recentItems.IsDefault) { var item1MRUIndex = GetRecentItemIndex(recentItems, item1); var item2MRUIndex = GetRecentItemIndex(recentItems, item2); // The one with the lower index is the better one. return(item1MRUIndex < item2MRUIndex); } return(false); }
private FilteredCompletionModel HandleNormalFiltering( Func <ImmutableArray <RoslynCompletionItem>, string, ImmutableArray <RoslynCompletionItem> > filterMethod, string filterText, ImmutableArray <CompletionFilterWithState> filters, CompletionTriggerKind initialRoslynTriggerKind, CompletionFilterReason filterReason, char typeChar, List <ExtendedFilterResult> itemsInList, ImmutableArray <CompletionItemWithHighlight> highlightedList, CompletionHelper completionHelper, 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. var matchingItems = itemsInList.Where(r => r.FilterResult.MatchedFilterText) .Select(t => t.FilterResult.CompletionItem) .AsImmutable(); var chosenItems = filterMethod(matchingItems, filterText); var recentItems = _recentItemsManager.RecentItems; // Of the items the service returned, pick the one most recently committed var bestItem = GetBestCompletionItemBasedOnMRU(chosenItems, recentItems); VSCompletionItem uniqueItem = null; int selectedItemIndex = 0; // 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. if (bestItem != null) { selectedItemIndex = itemsInList.IndexOf(i => Equals(i.FilterResult.CompletionItem, bestItem)); if (selectedItemIndex > -1 && bestItem != null && matchingItems.Length == 1 && filterText.Length > 0) { uniqueItem = highlightedList[selectedItemIndex].CompletionItem; } } // If we don't have a best completion item yet, then pick the first item from the list. var bestOrFirstCompletionItem = bestItem ?? itemsInList.First().FilterResult.CompletionItem; // Check that it is a filter symbol. We can be called for a non-filter symbol. if (filterReason == CompletionFilterReason.Insertion && !IsPotentialFilterCharacter(typeChar) && !string.IsNullOrEmpty(filterText) && !Helpers.IsFilterCharacter(bestOrFirstCompletionItem, typeChar, filterText)) { return(null); } bool isHardSelection = IsHardSelection( filterText, initialRoslynTriggerKind, bestOrFirstCompletionItem, completionHelper, filterReason, recentItems, hasSuggestedItemOptions); var updateSelectionHint = isHardSelection ? UpdateSelectionHint.Selected : UpdateSelectionHint.SoftSelected; // If no items found above, select the first item. if (selectedItemIndex == -1) { selectedItemIndex = 0; } return(new FilteredCompletionModel( highlightedList, selectedItemIndex, filters, updateSelectionHint, centerSelection: true, uniqueItem)); }
/// <summary> /// Returns true if the completion item matches the filter text typed so far. Returns 'true' /// iff the completion item matches and should be included in the filtered completion /// results, or false if it should not be. /// </summary> public virtual bool MatchesFilterText(CompletionItem item, string filterText, CompletionTriggerInfo triggerInfo, CompletionFilterReason filterReason) { // 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.Preselect || _completionService.GetMRUIndex(item) < 0) { return(true); } } if (IsAllDigits(filterText)) { // The user is just typing a number. We never want this to match against // anything we would put in a completion list. return(false); } var patternMatcher = this.GetPatternMatcher(_completionService.GetCultureSpecificQuirks(filterText)); var match = patternMatcher.GetFirstMatch(_completionService.GetCultureSpecificQuirks(item.FilterText)); return(match != null); }
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 = MatchesFilterText(helper, 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 || IsBetterFilterMatch(helper, 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 bool IsHardSelection( Model model, PresentationItem bestFilterMatch, ITextSnapshot textSnapshot, CompletionHelper completionHelper, CompletionTrigger trigger, CompletionFilterReason reason) { if (model.SuggestionModeItem != null) { return bestFilterMatch != null && bestFilterMatch.Item.DisplayText == model.SuggestionModeItem.Item.DisplayText; } if (bestFilterMatch == null || model.UseSuggestionMode) { 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. var viewSpan = model.GetViewBufferSpan(bestFilterMatch.Item.Span); var fullFilterText = model.GetCurrentTextInSnapshot(viewSpan, textSnapshot, endPoint: null); var shouldSoftSelect = ShouldSoftSelectItem(bestFilterMatch.Item, fullFilterText, trigger); if (shouldSoftSelect) { return false; } // 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 (!MatchesFilterText(completionHelper, bestFilterMatch.Item, fullFilterText, trigger, reason, this.Controller.GetRecentItems())) { return false; } // There was either filter text, or this was a preselect match. In either case, we // can hard select this. return true; }
private Model HandleNormalFiltering( Model model, CompletionFilterReason filterReason, ITextSnapshot textSnapshot, Document document, CompletionHelper helper, ImmutableArray<string> recentItems, string filterText, List<FilterResult> filterResults) { // 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. var service = this.Controller.GetCompletionService(); var matchingCompletionItems = filterResults.Where(r => r.MatchedFilterText) .Select(t => t.PresentationItem.Item) .AsImmutable(); var chosenItems = service.ChooseBestItems(document, matchingCompletionItems, filterText); // Of the items the service returned, pick the one most recently committed var bestCompletionItem = GetBestCompletionItemBasedOnMRU(chosenItems, recentItems); // If we don't have a best completion item yet, then pick the first item from the list. var bestOrFirstCompletionItem = bestCompletionItem ?? filterResults.First().PresentationItem.Item; var bestOrFirstPresentationItem = filterResults.Where( r => r.PresentationItem.Item == bestOrFirstCompletionItem).First().PresentationItem; var hardSelection = IsHardSelection( model, bestOrFirstPresentationItem, textSnapshot, helper, filterReason); // 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. var matchingItemCount = matchingCompletionItems.Length; var isUnique = bestCompletionItem != null && matchingItemCount == 1 && filterText.Length > 0; var result = model.WithFilteredItems(filterResults.Select(r => r.PresentationItem).AsImmutable()) .WithSelectedItem(bestOrFirstPresentationItem) .WithHardSelection(hardSelection) .WithIsUnique(isUnique); return result; }
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)); }
/// <summary> /// Returns true if the completion item matches the filter text typed so far. Returns 'true' /// iff the completion item matches and should be included in the filtered completion /// results, or false if it should not be. /// </summary> public virtual bool MatchesFilterText(CompletionItem item, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray <string> recentItems = default(ImmutableArray <string>)) { // 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.Preselect || (!recentItems.IsDefault && GetRecentItemIndex(recentItems, item) < 0)) { return(true); } } if (IsAllDigits(filterText)) { // The user is just typing a number. We never want this to match against // anything we would put in a completion list. return(false); } return(GetMatch(item, filterText) != null); }
private bool IsBetterFilterMatch( CompletionItem item, CompletionItem bestItem, string filterText, ICompletionRules completionRules, CompletionTriggerInfo triggerInfo, CompletionFilterReason filterReason) { return completionRules.IsBetterFilterMatch(item, bestItem, filterText, triggerInfo, filterReason) ?? false; }
private void FilterToSomeOrAllItems(bool filterItems, bool dismissIfEmptyAllowed, CompletionFilterReason filterReason) { if (filterItems) { sessionOpt.FilterModel( filterReason, recheckCaretPosition: false, dismissIfEmptyAllowed: dismissIfEmptyAllowed, filterState: null); } else { sessionOpt.IdentifyBestMatchAndFilterToAllItems( filterReason, recheckCaretPosition: false, dismissIfEmptyAllowed: dismissIfEmptyAllowed); } }
/// <summary> /// Returns true if item1 is a better completion item than item2 given the provided filter /// text, or false if it is not better. /// </summary> public virtual bool IsBetterFilterMatch(CompletionItem item1, CompletionItem item2, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray<string> recentItems = default(ImmutableArray<string>)) { var match1 = GetMatch(item1, GetCultureSpecificQuirks(filterText)); var match2 = GetMatch(item2, GetCultureSpecificQuirks(filterText)); if (match1 != null && match2 != null) { var result = CompareMatches(match1.Value, match2.Value, item1, item2); if (result != 0) { return result < 0; } } else if (match1 != null) { return true; } else if (match2 != null) { return false; } // If they both seemed just as good, but they differ on preselection, then // item1 is better if it is preselected, otherwise it is worse. if (item1.Rules.Preselect != item2.Rules.Preselect) { return item1.Rules.Preselect; } // Prefer things with a keyword tag, if the filter texts are the same. if (!TagsEqual(item1, item2) && item1.FilterText == item2.FilterText) { return IsKeywordItem(item1); } // They matched on everything, including preselection values. Item1 is better if it // has a lower MRU index. if (!recentItems.IsDefault) { var item1MRUIndex = GetRecentItemIndex(recentItems, item1); var item2MRUIndex = GetRecentItemIndex(recentItems, item2); // The one with the lower index is the better one. return item1MRUIndex < item2MRUIndex; } return false; }
private static bool IsBetterFilterMatch( CompletionHelper helper, CompletionItem item1, CompletionItem item2, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray<string> recentItems) { // For the deletion we bake in the core logic for how betterness should work. // This way deletion feels the same across all languages that opt into deletion // as a completion trigger. if (filterReason == CompletionFilterReason.BackspaceOrDelete) { var prefixLength1 = item1.FilterText.GetCaseInsensitivePrefixLength(filterText); var prefixLength2 = item2.FilterText.GetCaseInsensitivePrefixLength(filterText); // Prefer the item that matches a longer prefix of the filter text. if (prefixLength1 > prefixLength2) { return true; } // If the lengths are the same, prefer the one with the higher match priority. // But only if it's an item that would have been hard selected. We don't want // to aggressively select an item that was only going to be softly offered. var item1Priority = item1.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection ? item1.Rules.MatchPriority : MatchPriority.Default; var item2Priority = item2.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection ? item2.Rules.MatchPriority : MatchPriority.Default; if (item1Priority > item2Priority) { return true; } return false; } return helper.IsBetterFilterMatch(item1, item2, filterText, trigger, recentItems); }
private static bool MatchesFilterText( CompletionHelper helper, CompletionItem item, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray<string> recentItems) { // 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.BackspaceOrDelete && trigger.Kind == CompletionTriggerKind.Deletion) { return item.FilterText.GetCaseInsensitivePrefixLength(filterText) > 0; } return helper.MatchesFilterText(item, filterText, trigger, recentItems); }
private static Model HandleAllItemsFilteredOut( Model model, CompletionFilterReason filterReason, bool dismissIfEmptyAllowed) { if (dismissIfEmptyAllowed && model.DismissIfEmpty && filterReason == CompletionFilterReason.TypeChar) { // 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. return null; } if (model.FilterState?.Values.Any(b => b) == true) { // 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. return model.WithFilteredItems(ImmutableArray<PresentationItem>.Empty) .WithFilterText("") .WithHardSelection(false) .WithIsUnique(false); } else { // 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. return model.WithHardSelection(false) .WithIsUnique(false); } }
private bool MatchesFilterText( CompletionItem item, string filterText, ICompletionRules completionRules, CompletionTriggerInfo triggerInfo, CompletionFilterReason reason) { return completionRules.MatchesFilterText(item, filterText, triggerInfo, reason) ?? false; }
private static bool MatchesFilterText( CompletionHelper helper, CompletionItem item, string filterText, CompletionTrigger trigger, CompletionFilterReason filterReason, ImmutableArray<string> recentItems) { // 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.BackspaceOrDelete && trigger.Kind == 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; } if (!recentItems.IsDefault && GetRecentItemIndex(recentItems, item) < 0) { return true; } } if (filterText.Length > 0 && IsAllDigits(filterText)) { // The user is just typing a number. We never want this to match against // anything we would put in a completion list. return false; } return helper.MatchesFilterText(item, filterText, CultureInfo.CurrentCulture); }
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); // If the user was typing a number, then immediately dismiss completion. var filterTextStartsWithANumber = filterText.Length > 0 && char.IsNumber(filterText[0]); if (filterTextStartsWithANumber) { 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.Item, filterState)) { continue; } // Check if the item matches the filter text typed so far. var matchesFilterText = MatchesFilterText(helper, currentItem.Item, 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, filterReason, textSnapshot, document, helper, recentItems, filterText, filterResults); }
private Model FilterModelInBackgroundWorker( Model model, int id, SnapshotPoint caretPosition, bool recheckCaretPosition, bool dismissIfEmptyAllowed, CompletionFilterReason filterReason) { 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 (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; } } 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 (ItemIsFilteredOut(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 { 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, 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)); }
/// <summary> /// Returns true if item1 is a better completion item than item2 given the provided filter /// text, or false if it is not better. /// </summary> public virtual bool IsBetterFilterMatch(CompletionItem item1, CompletionItem item2, string filterText, CompletionTriggerInfo triggerInfo, CompletionFilterReason filterReason) { var patternMatcher = GetPatternMatcher(_completionService.GetCultureSpecificQuirks(filterText)); var match1 = patternMatcher.GetFirstMatch(_completionService.GetCultureSpecificQuirks(item1.FilterText)); var match2 = patternMatcher.GetFirstMatch(_completionService.GetCultureSpecificQuirks(item2.FilterText)); if (match1 != null && match2 != null) { var result = CompareMatches(match1.Value, match2.Value, item1, item2); if (result != 0) { return(result < 0); } } else if (match1 != null) { return(true); } else if (match2 != null) { return(false); } // If they both seemed just as good, but they differ on preselection, then // item1 is better if it is preselected, otherwise it it worse. if (item1.Preselect != item2.Preselect) { return(item1.Preselect); } // Prefer things with a keyword glyph, if the filter texts are the same. if (item1.Glyph != item2.Glyph && item1.FilterText == item2.FilterText) { return(item1.Glyph == Glyph.Keyword); } // They matched on everything, including preselection values. Item1 is better if it // has a lower MRU index. var item1MRUIndex = _completionService.GetMRUIndex(item1); var item2MRUIndex = _completionService.GetMRUIndex(item2); // The one with the lower index is the better one. return(item1MRUIndex < item2MRUIndex); }
private bool IsHardSelection( Model model, CompletionItem bestFilterMatch, SnapshotPoint caretPosition, CompletionHelper completionHelper, CompletionFilterReason reason) { if (bestFilterMatch == null || model.UseSuggestionMode) { return false; } var textSnapshot = caretPosition.Snapshot; // 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. var itemViewSpan = model.GetViewBufferSpan(bestFilterMatch.Span); var fullFilterText = model.GetCurrentTextInSnapshot(itemViewSpan, textSnapshot, endPoint: null); var trigger = model.Trigger; var shouldSoftSelect = ShouldSoftSelectItem(bestFilterMatch, fullFilterText, trigger); if (shouldSoftSelect) { return false; } // 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 (!MatchesFilterText(completionHelper, bestFilterMatch, fullFilterText, trigger, reason, this.Controller.GetRecentItems())) { return false; } // Switch to soft selection, if user moved caret to the start of a non-empty filter span. // This prevents commiting if user types a commit character at this position later, but still has the list if user types filter character // i.e. blah| -> |blah -> !|blah // We want the filter span non-empty because we still want hard selection in the following case: // // A a = new | if (caretPosition == itemViewSpan.TextSpan.Start && itemViewSpan.TextSpan.Length > 0) { return false; } // There was either filter text, or this was a preselect match. In either case, we // can hard select this. return true; }