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)); }
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)); }