Repository: dotnet/roslyn
File: src/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs (C#)

Code before:
        }

        return await DiagnosticSourceManager.CreateWorkspaceDiagnosticSourcesAsync(context, requestDiagnosticCategory, cancellationToken).ConfigureAwait(false);
    }

    private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
    {
        UpdateLspChanged();
    }

    private void OnLspTextChanged(object? sender, EventArgs e)
    {
        UpdateLspChanged();
    }

    private void OnWorkspaceRefreshRequested()
    {
        UpdateLspChanged();
    }

    private void UpdateLspChanged()
    {
        // Loop through our map of source -> has changed and mark them as all having changed.
        foreach (var categoryResetEvent in _categoryToLspChanged.Values)
            categoryResetEvent.Set();
    }

    protected override async Task WaitForChangesAsync(string? category, RequestContext context, CancellationToken cancellationToken)
    {
        // A null category counts a separate category and should track changes independently of other categories, so we'll add an empty entry in our map for it.
        category ??= string.Empty;

        // Wait until the workspace changes again (or was changed while we were in the middle of processing).
        // We'll use a variant of an AutoResetEvent so in the case we were to have multiple requests for the same category,
        // they're all released. That's not expected to happen, but it ensures better behavior in the case of a misbehaving client.
        var resetEvent = _categoryToLspChanged.GetOrAdd(category, static _ => new ReleaseAllAutoResetEvent(initialState: true));
        await resetEvent.WaitAsync().WithCancellation(cancellationToken).ConfigureAwait(false);

        // We've hit a change, so we close the current request to allow the client to open a new one.
        context.TraceDebug($"Closing workspace/diagnostics request for {category}");
    }

    internal abstract TestAccessor GetTestAccessor();

    internal readonly struct TestAccessor(AbstractWorkspacePullDiagnosticsHandler<TDiagnosticsParams, TReport, TReturn> handler)
    {
        public void TriggerConnectionClose() => handler.UpdateLspChanged();
    }

    /// <summary>
    /// An <see cref="AutoResetEvent"/> with two differences: it supports async waiting, and in the case Set() releases a waiter, it releases all waiters rather than just one.

Review comment (suggestion):
any concern about not seeing everything _categoryToLspChanged?  For example, if it changes while you are iterating?

generally, i'm a big fan of `lock+dict` because the semantics are clear.  ConcurrentDict can be fine, it just now has a lot more scenarios to have to consider.

Code after:
            return [];
        }

        return await DiagnosticSourceManager.CreateWorkspaceDiagnosticSourcesAsync(context, requestDiagnosticCategory, cancellationToken).ConfigureAwait(false);
    }

    private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
    {
        UpdateLspChanged();
    }

    private void OnLspTextChanged(object? sender, EventArgs e)
    {
        UpdateLspChanged();
    }

    private void OnWorkspaceRefreshRequested()
    {
        UpdateLspChanged();
    }

    private void UpdateLspChanged()
    {
        // Loop through our map of source -> has changed and mark them as all having changed.
        foreach (var categoryResetEvent in _categoryToLspChanged.Values)
            categoryResetEvent.Set();
    }

    protected override async Task WaitForChangesAsync(string? category, RequestContext context, CancellationToken cancellationToken)
    {
        // A null category counts a separate category and should track changes independently of other categories, so we'll add an empty entry in our map for it.
        category ??= string.Empty;

        // Wait until the workspace changes again (or was changed while we were in the middle of processing).
        // We'll use a variant of an AutoResetEvent so in the case we were to have multiple requests for the same category,
        // they're all released. That's not expected to happen, but it ensures better behavior in the case of a misbehaving client.
        var resetEvent = _categoryToLspChanged.GetOrAdd(category, static _ => new ReleaseAllAutoResetEvent(initialState: true));
        await resetEvent.WaitAsync().WithCancellation(cancellationToken).ConfigureAwait(false);

        // We've hit a change, so we close the current request to allow the client to open a new one.
        context.TraceDebug($"Closing workspace/diagnostics request for {category}");
    }

    internal abstract TestAccessor GetTestAccessor();

    internal readonly struct TestAccessor(AbstractWorkspacePullDiagnosticsHandler<TDiagnosticsParams, TReport, TReturn> handler)
    {
        public void TriggerConnectionClose() => handler.UpdateLspChanged();
    }

    /// <summary>