File: Handler\TextDocumentContent\AbstractTextDocumentContentRefreshQueue.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.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Threading;
using Roslyn.LanguageServer.Protocol;
using StreamJsonRpc;
 
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.TextDocumentContent;
 
/// <summary>
/// Abstract refresh queue for text document content providers. Subclasses specify which URI scheme they handle
/// and implement custom change detection logic via <see cref="AbstractRefreshQueue.OnLspSolutionChanged"/>.
/// Refresh notifications are sent for any open document matching the specified scheme.
/// </summary>
internal abstract class AbstractTextDocumentContentRefreshQueue :
    IOnInitialized,
    ILspService,
    IDisposable
{
    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 AbstractTextDocumentContentRefreshQueue(
        IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider,
        LspWorkspaceRegistrationService lspWorkspaceRegistrationService,
        LspWorkspaceManager lspWorkspaceManager,
        IClientLanguageServerManager notificationManager)
    {
        _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService;
        _lspWorkspaceManager = lspWorkspaceManager;
        _notificationManager = notificationManager;
        _asyncListener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Workspace);
 
        // Batch up workspace notifications so that we only send a notification to refresh virtual files
        // every 2 seconds - long enough to avoid spamming the client with notifications, but short enough to refresh
        // the virtual files relatively frequently.
        _refreshQueue = _refreshQueue = new AsyncBatchingWorkQueue(
            delay: DelayTimeSpan.Idle,
            processBatchAsync: RefreshVirtualDocumentsAsync,
            asyncListener: _asyncListener,
            _disposalTokenSource.Token);
    }
 
    public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken)
    {
        if (clientCapabilities.Workspace?.TextDocumentContent == null)
        {
            return;
        }
 
        // After we have initialized we can start listening for workspace changes.
        _lspWorkspaceRegistrationService.LspSolutionChanged += OnLspSolutionChanged;
    }
 
    private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
    {
        var asyncToken = _asyncListener.BeginAsyncOperation($"{nameof(AbstractTextDocumentContentRefreshQueue)}.{nameof(OnLspSolutionChanged)}");
        _ = OnLspSolutionChangedAsync(e)
            .CompletesAsyncOperation(asyncToken)
            .ReportNonFatalErrorUnlessCancelledAsync(_disposalTokenSource.Token);
    }
 
    protected async Task OnLspSolutionChangedAsync(WorkspaceChangeEventArgs e)
    {
        var shouldQueue = await ShouldEnqueueRefreshNotificationAsync(e, _disposalTokenSource.Token).ConfigureAwait(false);
        if (shouldQueue)
        {
            _refreshQueue.AddWork();
        }
    }
 
    protected abstract Task<bool> ShouldEnqueueRefreshNotificationAsync(WorkspaceChangeEventArgs e, CancellationToken cancellationToken);
 
    /// <summary>
    /// The scheme that this queue is responsible for.
    /// </summary>
    protected abstract string Scheme { get; }
 
    private async ValueTask RefreshVirtualDocumentsAsync(
        CancellationToken cancellationToken)
    {
        var trackedDocuments = _lspWorkspaceManager.GetTrackedLspText();
 
        foreach (var kvp in trackedDocuments)
        {
            var uri = kvp.Key;
            if (uri.ParsedUri is { } parsedUri && parsedUri.Scheme == Scheme)
            {
                try
                {
                    await _notificationManager.SendRequestAsync(
                        Methods.WorkspaceTextDocumentContentRefreshName,
                        new TextDocumentContentRefreshParams { Uri = uri },
                        cancellationToken).ConfigureAwait(false);
                }
                catch (Exception ex) when (ex is ObjectDisposedException or ConnectionLostException)
                {
                    // Connection may be lost during shutdown.
                    return;
                }
            }
        }
    }
 
    public void Dispose()
    {
        _lspWorkspaceRegistrationService.LspSolutionChanged -= OnLspSolutionChanged;
        _disposalTokenSource.Cancel();
        _disposalTokenSource.Dispose();
    }
}