// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Common; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editor.Shared.Preview; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.Editor.Tagging; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics { /// /// Diagnostics works slightly differently than the rest of the taggers. For diagnostics, /// we want to try to have an individual tagger per diagnostic producer per buffer. /// However, the editor only allows a single tagger provider per buffer. So in order to /// get the abstraction we want, we create one outer tagger provider that is associated /// with the buffer. Then, under the covers, we create individual async taggers for each /// diagnostic producer we hear about for that buffer. /// /// In essence, we have one tagger that wraps a multitude of taggers it delegates to. /// Each of these taggers is nicely asynchronous and properly works within the async /// tagging infrastructure. /// internal abstract partial class AbstractDiagnosticsTaggerProvider : AsynchronousTaggerProvider where TTag : ITag { private readonly IDiagnosticService _diagnosticService; protected AbstractDiagnosticsTaggerProvider( IDiagnosticService diagnosticService, IForegroundNotificationService notificationService, IAsynchronousOperationListener listener) : base(listener, notificationService) { _diagnosticService = diagnosticService; } protected override TaggerDelay AddedTagNotificationDelay => TaggerDelay.OnIdle; protected override ITaggerEventSource CreateEventSource(ITextView textViewOpt, ITextBuffer subjectBuffer) { return TaggerEventSources.Compose( TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer, TaggerDelay.Medium), TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer, TaggerDelay.Medium), TaggerEventSources.OnDiagnosticsChanged(subjectBuffer, _diagnosticService, TaggerDelay.Short)); } protected internal abstract bool IsEnabled { get; } protected internal abstract bool IncludeDiagnostic(DiagnosticData data); protected internal abstract ITagSpan CreateTagSpan(bool isLiveUpdate, SnapshotSpan span, DiagnosticData data); protected override Task ProduceTagsAsync(TaggerContext context, DocumentSnapshotSpan spanToTag, int? caretPosition) { ProduceTags(context, spanToTag); return SpecializedTasks.EmptyTask; } private void ProduceTags(TaggerContext context, DocumentSnapshotSpan spanToTag) { if (!this.IsEnabled) { return; } var document = spanToTag.Document; if (document == null) { return; } var editorSnapshot = spanToTag.SnapshotSpan.Snapshot; var cancellationToken = context.CancellationToken; var workspace = document.Project.Solution.Workspace; // See if we've marked any spans as those we want to suppress diagnostics for. // This can happen for buffers used in the preview workspace where some feature // is generating code that it doesn't want errors shown for. var buffer = editorSnapshot.TextBuffer; var suppressedDiagnosticsSpans = default(NormalizedSnapshotSpanCollection); buffer?.Properties.TryGetProperty(PredefinedPreviewTaggerKeys.SuppressDiagnosticsSpansKey, out suppressedDiagnosticsSpans); var eventArgs = _diagnosticService.GetDiagnosticsUpdatedEventArgs( workspace, document.Project.Id, document.Id, context.CancellationToken); var sourceText = editorSnapshot.AsText(); foreach (var updateArg in eventArgs) { ProduceTags( context, spanToTag, workspace, document, sourceText, suppressedDiagnosticsSpans, updateArg, cancellationToken); } } private void ProduceTags( TaggerContext context, DocumentSnapshotSpan spanToTag, Workspace workspace, Document document, SourceText sourceText, NormalizedSnapshotSpanCollection suppressedDiagnosticsSpans, UpdatedEventArgs updateArgs, CancellationToken cancellationToken) { try { var id = updateArgs.Id; var diagnostics = _diagnosticService.GetDiagnostics( workspace, document.Project.Id, document.Id, id, false, cancellationToken); var isLiveUpdate = id is ISupportLiveUpdate; var requestedSpan = spanToTag.SnapshotSpan; var editorSnapshot = requestedSpan.Snapshot; foreach (var diagnosticData in diagnostics) { if (this.IncludeDiagnostic(diagnosticData)) { // We're going to be retrieving the diagnostics against the last time the engine // computed them against this document *id*. That might have been a different // version of the document vs what we're looking at now. But that's ok: // // 1) GetExistingOrCalculatedTextSpan will ensure that the diagnostics spans are // contained within 'editorSnapshot'. // 2) We'll eventually hear about an update to the diagnostics for this document // for whatever edits happened between the last time and this current snapshot. // So we'll eventually reach a point where the diagnostics exactly match the // editorSnapshot. var diagnosticSpan = diagnosticData.GetExistingOrCalculatedTextSpan(sourceText) .ToSnapshotSpan(editorSnapshot); if (diagnosticSpan.IntersectsWith(requestedSpan) && !IsSuppressed(suppressedDiagnosticsSpans, diagnosticSpan)) { var tagSpan = this.CreateTagSpan(isLiveUpdate, diagnosticSpan, diagnosticData); if (tagSpan != null) { context.AddTag(tagSpan); } } } } } catch (ArgumentOutOfRangeException ex) when (FatalError.ReportWithoutCrash(ex)) { // https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_workitems?id=428328&_a=edit&triage=false // explicitly report NFW to find out what is causing us for out of range. // stop crashing on such occations return; } } private bool IsSuppressed(NormalizedSnapshotSpanCollection suppressedSpans, SnapshotSpan span) => suppressedSpans != null && suppressedSpans.IntersectsWith(span); } }