File: Workspaces\ITextBufferVisibilityTracker.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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
 
namespace Microsoft.CodeAnalysis.Workspaces;
 
/// <summary>
/// All methods must be called on UI thread.
/// </summary>
internal interface ITextBufferVisibilityTracker
{
    /// <summary>
    /// Whether or not this text buffer is in an actively visible <see cref="ITextView"/>.
    /// </summary>
    bool IsVisible(ITextBuffer subjectBuffer);
 
    /// <summary>
    /// Registers to hear about visibility changes for this particular buffer.  Note: registration will not trigger
    /// a call to <paramref name="callback"/>.  If clients need that information, they should check the <see
    /// cref="IsVisible"/> state of the <paramref name="subjectBuffer"/> themselves.
    /// </summary>
    void RegisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback);
 
    /// <summary>
    /// Unregister equivalent of <see cref="RegisterForVisibilityChanges"/>.
    /// </summary>
    void UnregisterForVisibilityChanges(ITextBuffer subjectBuffer, Action callback);
}
 
internal static class ITextBufferVisibilityTrackerExtensions
{
    /// <summary>
    /// Waits the specified amount of time while the specified <paramref name="subjectBuffer"/> is not visible.  If
    /// any document visibility changes happen, the delay will cancel.
    /// </summary>
    public static Task DelayWhileNonVisibleAsync(
        this ITextBufferVisibilityTracker? service,
        IThreadingContext threadingContext,
        IAsynchronousOperationListener listener,
        ITextBuffer subjectBuffer,
        TimeSpan timeSpan,
        CancellationToken cancellationToken)
    {
        // Only add a delay if we have access to a service that will tell us when the buffer become visible or not.
        if (service is null)
            return Task.CompletedTask;
 
        // Because cancellation is both expensive, and a super common thing to occur while we're delaying the caller
        // until visibility, we special case the implementation here and transition to the canceled state
        // explicitly, rather than throwing a cancellation exception.
 
        var delayTask = DelayWhileNonVisibleWorkerAsync();
 
        // it's very reasonable for the delay-task to complete synchronously (we've already been canceled, or the
        // buffer is already visible.  So fast path that out.
        if (delayTask.IsCompleted)
            return delayTask;
 
        var taskOfTask = delayTask.ContinueWith(
            // Convert a successfully completed task when we were canceled to a canceled task.  Otherwise, return
            // the faulted or non-canceled task as is.
            task => task.Status == TaskStatus.RanToCompletion && cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : task,
            CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
        return taskOfTask.Unwrap();
 
        // Normal delay logic, except that this does not throw in the event of cancellation, but instead returns
        // gracefully.  The above task continuation logic then ensures we return a canceled task without needing
        // exceptions.
        async Task DelayWhileNonVisibleWorkerAsync()
        {
            if (cancellationToken.IsCancellationRequested)
                return;
 
            await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable();
            if (cancellationToken.IsCancellationRequested)
                return;
 
            if (service.IsVisible(subjectBuffer))
                return;
 
            // ensure we listen for visibility changes before checking.  That way we don't have a race where we check
            // something see it is not visible, but then do not hear about its visibility change because we've hooked up
            // our event after that happens.
            var visibilityChangedTaskSource = new TaskCompletionSource<bool>();
            var callback = void () => visibilityChangedTaskSource.TrySetResult(true);
            service.RegisterForVisibilityChanges(subjectBuffer, callback);
 
            try
            {
                // Listen to when the active document changed so that we startup work on a document once it becomes visible.
                var delayTask = listener.Delay(timeSpan, cancellationToken);
                await Task.WhenAny(delayTask, visibilityChangedTaskSource.Task).NoThrowAwaitable(captureContext: true);
            }
            finally
            {
                service.UnregisterForVisibilityChanges(subjectBuffer, callback);
            }
        }
    }
}