Skip to content
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

Merged
merged 32 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9ad83d4
in progress
CyrusNajmabadi Jun 4, 2024
bd57ecb
Fill in the flat interval tree
CyrusNajmabadi Jun 4, 2024
1cb5d9e
Merge branch 'flatIntervalTree' into flatIntervalTree2
CyrusNajmabadi Jun 4, 2024
90bee92
in progress
CyrusNajmabadi Jun 4, 2024
1203922
Impl of any
CyrusNajmabadi Jun 4, 2024
31fd6cc
fill
CyrusNajmabadi Jun 4, 2024
5c246ae
fill
CyrusNajmabadi Jun 4, 2024
a1f7e22
Clean
CyrusNajmabadi Jun 4, 2024
54ab6b9
make iterable
CyrusNajmabadi Jun 4, 2024
543fb5e
Tests
CyrusNajmabadi Jun 4, 2024
a8f9b5e
Tests
CyrusNajmabadi Jun 4, 2024
a9ae4c5
Merge remote-tracking branch 'upstream/main' into flatIntervalTree2
CyrusNajmabadi Jun 5, 2024
cbc0932
Proper balanced tree
CyrusNajmabadi Jun 5, 2024
0424947
Fix dest indices
CyrusNajmabadi Jun 5, 2024
91b2e23
Fix and delete
CyrusNajmabadi Jun 5, 2024
773759f
Docs
CyrusNajmabadi Jun 5, 2024
79323da
Move to file
CyrusNajmabadi Jun 5, 2024
98497fd
docs
CyrusNajmabadi Jun 5, 2024
f18dd54
docs
CyrusNajmabadi Jun 5, 2024
b606bcf
Docs
CyrusNajmabadi Jun 5, 2024
878d6cb
docs
CyrusNajmabadi Jun 5, 2024
fb102df
usings
CyrusNajmabadi Jun 5, 2024
6ac3c90
iuse in another case
CyrusNajmabadi Jun 5, 2024
3e0e2d3
iuse in another case
CyrusNajmabadi Jun 5, 2024
790e5a8
iuse in another case
CyrusNajmabadi Jun 5, 2024
dc3d9ab
Update docs
CyrusNajmabadi Jun 5, 2024
4e22601
remove
CyrusNajmabadi Jun 5, 2024
8e147ff
Integral ops
CyrusNajmabadi Jun 5, 2024
831f599
Merge remote-tracking branch 'upstream/main' into flatIntervalTree2
CyrusNajmabadi Jun 5, 2024
3b3d05a
move tests
CyrusNajmabadi Jun 5, 2024
3c6632c
Docs
CyrusNajmabadi Jun 5, 2024
aa59cbe
Remove tes
CyrusNajmabadi Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal sealed partial class TagSpanIntervalTree<TTag>(SpanTrackingMode spanTra
public static readonly TagSpanIntervalTree<TTag> Empty = new(SpanTrackingMode.EdgeInclusive);

private readonly SpanTrackingMode _spanTrackingMode = spanTrackingMode;
private readonly BinaryIntervalTree<TagSpan<TTag>> _tree = BinaryIntervalTree<TagSpan<TTag>>.Empty;
private readonly FlatArrayIntervalTree<TagSpan<TTag>> _tree = FlatArrayIntervalTree<TagSpan<TTag>>.Empty;

public TagSpanIntervalTree(
ITextSnapshot textSnapshot,
Expand All @@ -39,7 +39,7 @@ public TagSpanIntervalTree(
// routines), and allows us to build the balanced tree directly without having to do any additional work.
values.Sort(static (t1, t2) => t1.Span.Start.Position - t2.Span.Start.Position);

_tree = BinaryIntervalTree<TagSpan<TTag>>.CreateFromSorted(
_tree = FlatArrayIntervalTree<TagSpan<TTag>>.CreateFromSorted(
new IntervalIntrospector(textSnapshot, trackingMode), values);
}

Expand Down
140 changes: 104 additions & 36 deletions src/EditorFeatures/Test/Collections/IntervalTreeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

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.

{
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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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));
}
Expand All @@ -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));
}
Expand All @@ -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));
}
Expand All @@ -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));
}
Expand All @@ -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));
}
Expand All @@ -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));
}
Expand All @@ -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));
}
Expand All @@ -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)));
}
}

Expand Down Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -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))
{
Expand All @@ -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));
Expand All @@ -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++)
Copy link
Member Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -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);
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -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));
Copy link
Member Author

Choose a reason for hiding this comment

The 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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -628,7 +630,7 @@ private static InterpolatedStringExpressionSyntax CleanInterpolatedString(

private static string ComputeCommonWhitespacePrefix(
ArrayBuilder<TextLine> lines,
TextSpanIntervalTree interpolationInteriorSpans)
FlatArrayIntervalTree<TextSpan> interpolationInteriorSpans)
{
string? commonLeadingWhitespace = null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ private sealed class DocumentOutlineViewState
/// Interval-tree view over <see cref="ViewModelItems"/> so that we can quickly determine which of them
/// intersect with a particular position in the document.
/// </summary>
public readonly BinaryIntervalTree<DocumentSymbolDataViewModel> ViewModelItemsTree;
public readonly FlatArrayIntervalTree<DocumentSymbolDataViewModel> ViewModelItemsTree;

public DocumentOutlineViewState(
ITextSnapshot textSnapshot,
string searchText,
ImmutableArray<DocumentSymbolDataViewModel> viewModelItems,
BinaryIntervalTree<DocumentSymbolDataViewModel> viewModelItemsTree)
FlatArrayIntervalTree<DocumentSymbolDataViewModel> viewModelItemsTree)
{
TextSnapshot = textSnapshot;
SearchText = searchText;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ private async ValueTask ComputeViewStateAsync(CancellationToken cancellationToke
// models given any position in the file with any particular text snapshot.
using var _ = SegmentedListPool.GetPooledList<DocumentSymbolDataViewModel>(out var models);
AddAllModels(newViewModelItems, models);
var intervalTree = BinaryIntervalTree.Create(new IntervalIntrospector(), models);
var intervalTree = FlatArrayIntervalTree<DocumentSymbolDataViewModel>.CreateFromUnsorted(
new IntervalIntrospector(), models);

var newViewState = new DocumentOutlineViewState(
newTextSnapshot,
Expand Down
Loading
Loading