private void CommitOnTypeChar(char ch)
        {
            AssertIsForeground();

            // Note: this function is called after the character has already been inserted into the
            // buffer.

            var model = sessionOpt.WaitForModel();

            // We only call CommitOnTypeChar if ch was a commit character.  And we only know if ch
            // was commit character if we had a selected item.
            Contract.ThrowIfNull(model);

            // Replace the selected text span with the desired insertion text.  Note: The provided
            // text span will end up including the last typed char (because we track it in an edge
            // inclusive manner).  Because the span includes the last typed character, it will get
            // lost unless we add it back in.  So we actually insert the desired item text *and* the
            // character.  By doing this we also get proper undo behavior.  i.e. if the user types:
            //
            // WriteL(
            //
            // Then we will first input "WriteL(" into the buffer.  We will then replace "WriteL("
            // with "WriteLine(".  That way if they undo, they will end up with "WriteL" again.

            var selectedItem = Controller.GetExternallyUsableCompletionItem(model.SelectedItem);
            var textChange   = selectedItem.CompletionProvider.GetTextChange(selectedItem, ch, GetCurrentFilterText(model, selectedItem));

            this.Commit(selectedItem, new TextChange(textChange.Span, textChange.NewText + ch), model, ch);
        }
        private void CommitOnEnter(out bool sendThrough, out bool committed)
        {
            AssertIsForeground();

            var model = sessionOpt.WaitForModel();

            // If there's no model, then there's nothing to commit.
            if (model == null)
            {
                // Make sure that the enter gets sent into the buffer.
                sendThrough = true;
                committed   = false;
                return;
            }

            // If we're in a normal editor or the Immediate window, we'll send the enter through
            // to the editor.  In single-line debugger windows (Watch, etc), however, we don't
            // want to send the enter though, because those windows don't support displaying
            // more than one line of text.
            sendThrough = !_isDebugger || _isImmediateWindow;

            if (model.IsSoftSelection)
            {
                // If the completion list is soft selected, then don't commit on enter.
                // Instead, just dismiss the completion list.
                committed = false;
                return;
            }

            var selectedItem = Controller.GetExternallyUsableCompletionItem(model.SelectedItem);

            // If the selected item is the builder, dismiss
            if (selectedItem.IsBuilder)
            {
                sendThrough = false;
                committed   = false;
                return;
            }

            var completionRules = GetCompletionRules();

            if (sendThrough)
            {
                // Get the text that the user has currently entered into the buffer
                var viewSpan       = model.GetSubjectBufferFilterSpanInViewBuffer(selectedItem.FilterSpan);
                var textTypedSoFar = model.GetCurrentTextInSnapshot(
                    viewSpan, this.TextView.TextSnapshot, this.GetCaretPointInViewBuffer());

                var options = GetOptions();
                if (options != null)
                {
                    sendThrough = completionRules.SendEnterThroughToEditor(selectedItem, textTypedSoFar, options);
                }
            }

            var textChange = completionRules.GetTextChange(selectedItem);

            this.Commit(selectedItem, textChange, model, null);
            committed = true;
        }
示例#3
0
        private void CommitOnTab(out bool committed)
        {
            AssertIsForeground();

            var model = sessionOpt.WaitForModel();

            // If there's no model, then there's nothing to commit.
            if (model == null)
            {
                committed = false;
                return;
            }

            var selectedItem = Controller.GetExternallyUsableCompletionItem(model.SelectedItem);

            // If the selected item is the builder, there's not actually any work to do to commit
            if (selectedItem.IsBuilder)
            {
                committed = true;
                this.StopModelComputation();
                return;
            }

            var textChange = selectedItem.CompletionProvider.GetTextChange(selectedItem);

            Commit(selectedItem, textChange, model, null);
            committed = true;
        }
示例#4
0
        internal override void OnCaretPositionChanged(object sender, EventArgs args)
        {
            AssertIsForeground();

            if (!IsSessionActive)
            {
                // No session, so we don't need to do anything.
                return;
            }

            // If we have a session active then it may be in the process of computing results.  If it
            // has computed the results, then compare where the caret is with all the items.  If the
            // caret isn't within the bounds of the items, then we dismiss completion.
            var caretPoint = this.GetCaretPointInViewBuffer();
            var model      = sessionOpt.Computation.InitialUnfilteredModel;

            if (model == null ||
                this.IsCaretOutsideAllItemBounds(model, caretPoint))
            {
                // Completions hadn't even been computed yet or the caret is out of bounds.
                // Just cancel everything we're doing.
                this.StopModelComputation();
                return;
            }

            // TODO(cyrusn): Find a way to allow the user to cancel out of this.
            model = sessionOpt.WaitForModel();
            if (model == null)
            {
                return;
            }

            if (model.SelectedItem != null && model.IsHardSelection)
            {
                // 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 completion in the following case:
                // A a = new | -> A a = new (|

                var selectedItem = Controller.GetExternallyUsableCompletionItem(model.SelectedItem);
                var currentSpan  = model.GetSubjectBufferFilterSpanInViewBuffer(selectedItem.FilterSpan).TextSpan;
                if (caretPoint == currentSpan.Start && currentSpan.Length > 0)
                {
                    sessionOpt.SetModelIsHardSelection(false);
                }
            }
        }
示例#5
0
        void ICommandHandler <CommitUniqueCompletionListItemCommandArgs> .ExecuteCommand(CommitUniqueCompletionListItemCommandArgs args, Action nextHandler)
        {
            AssertIsForeground();

            if (sessionOpt == null)
            {
                // User hit ctrl-space.  If there was no completion up then we want to trigger
                // completion.
                var completionService = this.CreateCompletionService();
                if (completionService == null)
                {
                    return;
                }

                if (!StartNewModelComputation(completionService, filterItems: true))
                {
                    return;
                }
            }

            // Get the selected item.  If it's unique, then we want to commit it.
            var model = this.sessionOpt.WaitForModel();

            if (model == null)
            {
                // Computation failed.  Just pass this command on.
                nextHandler();
                return;
            }

            // Note: Dev10 behavior seems to be that if there is no unique item that filtering is
            // turned off.  However, i do not know if this is desirable behavior, or merely a bug
            // with how that convoluted code worked.  So i'm not maintaining that behavior here.  If
            // we do want it through, it would be easy to get again simply by asking the model
            // computation to remove all filtering.

            if (model.IsUnique)
            {
                // We had a unique item in the list.  Commit it and dismiss this session.

                var selectedItem = Controller.GetExternallyUsableCompletionItem(model.SelectedItem);
                var textChange   = selectedItem.CompletionProvider.GetTextChange(selectedItem);
                this.Commit(selectedItem, textChange, model, null);
            }
        }
示例#6
0
        private void CommitOnEnter(out bool sendThrough, out bool committed)
        {
            AssertIsForeground();

            var model = sessionOpt.WaitForModel();

            // If there's no model, then there's nothing to commit.
            if (model == null)
            {
                // Make sure that the enter gets sent into the buffer.
                sendThrough = true;
                committed   = false;
                return;
            }

            if (model.IsSoftSelection)
            {
                // If the completion list is soft selected, then don't commit on enter.  Instead,
                // send the enter through to the editor and dismiss the completion list.
                sendThrough = true;
                committed   = false;
                return;
            }

            var selectedItem = Controller.GetExternallyUsableCompletionItem(model.SelectedItem);

            // If the selected item is the builder, dismiss
            if (selectedItem.IsBuilder)
            {
                sendThrough = false;
                committed   = false;
                return;
            }

            // Get the text that the user has currently entered into the buffer
            var viewSpan       = model.GetSubjectBufferFilterSpanInViewBuffer(selectedItem.FilterSpan);
            var textTypedSoFar = model.GetCurrentTextInSnapshot(
                viewSpan, this.TextView.TextSnapshot, this.GetCaretPointInViewBuffer());

            var textChange = selectedItem.CompletionProvider.GetTextChange(selectedItem);

            this.Commit(selectedItem, textChange, model, null);
            sendThrough = selectedItem.CompletionProvider.SendEnterThroughToEditor(selectedItem, textTypedSoFar);
            committed   = true;
        }
示例#7
0
        private void CommitItem(CompletionItem item)
        {
            AssertIsForeground();

            item = Controller.GetExternallyUsableCompletionItem(item);

            // We should not be getting called if we didn't even have a computation running.
            Contract.ThrowIfNull(this.sessionOpt);
            Contract.ThrowIfNull(this.sessionOpt.Computation.InitialUnfilteredModel);

            // If the selected item is the builder, there's not actually any work to do to commit
            if (item.IsBuilder)
            {
                this.StopModelComputation();
                return;
            }

            var textChange = item.CompletionProvider.GetTextChange(item);

            this.Commit(item, textChange, this.sessionOpt.Computation.InitialUnfilteredModel, null);
        }
        private bool IsFilterCharacter(char ch)
        {
            AssertIsForeground();

            // TODO(cyrusn): Find a way to allow the user to cancel out of this.
            var model = sessionOpt.WaitForModel();

            if (model == null)
            {
                return(false);
            }

            var selectedItem = Controller.GetExternallyUsableCompletionItem(model.SelectedItem);

            if (selectedItem.IsBuilder)
            {
                return(char.IsLetterOrDigit(ch));
            }

            var filterText = GetCurrentFilterText(model, selectedItem);

            return(selectedItem.CompletionProvider.IsFilterCharacter(selectedItem, ch, filterText));
        }
示例#9
0
        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>();

            if (formattingService != null &&
                (item.ShouldFormatOnCommit || (commitChar != null && formattingService.SupportsFormattingOnTypedCharacter(document, commitChar.GetValueOrDefault()))))
            {
                // 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 changes = formattingService.GetFormattingChangesAsync(document, textChange.Span, CancellationToken.None).WaitAndGetResult(CancellationToken.None);
                    document.Project.Solution.Workspace.ApplyTextChanges(document.Id, changes, CancellationToken.None);
                    formattingTransaction.Complete();
                }
            }

            // Let the rules factories know that this item was committed.
            foreach (var rules in GetCompletionRules())
            {
                rules.CompletionItemComitted(item);
            }
        }