From c6b74a29707c640b9a701ac5eeda0d91f0351822 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 11 Jan 2018 11:02:42 -0800 Subject: [PATCH] Simplify diagnostic tagging by making it use the standard tagging model. --- ...tractDiagnosticsAdornmentTaggerProvider.cs | 5 +- ...nosticsTaggerProvider.AggregatingTagger.cs | 423 ------------------ ...iagnosticsTaggerProvider.TaggerProvider.cs | 189 -------- .../AbstractDiagnosticsTaggerProvider.cs | 136 +++++- ...DiagnosticsClassificationTaggerProvider.cs | 2 +- .../DiagnosticsSquiggleTaggerProvider.cs | 2 +- .../DiagnosticsSuggestionTaggerProvider.cs | 9 +- .../Core/Tagging/TaggerContext.cs | 4 +- .../Portable/Diagnostics/DiagnosticData.cs | 11 +- 9 files changed, 131 insertions(+), 650 deletions(-) delete mode 100644 src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.AggregatingTagger.cs delete mode 100644 src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.TaggerProvider.cs diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.cs index 7a5a4ac564c..13d7b814d52 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsAdornmentTaggerProvider.cs @@ -1,6 +1,7 @@ -using System; +// 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.Collections.Generic; -using System.ComponentModel.Composition; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.VisualStudio.Text; diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.AggregatingTagger.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.AggregatingTagger.cs deleted file mode 100644 index f0b0e9ef3f1..00000000000 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.AggregatingTagger.cs +++ /dev/null @@ -1,423 +0,0 @@ -// 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.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Common; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.Text.Shared.Extensions; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Tagging; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics -{ - internal abstract partial class AbstractDiagnosticsTaggerProvider - { - private class AggregatingTagger : ForegroundThreadAffinitizedObject, IAccurateTagger, IDisposable - { - private readonly AbstractDiagnosticsTaggerProvider _owner; - private readonly ITextBuffer _subjectBuffer; - - private int _refCount; - private bool _disposed; - - /// - /// The current Document that our is associated with. - /// If our buffer becomes associated with another document, we will clear out any - /// cached diagnostic information we've collected so far as it's no longer valid. - /// - private DocumentId _currentDocumentId; - - private readonly Dictionary tagger)> _idToProviderAndTagger = new Dictionary tagger)>(); - - public event EventHandler TagsChanged; - - // Use a chain of tasks to make sure that we process each diagnostic event serially. - // This also ensures that the first diagnostic event we hear about will be processed - // after the initial background work to get the first group of diagnostics. - private readonly object _taskGate = new object(); - private Task _taskChain; - - private readonly CancellationTokenSource _initialDiagnosticsCancellationSource = new CancellationTokenSource(); - - public AggregatingTagger( - AbstractDiagnosticsTaggerProvider owner, - ITextBuffer subjectBuffer) - { - _owner = owner; - _subjectBuffer = subjectBuffer; - - var document = _subjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); - _currentDocumentId = document?.Id; - - // Kick off a background task to collect the initial set of diagnostics. - var cancellationToken = _initialDiagnosticsCancellationSource.Token; - var asyncToken = _owner._listener.BeginAsyncOperation(GetType() + ".GetInitialDiagnostics"); - var task = Task.Run(() => GetInitialDiagnosticsInBackground(document, cancellationToken), cancellationToken); - task.CompletesAsyncOperation(asyncToken); - - _taskChain = task; - - // Register to hear about diagnostics changing. When we're notified about new - // diagnostics (and those diagnostics are for our buffer), we'll ensure that - // we have an underlying tagger responsible for asynchronously handling diagnostics - // from the owner of that diagnostic update. - _owner._diagnosticService.DiagnosticsUpdated += OnDiagnosticsUpdated; - } - - private void GetInitialDiagnosticsInBackground( - Document document, CancellationToken cancellationToken) - { - this.AssertIsBackground(); - cancellationToken.ThrowIfCancellationRequested(); - - if (document != null) - { - var project = document.Project; - var workspace = project.Solution.Workspace; - foreach (var updateArgs in _owner._diagnosticService.GetDiagnosticsUpdatedEventArgs(workspace, project.Id, document.Id, cancellationToken)) - { - var diagnostics = AdjustInitialDiagnostics(project.Solution, updateArgs, cancellationToken); - if (diagnostics.Length == 0) - { - continue; - } - - OnDiagnosticsUpdatedOnBackground( - DiagnosticsUpdatedArgs.DiagnosticsCreated( - updateArgs.Id, updateArgs.Workspace, project.Solution, updateArgs.ProjectId, updateArgs.DocumentId, diagnostics)); - } - } - } - - private ImmutableArray AdjustInitialDiagnostics( - Solution solution, UpdatedEventArgs args, CancellationToken cancellationToken) - { - this.AssertIsBackground(); - - // we only reach here if there is the document - var document = solution.GetDocument(args.DocumentId); - Contract.ThrowIfNull(document); - // if there is no source text for this document, we don't populate the initial tags. this behavior is equivalent of existing - // behavior in OnDiagnosticsUpdated. - if (!document.TryGetText(out var text)) - { - return ImmutableArray.Empty; - } - - // GetDiagnostics returns whatever cached diagnostics in the service which can be stale ones. for example, build error will be most likely stale - // diagnostics. so here we make sure we filter out any diagnostics that is not in the text range. - var builder = ArrayBuilder.GetInstance(); - var fullSpan = new TextSpan(0, text.Length); - foreach (var diagnostic in _owner._diagnosticService.GetDiagnostics( - args.Workspace, args.ProjectId, args.DocumentId, args.Id, includeSuppressedDiagnostics: false, cancellationToken: cancellationToken)) - { - if (fullSpan.Contains(diagnostic.GetExistingOrCalculatedTextSpan(text))) - { - builder.Add(diagnostic); - } - } - - return builder.ToImmutableAndFree(); - } - - public void OnTaggerCreated() - { - this.AssertIsForeground(); - Debug.Assert(_refCount >= 0); - Debug.Assert(!_disposed); - - _refCount++; - } - - public void Dispose() - { - this.AssertIsForeground(); - Debug.Assert(_refCount > 0); - Debug.Assert(!_disposed); - - _refCount--; - - if (_refCount == 0) - { - _disposed = true; - - // Stop listening to diagnostic changes from the diagnostic service. - _owner._diagnosticService.DiagnosticsUpdated -= OnDiagnosticsUpdated; - _initialDiagnosticsCancellationSource.Cancel(); - - // Disconnect us from our underlying taggers and make sure they're - // released as well. - DisconnectFromAllTaggers(); - _owner.RemoveTagger(this, _subjectBuffer); - } - } - - private void DisconnectFromTagger(IAccurateTagger tagger) - { - this.AssertIsForeground(); - - tagger.TagsChanged -= OnUnderlyingTaggerTagsChanged; - if (tagger is IDisposable disposable) - { - disposable.Dispose(); - } - } - - private void DisconnectFromAllTaggers() - { - this.AssertIsForeground(); - - foreach (var kvp in _idToProviderAndTagger) - { - var tagger = kvp.Value.tagger; - - DisconnectFromTagger(tagger); - } - - _idToProviderAndTagger.Clear(); - } - - private void RegisterNotification(Action action) - { - var token = _owner._listener.BeginAsyncOperation(GetType().Name + "RegisterNotification"); - _owner._notificationService.RegisterNotification(action, token); - } - - private void OnDiagnosticsUpdated(object sender, DiagnosticsUpdatedArgs e) - { - lock (_taskGate) - { - // Chain the events so we process them serially. This also ensures - // that we don't process events while still getting our initial set - // of diagnostics. - var asyncToken = _owner._listener.BeginAsyncOperation(GetType() + ".OnDiagnosticsUpdated"); - _taskChain = _taskChain.SafeContinueWith( - _ => OnDiagnosticsUpdatedOnBackground(e), TaskScheduler.Default); - _taskChain.CompletesAsyncOperation(asyncToken); - } - } - - private void OnDiagnosticsUpdatedOnBackground(DiagnosticsUpdatedArgs e) - { - this.AssertIsBackground(); - RegisterNotification(() => OnDiagnosticsUpdatedOnForeground(e)); - } - - /// - /// We do all mutation work on the UI thread. That ensures that all mutation - /// is processed serially *and* it means we can safely examine things like - /// subject buffers and open documents without any threat of race conditions. - /// Note that we do not do any expensive work here. We just store (or remove) - /// the data we were given in appropriate buckets. - /// - /// For example, we create a TaggerProvider per unique DiagnosticUpdatedArgs.Id - /// we get. So if we see a new id we simply create a tagger for it and pass it - /// these args to store. Otherwise we pass these args to the existing tagger. - /// - /// Similarly, clearing out data is just a matter of us clearing our reference - /// to the data. - /// - private void OnDiagnosticsUpdatedOnForeground(DiagnosticsUpdatedArgs e) - { - this.AssertIsForeground(); - - if (_disposed) - { - return; - } - - // Do some quick checks to avoid doing any further work for diagnostics we don't - // care about. - var ourDocument = _subjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); - var ourDocumentId = ourDocument?.Id; - if (ourDocumentId != _currentDocumentId) - { - // Our buffer has started tracking some other document entirely. - // We have to clear out all of the diagnostics we have currently stored. - RemoveAllCachedDiagnostics(); - } - - _currentDocumentId = ourDocumentId; - - // Now see if the document we're tracking corresponds to the diagnostics - // we're hearing about. If not, just ignore them. - if (ourDocument == null || - ourDocument.Project.Solution.Workspace != e.Workspace || - ourDocument.Id != e.DocumentId) - { - return; - } - - // We're hearing about diagnostics for our document. We may be hearing - // about new diagnostics coming, or existing diagnostics being cleared - // out. - - // First see if this is a document/project removal. If so, clear out any state we - // have associated with any analyzers we have for that document/project. - ProcessRemovedDiagnostics(e); - - // Make sure we can find an editor snapshot for these errors. Otherwise we won't - // be able to make ITagSpans for them. If we can't, just bail out. This happens - // when the solution crawler is very far behind. However, it will have a more - // up to date document within it that it will eventually process. Until then - // we just keep around the stale tags we have. - // - // Note: if the Solution or Document is null here, then that means the document - // was removed. In that case, we do want to proceed so that we'll produce 0 - // tags and we'll update the editor appropriately. - SourceText sourceText = null; - ITextSnapshot editorSnapshot = null; - if (e.Solution != null) - { - var diagnosticDocument = e.Solution.GetDocument(e.DocumentId); - if (diagnosticDocument != null) - { - if (!diagnosticDocument.TryGetText(out sourceText)) - { - return; - } - - editorSnapshot = sourceText.FindCorrespondingEditorTextSnapshot(); - if (editorSnapshot == null) - { - return; - } - - // Make sure the editor we've got associated with these diagnostics is the - // same one we're a tagger for. It is possible for us to hear about diagnostics - // for the *same* Document that are not from the *same* buffer. For example, - // say we have the following chain of events: - // - // Document is opened. - // Diagnostics start analyzing. - // Document is closed. - // Document is opened. - // Diagnostics finish and report for document. - // - // We'll hear about diagnostics for the original Document/Buffer that was - // opened. But we'll be trying to apply them to this current Document/Buffer. - // That won't work since these will be different buffers (and thus, we won't - // be able to map the diagnostic spans appropriately). - // - // Note: returning here is safe. Because the file is closed/opened, The - // Diagnostics Service will reanalyze it. It will then report the new results - // which we will hear about and use. - if (editorSnapshot.TextBuffer != _subjectBuffer) - { - return; - } - } - } - - OnDiagnosticsUpdatedOnForeground(e, sourceText, editorSnapshot); - } - - private void ProcessRemovedDiagnostics(DiagnosticsUpdatedArgs e) - { - this.AssertIsForeground(); - Debug.Assert(!_disposed); - - if (e.Kind != DiagnosticsUpdatedKind.DiagnosticsRemoved) - { - // Wasn't a removal. We don't need to do anything here. - return; - } - - // See if we're being told about diagnostics going away because a document/project - // was removed. If so, clear out any diagnostics we have associated with this - // diagnostic source ID and notify any listeners that - - var id = e.Id; - if (!_idToProviderAndTagger.TryGetValue(id, out var providerAndTagger)) - { - // Wasn't a diagnostic source we care about. - return; - } - - _idToProviderAndTagger.Remove(id); - DisconnectFromTagger(providerAndTagger.tagger); - - OnUnderlyingTaggerTagsChanged(this, new SnapshotSpanEventArgs(_subjectBuffer.CurrentSnapshot.GetFullSpan())); - } - - private void RemoveAllCachedDiagnostics() - { - this.AssertIsForeground(); - - DisconnectFromAllTaggers(); - OnUnderlyingTaggerTagsChanged(this, new SnapshotSpanEventArgs(_subjectBuffer.CurrentSnapshot.GetFullSpan())); - } - - private void OnDiagnosticsUpdatedOnForeground( - DiagnosticsUpdatedArgs e, SourceText sourceText, ITextSnapshot editorSnapshot) - { - this.AssertIsForeground(); - Debug.Assert(!_disposed); - - // Find the appropriate async tagger for this diagnostics id, and let it know that - // there were new diagnostics produced for it. - var id = e.Id; - if (!_idToProviderAndTagger.TryGetValue(id, out var providerAndTagger)) - { - // We didn't have an existing tagger for this diagnostic id. If there are no actual - // diagnostics being reported, then don't bother actually doing anything. This saves - // us from creating a lot of objects, and subscribing to tons of events that we don't - // actually need (since we don't even have any diagnostics to show!). - if (e.Diagnostics.Length == 0) - { - return; - } - - // Didn't have an existing tagger for this diagnostic id. Make a new one - // and cache it so we can use it in the future. - var taggerProvider = new TaggerProvider(_owner); - var tagger = taggerProvider.CreateTagger(_subjectBuffer); - providerAndTagger = (taggerProvider, tagger); - - _idToProviderAndTagger[id] = providerAndTagger; - - // Register for changes from the underlying tagger. When it tells us about - // changes, we'll let anyone know who is registered with us. - tagger.TagsChanged += OnUnderlyingTaggerTagsChanged; - } - - // Let the provider know that there are new diagnostics. It will then - // handle all the async processing of those diagnostics. - providerAndTagger.provider.OnDiagnosticsUpdated(e, sourceText, editorSnapshot); - } - - private void OnUnderlyingTaggerTagsChanged(object sender, SnapshotSpanEventArgs args) - { - this.AssertIsForeground(); - if (_disposed) - { - return; - } - - this.TagsChanged?.Invoke(sender, args); - } - - public IEnumerable> GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancel) - { - this.AssertIsForeground(); - return _idToProviderAndTagger.Values.SelectMany(t => t.tagger.GetAllTags(spans, cancel)).ToList(); - } - - public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) - { - this.AssertIsForeground(); - return _idToProviderAndTagger.Values.SelectMany(t => t.tagger.GetTags(spans)).ToList(); - } - } - } -} diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.TaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.TaggerProvider.cs deleted file mode 100644 index d0d8c5fd36e..00000000000 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.TaggerProvider.cs +++ /dev/null @@ -1,189 +0,0 @@ -// 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.Collections.Generic; -using System.Collections.Immutable; -using System.Threading.Tasks; -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.Options; -using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.Text.Shared.Extensions; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics -{ - internal abstract partial class AbstractDiagnosticsTaggerProvider - { - /// - /// we create an instance of this async tagger provider for each diagnostic source - /// we hear about for a particular buffer. Each async tagger is then responsible - /// for asynchronous producing tags for that diagnostic source. This allows each - /// individual async tagger to collect diagnostics, diff them against the last set - /// produced by that diagnostic source, and then notify any interested parties about - /// what changed. - /// - private class TaggerProvider : AsynchronousTaggerProvider, ITaggerEventSource - { - private readonly AbstractDiagnosticsTaggerProvider _owner; - private readonly object _gate = new object(); - - // The latest diagnostics we've head about for this - private object _latestId; - private ImmutableArray _latestDiagnostics; - private ITextSnapshot _latestEditorSnapshot; - private SourceText _latestSourceText; - - protected override IEnumerable> Options => _owner.Options; - - public TaggerProvider(AbstractDiagnosticsTaggerProvider owner) - : base(owner._listener, owner._notificationService) - { - _owner = owner; - } - - public event EventHandler Changed; - event EventHandler ITaggerEventSource.UIUpdatesPaused { add { } remove { } } - event EventHandler ITaggerEventSource.UIUpdatesResumed { add { } remove { } } - - void ITaggerEventSource.Connect() { } - void ITaggerEventSource.Disconnect() { } - - // we will show new tags to users very slowly. - // don't confused this with data changed event which is for tag producer (which is set to NearImmediate). - // this delay is for letting editor know about newly added tags. - protected override TaggerDelay AddedTagNotificationDelay => TaggerDelay.OnIdle; - - protected override ITaggerEventSource CreateEventSource(ITextView textViewOpt, ITextBuffer subjectBuffer) - { - // We act as a source of events ourselves. When the diagnostics service tells - // us about new diagnostics, we'll use that to kick of the asynchronous tagging - // work. - - var eventSource = TaggerEventSources.Compose( - TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer, TaggerDelay.Medium), - this); - - // See if our owner has any additional events for us to listen to. - var ownerEventSource = _owner.GetTaggerEventSource(); - return ownerEventSource == null - ? eventSource - : TaggerEventSources.Compose(ownerEventSource, eventSource); - } - - protected override Task ProduceTagsAsync(TaggerContext context, DocumentSnapshotSpan spanToTag, int? caretPosition) - { - ProduceTags(context, spanToTag); - return SpecializedTasks.EmptyTask; - } - - private void ProduceTags(TaggerContext context, DocumentSnapshotSpan spanToTag) - { - try - { - if (!_owner.IsEnabled) - { - return; - } - - var document = spanToTag.Document; - if (document == null) - { - return; - } - - // 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 = spanToTag.SnapshotSpan.Snapshot.TextBuffer; - var suppressedDiagnosticsSpans = default(NormalizedSnapshotSpanCollection); - buffer?.Properties.TryGetProperty(PredefinedPreviewTaggerKeys.SuppressDiagnosticsSpansKey, out suppressedDiagnosticsSpans); - - // Producing tags is simple. We just grab the diagnostics we were already told about, - // and we convert them to tag spans. - object id; - ImmutableArray diagnostics; - SourceText sourceText; - ITextSnapshot editorSnapshot; - lock (_gate) - { - id = _latestId; - diagnostics = _latestDiagnostics; - sourceText = _latestSourceText; - editorSnapshot = _latestEditorSnapshot; - } - - if (sourceText == null || editorSnapshot == null) - { - return; - } - - var project = document.Project; - - var requestedSnapshot = spanToTag.SnapshotSpan.Snapshot; - var requestedSpan = spanToTag.SnapshotSpan; - var isLiveUpdate = id is ISupportLiveUpdate; - - foreach (var diagnosticData in diagnostics) - { - if (_owner.IncludeDiagnostic(diagnosticData)) - { - var actualSpan = diagnosticData - .GetExistingOrCalculatedTextSpan(sourceText) - .ToSnapshotSpan(editorSnapshot) - .TranslateTo(requestedSnapshot, SpanTrackingMode.EdgeExclusive); - - if (actualSpan.IntersectsWith(requestedSpan) && - !IsSuppressed(suppressedDiagnosticsSpans, actualSpan)) - { - var tagSpan = _owner.CreateTagSpan(isLiveUpdate, actualSpan, 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); - - internal void OnDiagnosticsUpdated(DiagnosticsUpdatedArgs e, SourceText sourceText, ITextSnapshot editorSnapshot) - { - // We were told about new diagnostics. Store them, and then let the - // AsynchronousTaggerProvider know it should ProduceTags again. - lock (_gate) - { - _latestId = e.Id; - _latestDiagnostics = e.Diagnostics; - _latestSourceText = sourceText; - _latestEditorSnapshot = editorSnapshot; - } - - // unlike any other tagger, actual work to produce data is done by other service rather than tag provider itself. - // so we don't need to do any big delay for diagnostic tagger (producer) to reduce doing expensive work repeatedly. that is already - // taken cared by the external service (diagnostic service). - this.Changed?.Invoke(this, new TaggerEventArgs(TaggerDelay.NearImmediate)); - } - } - - protected virtual ITaggerEventSource GetTaggerEventSource() - { - return null; - } - } -} diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.cs index 94e9bbf21c6..f13adc5e2ff 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.cs @@ -1,12 +1,21 @@ // 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.Collections.Generic; +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Common; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.Options; +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 { @@ -22,47 +31,132 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics /// Each of these taggers is nicely asynchronous and properly works within the async /// tagging infrastructure. /// - internal abstract partial class AbstractDiagnosticsTaggerProvider : - ForegroundThreadAffinitizedObject, - ITaggerProvider + internal abstract partial class AbstractDiagnosticsTaggerProvider : AsynchronousTaggerProvider where TTag : ITag { - private readonly object _uniqueKey = new object(); private readonly IDiagnosticService _diagnosticService; - private readonly IForegroundNotificationService _notificationService; - private readonly IAsynchronousOperationListener _listener; protected AbstractDiagnosticsTaggerProvider( IDiagnosticService diagnosticService, IForegroundNotificationService notificationService, IAsynchronousOperationListener listener) + : base(listener, notificationService) { _diagnosticService = diagnosticService; - _notificationService = notificationService; - _listener = listener; } - protected internal abstract IEnumerable> Options { get; } + 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); - ITagger ITaggerProvider.CreateTagger(ITextBuffer buffer) + protected override Task ProduceTagsAsync(TaggerContext context, DocumentSnapshotSpan spanToTag, int? caretPosition) { - return CreateTagger(buffer); + ProduceTags(context, spanToTag); + return SpecializedTasks.EmptyTask; } - public IAccurateTagger CreateTagger(ITextBuffer buffer) where T : ITag + private void ProduceTags(TaggerContext context, DocumentSnapshotSpan spanToTag) { - var tagger = buffer.Properties.GetOrCreateSingletonProperty( - _uniqueKey, () => new AggregatingTagger(this, buffer)); - tagger.OnTaggerCreated(); - return tagger as IAccurateTagger; + 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, editorSnapshot, + suppressedDiagnosticsSpans, updateArg, cancellationToken); + } } - private void RemoveTagger(AggregatingTagger tagger, ITextBuffer buffer) + private void ProduceTags( + TaggerContext context, DocumentSnapshotSpan spanToTag, + Workspace workspace, Document document, SourceText sourceText, ITextSnapshot editorSnapshot, + NormalizedSnapshotSpanCollection suppressedDiagnosticsSpans, UpdatedEventArgs updateArgs, CancellationToken cancellationToken) { - buffer.Properties.RemoveProperty(_uniqueKey); + 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 requestedSnapshot = 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. As such, we have to + // ensure that the information we get back is not outside the bounds of the editor + // snapshot we're currently looking at. + + // Note: GetExistingOrCalculatedTextSpan always succeeds. But it does not ensure + // that the span it returns is within the span of sourceText. So we make sure that + // the start/end of it fits in our snapshot. + + 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); } } diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsClassificationTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsClassificationTaggerProvider.cs index 27f99c7526e..079fc80d689 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsClassificationTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsClassificationTaggerProvider.cs @@ -29,7 +29,7 @@ internal partial class DiagnosticsClassificationTaggerProvider : AbstractDiagnos private readonly ClassificationTag _classificationTag; private readonly IEditorOptionsFactoryService _editorOptionsFactoryService; - protected internal override IEnumerable> Options => s_tagSourceOptions; + protected override IEnumerable> Options => s_tagSourceOptions; [ImportingConstructor] public DiagnosticsClassificationTaggerProvider( diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsSquiggleTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsSquiggleTaggerProvider.cs index 0d0ccf471b3..d51095c12bb 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsSquiggleTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsSquiggleTaggerProvider.cs @@ -27,7 +27,7 @@ internal partial class DiagnosticsSquiggleTaggerProvider : AbstractDiagnosticsAd private static readonly IEnumerable> s_tagSourceOptions = ImmutableArray.Create(EditorComponentOnOffOptions.Tagger, InternalFeatureOnOffOptions.Squiggles, ServiceComponentOnOffOptions.DiagnosticProvider); - protected internal override IEnumerable> Options => s_tagSourceOptions; + protected override IEnumerable> Options => s_tagSourceOptions; private bool? _blueSquiggleForBuildDiagnostic; diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsSuggestionTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsSuggestionTaggerProvider.cs index 1db6f401f3d..ca4f1b51854 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsSuggestionTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsSuggestionTaggerProvider.cs @@ -6,8 +6,6 @@ using System.ComponentModel.Composition; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editor.Shared.Options; -using Microsoft.CodeAnalysis.Editor.Shared.Tagging; -using Microsoft.CodeAnalysis.Editor.Tagging; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Options; using Microsoft.CodeAnalysis.Shared.TestHooks; @@ -29,8 +27,7 @@ internal partial class DiagnosticsSuggestionTaggerProvider : private static readonly IEnumerable> s_tagSourceOptions = ImmutableArray.Create(EditorComponentOnOffOptions.Tagger, InternalFeatureOnOffOptions.Squiggles, ServiceComponentOnOffOptions.DiagnosticProvider); - protected internal override IEnumerable> Options => s_tagSourceOptions; - + protected override IEnumerable> Options => s_tagSourceOptions; [ImportingConstructor] public DiagnosticsSuggestionTaggerProvider( @@ -42,9 +39,7 @@ internal partial class DiagnosticsSuggestionTaggerProvider : } protected internal override bool IncludeDiagnostic(DiagnosticData diagnostic) - { - return diagnostic.Severity == DiagnosticSeverity.Info; - } + => diagnostic.Severity == DiagnosticSeverity.Info; protected override IErrorTag CreateTag(DiagnosticData diagnostic) => new ErrorTag(PredefinedErrorTypeNames.HintedSuggestion, diagnostic.Message); diff --git a/src/EditorFeatures/Core/Tagging/TaggerContext.cs b/src/EditorFeatures/Core/Tagging/TaggerContext.cs index a8d34dc488a..9db4ad0897b 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerContext.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerContext.cs @@ -47,8 +47,8 @@ internal class TaggerContext where TTag : ITag SnapshotPoint? caretPosition = null, TextChangeRange? textChangeRange = null, CancellationToken cancellationToken = default) - : this(null, ImmutableArray.Create(new DocumentSnapshotSpan(document, snapshot.GetFullSpan())), - caretPosition, textChangeRange, null, cancellationToken) + : this(state: null, ImmutableArray.Create(new DocumentSnapshotSpan(document, snapshot.GetFullSpan())), + caretPosition, textChangeRange, existingTags: null, cancellationToken) { } diff --git a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticData.cs b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticData.cs index ac3877c4d15..7353a59be57 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticData.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticData.cs @@ -233,9 +233,12 @@ public override string ToString() } public TextSpan GetExistingOrCalculatedTextSpan(SourceText text) - { - return HasTextSpan ? TextSpan : GetTextSpan(this.DataLocation, text); - } + => HasTextSpan ? EnsureInBounds(TextSpan, text) : GetTextSpan(this.DataLocation, text); + + private static TextSpan EnsureInBounds(TextSpan textSpan, SourceText text) + => TextSpan.FromBounds( + Math.Min(textSpan.Start, text.Length), + Math.Min(textSpan.End, text.Length)); public DiagnosticData WithCalculatedSpan(SourceText text) { @@ -284,7 +287,7 @@ public static TextSpan GetTextSpan(DiagnosticDataLocation dataLocation, SourceTe SwapIfNeeded(ref startLinePosition, ref endLinePosition); var span = text.Lines.GetTextSpan(new LinePositionSpan(startLinePosition, endLinePosition)); - return TextSpan.FromBounds(Math.Min(Math.Max(span.Start, 0), text.Length), Math.Min(Math.Max(span.End, 0), text.Length)); + return EnsureInBounds(TextSpan.FromBounds(Math.Max(span.Start, 0), Math.Max(span.End, 0)), text); } private static void AdjustBoundaries(DiagnosticDataLocation dataLocation, -- GitLab