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));
            }
Exemplo n.º 5
0
            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));
                }
            }
Exemplo n.º 8
0
            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);
            }
Exemplo n.º 9
0
 private bool MatchesFilterText(
     CompletionItem item,
     string filterText,
     ICompletionRules completionRules,
     CompletionTriggerInfo triggerInfo,
     CompletionFilterReason reason)
 {
     return(completionRules.MatchesFilterText(item, filterText, triggerInfo, reason) ?? false);
 }
Exemplo n.º 10
0
 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));
     }
 }
            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 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);
            }
Exemplo n.º 22
0
        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);
        }
Exemplo n.º 23
0
            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));
            }
Exemplo n.º 25
0
        /// <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;
        }
Exemplo n.º 26
0
        /// <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;
        }
Exemplo n.º 27
0
        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;
            }
Exemplo n.º 31
0
        /// <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);
        }
Exemplo n.º 32
0
        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));
        }
Exemplo n.º 33
0
        /// <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));
            }
Exemplo n.º 38
0
        /// <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;
 }
Exemplo n.º 40
0
 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);
     }
 }
Exemplo n.º 41
0
        /// <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);
            }
Exemplo n.º 49
0
            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);
            }
Exemplo n.º 50
0
            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));
            }
Exemplo n.º 51
0
        /// <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;
            }