diff --git a/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs b/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs index 8dafcaae62f0..138d4e131766 100644 --- a/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs +++ b/src/EditorFeatures/Core/Classification/CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs @@ -159,8 +159,7 @@ await TotalClassificationAggregateTagger.AddTagsAsync( arg: default).ConfigureAwait(false); }); - var cachedTags = new TagSpanIntervalTree(snapshot.TextBuffer, SpanTrackingMode.EdgeExclusive, mergedTags); - + var cachedTags = new TagSpanIntervalTree(snapshot, SpanTrackingMode.EdgeExclusive, mergedTags); lock (_gate) { _cachedTaggedSpan = spanToTag; diff --git a/src/EditorFeatures/Core/KeywordHighlighting/HighlighterViewTaggerProvider.cs b/src/EditorFeatures/Core/KeywordHighlighting/HighlighterViewTaggerProvider.cs index eb69aee72fe8..259f5a86b41d 100644 --- a/src/EditorFeatures/Core/KeywordHighlighting/HighlighterViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/KeywordHighlighting/HighlighterViewTaggerProvider.cs @@ -93,10 +93,10 @@ protected override async Task ProduceTagsAsync( var position = caretPosition.Value; var snapshot = snapshotSpan.Snapshot; - // See if the user is just moving their caret around in an existing tag. If so, we don't - // want to actually go recompute things. Note: this only works for containment. If the - // user moves their caret to the end of a highlighted reference, we do want to recompute - // as they may now be at the start of some other reference that should be highlighted instead. + // See if the user is just moving their caret around in an existing tag. If so, we don't want to actually go + // recompute things. Note: this only works for containment. If the user moves their caret to the end of a + // highlighted reference, we do want to recompute as they may now be at the start of some other reference that + // should be highlighted instead. var onExistingTags = context.HasExistingContainingTags(new SnapshotPoint(snapshot, position)); if (onExistingTags) { diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs index ad02af6c7d1a..051e4cd5ad6c 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs @@ -122,10 +122,10 @@ protected override Task ProduceTagsAsync( return Task.CompletedTask; } - // See if the user is just moving their caret around in an existing tag. If so, we don't - // want to actually go recompute things. Note: this only works for containment. If the - // user moves their caret to the end of a highlighted reference, we do want to recompute - // as they may now be at the start of some other reference that should be highlighted instead. + // See if the user is just moving their caret around in an existing tag. If so, we don't want to actually go + // recompute things. Note: this only works for containment. If the user moves their caret to the end of a + // highlighted reference, we do want to recompute as they may now be at the start of some other reference that + // should be highlighted instead. var onExistingTags = context.HasExistingContainingTags(caretPosition); if (onExistingTags) { diff --git a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs index 793642e20570..0825b5c37bf6 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs @@ -21,66 +21,53 @@ namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging; /// tracked. That way you can query for intersecting/overlapping spans in a different snapshot /// than the one for the tag spans that were added. /// -internal sealed partial class TagSpanIntervalTree( - ITextBuffer textBuffer, - SpanTrackingMode trackingMode, - IEnumerable>? values1 = null, - IEnumerable>? values2 = null) where TTag : ITag +internal sealed partial class TagSpanIntervalTree(SpanTrackingMode spanTrackingMode) where TTag : ITag { - private readonly ITextBuffer _textBuffer = textBuffer; - private readonly SpanTrackingMode _spanTrackingMode = trackingMode; - private readonly IntervalTree> _tree = IntervalTree.Create( - new IntervalIntrospector(textBuffer.CurrentSnapshot, trackingMode), - values1, values2); - - private static SnapshotSpan GetTranslatedSpan( - TagSpan originalTagSpan, ITextSnapshot textSnapshot, SpanTrackingMode trackingMode) + // Tracking mode passed in here doesn't matter (since the tree is empty). + public static readonly TagSpanIntervalTree Empty = new(SpanTrackingMode.EdgeInclusive); + + private readonly SpanTrackingMode _spanTrackingMode = spanTrackingMode; + private readonly IntervalTree> _tree = IntervalTree>.Empty; + + public TagSpanIntervalTree( + ITextSnapshot textSnapshot, + SpanTrackingMode trackingMode, + IEnumerable>? values1 = null, + IEnumerable>? values2 = null) + : this(trackingMode) { - var localSpan = originalTagSpan.Span; - - return localSpan.Snapshot == textSnapshot - ? localSpan - : localSpan.TranslateTo(textSnapshot, trackingMode); + _tree = IntervalTree.Create( + new IntervalIntrospector(textSnapshot, trackingMode), + values1, values2); } - private TagSpan GetTranslatedITagSpan(TagSpan originalTagSpan, ITextSnapshot textSnapshot) - // Avoid reallocating in the case where we're on the same snapshot. - => originalTagSpan.Span.Snapshot == textSnapshot - ? originalTagSpan - : GetTranslatedTagSpan(originalTagSpan, textSnapshot, _spanTrackingMode); + private static SnapshotSpan GetTranslatedSpan(TagSpan originalTagSpan, ITextSnapshot textSnapshot, SpanTrackingMode trackingMode) + // SnapshotSpan no-ops if you pass it the same snapshot that it is holding onto. + => originalTagSpan.Span.TranslateTo(textSnapshot, trackingMode); + + private TagSpan GetTranslatedTagSpan(TagSpan originalTagSpan, ITextSnapshot textSnapshot) + => GetTranslatedTagSpan(originalTagSpan, textSnapshot, _spanTrackingMode); private static TagSpan GetTranslatedTagSpan(TagSpan originalTagSpan, ITextSnapshot textSnapshot, SpanTrackingMode trackingMode) // Avoid reallocating in the case where we're on the same snapshot. - => originalTagSpan is TagSpan tagSpan && tagSpan.Span.Snapshot == textSnapshot - ? tagSpan + => originalTagSpan.Span.Snapshot == textSnapshot + ? originalTagSpan : new(GetTranslatedSpan(originalTagSpan, textSnapshot, trackingMode), originalTagSpan.Tag); - public ITextBuffer Buffer => _textBuffer; - - public SpanTrackingMode SpanTrackingMode => _spanTrackingMode; - public bool HasSpanThatContains(SnapshotPoint point) - { - var snapshot = point.Snapshot; - Debug.Assert(snapshot.TextBuffer == _textBuffer); - - return _tree.HasIntervalThatContains(point.Position, length: 0, new IntervalIntrospector(snapshot, _spanTrackingMode)); - } + => _tree.HasIntervalThatContains(point.Position, length: 0, new IntervalIntrospector(point.Snapshot, _spanTrackingMode)); - public IReadOnlyList> GetIntersectingSpans(SnapshotSpan snapshotSpan) - => SegmentedListPool>.ComputeList( - static (args, tags) => args.@this.AppendIntersectingSpansInSortedOrder(args.snapshotSpan, tags), - (@this: this, snapshotSpan)); + public bool HasSpanThatIntersects(SnapshotPoint point) + => _tree.HasIntervalThatIntersectsWith(point.Position, new IntervalIntrospector(point.Snapshot, _spanTrackingMode)); /// /// Gets all the spans that intersect with in sorted order and adds them to /// . Note the sorted chunk of items are appended to . This /// means that may not be sorted if there were already items in them. /// - private void AppendIntersectingSpansInSortedOrder(SnapshotSpan snapshotSpan, SegmentedList> result) + public void AddIntersectingTagSpans(SnapshotSpan snapshotSpan, SegmentedList> result) { var snapshot = snapshotSpan.Snapshot; - Debug.Assert(snapshot.TextBuffer == _textBuffer); using var intersectingIntervals = TemporaryArray>.Empty; _tree.FillWithIntervalsThatIntersectWith( @@ -92,8 +79,24 @@ ref intersectingIntervals.AsRef(), result.Add(GetTranslatedTagSpan(tagSpan, snapshot, _spanTrackingMode)); } - public IEnumerable> GetSpans(ITextSnapshot snapshot) - => _tree.Select(tn => GetTranslatedITagSpan(tn, snapshot)); + /// + /// Gets all the tag spans in this tree, remapped to , and returns them as a . + /// + public NormalizedSnapshotSpanCollection GetSnapshotSpanCollection(ITextSnapshot snapshot) + { + if (this == Empty) + return NormalizedSnapshotSpanCollection.Empty; + + using var _ = ArrayBuilder.GetInstance(out var spans); + + foreach (var tagSpan in _tree) + spans.Add(GetTranslatedSpan(tagSpan, snapshot, _spanTrackingMode)); + + return spans.Count == 0 + ? NormalizedSnapshotSpanCollection.Empty + : new(spans); + } /// /// Adds all the tag spans in to , translating them to the given @@ -102,7 +105,15 @@ public IEnumerable> GetSpans(ITextSnapshot snapshot) public void AddAllSpans(ITextSnapshot textSnapshot, HashSet> tagSpans) { foreach (var tagSpan in _tree) - tagSpans.Add(GetTranslatedITagSpan(tagSpan, textSnapshot)); + tagSpans.Add(GetTranslatedTagSpan(tagSpan, textSnapshot)); + } + + /// + /// Spans will be added in sorted order + public void AddAllSpans(ITextSnapshot textSnapshot, SegmentedList> tagSpans) + { + foreach (var tagSpan in _tree) + tagSpans.Add(GetTranslatedTagSpan(tagSpan, textSnapshot)); } /// @@ -126,127 +137,118 @@ ref buffer.AsRef(), new IntervalIntrospector(textSnapshot, _spanTrackingMode)); foreach (var tagSpan in buffer) - tagSpans.Remove(GetTranslatedITagSpan(tagSpan, textSnapshot)); + tagSpans.Remove(GetTranslatedTagSpan(tagSpan, textSnapshot)); } } - public bool IsEmpty() - => _tree.IsEmpty(); - public void AddIntersectingTagSpans(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) - { - AddIntersectingTagSpansWorker(requestedSpans, tags); - DebugVerifyTags(requestedSpans, tags); - } - - [Conditional("DEBUG")] - private static void DebugVerifyTags(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) - { - if (tags == null) - { - return; - } - - foreach (var tag in tags) - { - var span = tag.Span; - - if (!requestedSpans.Any(s => s.IntersectsWith(span))) - { - Contract.Fail(tag + " doesn't intersects with any requested span"); - } - } - } - - private void AddIntersectingTagSpansWorker( - NormalizedSnapshotSpanCollection requestedSpans, - SegmentedList> tags) { const int MaxNumberOfRequestedSpans = 100; // Special case the case where there is only one requested span. In that case, we don't // need to allocate any intermediate collections if (requestedSpans.Count == 1) - AppendIntersectingSpansInSortedOrder(requestedSpans[0], tags); + { + AddIntersectingTagSpans(requestedSpans[0], tags); + } else if (requestedSpans.Count < MaxNumberOfRequestedSpans) - AddTagsForSmallNumberOfSpans(requestedSpans, tags); + { + foreach (var span in requestedSpans) + AddIntersectingTagSpans(span, tags); + } else + { AddTagsForLargeNumberOfSpans(requestedSpans, tags); - } + } - private void AddTagsForSmallNumberOfSpans( - NormalizedSnapshotSpanCollection requestedSpans, - SegmentedList> tags) - { - foreach (var span in requestedSpans) - AppendIntersectingSpansInSortedOrder(span, tags); - } + DebugVerifyTags(requestedSpans, tags); + return; - private void AddTagsForLargeNumberOfSpans(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) - { - // we are asked with bunch of spans. rather than asking same question again and again, ask once with big span - // which will return superset of what we want. and then filter them out in O(m+n) cost. - // m == number of requested spans, n = number of returned spans - var mergedSpan = new SnapshotSpan(requestedSpans[0].Start, requestedSpans[^1].End); + void AddTagsForLargeNumberOfSpans(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) + { + // we are asked with bunch of spans. rather than asking same question again and again, ask once with big span + // which will return superset of what we want. and then filter them out in O(m+n) cost. + // m == number of requested spans, n = number of returned spans + var mergedSpan = new SnapshotSpan(requestedSpans[0].Start, requestedSpans[^1].End); - using var _1 = SegmentedListPool.GetPooledList>(out var tempList); + using var _1 = SegmentedListPool.GetPooledList>(out var tempList); - AppendIntersectingSpansInSortedOrder(mergedSpan, tempList); - if (tempList.Count == 0) - return; + AddIntersectingTagSpans(mergedSpan, tempList); + if (tempList.Count == 0) + return; - // Note: both 'requstedSpans' and 'tempList' are in sorted order. + // Note: both 'requestedSpans' and 'tempList' are in sorted order. - using var enumerator = tempList.GetEnumerator(); + using var enumerator = tempList.GetEnumerator(); - if (!enumerator.MoveNext()) - return; + if (!enumerator.MoveNext()) + return; - using var _2 = PooledHashSet>.GetInstance(out var hashSet); + using var _2 = PooledHashSet>.GetInstance(out var hashSet); - var requestIndex = 0; - while (true) - { - var currentTag = enumerator.Current; + var requestIndex = 0; + while (true) + { + var currentTag = enumerator.Current; - var currentRequestSpan = requestedSpans[requestIndex]; - var currentTagSpan = currentTag.Span; + var currentRequestSpan = requestedSpans[requestIndex]; + var currentTagSpan = currentTag.Span; - // The current tag is *before* the current span we're trying to intersect with. Move to the next tag to - // see if it intersects with the current span. - if (currentTagSpan.End < currentRequestSpan.Start) - { - // If there are no more tags, then we're done. - if (!enumerator.MoveNext()) - return; + // The current tag is *before* the current span we're trying to intersect with. Move to the next tag to + // see if it intersects with the current span. + if (currentTagSpan.End < currentRequestSpan.Start) + { + // If there are no more tags, then we're done. + if (!enumerator.MoveNext()) + return; - continue; - } + continue; + } - // The current tag is *after* teh current span we're trying to intersect with. Move to the next span to - // see if it intersects with the current tag. - if (currentTagSpan.Start > currentRequestSpan.End) - { - requestIndex++; + // The current tag is *after* teh current span we're trying to intersect with. Move to the next span to + // see if it intersects with the current tag. + if (currentTagSpan.Start > currentRequestSpan.End) + { + requestIndex++; + + // If there are no more spans to intersect with, then we're done. + if (requestIndex >= requestedSpans.Count) + return; + + continue; + } - // If there are no more spans to intersect with, then we're done. - if (requestIndex >= requestedSpans.Count) - return; + // This tag intersects the current span we're trying to intersect with. Ensure we only see and add a + // particular tag once. - continue; + if (currentTagSpan.Length > 0 && + hashSet.Add(currentTag)) + { + tags.Add(currentTag); + } + + if (!enumerator.MoveNext()) + break; } + } + } + + [Conditional("DEBUG")] + private static void DebugVerifyTags(NormalizedSnapshotSpanCollection requestedSpans, SegmentedList> tags) + { + if (tags == null) + { + return; + } - // This tag intersects the current span we're trying to intersect with. Ensure we only see and add a - // particular tag once. + foreach (var tag in tags) + { + var span = tag.Span; - if (currentTagSpan.Length > 0 && - hashSet.Add(currentTag)) + if (!requestedSpans.Any(s => s.IntersectsWith(span))) { - tags.Add(currentTag); + Contract.Fail(tag + " doesn't intersects with any requested span"); } - - if (!enumerator.MoveNext()) - break; } } } diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs index da3fd9653023..458835c9a704 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading; +using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.Options; @@ -51,6 +52,9 @@ private sealed partial class TagSource /// private const int CoalesceDifferenceCount = 10; + private static readonly ObjectPool>> s_tagSpanListPool = new(() => new(), trimOnFree: false); + private readonly ObjectPool>> _tagSpanSetPool; + #region Fields that can be accessed from either thread private readonly AbstractAsynchronousTaggerProvider _dataSource; diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_IEqualityComparer.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_IEqualityComparer.cs index 531badc090be..4e4abff3bcf0 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_IEqualityComparer.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_IEqualityComparer.cs @@ -12,8 +12,6 @@ internal abstract partial class AbstractAsynchronousTaggerProvider { private partial class TagSource : IEqualityComparer> { - private readonly ObjectPool>> _tagSpanSetPool; - public bool Equals(TagSpan? x, TagSpan? y) { if (x == y) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index e9e4fdfa3b48..f3556bf4bcc3 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Utilities; using Microsoft.CodeAnalysis.Workspaces; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -40,11 +41,8 @@ private void OnCaretPositionChanged(object? _, CaretPositionChangedEventArgs e) { // If it changed position and we're still in a tag, there's nothing more to do var currentTags = TryGetTagIntervalTreeForBuffer(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. + if (currentTags != null && currentTags.HasSpanThatIntersects(caret.Value)) return; - } } RemoveAllTags(); @@ -58,10 +56,12 @@ private void RemoveAllTags() ref _cachedTagTrees_mayChangeFromAnyThread, ImmutableDictionary>.Empty); var snapshot = _subjectBuffer.CurrentSnapshot; - var oldTagTree = GetTagTree(snapshot, oldTagTrees); + var oldTagTree = oldTagTrees.TryGetValue(snapshot.TextBuffer, out var tagTree) + ? tagTree + : TagSpanIntervalTree.Empty; // everything from old tree is removed. - RaiseTagsChanged(snapshot.TextBuffer, new DiffResult(added: null, removed: new(oldTagTree.GetSpans(snapshot).Select(s => s.Span)))); + RaiseTagsChanged(snapshot.TextBuffer, new DiffResult(added: null, removed: oldTagTree.GetSnapshotSpanCollection(snapshot))); } private void OnSubjectBufferChanged(object? _, TextContentChangedEventArgs e) @@ -96,21 +96,32 @@ private void RemoveTagsThatIntersectEdit(TextContentChangedEventArgs e) var snapshot = e.After; var buffer = snapshot.TextBuffer; + using var _1 = SegmentedListPool.GetPooledList>(out var tagsToRemove); + using var _2 = _tagSpanSetPool.GetPooledObject(out var allTags); + // Everything we're passing in here is synchronous. So we can assert that this must complete synchronously // as well. var (oldTagTrees, newTagTrees) = CompareAndSwapTagTreesAsync( oldTagTrees => { + tagsToRemove.Clear(); + allTags.Clear(); + if (oldTagTrees.TryGetValue(buffer, out var treeForBuffer)) { - var tagsToRemove = e.Changes.SelectMany(c => treeForBuffer.GetIntersectingSpans(new SnapshotSpan(snapshot, c.NewSpan))); - if (tagsToRemove.Any()) + foreach (var change in e.Changes) + treeForBuffer.AddIntersectingTagSpans(new SnapshotSpan(snapshot, change.NewSpan), tagsToRemove); + + if (tagsToRemove.Count > 0) { - var allTags = treeForBuffer.GetSpans(e.After).ToList(); + treeForBuffer.AddAllSpans(snapshot, allTags); + + allTags.RemoveAll(tagsToRemove); + var newTagTree = new TagSpanIntervalTree( - buffer, - treeForBuffer.SpanTrackingMode, - allTags.Except(tagsToRemove, comparer: this)); + snapshot, + this._dataSource.SpanTrackingMode, + allTags); return new(oldTagTrees.SetItem(buffer, newTagTree)); } } @@ -137,13 +148,6 @@ private void RemoveTagsThatIntersectEdit(TextContentChangedEventArgs e) RaiseTagsChanged(buffer, difference); } - private TagSpanIntervalTree GetTagTree(ITextSnapshot snapshot, ImmutableDictionary> tagTrees) - { - return tagTrees.TryGetValue(snapshot.TextBuffer, out var tagTree) - ? tagTree - : new TagSpanIntervalTree(snapshot.TextBuffer, _dataSource.SpanTrackingMode); - } - private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) => EnqueueWork(highPriority: false); @@ -393,7 +397,7 @@ private ImmutableDictionary> ComputeNewTa foreach (var spanToTag in context.SpansToTag) buffersToTag.Add(spanToTag.SnapshotSpan.Snapshot.TextBuffer); - using var _2 = ArrayBuilder>.GetInstance(out var newTagsInBuffer); + using var _2 = s_tagSpanListPool.GetPooledObject(out var newTagsInBuffer); using var _3 = ArrayBuilder.GetInstance(out var spansToInvalidateInBuffer); var newTagTrees = ImmutableDictionary.CreateBuilder>(); @@ -427,10 +431,10 @@ private ImmutableDictionary> ComputeNewTa private TagSpanIntervalTree? ComputeNewTagTree( ImmutableDictionary> oldTagTrees, ITextBuffer textBuffer, - ArrayBuilder> newTags, + SegmentedList> newTags, ArrayBuilder spansToInvalidate) { - var noNewTags = newTags.IsEmpty; + var noNewTags = newTags.Count == 0; var noSpansToInvalidate = spansToInvalidate.IsEmpty; oldTagTrees.TryGetValue(textBuffer, out var oldTagTree); @@ -441,7 +445,7 @@ private ImmutableDictionary> ComputeNewTa return null; // If we don't have any old tags then we just need to return the new tags. - return new TagSpanIntervalTree(textBuffer, _dataSource.SpanTrackingMode, newTags); + return new TagSpanIntervalTree(newTags[0].Span.Snapshot, _dataSource.SpanTrackingMode, newTags); } // If we don't have any new tags, and there was nothing to invalidate, then we can @@ -452,35 +456,34 @@ private ImmutableDictionary> ComputeNewTa if (noSpansToInvalidate) { // If we have no spans to invalidate, then we can just keep the old tags and add the new tags. - var oldTagsToKeep = oldTagTree.GetSpans(newTags.First().Span.Snapshot); + var snapshot = newTags.First().Span.Snapshot; + + // For efficiency, just grab the old tags, remap them to the current snapshot, and place them in the + // newTags buffer. This is a safe mutation of this buffer as the caller doesn't use it after this point + // and instead immediately clears it. + oldTagTree.AddAllSpans(snapshot, newTags); return new TagSpanIntervalTree( - textBuffer, _dataSource.SpanTrackingMode, oldTagsToKeep, newTags); + snapshot, _dataSource.SpanTrackingMode, newTags); } else { // We do have spans to invalidate. Get the set of old tags that don't intersect with those and add the new tags. - using var _1 = _tagSpanSetPool.GetPooledObject(out var nonIntersectingTagSpans); - AddNonIntersectingTagSpans(spansToInvalidate, oldTagTree, nonIntersectingTagSpans); - return new TagSpanIntervalTree( - textBuffer, _dataSource.SpanTrackingMode, nonIntersectingTagSpans, newTags); - } - } + using var _1 = _tagSpanSetPool.GetPooledObject(out var nonIntersectingOldTags); - private static void AddNonIntersectingTagSpans( - ArrayBuilder spansToInvalidate, - TagSpanIntervalTree oldTagTree, - HashSet> nonIntersectingTagSpans) - { - var firstSpanToInvalidate = spansToInvalidate.First(); - var snapshot = firstSpanToInvalidate.Snapshot; + var firstSpanToInvalidate = spansToInvalidate.First(); + var snapshot = firstSpanToInvalidate.Snapshot; - // Performance: No need to fully realize spansToInvalidate or do any of the calculations below if the - // full snapshot is being invalidated. - if (firstSpanToInvalidate.Length == snapshot.Length) - return; + // Performance: No need to fully realize spansToInvalidate or do any of the calculations below if the + // full snapshot is being invalidated. + if (firstSpanToInvalidate.Length != snapshot.Length) + { + oldTagTree.AddAllSpans(snapshot, nonIntersectingOldTags); + oldTagTree.RemoveIntersectingTagSpans(spansToInvalidate, nonIntersectingOldTags); + } - oldTagTree.AddAllSpans(snapshot, nonIntersectingTagSpans); - oldTagTree.RemoveIntersectingTagSpans(spansToInvalidate, nonIntersectingTagSpans); + return new TagSpanIntervalTree( + snapshot, _dataSource.SpanTrackingMode, nonIntersectingOldTags, newTags); + } } private bool ShouldSkipTagProduction() @@ -521,7 +524,7 @@ private Dictionary ProcessNewTagTrees( else { // It's a new buffer, so report all spans are changed - bufferToChanges[latestBuffer] = new DiffResult(added: new(latestSpans.GetSpans(snapshot).Select(t => t.Span)), removed: null); + bufferToChanges[latestBuffer] = new DiffResult(added: latestSpans.GetSnapshotSpanCollection(snapshot), removed: null); } } @@ -530,7 +533,7 @@ private Dictionary ProcessNewTagTrees( if (!newTagTrees.ContainsKey(oldBuffer)) { // This buffer disappeared, so let's notify that the old tags are gone - bufferToChanges[oldBuffer] = new DiffResult(added: null, removed: new(previousSpans.GetSpans(oldBuffer.CurrentSnapshot).Select(t => t.Span))); + bufferToChanges[oldBuffer] = new DiffResult(added: null, removed: previousSpans.GetSnapshotSpanCollection(oldBuffer.CurrentSnapshot)); } } @@ -546,16 +549,20 @@ private DiffResult ComputeDifference( TagSpanIntervalTree latestTree, TagSpanIntervalTree previousTree) { - var latestSpans = latestTree.GetSpans(snapshot); - var previousSpans = previousTree.GetSpans(snapshot); + using var _1 = s_tagSpanListPool.GetPooledObject(out var latestSpans); + using var _2 = s_tagSpanListPool.GetPooledObject(out var previousSpans); + + using var _3 = ArrayBuilder.GetInstance(out var added); + using var _4 = ArrayBuilder.GetInstance(out var removed); + + latestTree.AddAllSpans(snapshot, latestSpans); + previousTree.AddAllSpans(snapshot, previousSpans); - using var _1 = ArrayBuilder.GetInstance(out var added); - using var _2 = ArrayBuilder.GetInstance(out var removed); - using var latestEnumerator = latestSpans.GetEnumerator(); - using var previousEnumerator = previousSpans.GetEnumerator(); + var latestEnumerator = latestSpans.GetEnumerator(); + var previousEnumerator = previousSpans.GetEnumerator(); - var latest = NextOrNull(latestEnumerator); - var previous = NextOrNull(previousEnumerator); + var latest = NextOrNull(ref latestEnumerator); + var previous = NextOrNull(ref previousEnumerator); while (latest != null && previous != null) { @@ -565,12 +572,12 @@ private DiffResult ComputeDifference( if (latestSpan.Start < previousSpan.Start) { added.Add(latestSpan); - latest = NextOrNull(latestEnumerator); + latest = NextOrNull(ref latestEnumerator); } else if (previousSpan.Start < latestSpan.Start) { removed.Add(previousSpan); - previous = NextOrNull(previousEnumerator); + previous = NextOrNull(ref previousEnumerator); } else { @@ -579,20 +586,20 @@ private DiffResult ComputeDifference( if (previousSpan.End > latestSpan.End) { removed.Add(previousSpan); - latest = NextOrNull(latestEnumerator); + latest = NextOrNull(ref latestEnumerator); } else if (latestSpan.End > previousSpan.End) { added.Add(latestSpan); - previous = NextOrNull(previousEnumerator); + previous = NextOrNull(ref previousEnumerator); } else { if (!_dataSource.TagEquals(latest.Tag, previous.Tag)) added.Add(latestSpan); - latest = NextOrNull(latestEnumerator); - previous = NextOrNull(previousEnumerator); + latest = NextOrNull(ref latestEnumerator); + previous = NextOrNull(ref previousEnumerator); } } } @@ -600,18 +607,18 @@ private DiffResult ComputeDifference( while (latest != null) { added.Add(latest.Span); - latest = NextOrNull(latestEnumerator); + latest = NextOrNull(ref latestEnumerator); } while (previous != null) { removed.Add(previous.Span); - previous = NextOrNull(previousEnumerator); + previous = NextOrNull(ref previousEnumerator); } return new DiffResult(new(added), new(removed)); - static TagSpan? NextOrNull(IEnumerator> enumerator) + static TagSpan? NextOrNull(ref SegmentedList>.Enumerator enumerator) => enumerator.MoveNext() ? enumerator.Current : null; } diff --git a/src/EditorFeatures/Test/Tagging/TagSpanIntervalTreeTests.cs b/src/EditorFeatures/Test/Tagging/TagSpanIntervalTreeTests.cs index 9a7a43275235..6adc3a47b018 100644 --- a/src/EditorFeatures/Test/Tagging/TagSpanIntervalTreeTests.cs +++ b/src/EditorFeatures/Test/Tagging/TagSpanIntervalTreeTests.cs @@ -4,9 +4,12 @@ #nullable disable +using System.Collections.Generic; using System.Linq; +using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; using Roslyn.Test.EditorUtilities; @@ -17,49 +20,58 @@ namespace Microsoft.CodeAnalysis.Editor.UnitTests.Tagging; [UseExportProvider] public class TagSpanIntervalTreeTests { - private static TagSpanIntervalTree CreateTree(string text, params Span[] spans) + private static (TagSpanIntervalTree, ITextBuffer) CreateTree(string text, params Span[] spans) { var exportProvider = EditorTestCompositions.Editor.ExportProviderFactory.CreateExportProvider(); var buffer = EditorFactory.CreateBuffer(exportProvider, text); var tags = spans.Select(s => new TagSpan(new SnapshotSpan(buffer.CurrentSnapshot, s), new TextMarkerTag(string.Empty))); - return new TagSpanIntervalTree(buffer, SpanTrackingMode.EdgeInclusive, tags); + return (new TagSpanIntervalTree(buffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive, tags), buffer); + } + + private static IReadOnlyList> GetIntersectingSpans( + TagSpanIntervalTree tree, SnapshotSpan snapshotSpan) + where TTag : ITag + { + var result = new SegmentedList>(); + tree.AddIntersectingTagSpans(snapshotSpan, result); + return result; } [Fact] public void TestEmptyTree() { - var tree = CreateTree(string.Empty); + var (tree, buffer) = CreateTree(string.Empty); - Assert.Empty(tree.GetSpans(tree.Buffer.CurrentSnapshot)); + Assert.Empty(GetIntersectingSpans(tree, buffer.CurrentSnapshot.GetFullSpan())); } [Fact] public void TestSingleSpan() { - var tree = CreateTree("Hello, World", new Span(0, 5)); + var (tree, buffer) = CreateTree("Hello, World", new Span(0, 5)); - Assert.Equal(new Span(0, 5), tree.GetSpans(tree.Buffer.CurrentSnapshot).Single().Span); + Assert.Equal(new Span(0, 5), GetIntersectingSpans(tree, buffer.CurrentSnapshot.GetFullSpan()).Single().Span); } [Fact] public void TestSingleIntersectingSpanAtStartWithEdit() { - var tree = CreateTree("Hello, World", new Span(7, 5)); - tree.Buffer.Insert(0, new string('c', 100)); + var (tree, buffer) = CreateTree("Hello, World", new Span(7, 5)); + buffer.Insert(0, new string('c', 100)); // The span should start at 107 - var spans = tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 107, 0)); + var spans = GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 107, 0)); Assert.Equal(new Span(107, 5), spans.Single().Span); } [Fact] public void TestSingleIntersectingSpanAtEndWithEdit() { - var tree = CreateTree("Hello, World", new Span(7, 5)); - tree.Buffer.Insert(0, new string('c', 100)); + var (tree, buffer) = CreateTree("Hello, World", new Span(7, 5)); + buffer.Insert(0, new string('c', 100)); // The span should end at 112 - var spans = tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 112, 0)); + var spans = GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 112, 0)); Assert.Equal(new Span(107, 5), spans.Single().Span); } @@ -67,67 +79,67 @@ public void TestSingleIntersectingSpanAtEndWithEdit() public void TestManySpansWithEdit() { // Create a buffer with the second half of the buffer covered with spans - var tree = CreateTree(new string('c', 100), Enumerable.Range(50, count: 50).Select(s => new Span(s, 1)).ToArray()); - tree.Buffer.Insert(0, new string('c', 100)); + var (tree, buffer) = CreateTree(new string('c', 100), Enumerable.Range(50, count: 50).Select(s => new Span(s, 1)).ToArray()); + buffer.Insert(0, new string('c', 100)); // We should have 50 spans if we start looking at just the end - Assert.Equal(50, tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 150, 50)).Count()); + Assert.Equal(50, GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 150, 50)).Count()); // And we should have 26 here. We directly cover 25 spans, and we touch one more - Assert.Equal(26, tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 175, 25)).Count()); + Assert.Equal(26, GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 175, 25)).Count()); } [Fact] public void TestManySpansWithEdit2() { // Cover the full buffer with spans - var tree = CreateTree(new string('c', 100), Enumerable.Range(0, count: 100).Select(s => new Span(s, 1)).ToArray()); - tree.Buffer.Insert(0, new string('c', 100)); + var (tree, buffer) = CreateTree(new string('c', 100), Enumerable.Range(0, count: 100).Select(s => new Span(s, 1)).ToArray()); + buffer.Insert(0, new string('c', 100)); // We should see one span anywhere in the beginning of the buffer, since this is edge inclusive - Assert.Equal(1, tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 0, 1)).Count()); - Assert.Equal(1, tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 50, 1)).Count()); + Assert.Equal(1, GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 0, 1)).Count()); + Assert.Equal(1, GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 50, 1)).Count()); // We should see two at position 100 (the first span that is now expanded, and the second of width 1) - Assert.Equal(2, tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 100, 1)).Count()); + Assert.Equal(2, GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 100, 1)).Count()); } [Fact] public void TestManySpansWithDeleteAndEditAtStart() { // Cover the full buffer with spans - var tree = CreateTree(new string('c', 100), Enumerable.Range(0, count: 100).Select(s => new Span(s, 1)).ToArray()); + var (tree, buffer) = CreateTree(new string('c', 100), Enumerable.Range(0, count: 100).Select(s => new Span(s, 1)).ToArray()); - tree.Buffer.Delete(new Span(0, 50)); - tree.Buffer.Insert(0, new string('c', 50)); + buffer.Delete(new Span(0, 50)); + buffer.Insert(0, new string('c', 50)); // We should see 51 spans intersecting the start. When we did the delete, we contracted 50 spans to size // zero, and then the insert will have expanded all of those, plus the span right next to it. - Assert.Equal(51, tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 0, 1)).Count()); + Assert.Equal(51, GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 0, 1)).Count()); } [Fact] public void TestManySpansWithDeleteAndEditAtEnd() { // Cover the full buffer with spans - var tree = CreateTree(new string('c', 100), Enumerable.Range(0, count: 100).Select(s => new Span(s, 1)).ToArray()); + var (tree, buffer) = CreateTree(new string('c', 100), Enumerable.Range(0, count: 100).Select(s => new Span(s, 1)).ToArray()); - tree.Buffer.Delete(new Span(50, 50)); - tree.Buffer.Insert(50, new string('c', 50)); + buffer.Delete(new Span(50, 50)); + buffer.Insert(50, new string('c', 50)); // We should see 51 spans intersecting the end. When we did the delete, we contracted 50 spans to size zero, // and then the insert will have expanded all of those, plus the span right next to it. - Assert.Equal(51, tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 99, 1)).Count()); + Assert.Equal(51, GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 99, 1)).Count()); } [Fact] public void TestTagSpanOrdering() { // Cover the full buffer with spans - var tree = CreateTree(new string('c', 100), Enumerable.Range(0, count: 100).Select(s => new Span(s, 1)).ToArray()); + var (tree, buffer) = CreateTree(new string('c', 100), Enumerable.Range(0, count: 100).Select(s => new Span(s, 1)).ToArray()); var lastStart = -1; - foreach (var tag in tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, 0, tree.Buffer.CurrentSnapshot.Length))) + foreach (var tag in GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, 0, buffer.CurrentSnapshot.Length))) { Assert.True(lastStart < tag.Span.Start.Position); lastStart = tag.Span.Start.Position; @@ -137,40 +149,40 @@ public void TestTagSpanOrdering() [Fact] public void TestEmptySpanIntersects1() { - var tree = CreateTree("goo", new Span(0, 0)); - var spans = tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, new Span(0, 0))); + var (tree, buffer) = CreateTree("goo", new Span(0, 0)); + var spans = GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, new Span(0, 0))); Assert.Single(spans); } [Fact] public void TestEmptySpanIntersects2() { - var tree = CreateTree("goo", new Span(0, 0)); - var spans = tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, new Span(0, "goo".Length))); + var (tree, buffer) = CreateTree("goo", new Span(0, 0)); + var spans = GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, new Span(0, "goo".Length))); Assert.Single(spans); } [Fact] public void TestEmptySpanIntersects3() { - var tree = CreateTree("goo", new Span(1, 0)); - var spans = tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, new Span(0, 1))); + var (tree, buffer) = CreateTree("goo", new Span(1, 0)); + var spans = GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, new Span(0, 1))); Assert.Single(spans); } [Fact] public void TestEmptySpanIntersects4() { - var tree = CreateTree("goo", new Span(1, 0)); - var spans = tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, new Span(1, 0))); + var (tree, buffer) = CreateTree("goo", new Span(1, 0)); + var spans = GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, new Span(1, 0))); Assert.Single(spans); } [Fact] public void TestEmptySpanIntersects5() { - var tree = CreateTree("goo", new Span(1, 0)); - var spans = tree.GetIntersectingSpans(new SnapshotSpan(tree.Buffer.CurrentSnapshot, new Span(1, 1))); + var (tree, buffer) = CreateTree("goo", new Span(1, 0)); + var spans = GetIntersectingSpans(tree, new SnapshotSpan(buffer.CurrentSnapshot, new Span(1, 1))); Assert.Single(spans); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Collections/IntervalTree`1.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Collections/IntervalTree`1.cs index 44e993bba4b6..0b3a0e68e0fd 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Collections/IntervalTree`1.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Collections/IntervalTree`1.cs @@ -10,7 +10,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Shared.Collections; @@ -352,26 +351,27 @@ private static Node Balance(Node node, in TIntrospector introspec public IEnumerator GetEnumerator() { - if (root == null) - { - yield break; - } + return this == Empty || root == null ? SpecializedCollections.EmptyEnumerator() : GetEnumeratorWorker(); - using var _ = ArrayBuilder<(Node? node, bool firstTime)>.GetInstance(out var candidates); - candidates.Push((root, firstTime: true)); - while (candidates.TryPop(out var tuple)) + IEnumerator GetEnumeratorWorker() { - var (currentNode, firstTime) = tuple; - if (currentNode != null) + Contract.ThrowIfNull(root); + using var _ = ArrayBuilder<(Node node, bool firstTime)>.GetInstance(out var candidates); + candidates.Push((root, firstTime: true)); + while (candidates.TryPop(out var tuple)) { + var (currentNode, firstTime) = tuple; if (firstTime) { - // First time seeing this node. Mark that we've been seen and recurse - // down the left side. The next time we see this node we'll yield it - // out. - candidates.Push((currentNode.Right, firstTime: true)); + // First time seeing this node. Mark that we've been seen and recurse down the left side. The + // next time we see this node we'll yield it out. + if (currentNode.Right != null) + candidates.Push((currentNode.Right, firstTime: true)); + candidates.Push((currentNode, firstTime: false)); - candidates.Push((currentNode.Left, firstTime: true)); + + if (currentNode.Left != null) + candidates.Push((currentNode.Left, firstTime: true)); } else { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs index c4cabfde2e7f..c1ae40468ea7 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs @@ -50,6 +50,13 @@ public static PooledObject> GetPooledObject(this ObjectPool> GetPooledObject(this ObjectPool> pool, out SegmentedList list) + { + var pooledObject = PooledObject>.Create(pool); + list = pooledObject.Object; + return pooledObject; + } + public static PooledObject> GetPooledObject(this ObjectPool> pool, out HashSet list) { var pooledObject = PooledObject>.Create(pool);