-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce an interval tree implementation backed by an array. #73855
Changes from 25 commits
9ad83d4
bd57ecb
1cb5d9e
90bee92
1203922
31fd6cc
5c246ae
a1f7e22
54ab6b9
543fb5e
a8f9b5e
a9ae4c5
cbc0932
0424947
91b2e23
773759f
79323da
98497fd
f18dd54
b606bcf
878d6cb
fb102df
6ac3c90
3e0e2d3
790e5a8
dc3d9ab
4e22601
8e147ff
831f599
3b3d05a
3c6632c
aa59cbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,36 +6,40 @@ | |
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis.Collections; | ||
using Microsoft.CodeAnalysis.Shared.Collections; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Microsoft.VisualStudio.Text; | ||
using Roslyn.Test.Utilities; | ||
using Xunit; | ||
|
||
namespace Microsoft.CodeAnalysis.Editor.UnitTests.Collections; | ||
|
||
public sealed class IntervalTreeTests | ||
public abstract class IntervalTreeTests | ||
{ | ||
private readonly struct TupleIntrospector<T> : IIntervalIntrospector<Tuple<int, int, T>> | ||
private protected readonly struct TupleIntrospector<T> : IIntervalIntrospector<Tuple<int, int, T>> | ||
{ | ||
public TextSpan GetSpan(Tuple<int, int, T> value) | ||
=> new(value.Item1, value.Item2); | ||
} | ||
|
||
private static IEnumerable<SimpleBinaryIntervalTree<Tuple<int, int, string>, TupleIntrospector<string>>> CreateTrees(params Tuple<int, int, string>[] values) | ||
private IEnumerable<IIntervalTree<Tuple<int, int, string>>> CreateTrees(params Tuple<int, int, string>[] values) | ||
=> CreateTrees((IEnumerable<Tuple<int, int, string>>)values); | ||
|
||
private static IEnumerable<SimpleBinaryIntervalTree<Tuple<int, int, string>, TupleIntrospector<string>>> CreateTrees(IEnumerable<Tuple<int, int, string>> values) | ||
{ | ||
yield return BinaryIntervalTree.Create(new TupleIntrospector<string>(), values); | ||
} | ||
private protected abstract IEnumerable<IIntervalTree<Tuple<int, int, string>>> CreateTrees(IEnumerable<Tuple<int, int, string>> values); | ||
|
||
private protected abstract ImmutableArray<Tuple<int, int, string>> GetIntervalsThatIntersectWith(IIntervalTree<Tuple<int, int, string>> tree, int start, int length); | ||
private protected abstract ImmutableArray<Tuple<int, int, string>> GetIntervalsThatOverlapWith(IIntervalTree<Tuple<int, int, string>> tree, int start, int length); | ||
private protected abstract bool HasIntervalThatIntersectsWith(IIntervalTree<Tuple<int, int, string>> tree, int position); | ||
|
||
[Fact] | ||
public void TestEmpty() | ||
{ | ||
foreach (var tree in CreateTrees()) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(0, 1); | ||
var spans = GetIntervalsThatOverlapWith(tree, 0, 1); | ||
|
||
Assert.Empty(spans); | ||
} | ||
|
@@ -46,7 +50,7 @@ public void TestBeforeSpan() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(0, 1); | ||
var spans = GetIntervalsThatOverlapWith(tree, 0, 1); | ||
|
||
Assert.Empty(spans); | ||
} | ||
|
@@ -57,7 +61,7 @@ public void TestAbuttingBeforeSpan() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(0, 5); | ||
var spans = GetIntervalsThatOverlapWith(tree, 0, 5); | ||
|
||
Assert.Empty(spans); | ||
} | ||
|
@@ -68,7 +72,7 @@ public void TestAfterSpan() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(15, 5); | ||
var spans = GetIntervalsThatOverlapWith(tree, 15, 5); | ||
|
||
Assert.Empty(spans); | ||
} | ||
|
@@ -79,7 +83,7 @@ public void TestAbuttingAfterSpan() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(10, 5); | ||
var spans = GetIntervalsThatOverlapWith(tree, 10, 5); | ||
|
||
Assert.Empty(spans); | ||
} | ||
|
@@ -90,7 +94,7 @@ public void TestMatchingSpan() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(5, 5).Select(t => t.Item3); | ||
var spans = GetIntervalsThatOverlapWith(tree, 5, 5).Select(t => t.Item3); | ||
|
||
Assert.True(Set("A").SetEquals(spans)); | ||
} | ||
|
@@ -101,7 +105,7 @@ public void TestContainedAbuttingStart() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(5, 2).Select(i => i.Item3); | ||
var spans = GetIntervalsThatOverlapWith(tree, 5, 2).Select(i => i.Item3); | ||
|
||
Assert.True(Set("A").SetEquals(spans)); | ||
} | ||
|
@@ -112,7 +116,7 @@ public void TestContainedAbuttingEnd() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(8, 2).Select(i => i.Item3); | ||
var spans = GetIntervalsThatOverlapWith(tree, 8, 2).Select(i => i.Item3); | ||
|
||
Assert.True(Set("A").SetEquals(spans)); | ||
} | ||
|
@@ -123,7 +127,7 @@ public void TestCompletedContained() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(7, 2).Select(i => i.Item3); | ||
var spans = GetIntervalsThatOverlapWith(tree, 7, 2).Select(i => i.Item3); | ||
|
||
Assert.True(Set("A").SetEquals(spans)); | ||
} | ||
|
@@ -134,7 +138,7 @@ public void TestOverlappingStart() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(4, 2).Select(i => i.Item3); | ||
var spans = GetIntervalsThatOverlapWith(tree, 4, 2).Select(i => i.Item3); | ||
|
||
Assert.True(Set("A").SetEquals(spans)); | ||
} | ||
|
@@ -145,7 +149,7 @@ public void TestOverlappingEnd() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(9, 2).Select(i => i.Item3); | ||
var spans = GetIntervalsThatOverlapWith(tree, 9, 2).Select(i => i.Item3); | ||
|
||
Assert.True(Set("A").SetEquals(spans)); | ||
} | ||
|
@@ -156,7 +160,7 @@ public void TestOverlappingAll() | |
{ | ||
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"))) | ||
{ | ||
var spans = tree.GetIntervalsThatOverlapWith(4, 7).Select(i => i.Item3); | ||
var spans = GetIntervalsThatOverlapWith(tree, 4, 7).Select(i => i.Item3); | ||
|
||
Assert.True(Set("A").SetEquals(spans)); | ||
} | ||
|
@@ -168,19 +172,19 @@ public void TestNonOverlappingSpans() | |
foreach (var tree in CreateTrees(Tuple.Create(5, 5, "A"), Tuple.Create(15, 5, "B"))) | ||
{ | ||
// Test between the spans | ||
Assert.Empty(tree.GetIntervalsThatOverlapWith(2, 2)); | ||
Assert.Empty(tree.GetIntervalsThatOverlapWith(11, 2)); | ||
Assert.Empty(tree.GetIntervalsThatOverlapWith(22, 2)); | ||
Assert.Empty(GetIntervalsThatOverlapWith(tree, 2, 2)); | ||
Assert.Empty(GetIntervalsThatOverlapWith(tree, 11, 2)); | ||
Assert.Empty(GetIntervalsThatOverlapWith(tree, 22, 2)); | ||
|
||
// Test in the spans | ||
Assert.True(Set("A").SetEquals(tree.GetIntervalsThatOverlapWith(6, 2).Select(i => i.Item3))); | ||
Assert.True(Set("B").SetEquals(tree.GetIntervalsThatOverlapWith(16, 2).Select(i => i.Item3))); | ||
Assert.True(Set("A").SetEquals(GetIntervalsThatOverlapWith(tree, 6, 2).Select(i => i.Item3))); | ||
Assert.True(Set("B").SetEquals(GetIntervalsThatOverlapWith(tree, 16, 2).Select(i => i.Item3))); | ||
|
||
// Test covering both spans | ||
Assert.True(Set("A", "B").SetEquals(tree.GetIntervalsThatOverlapWith(2, 20).Select(i => i.Item3))); | ||
Assert.True(Set("A", "B").SetEquals(tree.GetIntervalsThatOverlapWith(2, 14).Select(i => i.Item3))); | ||
Assert.True(Set("A", "B").SetEquals(tree.GetIntervalsThatOverlapWith(6, 10).Select(i => i.Item3))); | ||
Assert.True(Set("A", "B").SetEquals(tree.GetIntervalsThatOverlapWith(6, 20).Select(i => i.Item3))); | ||
Assert.True(Set("A", "B").SetEquals(GetIntervalsThatOverlapWith(tree, 2, 20).Select(i => i.Item3))); | ||
Assert.True(Set("A", "B").SetEquals(GetIntervalsThatOverlapWith(tree, 2, 14).Select(i => i.Item3))); | ||
Assert.True(Set("A", "B").SetEquals(GetIntervalsThatOverlapWith(tree, 6, 10).Select(i => i.Item3))); | ||
Assert.True(Set("A", "B").SetEquals(GetIntervalsThatOverlapWith(tree, 6, 20).Select(i => i.Item3))); | ||
} | ||
} | ||
|
||
|
@@ -214,11 +218,11 @@ public void TestIntersectsWith() | |
|
||
foreach (var tree in CreateTrees(spans)) | ||
{ | ||
Assert.False(tree.HasIntervalThatIntersectsWith(-1)); | ||
Assert.True(tree.HasIntervalThatIntersectsWith(0)); | ||
Assert.True(tree.HasIntervalThatIntersectsWith(1)); | ||
Assert.True(tree.HasIntervalThatIntersectsWith(2)); | ||
Assert.False(tree.HasIntervalThatIntersectsWith(3)); | ||
Assert.False(HasIntervalThatIntersectsWith(tree, -1)); | ||
Assert.True(HasIntervalThatIntersectsWith(tree, 0)); | ||
Assert.True(HasIntervalThatIntersectsWith(tree, 1)); | ||
Assert.True(HasIntervalThatIntersectsWith(tree, 2)); | ||
Assert.False(HasIntervalThatIntersectsWith(tree, 3)); | ||
} | ||
} | ||
|
||
|
@@ -307,7 +311,7 @@ public void TestSortedEnumerable2() | |
Assert.Equal(tree, new[] { 0, 1 }); | ||
} | ||
|
||
private static void TestOverlapsAndIntersects(IList<Tuple<int, int, string>> spans) | ||
private void TestOverlapsAndIntersects(IList<Tuple<int, int, string>> spans) | ||
{ | ||
foreach (var tree in CreateTrees(spans)) | ||
{ | ||
|
@@ -318,14 +322,14 @@ private static void TestOverlapsAndIntersects(IList<Tuple<int, int, string>> spa | |
{ | ||
var span = new Span(start, length); | ||
|
||
var set1 = new HashSet<string>(tree.GetIntervalsThatOverlapWith(start, length).Select(i => i.Item3)); | ||
var set1 = new HashSet<string>(GetIntervalsThatOverlapWith(tree, start, length).Select(i => i.Item3)); | ||
var set2 = new HashSet<string>(spans.Where(t => | ||
{ | ||
return span.OverlapsWith(new Span(t.Item1, t.Item2)); | ||
}).Select(t => t.Item3)); | ||
Assert.True(set1.SetEquals(set2)); | ||
|
||
var set3 = new HashSet<string>(tree.GetIntervalsThatIntersectWith(start, length).Select(i => i.Item3)); | ||
var set3 = new HashSet<string>(GetIntervalsThatIntersectWith(tree, start, length).Select(i => i.Item3)); | ||
var set4 = new HashSet<string>(spans.Where(t => | ||
{ | ||
return span.IntersectsWith(new Span(t.Item1, t.Item2)); | ||
|
@@ -345,3 +349,67 @@ private static ISet<T> Set<T>(params T[] values) | |
private static IList<T> List<T>(params T[] values) | ||
=> new List<T>(values); | ||
} | ||
|
||
public sealed class BinaryIntervalTreeTests : IntervalTreeTests | ||
{ | ||
private protected override IEnumerable<IIntervalTree<Tuple<int, int, string>>> CreateTrees(IEnumerable<Tuple<int, int, string>> values) | ||
{ | ||
yield return BinaryIntervalTree.Create(new TupleIntrospector<string>(), values); | ||
} | ||
|
||
private protected override bool HasIntervalThatIntersectsWith(IIntervalTree<Tuple<int, int, string>> tree, int position) | ||
{ | ||
return ((BinaryIntervalTree<Tuple<int, int, string>>)tree).Algorithms.HasIntervalThatIntersectsWith(position, new TupleIntrospector<string>()); | ||
} | ||
|
||
private protected override ImmutableArray<Tuple<int, int, string>> GetIntervalsThatIntersectWith(IIntervalTree<Tuple<int, int, string>> tree, int start, int length) | ||
{ | ||
return ((BinaryIntervalTree<Tuple<int, int, string>>)tree).Algorithms.GetIntervalsThatIntersectWith(start, length, new TupleIntrospector<string>()); | ||
} | ||
|
||
private protected override ImmutableArray<Tuple<int, int, string>> GetIntervalsThatOverlapWith(IIntervalTree<Tuple<int, int, string>> tree, int start, int length) | ||
{ | ||
return ((BinaryIntervalTree<Tuple<int, int, string>>)tree).Algorithms.GetIntervalsThatOverlapWith(start, length, new TupleIntrospector<string>()); | ||
} | ||
} | ||
|
||
public sealed class FlatArrayIntervalTreeTests : IntervalTreeTests | ||
{ | ||
private protected override IEnumerable<IIntervalTree<Tuple<int, int, string>>> CreateTrees(IEnumerable<Tuple<int, int, string>> values) | ||
{ | ||
yield return FlatArrayIntervalTree<Tuple<int, int, string>>.CreateFromUnsorted(new TupleIntrospector<string>(), new SegmentedList<Tuple<int, int, string>>(values)); | ||
} | ||
|
||
private protected override bool HasIntervalThatIntersectsWith(IIntervalTree<Tuple<int, int, string>> tree, int position) | ||
{ | ||
return ((FlatArrayIntervalTree<Tuple<int, int, string>>)tree).Algorithms.HasIntervalThatIntersectsWith(position, new TupleIntrospector<string>()); | ||
} | ||
|
||
private protected override ImmutableArray<Tuple<int, int, string>> GetIntervalsThatIntersectWith(IIntervalTree<Tuple<int, int, string>> tree, int start, int length) | ||
{ | ||
return ((FlatArrayIntervalTree<Tuple<int, int, string>>)tree).Algorithms.GetIntervalsThatIntersectWith(start, length, new TupleIntrospector<string>()); | ||
} | ||
|
||
private protected override ImmutableArray<Tuple<int, int, string>> GetIntervalsThatOverlapWith(IIntervalTree<Tuple<int, int, string>> tree, int start, int length) | ||
{ | ||
return ((FlatArrayIntervalTree<Tuple<int, int, string>>)tree).Algorithms.GetIntervalsThatOverlapWith(start, length, new TupleIntrospector<string>()); | ||
} | ||
|
||
private readonly struct Int32IntervalIntrospector : IIntervalIntrospector<int> | ||
{ | ||
public TextSpan GetSpan(int value) | ||
=> new(value, 0); | ||
} | ||
|
||
[Fact] | ||
public void TestProperBalancing() | ||
{ | ||
for (var i = 0; i < 3000; i++) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hammers a ton of configurations of interval trees, ensuring that our creation of the complete binary tree is correct. |
||
{ | ||
var tree = FlatArrayIntervalTree<int>.CreateFromUnsorted(new Int32IntervalIntrospector(), new(Enumerable.Range(1, i))); | ||
|
||
// Ensure that the tree produces the same elements in sorted order. | ||
AssertEx.Equal(tree, Enumerable.Range(1, i)); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -297,7 +297,7 @@ static string Indent( | |
{ | ||
var line = text.Lines[i]; | ||
|
||
if (restrictedSpans.HasIntervalThatIntersectsWith(line.Start)) | ||
if (restrictedSpans.Algorithms.HasIntervalThatIntersectsWith(line.Start, new TextSpanIntervalIntrospector())) | ||
{ | ||
// Inside something we must not touch. Include the line verbatim. | ||
AppendFullLine(builder, line); | ||
|
@@ -475,7 +475,7 @@ private static string GetIndentationStringForPosition(SourceText text, SyntaxFor | |
private static void AppendFullLine(StringBuilder builder, TextLine line) | ||
=> builder.Append(line.Text!.ToString(line.SpanIncludingLineBreak)); | ||
|
||
private static (TextSpanIntervalTree interpolationInteriorSpans, TextSpanIntervalTree restrictedSpans) GetInterpolationSpans( | ||
private static (FlatArrayIntervalTree<TextSpan> interpolationInteriorSpans, FlatArrayIntervalTree<TextSpan> restrictedSpans) GetInterpolationSpans( | ||
InterpolatedStringExpressionSyntax stringExpression, CancellationToken cancellationToken) | ||
{ | ||
var interpolationInteriorSpans = new SegmentedList<TextSpan>(); | ||
|
@@ -514,7 +514,9 @@ private static (TextSpanIntervalTree interpolationInteriorSpans, TextSpanInterva | |
} | ||
} | ||
|
||
return (new TextSpanIntervalTree(interpolationInteriorSpans), new TextSpanIntervalTree(restrictedSpans)); | ||
return ( | ||
FlatArrayIntervalTree<TextSpan>.CreateFromUnsorted(new TextSpanIntervalIntrospector(), interpolationInteriorSpans), | ||
FlatArrayIntervalTree<TextSpan>.CreateFromUnsorted(new TextSpanIntervalIntrospector(), restrictedSpans)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note; i'm usnig CreateFromUnsorted here as i don't want to have to prove yet if the above algorithm produces a sorted list of elements. |
||
} | ||
|
||
private static InterpolatedStringExpressionSyntax CleanInterpolatedString( | ||
|
@@ -560,7 +562,7 @@ private static InterpolatedStringExpressionSyntax CleanInterpolatedString( | |
// ignore any blank lines we see. | ||
var line = lines[i]; | ||
|
||
if (restrictedSpans.HasIntervalThatIntersectsWith(line.Start)) | ||
if (restrictedSpans.Algorithms.HasIntervalThatIntersectsWith(line.Start, new TextSpanIntervalIntrospector())) | ||
{ | ||
// Inside something we must not touch. Include the line verbatim. | ||
AppendFullLine(builder, line); | ||
|
@@ -628,7 +630,7 @@ private static InterpolatedStringExpressionSyntax CleanInterpolatedString( | |
|
||
private static string ComputeCommonWhitespacePrefix( | ||
ArrayBuilder<TextLine> lines, | ||
TextSpanIntervalTree interpolationInteriorSpans) | ||
FlatArrayIntervalTree<TextSpan> interpolationInteriorSpans) | ||
{ | ||
string? commonLeadingWhitespace = null; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this allows me to run the same tests on both impls of interval trees. note: this test code is not pretty. I didn't want to spend a lot of time figuring out how to make it pretty.