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; }
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; }
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); }
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; }
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); }
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] == '.'; }