File: SolutionExplorer\AnalyzerItem\AnalyzerItemSource.cs
Web Access
Project: src\src\VisualStudio\Core\Impl\Microsoft.VisualStudio.LanguageServices.Implementation.csproj (Microsoft.VisualStudio.LanguageServices.Implementation)
// 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.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SourceGeneration;
using Microsoft.CodeAnalysis.Threading;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Shell;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer;
 
internal sealed class AnalyzerItemSource : IAttachedCollectionSource
{
    private readonly AnalyzersFolderItem _analyzersFolder;
    private readonly IAnalyzersCommandHandler _commandHandler;
 
    private readonly BulkObservableCollection<AnalyzerItem> _items = [];
 
    private readonly CancellationTokenSource _cancellationTokenSource = new();
    private readonly AsyncBatchingWorkQueue _workQueue;
 
    private IReadOnlyCollection<AnalyzerReference>? _analyzerReferences;
 
    private Workspace Workspace => _analyzersFolder.Workspace;
    private ProjectId ProjectId => _analyzersFolder.ProjectId;
 
    private WorkspaceEventRegistration? _workspaceChangedDisposer;
 
    public AnalyzerItemSource(
        AnalyzersFolderItem analyzersFolder,
        IAnalyzersCommandHandler commandHandler,
        IAsynchronousOperationListenerProvider listenerProvider)
    {
        _analyzersFolder = analyzersFolder;
        _commandHandler = commandHandler;
 
        _workQueue = new AsyncBatchingWorkQueue(
            DelayTimeSpan.Idle,
            ProcessQueueAsync,
            listenerProvider.GetListener(FeatureAttribute.SourceGenerators),
            _cancellationTokenSource.Token);
 
        _workspaceChangedDisposer = this.Workspace.RegisterWorkspaceChangedHandler(OnWorkspaceChanged);
 
        // Kick off the initial work to determine the starting set of items.
        _workQueue.AddWork();
    }
 
    public object SourceItem => _analyzersFolder;
 
    // Defer actual determination and computation of the items until later.
    public bool HasItems => !_cancellationTokenSource.IsCancellationRequested;
 
    public IEnumerable Items => _items;
 
    private void OnWorkspaceChanged(WorkspaceChangeEventArgs e)
    {
        switch (e.Kind)
        {
            case WorkspaceChangeKind.SolutionAdded:
            case WorkspaceChangeKind.SolutionChanged:
            case WorkspaceChangeKind.SolutionReloaded:
            case WorkspaceChangeKind.SolutionRemoved:
            case WorkspaceChangeKind.SolutionCleared:
                _workQueue.AddWork();
                break;
 
            case WorkspaceChangeKind.ProjectAdded:
            case WorkspaceChangeKind.ProjectReloaded:
            case WorkspaceChangeKind.ProjectChanged:
            case WorkspaceChangeKind.ProjectRemoved:
                if (e.ProjectId == this.ProjectId)
                    _workQueue.AddWork();
 
                break;
        }
    }
 
    private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken)
    {
        // If the project went away, then shut ourselves down.
        var project = this.Workspace.CurrentSolution.GetProject(this.ProjectId);
        if (project is null)
        {
            _workspaceChangedDisposer?.Dispose();
            _workspaceChangedDisposer = null;
 
            _cancellationTokenSource.Cancel();
 
            // Note: mutating _items will be picked up automatically by clients who are bound to the collection.  We do
            // not need to notify them through some other mechanism.
 
            if (_items.Count > 0)
            {
                // Go back to UI thread to update the observable collection.  Otherwise, it enqueue its own UI work that we cannot track.
                await _analyzersFolder.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                _items.Clear();
            }
 
            return;
        }
 
        // If nothing changed wrt analyzer references, then there's nothing we need to do.
        if (project.AnalyzerReferences == _analyzerReferences)
            return;
 
        // Set the new set of analyzer references we're going to have AnalyzerItems for.
        _analyzerReferences = project.AnalyzerReferences;
 
        var references = await GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync(
            project, cancellationToken).ConfigureAwait(false);
 
        // Go back to UI thread to update the observable collection.  Otherwise, it enqueue its own UI work that we cannot track.
        await _analyzersFolder.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
        try
        {
            _items.BeginBulkOperation();
 
            _items.Clear();
            foreach (var analyzerReference in references.OrderBy(static r => r.Display))
                _items.Add(new AnalyzerItem(_analyzersFolder, analyzerReference, _commandHandler.AnalyzerContextMenuController));
 
            return;
        }
        finally
        {
            _items.EndBulkOperation();
        }
 
        async Task<ImmutableArray<AnalyzerReference>> GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync(
            Project project,
            CancellationToken cancellationToken)
        {
            var client = await RemoteHostClient.TryGetClientAsync(this.Workspace, cancellationToken).ConfigureAwait(false);
 
            // If we can't make a remote call.  Fall back to processing in the VS host.
            if (client is null)
                return [.. project.AnalyzerReferences.Where(r => r is not AnalyzerFileReference || r.HasAnalyzersOrSourceGenerators(project.Language))];
 
            using var connection = client.CreateConnection<IRemoteSourceGenerationService>(callbackTarget: null);
 
            using var _ = ArrayBuilder<AnalyzerReference>.GetInstance(out var builder);
            foreach (var reference in project.AnalyzerReferences)
            {
                // Can only remote AnalyzerFileReferences over to the oop side.
                if (reference is AnalyzerFileReference analyzerFileReference)
                {
                    var result = await connection.TryInvokeAsync<bool>(
                        project,
                        (service, solutionChecksum, cancellationToken) => service.HasAnalyzersOrSourceGeneratorsAsync(
                            solutionChecksum, project.Id, analyzerFileReference.FullPath, cancellationToken),
                        cancellationToken).ConfigureAwait(false);
 
                    // If the call fails, the OOP substrate will have already reported an error
                    if (!result.HasValue)
                        return [];
 
                    if (result.Value)
                        builder.Add(analyzerFileReference);
                }
                else if (reference.HasAnalyzersOrSourceGenerators(project.Language))
                {
                    builder.Add(reference);
                }
            }
 
            return builder.ToImmutableAndClear();
        }
    }
}