private Model SetModelSelectedItemInBackground(
                Model model,
                Func<Model, CompletionItem> selector)
            {
                if (model == null)
                {
                    return null;
                }

                // Switch to hard selection.
                var selectedItem = selector(model);
                Contract.ThrowIfFalse(model.TotalItems.Contains(selectedItem) || model.DefaultBuilder == selectedItem);

                if (model.FilteredItems.Contains(selectedItem))
                {
                    // Easy case, just set the selected item that's already in the filtered items
                    // list.

                    return model.WithSelectedItem(selector(model))
                                .WithHardSelection(true);
                }
                else
                {
                    // Item wasn't in the filtered list, so we need to recreate the filtered list
                    // with that item in it.
                    var filteredItemsSet = new HashSet<CompletionItem>(model.FilteredItems,
                        ReferenceEqualityComparer.Instance);

                    var newFilteredItems = model.TotalItems.Where(
                        i => filteredItemsSet.Contains(i) || i == selectedItem).ToList();
                    return model.WithFilteredItems(newFilteredItems)
                                .WithSelectedItem(selectedItem)
                                .WithHardSelection(true);
                }
            }
        private bool IsCaretOutsideItemBounds(
            Model model,
            SnapshotPoint caretPoint,
            CompletionItem item,
            Dictionary<TextSpan, string> textSpanToText,
            Dictionary<TextSpan, ViewTextSpan> textSpanToViewSpan)
        {
            // Easy first check.  See if the caret point is before the start of the item.
            if (!textSpanToViewSpan.TryGetValue(item.Span, out var filterSpanInViewBuffer))
            {
                filterSpanInViewBuffer = model.GetViewBufferSpan(item.Span);
                textSpanToViewSpan[item.Span] = filterSpanInViewBuffer;
            }

            if (caretPoint < filterSpanInViewBuffer.TextSpan.Start)
            {
                return true;
            }

            var textSnapshot = caretPoint.Snapshot;

            var currentText = model.GetCurrentTextInSnapshot(item.Span, textSnapshot, textSpanToText);
            var currentTextSpan = new TextSpan(filterSpanInViewBuffer.TextSpan.Start, currentText.Length);

            return !currentTextSpan.IntersectsWith(caretPoint);
        }
 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);
     }
 }
        internal bool IsCaretOutsideAllItemBounds(Model model, SnapshotPoint caretPoint)
        {
            var textSpanToTextCache = new Dictionary<TextSpan, string>();
            var textSpanToViewSpanCache = new Dictionary<TextSpan, ViewTextSpan>();

            foreach (var item in model.TotalItems)
            {
                if (!IsCaretOutsideItemBounds(model, caretPoint, item, textSpanToTextCache, textSpanToViewSpanCache))
                {
                    return false;
                }
            }

            return true;
        }
Beispiel #5
0
        private void Commit(PresentationItem item, Model model, char? commitChar)
        {
            AssertIsForeground();

            // We could only be called if we had a model at this point.
            Contract.ThrowIfNull(model);

            // Now that we've captured the model at this point, we can stop ourselves entirely.  
            // This is also desirable as we may have reentrancy problems when we call into 
            // custom commit completion providers.  I.e. if the custom provider moves the caret,
            // then we do not want to process that move as it may put us into an unexpected state.
            //
            // TODO(cyrusn): We still have a general reentrancy problem where calling into a custom
            // commit provider (or just calling into the editor) may cause something to call back
            // into us.  However, for now, we just hope that no such craziness will occur.
            this.StopModelComputation();

            Commit(item, model, commitChar, CancellationToken.None);
        }
            private Model SetModelBuilderStateInBackground(
                Model model,
                bool includeBuilder)
            {
                if (model == null)
                {
                    return null;
                }

                // We want to soft select if the user is switching the builder on, or if we were
                // already in soft select mode.
                var softSelect = includeBuilder || model.IsSoftSelection;

                // If the selected item is the builder, select the first filtered item instead.
                if (model.SelectedItem == model.DefaultSuggestionModeItem)
                {
                    return model.WithSelectedItem(model.FilteredItems.First())
                                .WithHardSelection(!softSelect);
                }

                return model.WithHardSelection(!softSelect).WithUseSuggestionMode(includeBuilder);
            }
            private Model SetModelBuilderStateInBackground(
                Model model,
                bool includeBuilder)
            {
                if (model == null)
                {
                    return null;
                }

                // We want to soft select if the user is switching the builder on, or if we were
                // already in soft select mode.
                var softSelect = includeBuilder || model.IsSoftSelection;

                if (model.SelectedItem == model.SuggestionModeItem &&
                    !includeBuilder)
                {
                    // Use had the builder selected, but turned off the builder.  Switch to the
                    // first filtered item.
                    model = model.WithSelectedItem(model.FilteredItems.First());
                }

                return model.WithHardSelection(!softSelect)
                            .WithUseSuggestionMode(includeBuilder);
            }
            private static ImmutableDictionary<CompletionItemFilter, bool> ComputeEffectiveFilterItemState(Model model)
            {
                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))
                    {
                        return null;
                    }
                }

                return filterState;
            }
            private Model FilterModelInBackgroundWorker(
                Model model,
                int id,
                SnapshotPoint caretPosition,
                bool recheckCaretPosition,
                bool dismissIfEmptyAllowed,
                CompletionFilterReason filterReason)
            {
                if (model == null)
                {
                    return null;
                }

                // We want to dismiss the session if the caret ever moved outside our bounds.
                // Do this before we check the _filterId.  We don't want this work to not happen
                // just because the user typed more text and added more filter items.
                if (recheckCaretPosition && Controller.IsCaretOutsideAllItemBounds(model, caretPosition))
                {
                    return null;
                }

                if (id != _filterId)
                {
                    return model;
                }

                var textSnapshot = caretPosition.Snapshot;
                var textSpanToText = new Dictionary<TextSpan, string>();

                var document = this.Controller.GetDocument();
                var helper = this.Controller.GetCompletionHelper();

                var recentItems = this.Controller.GetRecentItems();

                var filterResults = new List<FilterResult>();

                var filterText = model.GetCurrentTextInSnapshot(
                    model.OriginalList.Span, textSnapshot, textSpanToText);

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

                var effectiveFilterItemState = ComputeEffectiveFilterItemState(model);
                foreach (var currentItem in model.TotalItems)
                {
                    // Check if something new has happened and there's a later on filter operation
                    // in the chain.  If so, there's no need for us to do any more work (as it will
                    // just be superceded by the later work).
                    if (id != _filterId)
                    {
                        return model;
                    }

                    if (ItemIsFilteredOut(currentItem, effectiveFilterItemState))
                    {
                        continue;
                    }

                    // Check if the item matches the filter text typed so far.
                    var matchesFilterText = MatchesFilterText(helper, currentItem, filterText, model.Trigger, filterReason, recentItems);

                    if (matchesFilterText)
                    {
                        filterResults.Add(new FilterResult(
                            currentItem, filterText, matchedFilterText: true));
                    }
                    else
                    {
                        if (filterText.Length <= 1)
                        {
                            // Even though the rule provider didn't match this, we'll still include it
                            // since we want to allow a user typing a single character and seeing all
                            // possibly completions.
                            filterResults.Add(new FilterResult(
                                currentItem, filterText, matchedFilterText: false));
                        }
                    }
                }

                model = model.WithFilterText(filterText);

                // If no items matched the filter text then determine what we should do.
                if (filterResults.Count == 0)
                {
                    return HandleAllItemsFilteredOut(model, filterReason, dismissIfEmptyAllowed);
                }

                // If this was deletion, then we control the entire behavior of deletion
                // ourselves.
                if (model.Trigger.Kind == CompletionTriggerKind.Deletion)
                {
                    return HandleDeletionTrigger(model, filterResults);
                }

                return HandleNormalFiltering(
                    model, document, filterReason, caretPosition,
                    helper, recentItems, filterText, filterResults);
            }
            private Model FilterModelInBackgroundWorker(
                Model model,
                int id,
                SnapshotPoint caretPosition,
                bool recheckCaretPosition,
                bool dismissIfEmptyAllowed,
                CompletionFilterReason filterReason)
            {
                if (model == null)
                {
                    return null;
                }

                var filterState = model.FilterState;

                // If all the filters are on, or all the filters are off then we don't actually 
                // need to filter.
                if (filterState != null)
                {
                    if (filterState.Values.All(b => b) ||
                        filterState.Values.All(b => !b))
                    {
                        filterState = null;
                    }
                }

                // We want to dismiss the session if the caret ever moved outside our bounds.
                if (recheckCaretPosition && Controller.IsCaretOutsideAllItemBounds(model, caretPosition))
                {
                    return null;
                }

                if (id != _filterId)
                {
                    return model;
                }

                var textSnapshot = caretPosition.Snapshot;
                var allFilteredItems = new List<PresentationItem>();
                var textSpanToText = new Dictionary<TextSpan, string>();
                var helper = this.Controller.GetCompletionHelper();

                // isUnique tracks if there is a single 
                bool? isUnique = null;
                PresentationItem bestFilterMatch = null;
                bool filterTextIsPotentialIdentifier = false;

                var recentItems = this.Controller.GetRecentItems();

                var itemToFilterText = new Dictionary<CompletionItem, string>();
                model = model.WithCompletionItemToFilterText(itemToFilterText);

                foreach (var currentItem in model.TotalItems)
                {
                    // Check if something new has happened and there's a later on filter operation
                    // in the chain.  If so, there's no need for us to do any more work (as it will
                    // just be superceded by the later work).
                    if (id != _filterId)
                    {
                        return model;
                    }

                    // We may have wrapped some items in the list in DescriptionModifying items,
                    // but we should use the actual underlying items when filtering. That way
                    // our rules can access the underlying item's provider.

                    if (ItemIsFilteredOut(currentItem.Item, filterState))
                    {
                        continue;
                    }

                    var filterText = model.GetCurrentTextInSnapshot(currentItem.Item.Span, textSnapshot, textSpanToText);
                    var matchesFilterText = 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 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 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 Model HandleDeletionTrigger(Model model, List<FilterResult> filterResults)
            {
                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.PresentationItem).AsImmutable();
                model = model.WithFilteredItems(filteredItems);

                if (bestFilterResult != null)
                {
                    return model.WithSelectedItem(bestFilterResult.Value.PresentationItem)
                                .WithHardSelection(true)
                                .WithIsUnique(matchCount == 1);
                }
                else
                {
                    return model.WithHardSelection(false)
                                .WithIsUnique(false);
                }
            }
            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;
            }
Beispiel #16
0
        private void Commit(
            CompletionItem item, Model model, char? commitChar,
            ITextSnapshot initialTextSnapshot, Action nextHandler)
        {
            AssertIsForeground();

            // We could only be called if we had a model at this point.
            Contract.ThrowIfNull(model);

            // Now that we've captured the model at this point, we can stop ourselves entirely.
            // This is also desirable as we may have reentrancy problems when we call into
            // custom commit completion providers.  I.e. if the custom provider moves the caret,
            // then we do not want to process that move as it may put us into an unexpected state.
            //
            // TODO(cyrusn): We still have a general reentrancy problem where calling into a custom
            // commit provider (or just calling into the editor) may cause something to call back
            // into us.  However, for now, we just hope that no such craziness will occur.
            this.DismissSessionIfActive();

            CompletionChange completionChange;
            using (var transaction = CaretPreservingEditTransaction.TryCreate(
                EditorFeaturesResources.IntelliSense, TextView, _undoHistoryRegistry, _editorOperationsFactoryService))
            {
                if (transaction == null)
                {
                    // This text buffer has no undo history and has probably been unmapped.
                    // (Workflow unmaps its projections when losing focus (such as double clicking the completion list)).
                    // Bail on committing completion because we won't be able to find a Document to update either.

                    return;
                }

                // We want to merge with any of our other programmatic edits (e.g. automatic brace completion)
                transaction.MergePolicy = AutomaticCodeChangeMergePolicy.Instance;

                var provider = GetCompletionProvider(item) as ICustomCommitCompletionProvider;
                if (provider != null)
                {
                    provider.Commit(item, this.TextView, this.SubjectBuffer, model.TriggerSnapshot, commitChar);
                }
                else
                {
                    // Right before calling Commit, we may have passed the commitChar through to the
                    // editor.  That was so that undoing completion will get us back to the state we
                    // we would be in if completion had done nothing.  However, now that we're going
                    // to actually commit, we want to roll back to where we were before we pushed
                    // commit character into the buffer.  This has multiple benefits:
                    //
                    //   1) the buffer is in a state we expect it to be in.  i.e. we don't have to
                    //      worry about what might have happened (like brace-completion) when the
                    //      commit char was inserted.
                    //   2) after we commit the item, we'll pass the commit character again into the
                    //      buffer (unless the items asks us not to).  By doing this, we can make sure
                    //      that things like brace-completion or formatting trigger as we expect them
                    //      to.
                    var characterWasSentIntoBuffer = commitChar != null &&
                                                     initialTextSnapshot.Version.VersionNumber != this.SubjectBuffer.CurrentSnapshot.Version.VersionNumber;
                    if (characterWasSentIntoBuffer)
                    {
                        RollbackToBeforeTypeChar(initialTextSnapshot);
                    }

                    // Now, get the change the item wants to make.  Note that the change will be relative
                    // to the initial snapshot/document the item was triggered from.  We'll map that change
                    // forward, then apply it to our current snapshot.
                    var triggerDocument = model.TriggerDocument;
                    var triggerSnapshot = model.TriggerSnapshot;

                    var completionService = CompletionService.GetService(triggerDocument);
                    Contract.ThrowIfNull(completionService, nameof(completionService));

                    completionChange = completionService.GetChangeAsync(
                        triggerDocument, item, commitChar, CancellationToken.None).WaitAndGetResult(CancellationToken.None);
                    var textChange = completionChange.TextChange;

                    var triggerSnapshotSpan = new SnapshotSpan(triggerSnapshot, textChange.Span.ToSpan());
                    var mappedSpan = triggerSnapshotSpan.TranslateTo(
                        this.SubjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);

                    // Now actually make the text change to the document.
                    using (var textEdit = this.SubjectBuffer.CreateEdit(EditOptions.None, reiteratedVersionNumber: null, editTag: null))
                    {
                        var adjustedNewText = AdjustForVirtualSpace(textChange);

                        textEdit.Replace(mappedSpan.Span, adjustedNewText);
                        textEdit.Apply();
                    }

                    // adjust the caret position if requested by completion service
                    if (completionChange.NewPosition != null)
                    {
                        TextView.Caret.MoveTo(new SnapshotPoint(
                            this.SubjectBuffer.CurrentSnapshot, completionChange.NewPosition.Value));
                    }

                    // Now, pass along the commit character unless the completion item said not to
                    if (characterWasSentIntoBuffer && !completionChange.IncludesCommitCharacter)
                    {
                        nextHandler();
                    }

                    // If the insertion is long enough, the caret will scroll out of the visible area.
                    // Re-center the view.
                    this.TextView.Caret.EnsureVisible();
                }

                transaction.Complete();
            }

            // Let the completion rules know that this item was committed.
            this.MakeMostRecentItem(item.DisplayText);
        }
Beispiel #17
0
 private void CommitOnNonTypeChar(
     CompletionItem item, Model model)
 {
     Commit(item, model, commitChar: null, initialTextSnapshot: null, nextHandler: null);
 }
            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;
            }
Beispiel #19
0
        private void Commit(PresentationItem item, Model model, char? commitChar, CancellationToken cancellationToken)
        {
            var textChanges = ImmutableArray<TextChange>.Empty;

            // NOTE(cyrusn): It is intentional that we get the undo history for the
            // surface buffer and not the subject buffer.
            // There have been some watsons where the ViewBuffer hadn't been registered,
            // so use TryGetHistory instead.
            ITextUndoHistory undoHistory;
            _undoHistoryRegistry.TryGetHistory(this.TextView.TextBuffer, out undoHistory);

            using (var transaction = undoHistory?.CreateTransaction(EditorFeaturesResources.IntelliSense))
            {
                // We want to merge with any of our other programmatic edits (e.g. automatic brace completion)
                if (transaction != null)
                {
                    transaction.MergePolicy = AutomaticCodeChangeMergePolicy.Instance;
                }

                // Check if the provider wants to perform custom commit itself.  Otherwise we will
                // handle things.
                var provider = GetCompletionProvider(item.Item) as ICustomCommitCompletionProvider;
                if (provider == null)
                {
                    var viewBuffer = this.TextView.TextBuffer;
                    var commitDocument = this.SubjectBuffer.CurrentSnapshot.AsText().GetDocumentWithFrozenPartialSemanticsAsync(cancellationToken).WaitAndGetResult(cancellationToken);

                    // adjust commit item span foward to match current document that is passed to GetChangeAsync below
                    var commitItem = item.Item;
                    var currentItemSpan = GetCurrentItemSpan(commitItem, model);
                    commitItem = commitItem.WithSpan(currentItemSpan);

                    var completionService = CompletionService.GetService(commitDocument);
                    var commitChange = completionService.GetChangeAsync(commitDocument, commitItem, commitChar, cancellationToken).WaitAndGetResult(cancellationToken);
                    textChanges = commitChange.TextChanges;

                    // Use character based diffing here to avoid overwriting the commit character placed into the editor.
                    var editOptions = new EditOptions(new StringDifferenceOptions
                    {
                        DifferenceType = StringDifferenceTypes.Character,
                        IgnoreTrimWhiteSpace = EditOptions.DefaultMinimalChange.DifferenceOptions.IgnoreTrimWhiteSpace
                    });

                    // edit subject buffer (not view) because text changes are in terms of current document.
                    using (var textEdit = this.SubjectBuffer.CreateEdit(editOptions, reiteratedVersionNumber: null, editTag: null))
                    {
                        for (int iChange = 0; iChange < textChanges.Length; iChange++)
                        { 
                            var textChange = textChanges[iChange];
                            var isFirst = iChange == 0;
                            var isLast = iChange == textChanges.Length - 1;

                            // add commit char to end of last change if not already included 
                            if (isLast && !commitChange.IncludesCommitCharacter && commitChar.HasValue)
                            {
                                textChange = new TextChange(textChange.Span, textChange.NewText + commitChar.Value);
                            }

                            var currentSpan = new SnapshotSpan(this.SubjectBuffer.CurrentSnapshot, new Span(textChange.Span.Start, textChange.Span.Length));

                            // In order to play nicely with automatic brace completion, we need to 
                            // not touch the opening paren. We'll check our span and textchange 
                            // for ( and adjust them accordingly if we find them.

                            // all this is needed since we don't use completion set mechanism provided by VS but we implement everything ourselves.
                            // due to that, existing brace completion engine in editor that should take care of interaction between brace completion
                            // and intellisense doesn't work for us. so we need this kind of workaround to support it nicely.
                            bool textChanged;
                            string newText = textChange.NewText;

                            if (isFirst)
                            {
                                newText = AdjustFirstText(textChange);
                            }

                            if (isLast)
                            {
                                newText = AdjustLastText(newText, commitChar.GetValueOrDefault(), out textChanged);
                                currentSpan = AdjustLastSpan(currentSpan, commitChar.GetValueOrDefault(), textChanged);
                            }

                            var caretPoint = this.TextView.GetCaretPoint(this.SubjectBuffer);
                            var virtualCaretPoint = this.TextView.GetVirtualCaretPoint(this.SubjectBuffer);

                            if (caretPoint.HasValue && virtualCaretPoint.HasValue)
                            {
                                // TODO(dustinca): We need to call a different API here. TryMoveCaretToAndEnsureVisible might center within the view.
                                this.TextView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(caretPoint.Value));
                            }

                            caretPoint = this.TextView.GetCaretPoint(this.SubjectBuffer);

                            // Now that we're doing character level diffing, we need to move the caret to the end of 
                            // the span being replaced. Otherwise, we can replace M|ai with Main and wind up with 
                            // M|ain, since character based diffing makes that quite legit.
                            if (caretPoint.HasValue)
                            {
                                var endInSubjectBuffer = this.TextView.BufferGraph.MapDownToBuffer(currentSpan.End, PointTrackingMode.Positive, caretPoint.Value.Snapshot.TextBuffer, PositionAffinity.Predecessor);
                                if (caretPoint.Value < endInSubjectBuffer)
                                {
                                    this.TextView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(currentSpan.Snapshot.TextBuffer.CurrentSnapshot, currentSpan.End.Position));
                                }
                            }

                            textEdit.Replace(currentSpan, newText);
                        }

                        textEdit.Apply();
                    }

                    // adjust the caret position if requested by completion service
                    if (commitChange.NewPosition != null)
                    {
                        var target = new SnapshotPoint(this.SubjectBuffer.CurrentSnapshot, commitChange.NewPosition.Value);
                        this.TextView.TryMoveCaretToAndEnsureVisible(target);
                    }

                    // We've manipulated the caret position in order to generate the correct edit. However, 
                    // if the insertion is long enough, the caret will scroll out of the visible area.
                    // Re-center the view.
                    using (var textEdit = viewBuffer.CreateEdit(editOptions, reiteratedVersionNumber: null, editTag: null))
                    {
                        var caretPoint = this.TextView.GetCaretPoint(this.SubjectBuffer);
                        if (caretPoint.HasValue)
                        {
                            this.TextView.Caret.EnsureVisible();
                        }
                    }

                    transaction?.Complete();
                }
                else
                {
                    // Let the provider handle this.
                    provider.Commit(item.Item, this.TextView, this.SubjectBuffer, model.TriggerSnapshot, commitChar);
                    transaction?.Complete();
                }
            }

            var document = this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            var formattingService = document.GetLanguageService<IEditorFormattingService>();

            var commitCharTriggersFormatting = commitChar != null &&
                    (formattingService?.SupportsFormattingOnTypedCharacter(document, commitChar.GetValueOrDefault())
                     ?? false);

            if (formattingService != null && (item.Item.Rules.FormatOnCommit || commitCharTriggersFormatting))
            {
                // Formatting the completion item affected span is done as a separate transaction because this gives the user
                // the flexibility to undo the formatting but retain the changes associated with the completion item
                using (var formattingTransaction = _undoHistoryRegistry.GetHistory(this.TextView.TextBuffer).CreateTransaction(EditorFeaturesResources.IntelliSenseCommitFormatting))
                {
                    var caretPoint = this.TextView.GetCaretPoint(this.SubjectBuffer);
                    IList<TextChange> changes = null;

                    if (commitCharTriggersFormatting && caretPoint.HasValue)
                    {
                        // if the commit character is supported by formatting service, then let the formatting service
                        // find the appropriate range to format.
                        changes = formattingService.GetFormattingChangesAsync(document, commitChar.Value, caretPoint.Value.Position, cancellationToken).WaitAndGetResult(cancellationToken);
                    }
                    else if (textChanges.Length > 0)
                    {
                        // if this is not a supported trigger character for formatting service (space or tab etc.)
                        // then format the span of the textchange.
                        var totalSpan = TextSpan.FromBounds(textChanges.Min(c => c.Span.Start), textChanges.Max(c => c.Span.End));
                        changes = formattingService.GetFormattingChangesAsync(document, totalSpan, cancellationToken).WaitAndGetResult(cancellationToken);
                    }

                    if (changes != null && !changes.IsEmpty())
                    {
                        document.Project.Solution.Workspace.ApplyTextChanges(document.Id, changes, cancellationToken);
                    }

                    formattingTransaction.Complete();
                }
            }

            // Let the completion rules know that this item was committed.
            this.MakeMostRecentItem(item.Item.DisplayText);
        }
Beispiel #20
0
 private TextSpan GetCurrentItemSpan(CompletionItem item, Model model)
 {
     var originalSpanInView = model.GetViewBufferSpan(item.Span);
     var currentSpanInView = model.GetCurrentSpanInSnapshot(originalSpanInView, this.TextView.TextBuffer.CurrentSnapshot);
     var newStart = item.Span.Start + (currentSpanInView.Span.Start - originalSpanInView.TextSpan.Start);
     return new TextSpan(newStart, currentSpanInView.Length);
 }
        private void Commit(CompletionItem item, TextChange textChange, Model model, char? commitChar)
        {
            AssertIsForeground();

            // We could only be called if we had a model at this point.
            Contract.ThrowIfNull(model);

            item = Controller.GetExternallyUsableCompletionItem(item);

            // Now that we've captured the model at this point, we can stop ourselves entirely.  
            // This is also desirable as we may have reentrancy problems when we call into 
            // custom commit completion providers.  I.e. if the custom provider moves the caret,
            // then we do not want to process that move as it may put us into an unexpected state.
            //
            // TODO(cyrusn): We still have a general reentrancy problem where calling into a custom
            // commit provider (or just calling into the editor) may cause something to call back
            // into us.  However, for now, we just hope that no such craziness will occur.
            this.StopModelComputation();

            // NOTE(cyrusn): It is intentional that we get the undo history for the
            // surface buffer and not the subject buffer.
            using (var transaction = _undoHistoryRegistry.GetHistory(this.TextView.TextBuffer).CreateTransaction(EditorFeaturesResources.IntelliSense))
            {
                // We want to merge with any of our other programmatic edits (e.g. automatic brace completion)
                transaction.MergePolicy = AutomaticCodeChangeMergePolicy.Instance;

                // Check if the provider wants to perform custom commit itself.  Otherwise we will
                // handle things.
                var provider = item.CompletionProvider as ICustomCommitCompletionProvider;
                if (provider == null)
                {
                    var viewBuffer = this.TextView.TextBuffer;

                    // Use character based diffing here to avoid overwriting the commit character placed into the editor.
                    var editOptions = new EditOptions(new StringDifferenceOptions
                    {
                        DifferenceType = StringDifferenceTypes.Character,
                        IgnoreTrimWhiteSpace = EditOptions.DefaultMinimalChange.DifferenceOptions.IgnoreTrimWhiteSpace
                    });

                    using (var textEdit = viewBuffer.CreateEdit(editOptions, reiteratedVersionNumber: null, editTag: null))
                    {
                        var viewSpan = model.GetSubjectBufferFilterSpanInViewBuffer(textChange.Span);
                        var currentSpan = model.GetCurrentSpanInSnapshot(viewSpan, viewBuffer.CurrentSnapshot);

                        // In order to play nicely with automatic brace completion, we need to 
                        // not touch the opening paren. We'll check our span and textchange 
                        // for ( and adjust them accordingly if we find them.

                        // all this is needed since we don't use completion set mechanism provided by VS but we implement everything ourselves.
                        // due to that, existing brace completion engine in editor that should take care of interaction between brace completion
                        // and intellisense doesn't work for us. so we need this kind of workaround to support it nicely.
                        bool textChanged;
                        var finalText = GetFinalText(textChange, commitChar.GetValueOrDefault(), out textChanged);
                        currentSpan = GetFinalSpan(currentSpan, commitChar.GetValueOrDefault(), textChanged);

                        var caretPoint = this.TextView.GetCaretPoint(this.SubjectBuffer);
                        var virtualCaretPoint = this.TextView.GetVirtualCaretPoint(this.SubjectBuffer);

                        if (caretPoint.HasValue && virtualCaretPoint.HasValue)
                        {
                            // TODO(dustinca): We need to call a different API here. TryMoveCaretToAndEnsureVisible might center within the view.
                            this.TextView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(caretPoint.Value));
                        }

                        caretPoint = this.TextView.GetCaretPoint(this.SubjectBuffer);

                        // Now that we're doing character level diffing, we need to move the caret to the end of 
                        // the span being replaced. Otherwise, we can replace M|ai with Main and wind up with 
                        // M|ain, since character based diffing makes that quite legit.
                        if (caretPoint.HasValue)
                        {
                            var endInSubjectBuffer = this.TextView.BufferGraph.MapDownToBuffer(currentSpan.End, PointTrackingMode.Positive, caretPoint.Value.Snapshot.TextBuffer, PositionAffinity.Predecessor);
                            if (caretPoint.Value < endInSubjectBuffer)
                            {
                                this.TextView.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(currentSpan.Snapshot.TextBuffer.CurrentSnapshot, currentSpan.End.Position));
                            }
                        }

                        textEdit.Replace(currentSpan, finalText);
                        textEdit.Apply();
                    }

                    // We've manipulated the caret position in order to generate the correct edit. However, 
                    // if the insertion is long enough, the caret will scroll out of the visible area.
                    // Re-center the view.
                    using (var textEdit = viewBuffer.CreateEdit(editOptions, reiteratedVersionNumber: null, editTag: null))
                    {
                        var caretPoint = this.TextView.GetCaretPoint(this.SubjectBuffer);
                        if (caretPoint.HasValue)
                        {
                            this.TextView.Caret.EnsureVisible();
                        }
                    }

                    transaction.Complete();
                }
                else
                {
                    // Let the provider handle this.
                    provider.Commit(item, this.TextView, this.SubjectBuffer, model.TriggerSnapshot, commitChar);
                    transaction.Complete();
                }
            }

            var document = this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            var formattingService = document.GetLanguageService<IEditorFormattingService>();

            var commitCharTriggersFormatting = commitChar != null && 
                    (formattingService?.SupportsFormattingOnTypedCharacter(document, commitChar.GetValueOrDefault()) 
                     ?? false);

            if (formattingService != null && (item.ShouldFormatOnCommit || commitCharTriggersFormatting))
            {
                // Formatting the completion item affected span is done as a separate transaction because this gives the user
                // the flexibility to undo the formatting but retain the changes associated with the completion item
                using (var formattingTransaction = _undoHistoryRegistry.GetHistory(this.TextView.TextBuffer).CreateTransaction(EditorFeaturesResources.IntelliSenseCommitFormatting))
                {
                    var caretPoint = this.TextView.GetCaretPoint(this.SubjectBuffer);
                    IList<TextChange> changes;
                    if (commitCharTriggersFormatting && caretPoint.HasValue)
                    {
                        // if the commit character is supported by formatting service, then let the formatting service
                        // find the appropriate range to format.
                        changes = formattingService.GetFormattingChangesAsync(document, commitChar.Value, caretPoint.Value.Position, CancellationToken.None).WaitAndGetResult(CancellationToken.None);
                    }
                    else
                    {
                        // if this is not a supported trigger character for formatting service (space or tab etc.)
                        // then format the span of the textchange.
                        changes = formattingService.GetFormattingChangesAsync(document, textChange.Span, CancellationToken.None).WaitAndGetResult(CancellationToken.None);
                    }

                    if (changes != null && !changes.IsEmpty())
                    {
                        document.Project.Solution.Workspace.ApplyTextChanges(document.Id, changes, CancellationToken.None);
                    }

                    formattingTransaction.Complete();
                }
            }

            // Let the completion rules know that this item was committed.
            GetCompletionRules().CompletionItemCommitted(item);
        }
            private Boolean IsAfterDot(Model model, ITextSnapshot textSnapshot, Dictionary<TextSpan, string> textSpanToText)
            {
                var span = model.OriginalList.Span;

                // Move the span back one character if possible.
                span = TextSpan.FromBounds(Math.Max(0, span.Start - 1), span.End);

                var text = model.GetCurrentTextInSnapshot(span, textSnapshot, textSpanToText);
                return text.Length > 0 && text[0] == '.';
            }