diff --git a/src/EditorFeatures/Core/EditorFeatures.csproj b/src/EditorFeatures/Core/EditorFeatures.csproj index 2671eaa599ebcd4047484436bbe2dfb62f0b2323..d8943adccd71479b79be19b7dafdf4d6c4a062f8 100644 --- a/src/EditorFeatures/Core/EditorFeatures.csproj +++ b/src/EditorFeatures/Core/EditorFeatures.csproj @@ -762,7 +762,6 @@ - diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractAggregatedDiagnosticsTagSource.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractAggregatedDiagnosticsTagSource.cs index febbd4ebd81d2a4d0d685f7599ddcbfb5beeb912..ca04fcfd82d30f70182829f43b86a298470deb15 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractAggregatedDiagnosticsTagSource.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractAggregatedDiagnosticsTagSource.cs @@ -22,7 +22,7 @@ internal abstract partial class AbstractAggregatedDiagnosticsTagSource : T IForegroundNotificationService notificationService, DiagnosticService service, IAsynchronousOperationListener asyncListener) - : base(textViewOpt: null, subjectBuffer: subjectBuffer, ignoreCaretMovementToExistingTag: false, notificationService: notificationService, asyncListener: asyncListener) + : base(subjectBuffer, notificationService, asyncListener) { _service = service; _mode = GetMode(subjectBuffer); diff --git a/src/EditorFeatures/Core/Implementation/KeywordHighlighting/HighlighterViewTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/KeywordHighlighting/HighlighterViewTaggerProvider.cs index 382d6bc5b61a00bf7bb3028130079f9fd6d0cfdc..16f78f235cd9af22baf2213eb89dd98fa4ac7b1f 100644 --- a/src/EditorFeatures/Core/Implementation/KeywordHighlighting/HighlighterViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/KeywordHighlighting/HighlighterViewTaggerProvider.cs @@ -35,7 +35,6 @@ internal class HighlighterViewTaggerProvider : AsynchronousViewTaggerProvider TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits; - public override bool IgnoreCaretMovementToExistingTag => true; public override IEnumerable> Options => SpecializedCollections.SingletonEnumerable(InternalFeatureOnOffOptions.KeywordHighlight); [ImportingConstructor] diff --git a/src/EditorFeatures/Core/Implementation/ReferenceHighlighting/ReferenceHighlightingTagSource.cs b/src/EditorFeatures/Core/Implementation/ReferenceHighlighting/ReferenceHighlightingTagSource.cs deleted file mode 100644 index c6f65d09c4103ac9f672cc274460520f499ed1f7..0000000000000000000000000000000000000000 --- a/src/EditorFeatures/Core/Implementation/ReferenceHighlighting/ReferenceHighlightingTagSource.cs +++ /dev/null @@ -1,175 +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.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Editor.Shared.Tagging; -using Microsoft.CodeAnalysis.Editor.Tagging; -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.ReferenceHighlighting -{ - using Context = AsynchronousTaggerContext; - - internal partial class ReferenceHighlightingTagSource : ProducerPopulatedTagSource - { - private const int VoidVersion = -1; - - // last solution version we used to update tags - // * NOTE * here unfortunately, we only hold onto version without caret position since - // it is not easy to pass through it. for now, we will void the last version whenever - // we see caret changes. - private int _lastUpdateTagsSolutionVersion = VoidVersion; - - public ReferenceHighlightingTagSource( - ITextView textView, - ITextBuffer subjectBuffer, - ReferenceHighlightingViewTaggerProvider taggerProvider, - IAsynchronousOperationListener asyncListener, - IForegroundNotificationService notificationService) - : base(textView, subjectBuffer, taggerProvider, asyncListener, notificationService) - { - } - - protected override SnapshotPoint? GetCaretPoint() - { - return this.TextViewOpt.Caret.Position.Point.GetPoint(b => b.ContentType.IsOfType(ContentTypeNames.RoslynContentType), PositionAffinity.Successor); - } - - protected override void RecalculateTagsOnChangedCore(TaggerEventArgs e) - { - var cancellationToken = this.WorkQueue.CancellationToken; - - VoidLastSolutionVersionIfCaretChanged(e); - - RegisterNotification(() => - { - this.WorkQueue.AssertIsForeground(); - - var caret = GetCaretPoint(); - if (!caret.HasValue) - { - ClearTags(cancellationToken); - return; - } - - var spansToTag = TryGetSpansAndDocumentsToTag(e.Kind); - if (spansToTag != null) - { - // we will eagerly remove tags except for semantic change case. - // in semantic change case, we don't actually know whether it will affect highlight we currently - // have, so we will wait until we get new tags before removing them. - if (e.Kind != PredefinedChangedEventKinds.SemanticsChanged) - { - ClearTags(spansToTag, cancellationToken); - } - - base.RecalculateTagsOnChangedCore(e); - } - }, delay: TaggerConstants.NearImmediateDelay, cancellationToken: cancellationToken); - } - - private void VoidLastSolutionVersionIfCaretChanged(TaggerEventArgs e) - { - if (e.Kind == PredefinedChangedEventKinds.CaretPositionChanged) - { - _lastUpdateTagsSolutionVersion = VoidVersion; - } - } - - private void ClearTags(CancellationToken cancellationToken) - { - this.WorkQueue.AssertIsForeground(); - ClearTags(spansToTag: null, cancellationToken: cancellationToken); - } - - private void ClearTags(List spansToTag, CancellationToken cancellationToken) - { - this.WorkQueue.AssertIsForeground(); - - spansToTag = spansToTag ?? GetSpansAndDocumentsToTag(); - - // Save to access CachedTagTrees here because we're on the foreground thread. - var oldTagsTrees = this.CachedTagTrees; - - this.WorkQueue.EnqueueBackgroundTask( - c => this.ClearTagsAsync(spansToTag, oldTagsTrees, c), "ClearTags", cancellationToken); - } - - private List TryGetSpansAndDocumentsToTag(string kind) - { - this.WorkQueue.AssertIsForeground(); - - // TODO: tagger creates so much temporary objects. GetSpansAndDocumentsToTags creates handful of objects per events - // (in this case, on every caret move or text change). at some point of time, we should either re-write tagger framework - // or do some audit to reduce memory allocations. - var spansToTag = GetSpansAndDocumentsToTag(); - - if (kind == PredefinedChangedEventKinds.SemanticsChanged || kind == PredefinedChangedEventKinds.TextChanged) - { - // check whether we already processed highlight for this document - // * this can happen if we are called twice for same document due to two different change events caused by - // same root change (text edit) - var spanAndTag = spansToTag.First(s => s.SnapshotSpan.Snapshot.TextBuffer == this.SubjectBuffer); - var version = spanAndTag.SnapshotSpan.Snapshot.Version.ReiteratedVersionNumber; - var document = spanAndTag.Document; - - if (version == this.SubjectBuffer.CurrentSnapshot.Version.ReiteratedVersionNumber && - document != null && document.Project.Solution.WorkspaceVersion == _lastUpdateTagsSolutionVersion) - { - return null; - } - } - - // we are going to update tags, clear last update tags solution version - _lastUpdateTagsSolutionVersion = VoidVersion; - return spansToTag; - } - - private Task ClearTagsAsync( - List spansToTag, - ImmutableDictionary> oldTagTrees, - CancellationToken cancellationToken) - { - this.WorkQueue.AssertIsBackground(); - cancellationToken.ThrowIfCancellationRequested(); - - var tagSpans = SpecializedCollections.EmptyEnumerable>(); - - var newTagTrees = ConvertToTagTrees(oldTagTrees, tagSpans, spansToTag); - - // here we call base.ProcessNewTags so that we can clear tags without setting last solution version - // clear tags is a special update mechanism where it represents clearing tags not updating tags. - - // we don't care about accumulated text change, so give it null - base.ProcessNewTagTrees(spansToTag, oldTagTrees: oldTagTrees, newTagTrees: newTagTrees, newState: null, cancellationToken: cancellationToken); - - return SpecializedTasks.EmptyTask; - } - - protected override void ProcessNewTagTrees( - IEnumerable spansToCompute, - ImmutableDictionary> oldTagTrees, - ImmutableDictionary> newTags, - object newState, - CancellationToken cancellationToken) - { - base.ProcessNewTagTrees(spansToCompute, oldTagTrees, newTags, newState, cancellationToken); - - // remember last solution version we updated the tags - var document = spansToCompute.First().Document; - if (document != null) - { - _lastUpdateTagsSolutionVersion = document.Project.Solution.WorkspaceVersion; - } - } - } -} diff --git a/src/EditorFeatures/Core/Implementation/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs index f0d5085e21db7d461b044163e516f1c992f7383e..eeb23a805156b8a1e5e303955ed6021c4be82f06 100644 --- a/src/EditorFeatures/Core/Implementation/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs @@ -5,11 +5,9 @@ using System.Collections.Immutable; using System.ComponentModel.Composition; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Shared.Options; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Editor.Tagging; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Notification; @@ -31,43 +29,24 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.ReferenceHighlighting [ContentType(ContentTypeNames.RoslynContentType)] [TagType(typeof(AbstractNavigatableReferenceHighlightingTag))] [TextViewRole(PredefinedTextViewRoles.Interactive)] - internal partial class ReferenceHighlightingViewTaggerProvider : - ForegroundThreadAffinitizedObject, - IViewTaggerProvider, - IAsynchronousTaggerDataSource + internal partial class ReferenceHighlightingViewTaggerProvider : AsynchronousViewTaggerProvider { private readonly ISemanticChangeNotificationService _semanticChangeNotificationService; - private readonly Lazy _asynchronousTaggerProvider; - public TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.None; - public SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeExclusive; - public bool IgnoreCaretMovementToExistingTag => true; - public bool ComputeTagsSynchronouslyIfNoAsynchronousComputationHasCompleted => false; - public IEqualityComparer TagComparer => null; - public IEnumerable> Options => null; - public IEnumerable> PerLanguageOptions => SpecializedCollections.SingletonEnumerable(FeatureOnOffOptions.ReferenceHighlighting); + public override TaggerCaretChangeBehavior CaretChangeBehavior => TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag; + public override IEnumerable> PerLanguageOptions => SpecializedCollections.SingletonEnumerable(FeatureOnOffOptions.ReferenceHighlighting); [ImportingConstructor] public ReferenceHighlightingViewTaggerProvider( IForegroundNotificationService notificationService, ISemanticChangeNotificationService semanticChangeNotificationService, [ImportMany] IEnumerable> asyncListeners) + : base(new AggregateAsynchronousOperationListener(asyncListeners, FeatureAttribute.ReferenceHighlighting), notificationService) { _semanticChangeNotificationService = semanticChangeNotificationService; - _asynchronousTaggerProvider = new Lazy(() => - new AsynchronousViewTaggerProviderWithTagSource( - this, - new AggregateAsynchronousOperationListener(asyncListeners, FeatureAttribute.ReferenceHighlighting), - notificationService, - this.CreateTagSource)); } - public ITagger CreateTagger(ITextView textView, ITextBuffer buffer) where T : ITag - { - return _asynchronousTaggerProvider.Value.CreateTagger(textView, buffer); - } - - public ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) + public override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) { // Note: we don't listen for OnTextChanged. Text changes to this this buffer will get // reported by OnSemanticChanged. @@ -78,22 +57,19 @@ public ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subj TaggerEventSources.OnOptionChanged(subjectBuffer, FeatureOnOffOptions.ReferenceHighlighting, TaggerDelay.NearImmediate)); } - private ProducerPopulatedTagSource CreateTagSource( - ITextView textViewOpt, ITextBuffer subjectBuffer, - IAsynchronousOperationListener asyncListener, - IForegroundNotificationService notificationService) + public override SnapshotPoint? GetCaretPoint(ITextView textViewOpt, ITextBuffer subjectBuffer) { - return new ReferenceHighlightingTagSource(textViewOpt, subjectBuffer, this, asyncListener, notificationService); + return textViewOpt.Caret.Position.Point.GetPoint(b => b.ContentType.IsOfType(ContentTypeNames.RoslynContentType), PositionAffinity.Successor); } - public IEnumerable GetSpansToTag(ITextView textViewOpt, ITextBuffer subjectBuffer) + public override IEnumerable GetSpansToTag(ITextView textViewOpt, ITextBuffer subjectBuffer) { return textViewOpt.BufferGraph.GetTextBuffers(b => b.ContentType.IsOfType(ContentTypeNames.RoslynContentType)) .Select(b => b.CurrentSnapshot.GetFullSpan()) .ToList(); } - public Task ProduceTagsAsync(Context context) + public override Task ProduceTagsAsync(Context context) { // NOTE(cyrusn): Normally we'd limit ourselves to producing tags in the span we were // asked about. However, we want to produce all tags here so that the user can actually @@ -104,21 +80,38 @@ public Task ProduceTagsAsync(Context context) return SpecializedTasks.EmptyTask; } - var position = context.CaretPosition.Value; + var caretPosition = context.CaretPosition.Value; Workspace workspace; - if (!Workspace.TryGetWorkspace(position.Snapshot.AsText().Container, out workspace)) + if (!Workspace.TryGetWorkspace(caretPosition.Snapshot.AsText().Container, out workspace)) { return SpecializedTasks.EmptyTask; } - var document = context.SpansToTag.First(vt => vt.SnapshotSpan.Snapshot == position.Snapshot).Document; + var document = context.SpansToTag.First(vt => vt.SnapshotSpan.Snapshot == caretPosition.Snapshot).Document; if (document == null) { return SpecializedTasks.EmptyTask; } - return ProduceTagsAsync(context, position, workspace, document); + // Don't produce tags if the feature is not enabled. + if (!workspace.Options.GetOption(FeatureOnOffOptions.ReferenceHighlighting, document.Project.Language)) + { + return SpecializedTasks.EmptyTask; + } + + var existingTags = context.GetExistingTags(new SnapshotSpan(caretPosition, 0)); + if (!existingTags.IsEmpty()) + { + // We already have a tag at this position. So the user is moving from one highlight + // tag to another. In this case we don't want to recompute anything. Let our caller + // know that we should preserve all tags. + context.SetSpansTagged(SpecializedCollections.EmptyEnumerable()); + return SpecializedTasks.EmptyTask; + } + + // Otherwise, we need to go produce all tags. + return ProduceTagsAsync(context, caretPosition, workspace, document); } internal async Task ProduceTagsAsync( @@ -128,11 +121,6 @@ public Task ProduceTagsAsync(Context context) Document document) { var cancellationToken = context.CancellationToken; - // Don't produce tags if the feature is not enabled. - if (!workspace.Options.GetOption(FeatureOnOffOptions.ReferenceHighlighting, document.Project.Language)) - { - return; - } var solution = document.Project.Solution; diff --git a/src/EditorFeatures/Core/Shared/Tagging/TagSources/ProducerPopulatedTagSource.cs b/src/EditorFeatures/Core/Shared/Tagging/TagSources/ProducerPopulatedTagSource.cs index 632c2a6137ced0fd36bcff0c19af0d99e2538f6f..8bcc5e52babe446e25a7d38b8410658fa5f88953 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/TagSources/ProducerPopulatedTagSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/TagSources/ProducerPopulatedTagSource.cs @@ -49,6 +49,8 @@ internal partial class ProducerPopulatedTagSource : TagSource /// Our tagger event source that lets us know when we should call into the tag producer for /// new tags. @@ -74,26 +76,26 @@ internal partial class ProducerPopulatedTagSource : TagSource dataSource, IAsynchronousOperationListener asyncListener, IForegroundNotificationService notificationService) - : base(textViewOpt, subjectBuffer, dataSource.IgnoreCaretMovementToExistingTag, notificationService, asyncListener) + : base(subjectBuffer, notificationService, asyncListener) { if (dataSource.SpanTrackingMode == SpanTrackingMode.Custom) { throw new ArgumentException("SpanTrackingMode.Custom not allowed.", "spanTrackingMode"); } + _textViewOpt = textViewOpt; _dataSource = dataSource; _tagSpanComparer = new TagSpanComparer(this.TagComparer); this.CachedTagTrees = ImmutableDictionary.Create>(); - this.AccumulatedTextChanges = null; _eventSource = dataSource.CreateEventSource(textViewOpt, subjectBuffer); AttachEventHandlersAndStart(); } - private IEqualityComparer TagComparer => + private IEqualityComparer TagComparer => _dataSource.TagComparer ?? EqualityComparer.Default; protected TextChangeRange? AccumulatedTextChanges @@ -154,6 +156,17 @@ private void AttachEventHandlersAndStart() this.SubjectBuffer.Changed += OnSubjectBufferChanged; } + if (_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)) + { + if (_textViewOpt == null) + { + throw new ArgumentException( + nameof(_dataSource.CaretChangeBehavior) + " can only be specified for an " + nameof(IViewTaggerProvider)); + } + + _textViewOpt.Caret.PositionChanged += OnCaretPositionChanged; + } + // Tell the interaction object to start issuing events. _eventSource.Connect(); } @@ -167,6 +180,11 @@ protected override void Disconnect() // Tell the interaction object to stop issuing events. _eventSource.Disconnect(); + if (_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)) + { + this._textViewOpt.Caret.PositionChanged -= OnCaretPositionChanged; + } + if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges)) { this.SubjectBuffer.Changed -= OnSubjectBufferChanged; @@ -193,13 +211,6 @@ private void OnUIUpdatesResumed(object sender, EventArgs e) RaiseResumed(); } - private void OnSubjectBufferChanged(object sender, TextContentChangedEventArgs e) - { - this.WorkQueue.AssertIsForeground(); - UpdateTagsForTextChange(e); - AccumulateTextChanges(e); - } - private void OnChanged(object sender, TaggerEventArgs e) { using (var token = this.Listener.BeginAsyncOperation("OnChanged")) @@ -209,12 +220,60 @@ private void OnChanged(object sender, TaggerEventArgs e) this.WorkQueue.CancelCurrentWork(); // We don't currently have a request issued to re-compute our tags. Issue it for some - // time in the future - + // time in the future. RecalculateTagsOnChanged(e); } } + private void OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e) + { + this.AssertIsForeground(); + + Debug.Assert(_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)); + + var caret = GetCaretPoint(); + if (caret.HasValue) + { + // If it changed position and we're still in a tag, there's nothing more to do + var currentTags = GetTagIntervalTreeForBuffer(caret.Value.Snapshot.TextBuffer); + if (currentTags != null && currentTags.GetIntersectingSpans(new SnapshotSpan(caret.Value, 0)).Count > 0) + { + // Caret is inside a tag. No need to do anything. + return; + } + } + + RemoveAllTags(); + } + + private void RemoveAllTags() + { + this.AssertIsForeground(); + + var oldTagTrees = this.CachedTagTrees; + this.CachedTagTrees = ImmutableDictionary>.Empty; + + var snapshot = this.SubjectBuffer.CurrentSnapshot; + var oldTagTree = GetTagTree(snapshot, oldTagTrees); + var newTagTree = GetTagTree(snapshot, this.CachedTagTrees); + + var difference = ComputeDifference(snapshot, newTagTree, oldTagTree); + RaiseTagsChanged(snapshot.TextBuffer, difference); + } + + protected SnapshotPoint? GetCaretPoint() + { + this.AssertIsForeground(); + return _dataSource.GetCaretPoint(_textViewOpt, SubjectBuffer) ?? _textViewOpt?.GetCaretPoint(SubjectBuffer); + } + + private void OnSubjectBufferChanged(object sender, TextContentChangedEventArgs e) + { + this.WorkQueue.AssertIsForeground(); + UpdateTagsForTextChange(e); + AccumulateTextChanges(e); + } + private void AccumulateTextChanges(TextContentChangedEventArgs contentChanged) { this.WorkQueue.AssertIsForeground(); @@ -254,12 +313,22 @@ private void UpdateTagsForTextChange(TextContentChangedEventArgs e) { this.WorkQueue.AssertIsForeground(); + if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveAllTags)) + { + this.RemoveAllTags(); + return; + } + // Don't bother going forward if we're not going adjust any tags based on edits. - if (!_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits)) + if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits)) { + RemoveTagsThatIntersectEdit(e); return; } + } + private void RemoveTagsThatIntersectEdit(TextContentChangedEventArgs e) + { if (!e.Changes.Any()) { return; @@ -285,34 +354,30 @@ private void UpdateTagsForTextChange(TextContentChangedEventArgs e) } var allTags = treeForBuffer.GetSpans(e.After).ToList(); - var newTreeForBuffer = new TagSpanIntervalTree( + var newTagTree = new TagSpanIntervalTree( buffer, treeForBuffer.SpanTrackingMode, allTags.Except(tagsToRemove, _tagSpanComparer)); - UpdateCachedTagsForBuffer(e.After, newTreeForBuffer); - } - - private void UpdateCachedTagsForBuffer(ITextSnapshot snapshot, TagSpanIntervalTree newTagsForBuffer) - { - this.WorkQueue.AssertIsForeground(); - var oldCachedTagTrees = this.CachedTagTrees; + var snapshot = e.After; - this.CachedTagTrees = oldCachedTagTrees.SetItem(snapshot.TextBuffer, newTagsForBuffer); + var oldTagTrees = this.CachedTagTrees; + this.CachedTagTrees = oldTagTrees.SetItem(snapshot.TextBuffer, newTagTree); // Grab our old tags. We might not have any, so in this case we'll just pretend it's // empty - TagSpanIntervalTree oldCachedTagsForBuffer = null; - if (!oldCachedTagTrees.TryGetValue(snapshot.TextBuffer, out oldCachedTagsForBuffer)) - { - oldCachedTagsForBuffer = new TagSpanIntervalTree(snapshot.TextBuffer, _dataSource.SpanTrackingMode); - } + var oldTagTree = GetTagTree(snapshot, oldTagTrees); - var difference = ComputeDifference(snapshot, oldCachedTagsForBuffer, newTagsForBuffer); - if (difference.Count > 0) - { - RaiseTagsChanged(snapshot.TextBuffer, difference); - } + var difference = ComputeDifference(snapshot, newTagTree, oldTagTree); + RaiseTagsChanged(snapshot.TextBuffer, difference); + } + + private TagSpanIntervalTree GetTagTree(ITextSnapshot snapshot, ImmutableDictionary> tagTrees) + { + TagSpanIntervalTree tagTree = null; + return tagTrees.TryGetValue(snapshot.TextBuffer, out tagTree) + ? tagTree + : new TagSpanIntervalTree(snapshot.TextBuffer, _dataSource.SpanTrackingMode); } private bool TryStealTagsFromRelatedTagSource(TextContentChangedEventArgs e) @@ -406,7 +471,7 @@ protected List GetSpansAndDocumentsToTag() // TODO: Update to tag spans from all related documents. var snapshotToDocumentMap = new Dictionary(); - var spansToTag = _dataSource.GetSpansToTag(TextViewOpt, SubjectBuffer) ?? this.GetFullBufferSpan(); + var spansToTag = _dataSource.GetSpansToTag(_textViewOpt, SubjectBuffer) ?? this.GetFullBufferSpan(); var spansAndDocumentsToTag = spansToTag.Select(span => { Document document = null; @@ -659,7 +724,8 @@ private IEnumerable> GetNonIntersectingTagSpans(IEnumerable>(); - var context = new AsynchronousTaggerContext(oldState, spansToTag, caretPosition, textChangeRange, cancellationToken); + var context = new AsynchronousTaggerContext( + oldState, spansToTag, caretPosition, textChangeRange, oldTagTrees, cancellationToken); await _dataSource.ProduceTagsAsync(context).ConfigureAwait(false); ProcessContext(oldTagTrees, context); @@ -669,7 +735,7 @@ private IEnumerable> GetNonIntersectingTagSpans(IEnumerable> oldTagTrees, AsynchronousTaggerContext context) { - var spansTagged = context.spansTagged; + var spansTagged = context._spansTagged; var newTagTrees = ConvertToTagTrees(oldTagTrees, context.tagSpans, spansTagged); ProcessNewTagTrees(spansTagged, oldTagTrees, newTagTrees, context.State, context.CancellationToken); @@ -790,7 +856,7 @@ public override ITagSpanIntervalTree GetTagIntervalTreeForBuffer(ITextBuff // use can cancel out if this takes a long time. var context = new AsynchronousTaggerContext( - this.State, spansToTag, GetCaretPoint(), this.AccumulatedTextChanges, CancellationToken.None); + this.State, spansToTag, GetCaretPoint(), this.AccumulatedTextChanges, oldTagTrees, CancellationToken.None); _dataSource.ProduceTagsAsync(context).Wait(); var newTagTrees = ProcessContext(oldTagTrees, context); diff --git a/src/EditorFeatures/Core/Shared/Tagging/TagSources/TagSource.cs b/src/EditorFeatures/Core/Shared/Tagging/TagSources/TagSource.cs index 2440ffb365a275f6b7ad3dde21f4df404aa7de27..ac339b9febeee748177c80477aa881dd51196136 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/TagSources/TagSource.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/TagSources/TagSource.cs @@ -36,7 +36,6 @@ internal abstract partial class TagSource : /// internal readonly AsynchronousSerialWorkQueue WorkQueue; - protected readonly ITextView TextViewOpt; protected readonly ITextBuffer SubjectBuffer; /// @@ -48,20 +47,15 @@ internal abstract partial class TagSource : /// foreground notification service /// private readonly IForegroundNotificationService _notificationService; - private readonly bool _ignoreCaretMovementToExistingTag; #endregion protected TagSource( - ITextView textViewOpt, ITextBuffer subjectBuffer, - bool ignoreCaretMovementToExistingTag, IForegroundNotificationService notificationService, IAsynchronousOperationListener asyncListener) { - TextViewOpt = textViewOpt; this.SubjectBuffer = subjectBuffer; - _ignoreCaretMovementToExistingTag = ignoreCaretMovementToExistingTag; _notificationService = notificationService; this.Listener = asyncListener; @@ -94,40 +88,10 @@ public void RegisterNotification(Action action, int delay, CancellationToken can /// Called by derived types to enqueue tags re-calculation request /// protected void RecalculateTagsOnChanged(TaggerEventArgs e) - { - if (_ignoreCaretMovementToExistingTag && e.Kind == PredefinedChangedEventKinds.CaretPositionChanged) - { - this.AssertIsForeground(); - - var caret = GetCaretPoint(); - if (caret.HasValue) - { - // If it changed position and we're still in a tag, there's nothing more to do - var currentTags = GetTagIntervalTreeForBuffer(caret.Value.Snapshot.TextBuffer); - if (currentTags != null && currentTags.GetIntersectingSpans(new SnapshotSpan(caret.Value, 0)).Count > 0) - { - return; - } - } - } - - RecalculateTagsOnChangedCore(e); - } - - protected virtual void RecalculateTagsOnChangedCore(TaggerEventArgs e) { RegisterNotification(RecomputeTagsForeground, e.Delay.ComputeTimeDelayMS(this.SubjectBuffer), this.WorkQueue.CancellationToken); } - /// - /// Implemented by derived types to return the caret position. - /// - /// Called on the foreground thread. - protected virtual SnapshotPoint? GetCaretPoint() - { - return TextViewOpt?.GetCaretPoint(SubjectBuffer); - } - protected virtual void Disconnect() { this.WorkQueue.AssertIsForeground(); diff --git a/src/EditorFeatures/Core/Tagging/AsynchronousTaggerContext.cs b/src/EditorFeatures/Core/Tagging/AsynchronousTaggerContext.cs index 3637bb7270829ed83a2672f7415c9351786573f9..56eae5adf228a2b754571122c5fac2275ed3503e 100644 --- a/src/EditorFeatures/Core/Tagging/AsynchronousTaggerContext.cs +++ b/src/EditorFeatures/Core/Tagging/AsynchronousTaggerContext.cs @@ -5,14 +5,21 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Tagging { internal class AsynchronousTaggerContext where TTag : ITag { + private readonly ImmutableDictionary> _existingTags; + + internal IEnumerable _spansTagged; + internal ImmutableArray>.Builder tagSpans = ImmutableArray.CreateBuilder>(); + public TState State { get; set; } public IEnumerable SpansToTag { get; } public SnapshotPoint? CaretPosition { get; } @@ -25,14 +32,23 @@ internal class AsynchronousTaggerContext where TTag : ITag public TextChangeRange? TextChangeRange { get; } public CancellationToken CancellationToken { get; } - internal IEnumerable spansTagged; - internal ImmutableArray>.Builder tagSpans = ImmutableArray.CreateBuilder>(); + // For testing only. + internal AsynchronousTaggerContext( + TState state, + IEnumerable spansToTag, + SnapshotPoint? caretPosition, + TextChangeRange? textChangeRange, + CancellationToken cancellationToken) + : this(state, spansToTag, caretPosition, textChangeRange, null, cancellationToken) + { + } internal AsynchronousTaggerContext( TState state, IEnumerable spansToTag, SnapshotPoint? caretPosition, TextChangeRange? textChangeRange, + ImmutableDictionary> existingTags, CancellationToken cancellationToken) { this.State = state; @@ -41,7 +57,8 @@ internal class AsynchronousTaggerContext where TTag : ITag this.TextChangeRange = textChangeRange; this.CancellationToken = cancellationToken; - this.spansTagged = spansToTag; + _spansTagged = spansToTag; + _existingTags = existingTags; } public void AddTag(ITagSpan tag) @@ -62,7 +79,15 @@ public void SetSpansTagged(IEnumerable spansTagged) throw new ArgumentNullException(nameof(spansTagged)); } - this.spansTagged = spansTagged; + this._spansTagged = spansTagged; + } + + public IEnumerable> GetExistingTags(SnapshotSpan span) + { + TagSpanIntervalTree tree; + return _existingTags.TryGetValue(span.Snapshot.TextBuffer, out tree) + ? tree.GetIntersectingSpans(span) + : SpecializedCollections.EmptyEnumerable>(); } } } diff --git a/src/EditorFeatures/Core/Tagging/AsynchronousTaggerDataSource.cs b/src/EditorFeatures/Core/Tagging/AsynchronousTaggerDataSource.cs index 49b9925422891271e8ff57f63d9aaa6e9cca3716..62e8889ebfeb92080427672e397478b8d2542b40 100644 --- a/src/EditorFeatures/Core/Tagging/AsynchronousTaggerDataSource.cs +++ b/src/EditorFeatures/Core/Tagging/AsynchronousTaggerDataSource.cs @@ -18,7 +18,7 @@ namespace Microsoft.CodeAnalysis.Editor.Tagging internal abstract class AsynchronousTaggerDataSource : IAsynchronousTaggerDataSource where TTag : ITag { public virtual TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.None; - public virtual bool IgnoreCaretMovementToExistingTag => false; + public virtual TaggerCaretChangeBehavior CaretChangeBehavior => TaggerCaretChangeBehavior.None; public virtual SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeExclusive; public virtual bool ComputeTagsSynchronouslyIfNoAsynchronousComputationHasCompleted => false; @@ -29,6 +29,12 @@ internal abstract class AsynchronousTaggerDataSource : IAsynchrono protected AsynchronousTaggerDataSource() { } + public virtual SnapshotPoint? GetCaretPoint(ITextView textViewOpt, ITextBuffer subjectBuffer) + { + // Use 'null' to indicate that the tagger should get the default caret position. + return null; + } + public virtual IEnumerable GetSpansToTag(ITextView textViewOpt, ITextBuffer subjectBuffer) { // Use 'null' to indicate that the tagger should tag the default set of spans. diff --git a/src/EditorFeatures/Core/Tagging/IAsynchronousTaggerDataSource.cs b/src/EditorFeatures/Core/Tagging/IAsynchronousTaggerDataSource.cs index d77a88c0f620e96088e87a9377dbfa79bc24e296..7b49cd5e927e5b5a0ec62ffbb8d649616c888d46 100644 --- a/src/EditorFeatures/Core/Tagging/IAsynchronousTaggerDataSource.cs +++ b/src/EditorFeatures/Core/Tagging/IAsynchronousTaggerDataSource.cs @@ -12,6 +12,23 @@ namespace Microsoft.CodeAnalysis.Editor.Tagging { + /// + /// Flags that affect how the tagger infrastructure responds to caret changes. + /// + [Flags] + internal enum TaggerCaretChangeBehavior + { + /// + /// No special caret change behavior. + /// + None = 0, + + /// + /// If the caret moves outside of a tag, immediately remove all existing tags. + /// + RemoveAllTagsOnCaretMoveOutsideOfTag = 1 << 0, + } + /// /// Data source for the . This type tells the /// when tags need to be recomputed, as well @@ -31,6 +48,11 @@ internal interface IAsynchronousTaggerDataSource where TTag : ITag /// TaggerTextChangeBehavior TextChangeBehavior { get; } + /// + /// The bahavior the tagger will have when changes happen to the caret. + /// + TaggerCaretChangeBehavior CaretChangeBehavior { get; } + /// /// The behavior of tags that are created by the async tagger. This will matter for tags /// created for a previous version of a document that are mapped forward by the async @@ -43,13 +65,6 @@ internal interface IAsynchronousTaggerDataSource where TTag : ITag /// bool ComputeTagsSynchronouslyIfNoAsynchronousComputationHasCompleted { get; } - /// - /// true if the tagger infrastructure can avoid recomputing tags when the - /// user's caret moves to an already existing tag. This is useful to avoid work for - /// features like Highlighting if the user is navigating between highlight tags. - /// - bool IgnoreCaretMovementToExistingTag { get; } - /// /// Options controlling this tagger. The tagger infrastructure will check this option /// against the buffer it is associated with to see if it should tag or not. @@ -77,6 +92,17 @@ internal interface IAsynchronousTaggerDataSource where TTag : ITag /// ITaggerEventSource CreateEventSource(ITextView textViewOpt, ITextBuffer subjectBuffer); + /// + /// Called by the infrastructure to + /// determine the caret position. This value will be passed in as the value to + /// in the call to + /// . + /// + /// Return null to get the default tagger behavior. This will the caret + /// position in the subject buffer this tagger is attached to. + /// + SnapshotPoint? GetCaretPoint(ITextView textViewOpt, ITextBuffer subjectBuffer); + /// /// Called by the infrastructure to determine /// the set of spans that it should asynchronously tag. This will be called in response to diff --git a/src/EditorFeatures/Core/Tagging/TaggerTextChangeBehavior.cs b/src/EditorFeatures/Core/Tagging/TaggerTextChangeBehavior.cs index be67d5319e2fcfe23b5e02ba310c08cd5cfe79c1..9ec944f68787ad3cf66d43ff545c398c594e08c0 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerTextChangeBehavior.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerTextChangeBehavior.cs @@ -3,7 +3,7 @@ namespace Microsoft.CodeAnalysis.Editor.Tagging { /// - /// What the async tagger infrastructure should do in the presence of text edits. + /// Flags that affect how the tagger infrastructure responds to text changes. /// [Flags] internal enum TaggerTextChangeBehavior @@ -22,12 +22,20 @@ internal enum TaggerTextChangeBehavior TrackTextChanges = 1 << 0, /// - /// The async tagger infrastructure will not track text changes to the subject buffer it is + /// The async tagger infrastructure will track text changes to the subject buffer it is /// attached to. The text changes will be provided to the /// that is passed to . /// - /// Tags that intersect the text change range will immediately removed. + /// On any edit, tags that intersect the text change range will immediately removed. + /// + RemoveTagsThatIntersectEdits = TrackTextChanges | (1 << 1), + + /// + /// The async tagger infrastructure will track text changes to the subject buffer it is + /// attached to. + /// + /// On any edit all tags will we be removed. /// - RemoveTagsThatIntersectEdits = TrackTextChanges | (1 << 1) + RemoveAllTags = TrackTextChanges | (1 << 2), } }