File: Editor\TextBufferAssociatedViewService.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.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor;
 
[Export(typeof(ITextViewConnectionListener))]
[ContentType(ContentTypeNames.RoslynContentType)]
[ContentType(ContentTypeNames.XamlContentType)]
[TextViewRole(PredefinedTextViewRoles.Interactive)]
[Export(typeof(ITextBufferAssociatedViewService))]
internal class TextBufferAssociatedViewService : ITextViewConnectionListener, ITextBufferAssociatedViewService
{
#if DEBUG
    private static readonly HashSet<ITextView> s_registeredViews = [];
#endif
 
    private static readonly object s_gate = new();
    private static readonly ConditionalWeakTable<ITextBuffer, HashSet<ITextView>> s_map = new();
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public TextBufferAssociatedViewService()
    {
    }
 
    public event EventHandler<SubjectBuffersConnectedEventArgs> SubjectBuffersConnected;
    public event EventHandler<SubjectBuffersConnectedEventArgs> SubjectBuffersDisconnected;
 
    void ITextViewConnectionListener.SubjectBuffersConnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers)
    {
        lock (s_gate)
        {
            // only add roslyn type to tracking map
            foreach (var buffer in subjectBuffers.Where(b => IsSupportedContentType(b.ContentType)))
            {
                if (!s_map.TryGetValue(buffer, out var set))
                {
                    set = [];
                    s_map.Add(buffer, set);
                }
 
                set.Add(textView);
                DebugRegisterView_NoLock(textView);
            }
        }
 
        this.SubjectBuffersConnected?.Invoke(this, new SubjectBuffersConnectedEventArgs(textView, subjectBuffers.ToReadOnlyCollection()));
    }
 
    void ITextViewConnectionListener.SubjectBuffersDisconnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers)
    {
        lock (s_gate)
        {
            // we need to check all buffers reported since we will be called after actual changes have happened. 
            // for example, if content type of a buffer changed, we will be called after it is changed, rather than before it.
            foreach (var buffer in subjectBuffers)
            {
                if (s_map.TryGetValue(buffer, out var set))
                {
                    set.Remove(textView);
                    if (set.Count == 0)
                    {
                        s_map.Remove(buffer);
                    }
                }
            }
        }
 
        this.SubjectBuffersDisconnected?.Invoke(this, new SubjectBuffersConnectedEventArgs(textView, subjectBuffers.ToReadOnlyCollection()));
    }
 
    private static bool IsSupportedContentType(IContentType contentType)
    {
        // This list should match the list of exported content types above
        return contentType.IsOfType(ContentTypeNames.RoslynContentType) ||
               contentType.IsOfType(ContentTypeNames.XamlContentType);
    }
 
    private static IList<ITextView> GetTextViews(ITextBuffer textBuffer)
    {
        lock (s_gate)
        {
            if (!s_map.TryGetValue(textBuffer, out var set))
            {
                return [];
            }
 
            return set.ToList();
        }
    }
 
    public IEnumerable<ITextView> GetAssociatedTextViews(ITextBuffer textBuffer)
        => GetTextViews(textBuffer);
 
    private static bool HasFocus(ITextView textView)
        => textView.HasAggregateFocus;
 
    public static bool AnyAssociatedViewHasFocus(ITextBuffer textBuffer)
    {
        if (textBuffer == null)
        {
            return false;
        }
 
        var views = GetTextViews(textBuffer);
        if (views.Count == 0)
        {
            // We haven't seen the view yet.  Assume it is visible.
            return true;
        }
 
        return views.Any(HasFocus);
    }
 
    [Conditional("DEBUG")]
    private static void DebugRegisterView_NoLock(ITextView textView)
    {
#if DEBUG
        if (s_registeredViews.Add(textView))
        {
            textView.Closed += OnTextViewClose;
        }
#endif
    }
 
#if DEBUG
    private static void OnTextViewClose(object sender, EventArgs e)
    {
        var view = sender as ITextView;
 
        lock (s_gate)
        {
            foreach (var buffer in view.BufferGraph.GetTextBuffers(b => IsSupportedContentType(b.ContentType)))
            {
                if (s_map.TryGetValue(buffer, out var set))
                {
                    Contract.ThrowIfTrue(set.Contains(view));
                }
            }
 
            s_registeredViews.Remove(view);
        }
    }
#endif
}