File: Diagnostics\AbstractPushOrPullDiagnosticsTaggerProvider.cs
Web Access
Project: ..\..\..\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.Collections.Immutable;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Tagging;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Workspaces;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
 
namespace Microsoft.CodeAnalysis.Diagnostics;
 
internal static class DiagnosticTaggingOptionsStorage
{
    public static readonly Option2<bool> PullDiagnosticTagging = new(
        "dotnet_pull_diagnostic_tagging", defaultValue: true);
}
 
/// <summary>
/// Base type of all diagnostic taggers (classification, squiggles, suggestions, inline-diags).  Subclasses can control
/// things by overriding functionality in this type.  Internally, this will switch to either a pull or push cased
/// approach at instantiation time depending on our internal feature flag.
/// </summary>
internal abstract partial class AbstractPushOrPullDiagnosticsTaggerProvider<TTag> : ITaggerProvider
    where TTag : ITag
{
    private readonly ITaggerProvider _underlyingTaggerProvider;
 
    protected readonly IGlobalOptionService GlobalOptions;
 
    protected AbstractPushOrPullDiagnosticsTaggerProvider(
        IThreadingContext threadingContext,
        IDiagnosticService diagnosticService,
        IDiagnosticAnalyzerService analyzerService,
        IGlobalOptionService globalOptions,
        ITextBufferVisibilityTracker? visibilityTracker,
        IAsynchronousOperationListener listener)
    {
        GlobalOptions = globalOptions;
 
        // We make an up front check if tagging itself is in 'pull' mode (directly using snapshots and calling through
        // IDiagnosticAnalyzerService) or in 'push' mode (listening to events from IDiagnosticService and trying to map
        // them to the current document snapshot).  Note that this flag is independent of the flag to determine if LSP
        // pull diagnostics is on or not.  We support the following combinations:
        //
        //  Diagnostic Mode | Tagging Mode | Classification | Squiggles   | Suggestions | Inline Diagnostics
        //  ------------------------------------------------------------------------------------------------
        //  Pull            | Pull         | LSP            | LSP         | LSP         | Pull Tagger
        //  Pull            | Push         | LSP            | LSP         | LSP         | Push Tagger
        //  Push            | Pull         | Pull Tagger    | Pull Tagger | Pull Tagger | Pull Tagger
        //  Push            | Push         | Push Tagger    | Push Tagger | Push Tagger | Push Tagger
        //
        // Put another way, if DiagnosticMode is 'Push' (non-LSP), then this type does all the work, choosing tagging
        // pull/push for all features.   If DiagnosticMode is 'pull' (LSP), then LSP takes over classification,
        // squiggles, and suggestions, while we still handle inline-diagnostics.
        if (globalOptions.GetOption(DiagnosticTaggingOptionsStorage.PullDiagnosticTagging))
        {
            _underlyingTaggerProvider = new PullDiagnosticsTaggerProvider(
                this, threadingContext, diagnosticService, analyzerService, globalOptions, visibilityTracker, listener);
        }
        else
        {
            _underlyingTaggerProvider = new PushDiagnosticsTaggerProvider(
                this, threadingContext, diagnosticService, globalOptions, visibilityTracker, listener);
        }
    }
 
    public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
        => _underlyingTaggerProvider.CreateTagger<T>(buffer);
 
    private static ITaggerEventSource CreateEventSourceWorker(ITextBuffer subjectBuffer, IDiagnosticService diagnosticService)
    {
        // OnTextChanged is added for diagnostics in source generated files: it's possible that the analyzer driver
        // executed on content which was produced by a source generator but is not yet reflected in an open text
        // buffer for that generated file. In this case, we need to update the tags after the buffer updates (which
        // triggers a text changed event) to ensure diagnostics are positioned correctly.
        return TaggerEventSources.Compose(
            TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer),
            TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer),
            TaggerEventSources.OnDiagnosticsChanged(subjectBuffer, diagnosticService),
            TaggerEventSources.OnTextChanged(subjectBuffer));
    }
 
    // Functionality for subclasses to control how this diagnostic tagging operates.  All the individual
    // SingleDiagnosticKindTaggerProvider will defer to these to do the work so that they otherwise operate
    // identically.
 
    protected abstract ImmutableArray<IOption2> Options { get; }
    protected virtual ImmutableArray<IOption2> FeatureOptions { get; } = ImmutableArray<IOption2>.Empty;
 
    protected abstract bool IsEnabled { get; }
 
    protected abstract bool SupportsDiagnosticMode(DiagnosticMode mode);
    protected abstract bool IncludeDiagnostic(DiagnosticData data);
 
    protected abstract bool TagEquals(TTag tag1, TTag tag2);
    protected abstract ITagSpan<TTag>? CreateTagSpan(Workspace workspace, bool isLiveUpdate, SnapshotSpan span, DiagnosticData data);
 
    /// <summary>
    /// Get the <see cref="DiagnosticDataLocation"/> that should have the tag applied to it.
    /// In most cases, this is the <see cref="DiagnosticData.DataLocation"/> but overrides can change it (e.g. unnecessary classifications).
    /// </summary>
    /// <param name="diagnosticData">the diagnostic containing the location(s).</param>
    /// <returns>an array of locations that should have the tag applied.</returns>
    protected virtual ImmutableArray<DiagnosticDataLocation> GetLocationsToTag(DiagnosticData diagnosticData)
        => diagnosticData.DataLocation is not null ? ImmutableArray.Create(diagnosticData.DataLocation) : ImmutableArray<DiagnosticDataLocation>.Empty;
}