File: Utilities\VsCodeWindowViewTracker.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_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 Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
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 : IDisposable, IVsCodeWindowEvents
{
    private readonly IVsCodeWindow _codeWindow;
    private readonly IThreadingContext _threadingContext;
    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 = [];
 
    public VsCodeWindowViewTracker(IVsCodeWindow codeWindow, IThreadingContext threadingContext, IVsEditorAdaptersFactoryService editorAdaptersFactoryService)
    {
        _codeWindow = codeWindow;
        _threadingContext = threadingContext;
        _editorAdaptersFactoryService = editorAdaptersFactoryService;
 
        _threadingContext.ThrowIfNotOnUIThread();
 
        _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)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        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)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        if (_trackedTextViews.TryGetValue(pView, out var view))
        {
            view.Caret.PositionChanged -= OnCaretPositionChanged;
            view.GotAggregateFocus -= OnViewGotAggregateFocus;
 
            _trackedTextViews.Remove(pView);
        }
    }
 
    public ITextView GetActiveView()
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        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)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        StartTrackingView(pView);
 
        return VSConstants.S_OK;
    }
 
    int IVsCodeWindowEvents.OnCloseView(IVsTextView pView)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        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();
    }
}