// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // // This file contain implementations details that are subject to change without notice. // Use at your own risk. // namespace Microsoft.VisualStudio.Text.BraceCompletion.Implementation { using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.BraceCompletion; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Utilities; using System; using System.Collections.Generic; using System.Collections.ObjectModel; /// /// Represents the stack of active brace completion sessions. /// The stack handles removing sessions no longer in focus as /// well as marking the inner most closing brace with the /// adornment. /// internal class BraceCompletionStack : IBraceCompletionStack { #region Private Members private Stack _stack; private ITextView _textView; private ITextBuffer _currentSubjectBuffer; private IBraceCompletionAdornmentServiceFactory _adornmentServiceFactory; private IBraceCompletionAdornmentService _adornmentService; private GuardedOperations _guardedOperations; #endregion #region Constructors public BraceCompletionStack(ITextView textView, IBraceCompletionAdornmentServiceFactory adornmentFactory, GuardedOperations guardedOperations) { _adornmentServiceFactory = adornmentFactory; _stack = new Stack(); _textView = textView; _guardedOperations = guardedOperations; RegisterEvents(); } #endregion #region IBraceCompletionStack public IBraceCompletionSession TopSession { get { return (_stack.Count > 0 ? _stack.Peek() : null); } } public void PushSession(IBraceCompletionSession session) { ITextView view = null; ITextBuffer buffer = null; _guardedOperations.CallExtensionPoint(() => { view = session.TextView; buffer = session.SubjectBuffer; }); if (view != null && buffer != null) { SetCurrentBuffer(buffer); bool validStart = false; // start the session to add the closing brace _guardedOperations.CallExtensionPoint(() => { session.Start(); // verify the session is valid before going on. // some sessions may want to leave the stack at this point validStart = (session.OpeningPoint != null && session.ClosingPoint != null); }); if (validStart) { // highlight the brace ITrackingPoint closingPoint = null; _guardedOperations.CallExtensionPoint(() => { closingPoint = session.ClosingPoint; }); HighlightSpan(closingPoint); // put it on the stack for tracking _stack.Push(session); } } } public ReadOnlyObservableCollection Sessions { get { return new ReadOnlyObservableCollection(new ObservableCollection(_stack)); } } public void RemoveOutOfRangeSessions(SnapshotPoint point) { bool updateHighlightSpan = false; while (_stack.Count > 0 && !Contains(TopSession, point)) { updateHighlightSpan = true; // remove the session and call Finish PopSession(); } if (updateHighlightSpan) { HighlightSpan(TopSession != null ? TopSession.ClosingPoint : null); } } public void Clear() { while (_stack.Count > 0) { PopSession(); } SetCurrentBuffer(null); HighlightSpan(null); } #endregion #region Events private void RegisterEvents() { if (_adornmentServiceFactory != null) { _adornmentService = _adornmentServiceFactory.GetOrCreateService(_textView); } _textView.Caret.PositionChanged += Caret_PositionChanged; _textView.Closed += TextView_Closed; } private void UnregisterEvents() { _textView.Caret.PositionChanged -= Caret_PositionChanged; _textView.Closed -= TextView_Closed; // unhook subject buffer SetCurrentBuffer(null); _textView = null; } public void ConnectSubjectBuffer(ITextBuffer subjectBuffer) { subjectBuffer.PostChanged += SubjectBuffer_PostChanged; } public void DisconnectSubjectBuffer(ITextBuffer subjectBuffer) { subjectBuffer.PostChanged -= SubjectBuffer_PostChanged; } private void TextView_Closed(object sender, EventArgs e) { UnregisterEvents(); } // Remove any sessions that no longer contain the caret private void Caret_PositionChanged(object sender, CaretPositionChangedEventArgs e) { if (_stack.Count > 0) { // use the new position if possible, otherwise map to the subject buffer if (_currentSubjectBuffer != null && e.TextView.TextBuffer != _currentSubjectBuffer) { SnapshotPoint? newPosition = e.NewPosition.Point.GetPoint(_currentSubjectBuffer, PositionAffinity.Successor); if (newPosition.HasValue) { RemoveOutOfRangeSessions(newPosition.Value); } else { // caret is no longer in the subject buffer. probably // moved to different buffer in the same view. // clear all tracks _stack.Clear(); } } else { RemoveOutOfRangeSessions(e.NewPosition.BufferPosition); } } } // Verify that the top most session is still valid after a buffer change // This handles any issues that could result from text being replaced // or multi view scenarios where the caret is not being moved in the // current view. private void SubjectBuffer_PostChanged(object sender, EventArgs e) { bool updateHighlightSpan = false; // only check the top most session // outer sessions could become invalid while the inner most // sessions stay valid, but there is no reason to check them every time while (_stack.Count > 0 && !IsSessionValid(TopSession)) { updateHighlightSpan = true; _stack.Pop().Finish(); } if (updateHighlightSpan) { ITrackingPoint closingPoint = null; if (TopSession != null) { _guardedOperations.CallExtensionPoint(() => closingPoint = TopSession.ClosingPoint); } HighlightSpan(closingPoint); } } private bool IsSessionValid(IBraceCompletionSession session) { bool isValid = false; _guardedOperations.CallExtensionPoint(() => { if (session.ClosingPoint != null && session.OpeningPoint != null && session.SubjectBuffer != null) { ITextSnapshot snapshot = session.SubjectBuffer.CurrentSnapshot; SnapshotPoint closingSnapshotPoint = session.ClosingPoint.GetPoint(snapshot); SnapshotPoint openingSnapshotPoint = session.OpeningPoint.GetPoint(snapshot); // Verify that the closing and opening points still match the expected braces isValid = closingSnapshotPoint.Position > 1 && openingSnapshotPoint.Position <= (closingSnapshotPoint.Position - 2) && openingSnapshotPoint.GetChar() == session.OpeningBrace && closingSnapshotPoint.Subtract(1).GetChar() == session.ClosingBrace; } }); return isValid; } #endregion #region Private Helpers private void SetCurrentBuffer(ITextBuffer buffer) { // Connect to the subject buffer of the session if (_currentSubjectBuffer != buffer) { if (_currentSubjectBuffer != null) { DisconnectSubjectBuffer(_currentSubjectBuffer); } _currentSubjectBuffer = buffer; if (_currentSubjectBuffer != null) { ConnectSubjectBuffer(_currentSubjectBuffer); } } } private void PopSession() { IBraceCompletionSession session = _stack.Pop(); ITextBuffer nextSubjectBuffer = null; _guardedOperations.CallExtensionPoint(() => { // call finish to allow the session to do any cleanup session.Finish(); if (TopSession != null) { nextSubjectBuffer = TopSession.SubjectBuffer; } }); SetCurrentBuffer(nextSubjectBuffer); } private void HighlightSpan(ITrackingPoint point) { if (_adornmentService != null) { _adornmentService.Point = point; } } private bool Contains(IBraceCompletionSession session, SnapshotPoint point) { bool contains = false; _guardedOperations.CallExtensionPoint(() => { // remove any sessions with nulls, if they decide they need to get off the stack // they can do it this way. if (session.OpeningPoint != null && session.ClosingPoint != null && session.OpeningPoint.TextBuffer == session.ClosingPoint.TextBuffer && point.Snapshot.TextBuffer == session.OpeningPoint.TextBuffer) { ITextSnapshot snapshot = point.Snapshot; contains = session.OpeningPoint.GetPosition(snapshot) < point.Position && session.ClosingPoint.GetPosition(snapshot) > point.Position; } }); return contains; } #endregion } }