Exemple #1
0
        public AsyncCompletionData.CommitResult TryCommit(
            IAsyncCompletionSession session,
            ITextBuffer subjectBuffer,
            VSCompletionItem item,
            char typeChar,
            CancellationToken cancellationToken)
        {
            // We can make changes to buffers. We would like to be sure nobody can change them at the same time.
            AssertIsForeground();

            var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();

            if (document == null)
            {
                return(CommitResultUnhandled);
            }

            var completionService = document.GetLanguageService <CompletionService>();

            if (completionService == null)
            {
                return(CommitResultUnhandled);
            }

            if (!item.Properties.TryGetProperty(CompletionSource.RoslynItem, out RoslynCompletionItem roslynItem))
            {
                // Roslyn should not be called if the item committing was not provided by Roslyn.
                return(CommitResultUnhandled);
            }

            var filterText = session.ApplicableToSpan.GetText(session.ApplicableToSpan.TextBuffer.CurrentSnapshot) + typeChar;

            if (Helpers.IsFilterCharacter(roslynItem, typeChar, filterText))
            {
                // Returning Cancel means we keep the current session and consider the character for further filtering.
                return(new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.CancelCommit));
            }

            var serviceRules = completionService.GetRules();

            // We can be called before for ShouldCommitCompletion. However, that call does not provide rules applied for the completion item.
            // Now we check for the commit charcter in the context of Rules that could change the list of commit characters.

            // Tab, Enter and Null (call invoke commit) are always commit characters.
            if (typeChar != '\t' && typeChar != '\n' && typeChar != '\0' && !IsCommitCharacter(serviceRules, roslynItem, typeChar, filterText))
            {
                // Returning None means we complete the current session with a void commit.
                // The Editor then will try to trigger a new completion session for the character.
                return(new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None));
            }

            if (!session.Properties.TryGetProperty(CompletionSource.TriggerSnapshot, out ITextSnapshot triggerSnapshot))
            {
                // Need the trigger snapshot to calculate the span when the commit changes to be applied.
                // It should be inserted into a property bag within GetCompletionContextAsync for each item created by Roslyn.
                // If not found here, Roslyn should not make a commit.
                return(CommitResultUnhandled);
            }

            if (!session.Properties.TryGetProperty(CompletionSource.CompletionListSpan, out TextSpan completionListSpan))
            {
                return(CommitResultUnhandled);
            }

            var triggerDocument = triggerSnapshot.GetOpenDocumentInCurrentContextWithChanges();

            if (triggerDocument == null)
            {
                return(CommitResultUnhandled);
            }

            // Commit with completion service assumes that null is provided is case of invoke. VS provides '\0' in the case.
            char?commitChar     = typeChar == '\0' ? null : (char?)typeChar;
            var  commitBehavior = Commit(
                triggerDocument, completionService, session.TextView, subjectBuffer, roslynItem,
                completionListSpan, commitChar, triggerSnapshot, serviceRules, filterText, cancellationToken);

            _recentItemsManager.MakeMostRecentItem(roslynItem.DisplayText);
            return(new AsyncCompletionData.CommitResult(isHandled: true, commitBehavior));
        }
Exemple #2
0
        private AsyncCompletionData.CommitResult Commit(
            Document document,
            CompletionService completionService,
            ITextView view,
            ITextBuffer subjectBuffer,
            RoslynCompletionItem roslynItem,
            TextSpan completionListSpan,
            char?commitCharacter,
            ITextSnapshot triggerSnapshot,
            CompletionRules rules,
            string filterText,
            CancellationToken cancellationToken)
        {
            AssertIsForeground();

            bool includesCommitCharacter;

            if (!subjectBuffer.CheckEditAccess())
            {
                // We are on the wrong thread.
                FatalError.ReportWithoutCrash(new InvalidOperationException("Subject buffer did not provide Edit Access"));
                return(new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None));
            }

            if (subjectBuffer.EditInProgress)
            {
                FatalError.ReportWithoutCrash(new InvalidOperationException("Subject buffer is editing by someone else."));
                return(new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None));
            }

            CompletionChange change;

            // We met an issue when external code threw an OperationCanceledException and the cancellationToken is not cancelled.
            // Catching this scenario for further investigations.
            // See https://github.com/dotnet/roslyn/issues/38455.
            try
            {
                change = completionService.GetChangeAsync(document, roslynItem, completionListSpan, commitCharacter, cancellationToken).WaitAndGetResult(cancellationToken);
            }
            catch (OperationCanceledException e) when(e.CancellationToken != cancellationToken && FatalError.ReportWithoutCrash(e))
            {
                return(CommitResultUnhandled);
            }

            cancellationToken.ThrowIfCancellationRequested();

            if (GetCompletionProvider(completionService, roslynItem) is ICustomCommitCompletionProvider provider)
            {
                provider.Commit(roslynItem, view, subjectBuffer, triggerSnapshot, commitCharacter);
                return(new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None));
            }

            var textChange          = change.TextChange;
            var triggerSnapshotSpan = new SnapshotSpan(triggerSnapshot, textChange.Span.ToSpan());
            var mappedSpan          = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);

            using (var edit = subjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null))
            {
                edit.Replace(mappedSpan.Span, change.TextChange.NewText);

                // edit.Apply() may trigger changes made by extensions.
                // updatedCurrentSnapshot will contain changes made by Roslyn but not by other extensions.
                var updatedCurrentSnapshot = edit.Apply();

                if (change.NewPosition.HasValue)
                {
                    // Roslyn knows how to positionate the caret in the snapshot we just created.
                    // If there were more edits made by extensions, TryMoveCaretToAndEnsureVisible maps the snapshot point to the most recent one.
                    view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(updatedCurrentSnapshot, change.NewPosition.Value));
                }
                else
                {
                    // Or, If we're doing a minimal change, then the edit that we make to the
                    // buffer may not make the total text change that places the caret where we
                    // would expect it to go based on the requested change. In this case,
                    // determine where the item should go and set the care manually.

                    // Note: we only want to move the caret if the caret would have been moved
                    // by the edit.  i.e. if the caret was actually in the mapped span that
                    // we're replacing.
                    var caretPositionInBuffer = view.GetCaretPoint(subjectBuffer);
                    if (caretPositionInBuffer.HasValue && mappedSpan.IntersectsWith(caretPositionInBuffer.Value))
                    {
                        view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, mappedSpan.Start.Position + textChange.NewText.Length));
                    }
                    else
                    {
                        view.Caret.EnsureVisible();
                    }
                }

                includesCommitCharacter = change.IncludesCommitCharacter;

                if (roslynItem.Rules.FormatOnCommit)
                {
                    // The edit updates the snapshot however other extensions may make changes there.
                    // Therefore, it is required to use subjectBuffer.CurrentSnapshot for further calculations rather than the updated current snapsot defined above.
                    document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
                    var spanToFormat      = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);
                    var formattingService = document?.GetLanguageService <IEditorFormattingService>();

                    if (formattingService != null)
                    {
                        var changes = formattingService.GetFormattingChangesAsync(
                            document, spanToFormat.Span.ToTextSpan(), CancellationToken.None).WaitAndGetResult(CancellationToken.None);
                        document.Project.Solution.Workspace.ApplyTextChanges(document.Id, changes, CancellationToken.None);
                    }
                }
            }

            _recentItemsManager.MakeMostRecentItem(roslynItem.FilterText);

            if (includesCommitCharacter)
            {
                return(new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.SuppressFurtherTypeCharCommandHandlers));
            }

            if (commitCharacter == '\n' && SendEnterThroughToEditor(rules, roslynItem, filterText))
            {
                return(new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers));
            }

            return(new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None));
        }