File: Utilities\VsCodeWindowViewTracker.cs
Web Access
Project: ..\..\..\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ckcrqypr_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.VisualStudio.Debugger.ComponentInterfaces;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.LanguageServices.Implementation;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Utilities
{
    /// <summary>
    /// A helper class that makes getting the "current" position of the cursor in an <see cref="IVsCodeWindow"/> easier to do. This is necessary
    /// because a <see cref="IVsCodeWindow"/> can have more than one view if there's a split involved. Watching for cursor changes also requires
    /// tracking the lifetime of the views as appropriate.
    /// </summary>
    /// <remarks>
    /// All members of this class are UI thread affinitized, including the constructor.
    /// </remarks>
    internal sealed class VsCodeWindowViewTracker : ForegroundThreadAffinitizedObject, IDisposable, IVsCodeWindowEvents
    {
        private readonly IVsCodeWindow _codeWindow;
        private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactoryService;
 
        private readonly ComEventSink _codeWindowEventsSink;
 
        /// <summary>
        /// The map from <see cref="IVsTextView"/> and corresponding <see cref="ITextView"/> for views that we are currently watching for caret movements.
        /// </summary>
        private readonly Dictionary<IVsTextView, ITextView> _trackedTextViews = new();
 
        public VsCodeWindowViewTracker(IVsCodeWindow codeWindow, IThreadingContext threadingContext, IVsEditorAdaptersFactoryService editorAdaptersFactoryService)
            : base(threadingContext, assertIsForeground: true)
        {
            _codeWindow = codeWindow;
            _editorAdaptersFactoryService = editorAdaptersFactoryService;
 
            _codeWindowEventsSink = ComEventSink.Advise<IVsCodeWindowEvents>(codeWindow, this);
 
            if (ErrorHandler.Succeeded(_codeWindow.GetPrimaryView(out var pTextView)) && pTextView != null)
                StartTrackingView(pTextView);
 
            // If there is no secondary view, GetSecondaryView will return null
            if (ErrorHandler.Succeeded(_codeWindow.GetSecondaryView(out pTextView)) && pTextView != null)
                StartTrackingView(pTextView);
        }
 
        private void StartTrackingView(IVsTextView pTextView)
        {
            AssertIsForeground();
 
            if (!_trackedTextViews.ContainsKey(pTextView))
            {
                var wpfTextView = _editorAdaptersFactoryService.GetWpfTextView(pTextView);
 
                if (wpfTextView != null)
                {
                    _trackedTextViews.Add(pTextView, wpfTextView);
                    wpfTextView.Caret.PositionChanged += OnCaretPositionChanged;
                    wpfTextView.GotAggregateFocus += OnViewGotAggregateFocus;
                }
            }
        }
 
        private void StopTrackingView(IVsTextView pView)
        {
            AssertIsForeground();
 
            if (_trackedTextViews.TryGetValue(pView, out var view))
            {
                view.Caret.PositionChanged -= OnCaretPositionChanged;
                view.GotAggregateFocus -= OnViewGotAggregateFocus;
 
                _trackedTextViews.Remove(pView);
            }
        }
 
        public ITextView GetActiveView()
        {
            AssertIsForeground();
 
            ErrorHandler.ThrowOnFailure(_codeWindow.GetLastActiveView(out var pView));
            Contract.ThrowIfNull(pView, $"{nameof(IVsCodeWindow.GetLastActiveView)} returned success, but did not provide a view.");
            var view = _editorAdaptersFactoryService.GetWpfTextView(pView);
            Contract.ThrowIfNull(view, "The active view should be initialized.");
            return view;
        }
 
        int IVsCodeWindowEvents.OnNewView(IVsTextView pView)
        {
            AssertIsForeground();
            StartTrackingView(pView);
 
            return VSConstants.S_OK;
        }
 
        int IVsCodeWindowEvents.OnCloseView(IVsTextView pView)
        {
            AssertIsForeground();
            StopTrackingView(pView);
 
            return VSConstants.S_OK;
        }
 
        /// <summary>
        /// Raised when the a caret has moved in a view, or when the current view has changed.
        /// </summary>
        /// <remarks>
        /// This is combined into one event since in practice consumers need to respond to either the same way, by refreshing what the current
        /// symbol or token or whatever is.
        /// </remarks>
        public event EventHandler<EventArgs>? CaretMovedOrActiveViewChanged;
 
        private void OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
            => CaretMovedOrActiveViewChanged?.Invoke(this, e);
 
        private void OnViewGotAggregateFocus(object sender, EventArgs e)
            => CaretMovedOrActiveViewChanged?.Invoke(this, e);
 
        public void Dispose()
        {
            // StopTrackingView will update _trackedTextViews; the ToList() avoids modification during enumeration
            foreach (var view in _trackedTextViews.Keys.ToList())
            {
                StopTrackingView(view);
            }
 
            Debug.Assert(_trackedTextViews.Count == 0);
 
            _codeWindowEventsSink.Unadvise();
        }
    }
}