File: AsyncCompletionTracker.cs
Web Access
Project: src\src\VisualStudio\IntegrationTest\TestSetup\Microsoft.VisualStudio.IntegrationTest.Setup.csproj (Microsoft.VisualStudio.IntegrationTest.Setup)
// 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.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
 
namespace Microsoft.VisualStudio.IntegrationTest.Setup
{
    [Export]
    [Shared]
    internal class AsyncCompletionTracker
    {
        private readonly IAsynchronousOperationListenerProvider _asynchronousOperationListenerProvider;
        private readonly IAsyncCompletionBroker _asyncCompletionBroker;
 
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public AsyncCompletionTracker(
            IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider,
            IAsyncCompletionBroker asyncCompletionBroker)
        {
            // Store the listener provider, but delay accessing the listener itself since tracking could still be
            // disabled during the initialization sequence for integration tests.
            _asynchronousOperationListenerProvider = asynchronousOperationListenerProvider;
            _asyncCompletionBroker = asyncCompletionBroker;
        }
 
        internal void StartListening()
        {
            _asyncCompletionBroker.CompletionTriggered += HandleAsyncCompletionTriggered;
        }
 
        internal void StopListening()
        {
            _asyncCompletionBroker.CompletionTriggered -= HandleAsyncCompletionTriggered;
        }
 
        private void HandleAsyncCompletionTriggered(object sender, CompletionTriggeredEventArgs e)
        {
            var cancellationSource = new CancellationTokenSource();
            var listener = _asynchronousOperationListenerProvider.GetListener(FeatureAttribute.CompletionSet);
            var token = listener.BeginAsyncOperation(nameof(IAsyncCompletionBroker.CompletionTriggered));
 
            e.CompletionSession.Dismissed += ReleaseTokenHandler;
            e.CompletionSession.ItemCommitted += ReleaseTokenHandler;
 
            _ = Task.Run(async () =>
                {
                    // AsyncCompletion might fire multiple ItemsUpdated events per keystroke typed, which means
                    // we could see the first ItemsUpdated event even though items don't change (but computation finished).
                    // If test attempts to assert state after seeing first event it would cause flakiness. 
                    // Use SelectedItemProvider to wait for all pending work to be completed.
                    var item = await ((ISelectedItemProvider)e.CompletionSession).GetSelectedItemAsync(GetSelectedItemOptions.WaitForContextAndComputation, cancellationSource.Token);
                    ReleaseToken();
                }, cancellationSource.Token);
 
            return;
 
            // Local function
            void ReleaseTokenHandler(object sender, EventArgs e) => ReleaseToken();
 
            void ReleaseToken()
            {
                Interlocked.Exchange(ref token, null)?.Dispose();
                if (Interlocked.Exchange(ref cancellationSource, null) is { } source)
                {
                    source.Cancel();
                    source.Dispose();
                }
            }
        }
    }
}