File: IntelliSense\ModelComputation.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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense;
 
internal sealed class ModelComputation<TModel> where TModel : class
{
    #region Fields that can be accessed from either thread
 
    public readonly IThreadingContext ThreadingContext;
    private readonly CancellationToken _stopCancellationToken;
 
    /// <summary>
    /// Set when the first compute task completes
    /// </summary>
    private TModel _initialUnfilteredModel = null;
 
    #endregion
 
    #region Fields that can only be accessed from the foreground thread
 
    private readonly IController<TModel> _controller;
 
    private readonly CancellationTokenSource _stopTokenSource;
 
    // There may be multiple compute tasks chained together.  When a compute task finishes it
    // may end up with a null model (i.e. it found no items).  At that point *if* it is the
    // *last* compute task, then we will want to stop everything.  However, if it is not the
    // last compute task, then we just want to ignore that result and allow the actual
    // latest compute task to proceed.
    private Task<TModel> _lastTask;
    private Task _notifyControllerTask;
 
    #endregion
 
    public ModelComputation(
        IThreadingContext threadingContext,
        IController<TModel> controller)
    {
        ThreadingContext = threadingContext;
        _controller = controller;
 
        _stopTokenSource = new CancellationTokenSource();
        _stopCancellationToken = _stopTokenSource.Token;
 
        // Dummy up a new task so we don't need to check for null.
        _notifyControllerTask = _lastTask = SpecializedTasks.Null<TModel>();
    }
 
    public TModel InitialUnfilteredModel
    {
        get
        {
            ThreadingContext.ThrowIfNotOnUIThread();
            return _initialUnfilteredModel;
        }
    }
 
    public Task<TModel> ModelTask
    {
        get
        {
            ThreadingContext.ThrowIfNotOnUIThread();
 
            // We should never be called if we were stopped.
            Contract.ThrowIfTrue(_stopCancellationToken.IsCancellationRequested);
            return _lastTask;
        }
    }
 
    public Task WaitForModelComputation_ForTestingPurposesOnlyAsync()
        => ModelTask;
 
    public void Stop()
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        // cancel all outstanding tasks.
        _stopTokenSource.Cancel();
 
        // reset task so that it doesn't hold onto things like WpfTextView
        _notifyControllerTask = _lastTask = SpecializedTasks.Null<TModel>();
    }
 
    public void ChainTaskAndNotifyControllerWhenFinished(
        Func<TModel, CancellationToken, Task<TModel>> transformModelAsync,
        bool updateController = true)
    {
        ThreadingContext.ThrowIfNotOnUIThread();
 
        Contract.ThrowIfTrue(_stopCancellationToken.IsCancellationRequested, "should not chain tasks after we've been cancelled");
 
        // Mark that an async operation has begun.  This way tests know to wait until the
        // async operation is done before verifying results.  We will consider this
        // background task complete when its result has finally been displayed on the UI.
        var asyncToken = _controller.BeginAsyncOperation();
 
        // Create the task that will actually run the transformation step.
        var nextTask = TransformModelAsync(_lastTask);// transformModelAsync(_lastTask, _stopCancellationToken);
        nextTask.ReportNonFatalErrorAsync();
 
        // The next task is now the last task in the chain.
        _lastTask = nextTask;
 
        // When this task is complete *and* the last notification to the controller is
        // complete, then issue the next notification to the controller.  When we try to
        // issue the notification, see if we're still at the end of the chain.  If we're not,
        // then we don't need to notify as a later task will do so.
        _notifyControllerTask = Task.Factory.ContinueWhenAll(
            [_notifyControllerTask, nextTask],
            async tasks =>
            {
                await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _stopCancellationToken);
 
                if (tasks.All(t => t.Status == TaskStatus.RanToCompletion))
                {
                    _stopCancellationToken.ThrowIfCancellationRequested();
 
                    // Check if we're still the last task.  If so then we should update the
                    // controller. Otherwise there's a pending task that should run.  We
                    // don't need to update the controller (and the presenters) until our
                    // chain is finished.
                    updateController &= nextTask == _lastTask;
                    OnModelUpdated(nextTask.Result, updateController);
                }
            },
            _stopCancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).Unwrap();
 
        // When we've notified the controller of our result, we consider the async operation
        // to be completed.
        _notifyControllerTask.CompletesAsyncOperation(asyncToken);
 
        async Task<TModel> TransformModelAsync(Task<TModel> lastTask)
        {
            // Ensure we're on the BG before doing any model transformation work.
            await TaskScheduler.Default;
            var model = await lastTask.ConfigureAwait(false);
            return await transformModelAsync(model, _stopCancellationToken).ConfigureAwait(false);
        }
    }
 
    private void OnModelUpdated(TModel result, bool updateController)
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
 
        // Store the first result so that anyone who cares knows we've computed something
        _initialUnfilteredModel ??= result;
 
        _controller.OnModelUpdated(result, updateController);
    }
}