File: Handler\SourceGenerators\SourceGeneratorRefreshQueue.cs
Web Access
Project: src\src\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
using StreamJsonRpc;
 
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SourceGenerators;
 
internal sealed class SourceGeneratorRefreshQueue :
    IOnInitialized,
    ILspService,
    IDisposable
{
    private const string RefreshSourceGeneratedDocumentName = "workspace/refreshSourceGeneratedDocument";
 
    private readonly IAsynchronousOperationListener _asyncListener;
    private readonly CancellationTokenSource _disposalTokenSource = new();
    private readonly LspWorkspaceRegistrationService _lspWorkspaceRegistrationService;
    private readonly LspWorkspaceManager _lspWorkspaceManager;
    private readonly IClientLanguageServerManager _notificationManager;
    private readonly AsyncBatchingWorkQueue _refreshQueue;
 
    public SourceGeneratorRefreshQueue(
        IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider,
        LspWorkspaceRegistrationService lspWorkspaceRegistrationService,
        LspWorkspaceManager lspWorkspaceManager,
        IClientLanguageServerManager notificationManager)
    {
        _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService;
        _lspWorkspaceManager = lspWorkspaceManager;
        _notificationManager = notificationManager;
        _asyncListener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.SourceGenerators);
 
        // Batch up workspace notifications so that we only send a notification to refresh source generated files
        // every 2 seconds - long enough to avoid spamming the client with notifications, but short enough to refresh
        // the source generated files relatively frequently.
        _refreshQueue = _refreshQueue = new AsyncBatchingWorkQueue(
            delay: DelayTimeSpan.Idle,
            processBatchAsync: RefreshSourceGeneratedDocumentsAsync,
            asyncListener: _asyncListener,
            _disposalTokenSource.Token);
    }
 
    public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken)
    {
        if (clientCapabilities.HasVisualStudioLspCapability())
        {
            // VS source generated document content is not provided by LSP.
            return Task.CompletedTask;
        }
 
        // After we have initialized we can start listening for workspace changes.
        _lspWorkspaceRegistrationService.LspSolutionChanged += OnLspSolutionChanged;
        return Task.CompletedTask;
    }
 
    private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
    {
        var asyncToken = _asyncListener.BeginAsyncOperation($"{nameof(SourceGeneratorRefreshQueue)}.{nameof(OnLspSolutionChanged)}");
        _ = OnLspSolutionChangedAsync(e)
            .CompletesAsyncOperation(asyncToken)
            .ReportNonFatalErrorUnlessCancelledAsync(_disposalTokenSource.Token);
    }
 
    private async Task OnLspSolutionChangedAsync(WorkspaceChangeEventArgs e)
    {
        var projectId = e.ProjectId ?? e.DocumentId?.ProjectId;
        if (projectId is not null)
        {
            // We have a specific changed project - do some additional checks to see if
            // source generators possibly changed.  Note that this overreports actual
            // changes to the source generated text; we rely on resultIds in the text retrieval to avoid unnecessary serialization.
 
            var oldProject = e.OldSolution.GetProject(projectId);
            var newProject = e.NewSolution.GetProject(projectId);
 
            // If the project has been added/removed, we need to update the generated files.
            if (oldProject is null || newProject is null)
            {
                _refreshQueue.AddWork();
                return;
            }
 
            // Trivial check.  see if the SG version of these projects changed.  If so, we definitely want to update generated files.
            if (e.OldSolution.GetSourceGeneratorExecutionVersion(projectId) !=
                e.NewSolution.GetSourceGeneratorExecutionVersion(projectId))
            {
                _refreshQueue.AddWork();
                return;
            }
 
            // More expensive check - see if the dependent versions are different.
            await CheckDependentVersionsAsync(oldProject, newProject).ConfigureAwait(false);
        }
        else
        {
            // We don't have a specific project change - if this is a solution change we need to queue a refresh anyway.
            if (e.Kind is WorkspaceChangeKind.SolutionChanged or WorkspaceChangeKind.SolutionAdded or WorkspaceChangeKind.SolutionRemoved or WorkspaceChangeKind.SolutionReloaded or WorkspaceChangeKind.SolutionCleared)
            {
                _refreshQueue.AddWork();
            }
        }
 
        async Task CheckDependentVersionsAsync(Project oldProject, Project newProject)
        {
            if (await oldProject.GetDependentVersionAsync(_disposalTokenSource.Token).ConfigureAwait(false) !=
                await newProject.GetDependentVersionAsync(_disposalTokenSource.Token).ConfigureAwait(false))
            {
                _refreshQueue.AddWork();
            }
        }
    }
 
    private ValueTask RefreshSourceGeneratedDocumentsAsync(
        CancellationToken cancellationToken)
    {
        var hasOpenSourceGeneratedDocuments = _lspWorkspaceManager.GetTrackedLspText().Keys.Any(uri => uri.Scheme == SourceGeneratedDocumentUri.Scheme);
        if (!hasOpenSourceGeneratedDocuments)
        {
            // There are no opened source generated documents - we don't need to bother asking the client to refresh anything.
            return ValueTaskFactory.CompletedTask;
        }
 
        try
        {
            return _notificationManager.SendNotificationAsync(RefreshSourceGeneratedDocumentName, cancellationToken);
        }
        catch (Exception ex) when (ex is ObjectDisposedException or ConnectionLostException)
        {
            // It is entirely possible that we're shutting down and the connection is lost while we're trying to send a notification
            // as this runs outside of the guaranteed ordering in the queue. We can safely ignore this exception.
        }
 
        return ValueTaskFactory.CompletedTask;
    }
 
    public void Dispose()
    {
        _lspWorkspaceRegistrationService.LspSolutionChanged -= OnLspSolutionChanged;
        _disposalTokenSource.Cancel();
        _disposalTokenSource.Dispose();
    }
}