File: AutomaticCompletion\BraceCompletionSessionProvider.BraceCompletionSession.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis.BraceCompletion;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.BraceCompletion;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.AutomaticCompletion;
 
internal partial class BraceCompletionSessionProvider
{
    // ported and modified from "Platform\Text\Impl\BraceCompletion\BraceCompletionDefaultSession.cs"
    //
    // we want to provide better context based brace completion but IBraceCompletionContext is too simple for that.
    // fortunately, editor provides another extension point where we have more control over brace completion but we do not
    // want to re-implement logics base session provider already provides. so I ported editor's default session and 
    // modified it little bit so that we can use it as base class.
    private class BraceCompletionSession : IBraceCompletionSession
    {
        public char OpeningBrace { get; }
        public char ClosingBrace { get; }
        public ITrackingPoint OpeningPoint { get; private set; }
        public ITrackingPoint ClosingPoint { get; private set; }
        public ITextBuffer SubjectBuffer { get; }
        public ITextView TextView { get; }
 
        private readonly ITextUndoHistory _undoHistory;
        private readonly IEditorOperations _editorOperations;
        private readonly EditorOptionsService _editorOptionsService;
        private readonly IBraceCompletionService _service;
        private readonly IThreadingContext _threadingContext;
 
        public BraceCompletionSession(
            ITextView textView, ITextBuffer subjectBuffer,
            SnapshotPoint openingPoint, char openingBrace, char closingBrace, ITextUndoHistory undoHistory,
            IEditorOperationsFactoryService editorOperationsFactoryService,
            EditorOptionsService editorOptionsService, IBraceCompletionService service, IThreadingContext threadingContext)
        {
            TextView = textView;
            SubjectBuffer = subjectBuffer;
            OpeningBrace = openingBrace;
            ClosingBrace = closingBrace;
            ClosingPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(openingPoint.Position, PointTrackingMode.Positive);
            _undoHistory = undoHistory;
            _editorOperations = editorOperationsFactoryService.GetEditorOperations(textView);
            _editorOptionsService = editorOptionsService;
            _service = service;
            _threadingContext = threadingContext;
        }
 
        #region IBraceCompletionSession Methods
 
        public void Start()
        {
            _threadingContext.ThrowIfNotOnUIThread();
            // Brace completion is not cancellable.
            if (!this.TryStart(CancellationToken.None))
            {
                EndSession();
            }
        }
 
        private bool TryStart(CancellationToken cancellationToken)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
 
            if (closingSnapshotPoint.Position < 1)
            {
                Debug.Fail("The closing point was not found at the expected position.");
                return false;
            }
 
            var openingSnapshotPoint = closingSnapshotPoint.Subtract(1);
 
            if (openingSnapshotPoint.GetChar() != OpeningBrace)
            {
                // there is a bug in editor brace completion engine on projection buffer that already fixed in vs_pro. until that is FIed to use
                // I will make this not to assert
                // Debug.Fail("The opening brace was not found at the expected position.");
                return false;
            }
 
            OpeningPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(openingSnapshotPoint, PointTrackingMode.Positive);
 
            var document = SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document == null)
            {
                return false;
            }
 
            var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken);
            var context = GetBraceCompletionContext(parsedDocument, document.Project.GetFallbackAnalyzerOptions());
 
            // Note: completes synchronously unless Semantic Model is needed to determine the result:
            if (!_service.HasBraceCompletionAsync(context, document, cancellationToken).WaitAndGetResult(cancellationToken))
            {
                return false;
            }
 
            var braceResult = _service.GetBraceCompletion(context);
 
            using var caretPreservingTransaction = new CaretPreservingEditTransaction(EditorFeaturesResources.Brace_Completion, _undoHistory, _editorOperations);
 
            // Apply the change to complete the brace.
            ApplyBraceCompletionResult(braceResult);
 
            // switch the closing point from positive to negative tracking so that the closing point stays against the closing brace
            ClosingPoint = SubjectBuffer.CurrentSnapshot.CreateTrackingPoint(ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot), PointTrackingMode.Negative);
 
            if (TryGetBraceCompletionContext(out var contextAfterStart, cancellationToken))
            {
                var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, document.Project.GetFallbackAnalyzerOptions(), contextAfterStart.Document.LanguageServices, explicitFormat: false);
                var changesAfterStart = _service.GetTextChangesAfterCompletion(contextAfterStart, indentationOptions, cancellationToken);
                if (changesAfterStart != null)
                {
                    ApplyBraceCompletionResult(changesAfterStart.Value);
                }
            }
 
            caretPreservingTransaction.Complete();
            return true;
        }
 
        public void PreBackspace(out bool handledCommand)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            handledCommand = false;
 
            var caretPos = this.GetCaretPosition();
            var snapshot = SubjectBuffer.CurrentSnapshot;
 
            if (caretPos.HasValue && caretPos.Value.Position > 0 && (caretPos.Value.Position - 1) == OpeningPoint.GetPoint(snapshot).Position
                && !HasForwardTyping)
            {
                using var undo = CreateUndoTransaction();
                using var edit = SubjectBuffer.CreateEdit();
 
                var span = new SnapshotSpan(OpeningPoint.GetPoint(snapshot), ClosingPoint.GetPoint(snapshot));
 
                edit.Delete(span);
 
                if (edit.HasFailedChanges)
                {
                    edit.Cancel();
                    undo.Cancel();
                    Debug.Fail("Unable to clear braces");
                }
                else
                {
                    // handle the command so the backspace does 
                    // not go through since we've already cleared the braces
                    handledCommand = true;
                    edit.ApplyAndLogExceptions();
                    undo.Complete();
                    EndSession();
                }
            }
        }
 
        public void PostBackspace()
        {
        }
 
        public void PreOverType(out bool handledCommand)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            handledCommand = false;
            if (ClosingPoint == null)
            {
                return;
            }
 
            // Brace completion is not cancellable.
            var cancellationToken = CancellationToken.None;
            var snapshot = this.SubjectBuffer.CurrentSnapshot;
 
            var closingSnapshotPoint = ClosingPoint.GetPoint(snapshot);
 
            if (HasForwardTyping)
            {
                return;
            }
 
            if (!TryGetBraceCompletionContext(out var context, cancellationToken) ||
                !_service.AllowOverType(context, cancellationToken))
            {
                return;
            }
 
            var caretPos = this.GetCaretPosition();
 
            Debug.Assert(caretPos.HasValue && caretPos.Value.Position < closingSnapshotPoint.Position);
 
            // ensure that we are within the session before clearing
            if (caretPos.HasValue && caretPos.Value.Position < closingSnapshotPoint.Position && closingSnapshotPoint.Position > 0)
            {
                using var undo = CreateUndoTransaction();
 
                _editorOperations.AddBeforeTextBufferChangePrimitive();
 
                var span = new SnapshotSpan(caretPos.Value, closingSnapshotPoint.Subtract(1));
 
                using var edit = SubjectBuffer.CreateEdit();
 
                edit.Delete(span);
 
                if (edit.HasFailedChanges)
                {
                    Debug.Fail("Unable to clear closing brace");
                    edit.Cancel();
                    undo.Cancel();
                }
                else
                {
                    handledCommand = true;
 
                    edit.ApplyAndLogExceptions();
 
                    MoveCaretToClosingPoint();
 
                    _editorOperations.AddAfterTextBufferChangePrimitive();
 
                    undo.Complete();
                }
            }
        }
 
        public void PostOverType()
        {
        }
 
        public void PreTab(out bool handledCommand)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            handledCommand = false;
 
            if (!HasForwardTyping)
            {
                handledCommand = true;
 
                using var undo = CreateUndoTransaction();
 
                _editorOperations.AddBeforeTextBufferChangePrimitive();
 
                MoveCaretToClosingPoint();
 
                _editorOperations.AddAfterTextBufferChangePrimitive();
 
                undo.Complete();
            }
        }
 
        public void PreReturn(out bool handledCommand)
            => handledCommand = false;
 
        public void PostReturn()
        {
            _threadingContext.ThrowIfNotOnUIThread();
            if (this.GetCaretPosition().HasValue)
            {
                var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
 
                if (closingSnapshotPoint.Position > 0 && HasNoForwardTyping(this.GetCaretPosition().Value, closingSnapshotPoint.Subtract(1)))
                {
                    if (!TryGetBraceCompletionContext(out var context, CancellationToken.None))
                    {
                        return;
                    }
 
                    var indentationOptions = SubjectBuffer.GetIndentationOptions(_editorOptionsService, context.FallbackOptions, context.Document.LanguageServices, explicitFormat: false);
                    var changesAfterReturn = _service.GetTextChangeAfterReturn(context, indentationOptions, CancellationToken.None);
                    if (changesAfterReturn != null)
                    {
                        using var caretPreservingTransaction = new CaretPreservingEditTransaction(EditorFeaturesResources.Brace_Completion, _undoHistory, _editorOperations);
                        ApplyBraceCompletionResult(changesAfterReturn.Value);
                        caretPreservingTransaction.Complete();
                    }
                }
            }
        }
 
        public void Finish()
        {
        }
 
        #endregion
 
        #region Unused IBraceCompletionSession Methods
 
        public void PostTab() { }
 
        public void PreDelete(out bool handledCommand)
            => handledCommand = false;
 
        public void PostDelete() { }
 
        #endregion
 
        #region Private Helpers
 
        private void EndSession()
        {
            // set the points to null to get off the stack
            // the stack will determine that the current point
            // is not contained within the session if either are null
            OpeningPoint = null;
            ClosingPoint = null;
        }
 
        // check if there any typing between the caret the closing point
        private bool HasForwardTyping
        {
            get
            {
                _threadingContext.ThrowIfNotOnUIThread();
                var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
 
                if (closingSnapshotPoint.Position > 0)
                {
                    var caretPos = this.GetCaretPosition();
 
                    if (caretPos.HasValue && !HasNoForwardTyping(caretPos.Value, closingSnapshotPoint.Subtract(1)))
                    {
                        return true;
                    }
                }
 
                return false;
            }
        }
 
        // verify that there is only whitespace between the two given points
        private static bool HasNoForwardTyping(SnapshotPoint caretPoint, SnapshotPoint endPoint)
        {
            Debug.Assert(caretPoint.Snapshot == endPoint.Snapshot, "snapshots do not match");
 
            if (caretPoint.Snapshot == endPoint.Snapshot)
            {
                if (caretPoint == endPoint)
                {
                    return true;
                }
 
                if (caretPoint.Position < endPoint.Position)
                {
                    var span = new SnapshotSpan(caretPoint, endPoint);
 
                    return string.IsNullOrWhiteSpace(span.GetText());
                }
            }
 
            return false;
        }
 
        internal ITextUndoTransaction CreateUndoTransaction()
            => _undoHistory.CreateTransaction(EditorFeaturesResources.Brace_Completion);
 
        private void MoveCaretToClosingPoint()
        {
            _threadingContext.ThrowIfNotOnUIThread();
            var closingSnapshotPoint = ClosingPoint.GetPoint(SubjectBuffer.CurrentSnapshot);
 
            // find the position just after the closing brace in the view's text buffer
            var afterBrace = TextView.BufferGraph.MapUpToBuffer(closingSnapshotPoint,
                PointTrackingMode.Negative, PositionAffinity.Predecessor, TextView.TextBuffer);
 
            Debug.Assert(afterBrace.HasValue, "Unable to move caret to closing point");
 
            if (afterBrace.HasValue)
            {
                TextView.Caret.MoveTo(afterBrace.Value);
            }
        }
 
        private bool TryGetBraceCompletionContext(out BraceCompletionContext context, CancellationToken cancellationToken)
        {
            var document = SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document == null)
            {
                context = default;
                return false;
            }
 
            context = GetBraceCompletionContext(ParsedDocument.CreateSynchronously(document, cancellationToken), document.Project.GetFallbackAnalyzerOptions());
            return true;
        }
 
        private BraceCompletionContext GetBraceCompletionContext(ParsedDocument document, StructuredAnalyzerConfigOptions fallbackOptions)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            var snapshot = SubjectBuffer.CurrentSnapshot;
 
            var closingSnapshotPoint = ClosingPoint.GetPosition(snapshot);
            var openingSnapshotPoint = OpeningPoint.GetPosition(snapshot);
            // The user is actively typing so the caret position should not be null.
            var caretPosition = this.GetCaretPosition().Value.Position;
 
            return new BraceCompletionContext(document, fallbackOptions, openingSnapshotPoint, closingSnapshotPoint, caretPosition);
        }
 
        private void ApplyBraceCompletionResult(BraceCompletionResult result)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            using var edit = SubjectBuffer.CreateEdit();
            foreach (var change in result.TextChanges)
            {
                edit.Replace(change.Span.ToSpan(), change.NewText);
            }
 
            edit.ApplyAndLogExceptions();
 
            try
            {
                Contract.ThrowIfFalse(SubjectBuffer.CurrentSnapshot[OpeningPoint.GetPosition(SubjectBuffer.CurrentSnapshot)] == OpeningBrace,
                    "The opening point does not match the opening brace character");
                Contract.ThrowIfFalse(SubjectBuffer.CurrentSnapshot[ClosingPoint.GetPosition(SubjectBuffer.CurrentSnapshot) - 1] == ClosingBrace,
                    "The closing point does not match the closing brace character");
            }
            catch (Exception e) when (FatalError.ReportAndCatch(e))
            {
                return;
            }
 
            var caretLine = SubjectBuffer.CurrentSnapshot.GetLineFromLineNumber(result.CaretLocation.Line);
            TextView.TryMoveCaretToAndEnsureVisible(new VirtualSnapshotPoint(caretLine, result.CaretLocation.Character));
        }
 
        #endregion
    }
}