File: Workspaces\AbstractTextBufferVisibilityTracker.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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Workspaces;
 
internal abstract class AbstractTextBufferVisibilityTracker<
    TTextView,
    TVisibilityChangedCallback> : ITextBufferVisibilityTracker
    where TTextView : ITextView
    where TVisibilityChangedCallback : System.Delegate
{
    private readonly ITextBufferAssociatedViewService _associatedViewService;
    private readonly IThreadingContext _threadingContext;
 
    private readonly Dictionary<ITextBuffer, VisibleTrackerData> _subjectBufferToCallbacks = [];
 
    protected AbstractTextBufferVisibilityTracker(
        ITextBufferAssociatedViewService associatedViewService,
        IThreadingContext threadingContext)
    {
        _associatedViewService = associatedViewService;
        _threadingContext = threadingContext;
 
        associatedViewService.SubjectBuffersConnected += AssociatedViewService_SubjectBuffersConnected;
        associatedViewService.SubjectBuffersDisconnected += AssociatedViewService_SubjectBuffersDisconnected;
    }
 
    protected abstract bool IsVisible(TTextView view);
    protected abstract TVisibilityChangedCallback GetVisiblityChangeCallback(VisibleTrackerData visibleTrackerData);
    protected abstract void AddVisibilityChangedCallback(TTextView view, TVisibilityChangedCallback visibilityChangedCallback);
    protected abstract void RemoveVisibilityChangedCallback(TTextView view, TVisibilityChangedCallback visibilityChangedCallback);
 
    private void AssociatedViewService_SubjectBuffersConnected(object? sender, SubjectBuffersConnectedEventArgs e)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        UpdateAllAssociatedViews(e.SubjectBuffers);
    }
 
    private void AssociatedViewService_SubjectBuffersDisconnected(object? sender, SubjectBuffersConnectedEventArgs e)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        UpdateAllAssociatedViews(e.SubjectBuffers);
    }
 
    private void UpdateAllAssociatedViews(ReadOnlyCollection<ITextBuffer> subjectBuffers)
    {
        // Whenever views get attached/detached from buffers, make sure we're hooked up to the appropriate events
        // for them.
        foreach (var buffer in subjectBuffers)
        {
            if (_subjectBufferToCallbacks.TryGetValue(buffer, out var data))
                data.UpdateAssociatedViews();
        }
    }
 
    public bool IsVisible(ITextBuffer subjectBuffer)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        var views = _associatedViewService.GetAssociatedTextViews(subjectBuffer).ToImmutableArrayOrEmpty();
 
        // If we don't have any views at all, then assume the buffer is visible.
        if (views.Length == 0)
            return true;
 
        // if any of the views were *not* the right kind of text views, assume the buffer is visible.  We don't know
        // how to determine the visibility of this buffer.  While unlikely to happen, this is possible with VS's
        // extensibility model, which allows for a plugin to host an ITextBuffer in their own impl of an ITextView.
        // For those cases, just assume these buffers are visible.
        if (views.Any(static v => v is not TTextView))
            return true;
 
        return views.OfType<TTextView>().Any(v => IsVisible(v));
    }
 
    public void RegisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        if (!_subjectBufferToCallbacks.TryGetValue(subjectBuffer, out var data))
        {
            data = new VisibleTrackerData(this, subjectBuffer);
            _subjectBufferToCallbacks.Add(subjectBuffer, data);
        }
 
        data.AddCallback(callback);
    }
 
    public void UnregisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        // Both of these methods must succeed.  Otherwise we're somehow unregistering something we don't know about.
        Contract.ThrowIfFalse(_subjectBufferToCallbacks.TryGetValue(subjectBuffer, out var data));
        Contract.ThrowIfFalse(data.Callbacks.Contains(callback));
        data.RemoveCallback(callback);
 
        // If we have nothing that wants to listen to information about this buffer anymore, then disconnect it
        // from all events and remove our map.
        if (data.Callbacks.Count == 0)
        {
            data.Dispose();
            _subjectBufferToCallbacks.Remove(subjectBuffer);
        }
    }
 
    public TestAccessor GetTestAccessor()
        => new(this);
 
    public readonly struct TestAccessor(AbstractTextBufferVisibilityTracker<TTextView, TVisibilityChangedCallback> visibilityTracker)
    {
        private readonly AbstractTextBufferVisibilityTracker<TTextView, TVisibilityChangedCallback> _visibilityTracker = visibilityTracker;
 
        public void TriggerCallbacks(ITextBuffer subjectBuffer)
        {
            var data = _visibilityTracker._subjectBufferToCallbacks[subjectBuffer];
            data.TriggerCallbacks();
        }
    }
 
    protected sealed class VisibleTrackerData : IDisposable
    {
        public readonly HashSet<ITextView> TextViews = [];
 
        private readonly AbstractTextBufferVisibilityTracker<TTextView, TVisibilityChangedCallback> _tracker;
        private readonly ITextBuffer _subjectBuffer;
        private readonly TVisibilityChangedCallback _visibilityChangedCallback;
 
        /// <summary>
        /// The callbacks that want to be notified when our <see cref="TextViews"/> change visibility.  Stored as an
        /// <see cref="ImmutableHashSet{T}"/> so we can enumerate it safely without it changing underneath us.
        /// </summary>
        public ImmutableHashSet<Action> Callbacks { get; private set; } = [];
 
        public VisibleTrackerData(
            AbstractTextBufferVisibilityTracker<TTextView, TVisibilityChangedCallback> tracker,
            ITextBuffer subjectBuffer)
        {
            _tracker = tracker;
            _subjectBuffer = subjectBuffer;
            _visibilityChangedCallback = tracker.GetVisiblityChangeCallback(this);
 
            UpdateAssociatedViews();
        }
 
        public void Dispose()
        {
            _tracker._threadingContext.ThrowIfNotOnUIThread();
 
            // Shouldn't be disposing of this if we still have clients that want to hear about visibility changes.
            Contract.ThrowIfTrue(Callbacks.Count > 0);
 
            // Clear out all our textviews.  This will disconnect us from any events we have registered with them.
            UpdateTextViews([]);
 
            Contract.ThrowIfTrue(TextViews.Count > 0);
        }
 
        public void AddCallback(Action callback)
        {
            _tracker._threadingContext.ThrowIfNotOnUIThread();
            this.Callbacks = this.Callbacks.Add(callback);
        }
 
        public void RemoveCallback(Action callback)
        {
            _tracker._threadingContext.ThrowIfNotOnUIThread();
            this.Callbacks = this.Callbacks.Remove(callback);
        }
 
        public void UpdateAssociatedViews()
        {
            _tracker._threadingContext.ThrowIfNotOnUIThread();
 
            // Update us to whatever the currently associated text views are for this buffer.
            UpdateTextViews(_tracker._associatedViewService.GetAssociatedTextViews(_subjectBuffer));
        }
 
        private void UpdateTextViews(IEnumerable<ITextView> associatedTextViews)
        {
            var removedViews = TextViews.Except(associatedTextViews);
            var addedViews = associatedTextViews.Except(TextViews);
 
            // Disconnect from hearing about visibility changes for any views we're no longer associated with.
            foreach (var removedView in removedViews)
            {
                if (removedView is TTextView genericView)
                    _tracker.RemoveVisibilityChangedCallback(genericView, _visibilityChangedCallback);
            }
 
            // Connect to hearing about visbility changes for any views we are associated with.
            foreach (var addedView in addedViews)
            {
                if (addedView is TTextView genericView)
                    _tracker.AddVisibilityChangedCallback(genericView, _visibilityChangedCallback);
            }
 
            TextViews.Clear();
            TextViews.AddRange(associatedTextViews);
        }
 
        public void TriggerCallbacks()
        {
            _tracker._threadingContext.ThrowIfNotOnUIThread();
            foreach (var callback in Callbacks)
                callback();
        }
    }
}