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

Interval tree refactorings. #73846

Merged
merged 20 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
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 IntervalTree<TagSpan<TTag>> _tree = IntervalTree<TagSpan<TTag>>.Empty;
private readonly BinaryIntervalTree<TagSpan<TTag>> _tree = BinaryIntervalTree<TagSpan<TTag>>.Empty;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renaming to add 'Binary' for clarity here. The upcoming interval tree will probably be called FlatArrayIntervalTree or something like that.


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 = IntervalTree<TagSpan<TTag>>.CreateFromSorted(
_tree = BinaryIntervalTree<TagSpan<TTag>>.CreateFromSorted(
new IntervalIntrospector(textSnapshot, trackingMode), values);
}

Expand All @@ -57,10 +57,10 @@ private static TagSpan<TTag> GetTranslatedTagSpan(TagSpan<TTag> originalTagSpan,
: new(GetTranslatedSpan(originalTagSpan, textSnapshot, trackingMode), originalTagSpan.Tag);

public bool HasSpanThatContains(SnapshotPoint point)
=> _tree.HasIntervalThatContains(point.Position, length: 0, new IntervalIntrospector(point.Snapshot, _spanTrackingMode));
=> _tree.Algorithms.HasIntervalThatContains(point.Position, length: 0, new IntervalIntrospector(point.Snapshot, _spanTrackingMode));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these helpers could not be extensions for two reasons:

  1. the types themselves have some existing intance methods that have hte same name. and we never look up extensions if we find a matching instance name (even if it fails).
  2. The extensions were on a TIntervalTree so that they can operate without calling through an interface once instantiated. but the inference required there is beyond what C# can do today. So i used the pattern of asking the instance for a wrapper struct that itself then exposes the operations. The property handles the initial part of generic inference, meaning that the only inference has to happen on the TIntrospector type, which works fine.


public bool HasSpanThatIntersects(SnapshotPoint point)
=> _tree.HasIntervalThatIntersectsWith(point.Position, new IntervalIntrospector(point.Snapshot, _spanTrackingMode));
=> _tree.Algorithms.HasIntervalThatIntersectsWith(point.Position, new IntervalIntrospector(point.Snapshot, _spanTrackingMode));

/// <summary>
/// Gets all the spans that intersect with <paramref name="snapshotSpan"/> in sorted order and adds them to
Expand All @@ -72,7 +72,7 @@ public void AddIntersectingTagSpans(SnapshotSpan snapshotSpan, SegmentedList<Tag
var snapshot = snapshotSpan.Snapshot;

using var intersectingIntervals = TemporaryArray<TagSpan<TTag>>.Empty;
_tree.FillWithIntervalsThatIntersectWith(
_tree.Algorithms.FillWithIntervalsThatIntersectWith(
snapshotSpan.Start, snapshotSpan.Length,
ref intersectingIntervals.AsRef(),
new IntervalIntrospector(snapshot, _spanTrackingMode));
Expand Down Expand Up @@ -132,7 +132,7 @@ public void RemoveIntersectingTagSpans(
buffer.Clear();

var textSnapshot = snapshotSpan.Snapshot;
_tree.FillWithIntervalsThatIntersectWith(
_tree.Algorithms.FillWithIntervalsThatIntersectWith(
snapshotSpan.Span.Start,
snapshotSpan.Span.Length,
ref buffer.AsRef(),
Expand Down
12 changes: 6 additions & 6 deletions src/EditorFeatures/Test/Collections/IntervalTreeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ public TextSpan GetSpan(Tuple<int, int, T> value)
=> new(value.Item1, value.Item2);
}

private static IEnumerable<SimpleIntervalTree<Tuple<int, int, string>, TupleIntrospector<string>>> CreateTrees(params Tuple<int, int, string>[] values)
private static IEnumerable<SimpleBinaryIntervalTree<Tuple<int, int, string>, TupleIntrospector<string>>> CreateTrees(params Tuple<int, int, string>[] values)
=> CreateTrees((IEnumerable<Tuple<int, int, string>>)values);

private static IEnumerable<SimpleIntervalTree<Tuple<int, int, string>, TupleIntrospector<string>>> 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 SimpleIntervalTree.Create(new TupleIntrospector<string>(), values);
yield return BinaryIntervalTree.Create(new TupleIntrospector<string>(), values);
}

[Fact]
Expand Down Expand Up @@ -262,8 +262,8 @@ public TextSpan GetSpan(int value)
=> new(value, 0);
}

private static IntervalTree<int> CreateIntTree(params int[] values)
=> IntervalTree<int>.Create(new Int32Introspector(), values);
private static BinaryIntervalTree<int> CreateIntTree(params int[] values)
=> BinaryIntervalTree<int>.Create(new Int32Introspector(), values);

[Fact]
public void TestSortedEnumerable1()
Expand Down Expand Up @@ -302,7 +302,7 @@ public void TestSortedEnumerable1()
[Fact]
public void TestSortedEnumerable2()
{
var tree = IntervalTree<int>.Create(new Int32Introspector(), new[] { 1, 0 });
var tree = BinaryIntervalTree<int>.Create(new Int32Introspector(), new[] { 1, 0 });

Assert.Equal(tree, new[] { 0, 1 });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -477,15 +478,15 @@ private static void AppendFullLine(StringBuilder builder, TextLine line)
private static (TextSpanIntervalTree interpolationInteriorSpans, TextSpanIntervalTree restrictedSpans) GetInterpolationSpans(
InterpolatedStringExpressionSyntax stringExpression, CancellationToken cancellationToken)
{
var interpolationInteriorSpans = new TextSpanIntervalTree();
var restrictedSpans = new TextSpanIntervalTree();
var interpolationInteriorSpans = new SegmentedList<TextSpan>();
var restrictedSpans = new SegmentedList<TextSpan>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving to tree construction at end, instead of as we go along.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tree construction at end. this will move to the FlatArrayIntervalTree in next pr.


SourceText? text = null;
foreach (var content in stringExpression.Contents)
{
if (content is InterpolationSyntax interpolation)
{
interpolationInteriorSpans.AddIntervalInPlace(TextSpan.FromBounds(interpolation.OpenBraceToken.Span.End, interpolation.CloseBraceToken.Span.Start));
interpolationInteriorSpans.Add(TextSpan.FromBounds(interpolation.OpenBraceToken.Span.End, interpolation.CloseBraceToken.Span.Start));

// We don't want to touch any nested strings within us, mark them as off limits. note, we only care if
// the nested strings actually span multiple lines. A nested string on a single line is safe to move
Expand All @@ -506,14 +507,14 @@ private static (TextSpanIntervalTree interpolationInteriorSpans, TextSpanInterva
var start = startLine.GetFirstNonWhitespacePosition() == descendantSpan.Start
? startLine.Start
: descendantSpan.Start;
restrictedSpans.AddIntervalInPlace(TextSpan.FromBounds(start, descendantSpan.End));
restrictedSpans.Add(TextSpan.FromBounds(start, descendantSpan.End));
}
}
}
}
}

return (interpolationInteriorSpans, restrictedSpans);
return (new TextSpanIntervalTree(interpolationInteriorSpans), new TextSpanIntervalTree(restrictedSpans));
}

private static InterpolatedStringExpressionSyntax CleanInterpolatedString(
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 IntervalTree<DocumentSymbolDataViewModel> ViewModelItemsTree;
public readonly BinaryIntervalTree<DocumentSymbolDataViewModel> ViewModelItemsTree;

public DocumentOutlineViewState(
ITextSnapshot textSnapshot,
string searchText,
ImmutableArray<DocumentSymbolDataViewModel> viewModelItems,
IntervalTree<DocumentSymbolDataViewModel> viewModelItemsTree)
BinaryIntervalTree<DocumentSymbolDataViewModel> viewModelItemsTree)
{
TextSnapshot = textSnapshot;
SearchText = searchText;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Threading.Tasks;
using System.Windows;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
Expand All @@ -19,6 +20,7 @@
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Utilities;
using Microsoft.VisualStudio.LanguageServer.Client;
using Microsoft.VisualStudio.LanguageServices.Utilities;
using Microsoft.VisualStudio.Text;
Expand Down Expand Up @@ -318,10 +320,11 @@ private async ValueTask ComputeViewStateAsync(CancellationToken cancellationToke
newItems: newViewModelItems);
}

// Now create an interval tree out of the view models. This will allow us to easily find the intersecting
// view models given any position in the file with any particular text snapshot.
var intervalTree = SimpleIntervalTree.Create(new IntervalIntrospector(), Array.Empty<DocumentSymbolDataViewModel>());
AddToIntervalTree(newViewModelItems);
// Now create an interval tree out of the view models. This will allow us to easily find the intersecting view
// 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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tree construction at end. this will move to the FlatArrayIntervalTree in next pr.


var newViewState = new DocumentOutlineViewState(
newTextSnapshot,
Expand All @@ -348,12 +351,12 @@ private async ValueTask ComputeViewStateAsync(CancellationToken cancellationToke

return;

void AddToIntervalTree(ImmutableArray<DocumentSymbolDataViewModel> viewModels)
static void AddAllModels(ImmutableArray<DocumentSymbolDataViewModel> viewModels, SegmentedList<DocumentSymbolDataViewModel> result)
{
foreach (var model in viewModels)
{
intervalTree.AddIntervalInPlace(model);
AddToIntervalTree(model.Children);
result.Equals(model);
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
AddAllModels(model.Children, result);
}
}

Expand Down Expand Up @@ -462,7 +465,7 @@ private async ValueTask UpdateSelectionAsync(CancellationToken cancellationToken
{
// Treat the caret as if it has length 1. That way if it is in between two items, it will naturally
// only intersect right the item on the right of it.
var overlappingModels = modelTree.GetIntervalsThatOverlapWith(
var overlappingModels = modelTree.Algorithms.GetIntervalsThatOverlapWith(
caretPosition.Position, 1, new IntervalIntrospector());

if (overlappingModels.Length == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ internal sealed partial class InheritanceGlyphManager : IDisposable
private readonly IEditorFormatMap _editorFormatMap;
private readonly IAsynchronousOperationListener _listener;
private readonly Canvas _glyphsContainer;
private readonly SimpleIntervalTree<GlyphData, GlyphDataIntrospector> _glyphDataTree;

/// <summary>
/// Mutable. Must only be accessed from the UI thread.
/// </summary>
private SimpleBinaryIntervalTree<GlyphData, GlyphDataIntrospector> _glyphDataTree;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tree construction at end. this will move to the FlatArrayIntervalTree in next pr.


public InheritanceGlyphManager(
Workspace workspace,
Expand Down Expand Up @@ -71,7 +75,7 @@ public InheritanceGlyphManager(
_heightAndWidthOfTheGlyph = heightAndWidthOfTheGlyph;
_editorFormatMap.FormatMappingChanged += FormatMappingChanged;

_glyphDataTree = new SimpleIntervalTree<GlyphData, GlyphDataIntrospector>(new GlyphDataIntrospector(), values: null);
_glyphDataTree = new(new GlyphDataIntrospector(), values: null);
UpdateBackgroundColor();
}

Expand Down Expand Up @@ -111,7 +115,7 @@ public void RemoveGlyphs(SnapshotSpan snapshotSpan)
}

var remainingGlyphData = _glyphDataTree.Except(glyphDataToRemove).ToImmutableArray();
_glyphDataTree.ClearInPlace();
_glyphDataTree = new(new GlyphDataIntrospector(), values: null);
foreach (var glyphData in remainingGlyphData)
{
_glyphDataTree.AddIntervalInPlace(glyphData);
Expand All @@ -130,7 +134,7 @@ public void SetSnapshotAndUpdate(ITextSnapshot snapshot, IList<ITextViewLine> ne
{
// Go through all the existing visuals and invalidate or transform as appropriate.
var allGlyphData = _glyphDataTree.ToImmutableArray();
_glyphDataTree.ClearInPlace();
_glyphDataTree = new(new GlyphDataIntrospector(), values: null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new GlyphDataIntrospector(

Does this object hold state? If not, would it be worth only having one of these?

foreach (var (span, glyph) in allGlyphData)
{
var newSpan = span.TranslateTo(snapshot, SpanTrackingMode.EdgeInclusive);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Utilities;

namespace Microsoft.CodeAnalysis.Classification;

Expand Down Expand Up @@ -238,18 +239,20 @@ public static void MergeParts<TClassifiedSpan, TClassifiedSpanIntervalIntrospect
{
// Create an interval tree so we can easily determine which semantic parts intersect with the
// syntactic parts we're looking at.
var semanticPartsTree = new SimpleIntervalTree<TClassifiedSpan, TClassifiedSpanIntervalIntrospector>(default, values: null);
using var _1 = SegmentedListPool.GetPooledList<TClassifiedSpan>(out var semanticSpans);

// Add all the non-empty semantic parts to the tree.
foreach (var part in semanticParts)
{
if (!getSpan(part).IsEmpty)
{
semanticPartsTree.AddIntervalInPlace(part);
semanticSpans.Add(part);
finalParts.Add(part);
}
}

var semanticPartsTree = new SimpleBinaryIntervalTree<TClassifiedSpan, TClassifiedSpanIntervalIntrospector>(default, values: semanticSpans);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tree construction at end. this will move to the FlatArrayIntervalTree in next pr.


using var tempBuffer = TemporaryArray<TClassifiedSpan>.Empty;

foreach (var syntacticPart in syntaxParts)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ private static ImmutableArray<TextSpan> GetNonOverlappingSpans(
SyntaxNode root, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken)
{
// Create interval tree for spans
var intervalTree = SimpleIntervalTree.Create(new TextSpanIntervalIntrospector(), spans);
var intervalTree = BinaryIntervalTree.Create(new TextSpanIntervalIntrospector(), spans);

// Find tokens that are outside of spans
var tokenSpans = new List<TextSpan>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public TextSpan GetSpan(TextChange value)
private readonly Document _oldDocument;
private readonly IDocumentTextDifferencingService _differenceService;

private readonly SimpleIntervalTree<TextChange, IntervalIntrospector> _totalChangesIntervalTree =
SimpleIntervalTree.Create(new IntervalIntrospector(), Array.Empty<TextChange>());
private readonly SimpleBinaryIntervalTree<TextChange, IntervalIntrospector> _totalChangesIntervalTree =
BinaryIntervalTree.Create(new IntervalIntrospector(), Array.Empty<TextChange>());

public TextChangeMerger(Document document)
{
Expand Down Expand Up @@ -78,7 +78,7 @@ public async Task<SourceText> GetFinalMergedTextAsync(CancellationToken cancella
}

private static bool AllChangesCanBeApplied(
SimpleIntervalTree<TextChange, IntervalIntrospector> cumulativeChanges,
SimpleBinaryIntervalTree<TextChange, IntervalIntrospector> cumulativeChanges,
ImmutableArray<TextChange> currentChanges)
{
using var overlappingSpans = TemporaryArray<TextChange>.Empty;
Expand All @@ -91,7 +91,7 @@ private static bool AllChangesCanBeApplied(
}

private static bool AllChangesCanBeApplied(
SimpleIntervalTree<TextChange, IntervalIntrospector> cumulativeChanges,
SimpleBinaryIntervalTree<TextChange, IntervalIntrospector> cumulativeChanges,
ImmutableArray<TextChange> currentChanges,
ref TemporaryArray<TextChange> overlappingSpans,
ref TemporaryArray<TextChange> intersectingSpans)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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.

namespace Microsoft.CodeAnalysis.Shared.Collections;

/// <summary>
/// Generic function representing the type of interval testing operation that can be performed on an interval tree. For
/// example checking if an interval 'contains', 'intersects', or 'overlaps' with a requested span.
/// </summary>
internal delegate bool TestInterval<T, TIntrospector>(T value, int start, int length, in TIntrospector introspector)
where TIntrospector : struct, IIntervalIntrospector<T>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new core types taht power the BinaryIntervalTree, and will power the upcoming FlatArrayIntervalTree.


/// <summary>
/// Base interface all interval trees need to implement to get full functionality. Callers are not expected to use
/// these methods directly. Instead, they are the low level building blocks that the higher level extension methods are
/// built upon. Consumers of an interface tree should use <c>.Algorithms</c> on the instance to get access to a wealth
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
/// of fast operations through the <see cref="IntervalTreeAlgorithms{T, TIntervalTree}"/> type.
/// </summary>
internal interface IIntervalTree<T>
{
/// <summary>
/// Adds all intervals within the tree within the given start/length pair that match the given <paramref
/// name="testInterval"/> predicate. Results are added to the <paramref name="builder"/> array. The <paramref
/// name="stopAfterFirst"/> indicates if the search should stop after the first interval is found. Results will be
/// returned in a sorted order based on the start point of the interval.
/// </summary>
/// <returns>The number of matching intervals found by the method.</returns>
int FillWithIntervalsThatMatch<TIntrospector>(
int start, int length, TestInterval<T, TIntrospector> testInterval,
ref TemporaryArray<T> builder, in TIntrospector introspector,
bool stopAfterFirst)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool stopAfterFirst

maybe use a max find count?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keeping the same logic for now. will change after flag goes in.

where TIntrospector : struct, IIntervalIntrospector<T>;

/// <summary>
/// Practically equivalent to <see cref="FillWithIntervalsThatMatch{TIntrospector}"/> with a check that at least one
/// item was found. However, separated out as a separate method as implementations can often be more efficient just
/// answering this question, versus the more complex "full with intervals" question above.
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
bool Any<TIntrospector>(int start, int length, TestInterval<T, TIntrospector> testInterval, in TIntrospector introspector)
where TIntrospector : struct, IIntervalIntrospector<T>;
}
Loading
Loading