File: SolutionEvents\HostLegacySolutionEventsWorkspaceEventListener.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.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;
 
/// <summary>
/// Event listener that hears about workspaces and exists solely to let unit testing continue to work using their own
/// fork of solution crawler.  Importantly, this is always active until the point that we can get unit testing to move
/// to an entirely differently (ideally 'pull') model for test discovery.
/// </summary>
[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)
    {
        // We only support this option to disable crawling in internal speedometer and ddrit perf runs to lower noise.
        // It is not exposed to the user.
        if (_globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler))
        {
            workspace.WorkspaceChanged += OnWorkspaceChanged;
            _threadingContext.DisposalToken.Register(() =>
            {
                workspace.WorkspaceChanged -= OnWorkspaceChanged;
            });
        }
    }
 
    private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e)
    {
        // Legacy workspace events exist solely to let unit testing continue to work using their own fork of solution
        // crawler.  As such, they only need events for the project types they care about.  Specifically, that is only
        // for VB and C#.  This is relevant as well as we don't sync any other project types to OOP.  So sending 
        // notifications about other projects that don't even exist on the other side isn't helpful.
 
        var projectId = e.ProjectId ?? e.DocumentId?.ProjectId;
        if (projectId != null)
        {
            var project = e.OldSolution.GetProject(projectId) ?? e.NewSolution.GetProject(projectId);
            if (project != null && !RemoteSupportedLanguages.IsSupported(project.Language))
                return;
        }
 
        _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);
            }
        }
    }
}