File: SolutionEvents\HostLegacySolutionEventsWorkspaceEventListener.cs
Web Access
Project: ..\..\..\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.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.LegacySolutionEvents
{
    [ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared]
    internal sealed partial class HostLegacySolutionEventsWorkspaceEventListener : IEventListener<object>
    {
        private readonly IGlobalOptionService _globalOptions;
        private readonly IThreadingContext _threadingContext;
        private readonly AsyncBatchingWorkQueue<WorkspaceChangeEventArgs> _eventQueue;
 
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public HostLegacySolutionEventsWorkspaceEventListener(
            IGlobalOptionService globalOptions,
            IThreadingContext threadingContext,
            IAsynchronousOperationListenerProvider listenerProvider)
        {
            _globalOptions = globalOptions;
            _threadingContext = threadingContext;
            _eventQueue = new AsyncBatchingWorkQueue<WorkspaceChangeEventArgs>(
                DelayTimeSpan.Short,
                ProcessWorkspaceChangeEventsAsync,
                listenerProvider.GetListener(FeatureAttribute.SolutionCrawlerUnitTesting),
                _threadingContext.DisposalToken);
        }
 
        public void StartListening(Workspace workspace, object? serviceOpt)
        {
            if (_globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler))
            {
                workspace.WorkspaceChanged += OnWorkspaceChanged;
                _threadingContext.DisposalToken.Register(() =>
                {
                    workspace.WorkspaceChanged -= OnWorkspaceChanged;
                });
            }
        }
 
        private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e)
            => _eventQueue.AddWork(e);
 
        private async ValueTask ProcessWorkspaceChangeEventsAsync(ImmutableSegmentedList<WorkspaceChangeEventArgs> events, CancellationToken cancellationToken)
        {
            if (events.IsEmpty)
                return;
 
            var workspace = events[0].OldSolution.Workspace;
            Contract.ThrowIfTrue(events.Any(e => e.OldSolution.Workspace != workspace || e.NewSolution.Workspace != workspace));
 
            var client = await RemoteHostClient.TryGetClientAsync(workspace, cancellationToken).ConfigureAwait(false);
 
            if (client is null)
            {
                var aggregationService = workspace.Services.GetRequiredService<ILegacySolutionEventsAggregationService>();
                var shouldReport = aggregationService.ShouldReportChanges(workspace.Services.SolutionServices);
                if (!shouldReport)
                    return;
 
                foreach (var args in events)
                    await aggregationService.OnWorkspaceChangedAsync(args, cancellationToken).ConfigureAwait(false);
            }
            else
            {
                // Notifying OOP of workspace events can be expensive (there may be a lot of them, and they involve
                // syncing over entire solution snapshots).  As such, do not bother to do this if the remote side says
                // that it's not interested in the events.  This will happen, for example, when the unittesting
                // Test-Explorer window has not been shown yet, and so the unit testing system will not have registered
                // an incremental analyzer with us.
                var shouldReport = await client.TryInvokeAsync<IRemoteLegacySolutionEventsAggregationService, bool>(
                    (service, cancellationToken) => service.ShouldReportChangesAsync(cancellationToken),
                    cancellationToken).ConfigureAwait(false);
                if (!shouldReport.HasValue || !shouldReport.Value)
                    return;
 
                foreach (var args in events)
                {
                    await client.TryInvokeAsync<IRemoteLegacySolutionEventsAggregationService>(
                        args.OldSolution, args.NewSolution,
                        (service, oldSolutionChecksum, newSolutionChecksum, cancellationToken) =>
                            service.OnWorkspaceChangedAsync(oldSolutionChecksum, newSolutionChecksum, args.Kind, args.ProjectId, args.DocumentId, cancellationToken),
                        cancellationToken).ConfigureAwait(false);
                }
            }
        }
    }
}