|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.Shared.Tagging
{
/// <summary>
/// A tag span interval tree represents an ordered tree data structure to store tag spans in. It
/// allows you to efficiently find all tag spans that intersect a provided span. Tag spans are
/// tracked. That way you can query for intersecting/overlapping spans in a different snapshot
/// than the one for the tag spans that were added.
/// </summary>
internal partial class TagSpanIntervalTree<TTag> where TTag : ITag
{
private readonly IntervalTree<TagNode> _tree;
private readonly ITextBuffer _textBuffer;
private readonly SpanTrackingMode _spanTrackingMode;
public TagSpanIntervalTree(ITextBuffer textBuffer,
SpanTrackingMode trackingMode,
IEnumerable<ITagSpan<TTag>>? values = null)
{
_textBuffer = textBuffer;
_spanTrackingMode = trackingMode;
var nodeValues = values?.Select(ts => new TagNode(ts, trackingMode));
_tree = IntervalTree.Create(new IntervalIntrospector(textBuffer.CurrentSnapshot), nodeValues);
}
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));
}
public IList<ITagSpan<TTag>> GetIntersectingSpans(SnapshotSpan snapshotSpan)
{
var snapshot = snapshotSpan.Snapshot;
Debug.Assert(snapshot.TextBuffer == _textBuffer);
var intersectingIntervals = _tree.GetIntervalsThatIntersectWith(
snapshotSpan.Start, snapshotSpan.Length, new IntervalIntrospector(snapshot));
List<ITagSpan<TTag>>? result = null;
foreach (var tagNode in intersectingIntervals)
{
result ??= new List<ITagSpan<TTag>>();
result.Add(new TagSpan<TTag>(tagNode.Span.GetSpan(snapshot), tagNode.Tag));
}
return result ?? SpecializedCollections.EmptyList<ITagSpan<TTag>>();
}
public IEnumerable<ITagSpan<TTag>> GetSpans(ITextSnapshot snapshot)
=> _tree.Select(tn => new TagSpan<TTag>(tn.Span.GetSpan(snapshot), tn.Tag));
public bool IsEmpty()
=> _tree.IsEmpty();
public IEnumerable<ITagSpan<TTag>> GetIntersectingTagSpans(NormalizedSnapshotSpanCollection requestedSpans)
{
var result = GetIntersectingTagSpansWorker(requestedSpans);
DebugVerifyTags(requestedSpans, result);
return result;
}
[Conditional("DEBUG")]
private static void DebugVerifyTags(NormalizedSnapshotSpanCollection requestedSpans, IEnumerable<ITagSpan<TTag>> 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 IEnumerable<ITagSpan<TTag>> GetIntersectingTagSpansWorker(NormalizedSnapshotSpanCollection requestedSpans)
{
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
return requestedSpans.Count == 1
? GetIntersectingSpans(requestedSpans[0])
: requestedSpans.Count < MaxNumberOfRequestedSpans
? GetTagsForSmallNumberOfSpans(requestedSpans)
: GetTagsForLargeNumberOfSpans(requestedSpans);
}
private IEnumerable<ITagSpan<TTag>> GetTagsForSmallNumberOfSpans(NormalizedSnapshotSpanCollection requestedSpans)
{
var result = new List<ITagSpan<TTag>>();
foreach (var s in requestedSpans)
{
result.AddRange(GetIntersectingSpans(s));
}
return result;
}
private IEnumerable<ITagSpan<TTag>> GetTagsForLargeNumberOfSpans(NormalizedSnapshotSpanCollection requestedSpans)
{
// 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[requestedSpans.Count - 1].End);
var result = GetIntersectingSpans(mergedSpan);
var requestIndex = 0;
var enumerator = result.GetEnumerator();
try
{
if (!enumerator.MoveNext())
{
return SpecializedCollections.EmptyEnumerable<ITagSpan<TTag>>();
}
var hashSet = new HashSet<ITagSpan<TTag>>();
while (true)
{
var currentTag = enumerator.Current;
var currentRequestSpan = requestedSpans[requestIndex];
var currentTagSpan = currentTag.Span;
if (currentRequestSpan.Start > currentTagSpan.End)
{
if (!enumerator.MoveNext())
{
break;
}
}
else if (currentTagSpan.Start > currentRequestSpan.End)
{
requestIndex++;
if (requestIndex >= requestedSpans.Count)
{
break;
}
}
else
{
if (currentTagSpan.Length > 0)
{
hashSet.Add(currentTag);
}
if (!enumerator.MoveNext())
{
break;
}
}
}
return hashSet;
}
finally
{
enumerator.Dispose();
}
}
}
}
|