1122 lines
33 KiB
C#
1122 lines
33 KiB
C#
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
// software and associated documentation files (the "Software"), to deal in the Software
|
|
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
// to whom the Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all copies or
|
|
// substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
using Unity.CodeEditor.Document;
|
|
using Unity.CodeEditor.Utils;
|
|
|
|
namespace Unity.CodeEditor.Rendering
|
|
{
|
|
/// <summary>
|
|
/// Red-black tree similar to DocumentLineTree, augmented with collapsing and height data.
|
|
/// </summary>
|
|
internal sealed class HeightTree : ILineTracker, IDisposable
|
|
{
|
|
// TODO: Optimize this. This tree takes alot of memory.
|
|
// (56 bytes for HeightTreeNode
|
|
// We should try to get rid of the dictionary and find height nodes per index. (DONE!)
|
|
// And we might do much better by compressing lines with the same height into a single node.
|
|
// That would also improve load times because we would always start with just a single node.
|
|
|
|
/* Idea:
|
|
class NewHeightTreeNode {
|
|
int totalCount; // =count+left.count+right.count
|
|
int count; // one node can represent multiple lines
|
|
double height; // height of each line in this node
|
|
double totalHeight; // =(collapsedSections!=null?0:height*count) + left.totalHeight + right.totalHeight
|
|
List<CollapsedSection> collapsedSections; // sections holding this line collapsed
|
|
// no "nodeCollapsedSections"/"totalCollapsedSections":
|
|
NewHeightTreeNode left, right, parent;
|
|
bool color;
|
|
}
|
|
totalCollapsedSections: are hard to update and not worth the effort. O(n log n) isn't too bad for
|
|
collapsing/uncollapsing, especially when compression reduces the n.
|
|
*/
|
|
|
|
#region Constructor
|
|
|
|
private readonly TextDocument _document;
|
|
private HeightTreeNode _root;
|
|
private WeakLineTracker _weakLineTracker;
|
|
|
|
internal HeightTree(TextDocument document, float defaultLineHeight)
|
|
{
|
|
this._document = document;
|
|
_weakLineTracker = WeakLineTracker.Register(document, this);
|
|
this.DefaultLineHeight = defaultLineHeight;
|
|
RebuildDocument();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_weakLineTracker != null)
|
|
_weakLineTracker.Deregister();
|
|
this._root = null;
|
|
this._weakLineTracker = null;
|
|
}
|
|
|
|
internal bool IsDisposed {
|
|
get {
|
|
return _root == null;
|
|
}
|
|
}
|
|
|
|
private float _defaultLineHeight;
|
|
|
|
internal float DefaultLineHeight {
|
|
get { return _defaultLineHeight; }
|
|
set {
|
|
var oldValue = _defaultLineHeight;
|
|
if (oldValue == value)
|
|
return;
|
|
_defaultLineHeight = value;
|
|
// update the stored value in all nodes:
|
|
foreach (var node in AllNodes) {
|
|
if (node.LineNode.Height == oldValue) {
|
|
node.LineNode.Height = value;
|
|
UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private HeightTreeNode GetNode(DocumentLine ls)
|
|
{
|
|
return GetNodeByIndex(ls.LineNumber - 1);
|
|
}
|
|
#endregion
|
|
|
|
#region RebuildDocument
|
|
void ILineTracker.ChangeComplete(DocumentChangeEventArgs e)
|
|
{
|
|
}
|
|
|
|
void ILineTracker.SetLineLength(DocumentLine ls, int newTotalLength)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rebuild the tree, in O(n).
|
|
/// </summary>
|
|
public void RebuildDocument()
|
|
{
|
|
foreach (var s in GetAllCollapsedSections()) {
|
|
s.Start = null;
|
|
s.End = null;
|
|
}
|
|
|
|
var nodes = new HeightTreeNode[_document.LineCount];
|
|
var lineNumber = 0;
|
|
foreach (var ls in _document.Lines) {
|
|
nodes[lineNumber++] = new HeightTreeNode(ls, _defaultLineHeight);
|
|
}
|
|
Debug.Assert(nodes.Length > 0);
|
|
// now build the corresponding balanced tree
|
|
var height = DocumentLineTree.GetTreeHeight(nodes.Length);
|
|
Debug.WriteLine("HeightTree will have height: " + height);
|
|
_root = BuildTree(nodes, 0, nodes.Length, height);
|
|
_root.Color = Black;
|
|
#if DEBUG
|
|
CheckProperties();
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// build a tree from a list of nodes
|
|
/// </summary>
|
|
private HeightTreeNode BuildTree(HeightTreeNode[] nodes, int start, int end, int subtreeHeight)
|
|
{
|
|
Debug.Assert(start <= end);
|
|
if (start == end) {
|
|
return null;
|
|
}
|
|
var middle = (start + end) / 2;
|
|
var node = nodes[middle];
|
|
node.Left = BuildTree(nodes, start, middle, subtreeHeight - 1);
|
|
node.Right = BuildTree(nodes, middle + 1, end, subtreeHeight - 1);
|
|
if (node.Left != null) node.Left.Parent = node;
|
|
if (node.Right != null) node.Right.Parent = node;
|
|
if (subtreeHeight == 1)
|
|
node.Color = Red;
|
|
UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.None);
|
|
return node;
|
|
}
|
|
#endregion
|
|
|
|
#region Insert/Remove lines
|
|
void ILineTracker.BeforeRemoveLine(DocumentLine line)
|
|
{
|
|
var node = GetNode(line);
|
|
if (node.LineNode.CollapsedSections != null) {
|
|
foreach (var cs in node.LineNode.CollapsedSections.ToArray()) {
|
|
if (cs.Start == line && cs.End == line) {
|
|
cs.Start = null;
|
|
cs.End = null;
|
|
} else if (cs.Start == line) {
|
|
Uncollapse(cs);
|
|
cs.Start = line.NextLine;
|
|
AddCollapsedSection(cs, cs.End.LineNumber - cs.Start.LineNumber + 1);
|
|
} else if (cs.End == line) {
|
|
Uncollapse(cs);
|
|
cs.End = line.PreviousLine;
|
|
AddCollapsedSection(cs, cs.End.LineNumber - cs.Start.LineNumber + 1);
|
|
}
|
|
}
|
|
}
|
|
BeginRemoval();
|
|
RemoveNode(node);
|
|
// clear collapsedSections from removed line: prevent damage if removed line is in "nodesToCheckForMerging"
|
|
node.LineNode.CollapsedSections = null;
|
|
EndRemoval();
|
|
}
|
|
|
|
// void ILineTracker.AfterRemoveLine(DocumentLine line)
|
|
// {
|
|
//
|
|
// }
|
|
|
|
void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine)
|
|
{
|
|
InsertAfter(GetNode(insertionPos), newLine);
|
|
#if DEBUG
|
|
CheckProperties();
|
|
#endif
|
|
}
|
|
|
|
private HeightTreeNode InsertAfter(HeightTreeNode node, DocumentLine newLine)
|
|
{
|
|
var newNode = new HeightTreeNode(newLine, _defaultLineHeight);
|
|
if (node.Right == null) {
|
|
if (node.LineNode.CollapsedSections != null) {
|
|
// we are inserting directly after node - so copy all collapsedSections
|
|
// that do not end at node.
|
|
foreach (var cs in node.LineNode.CollapsedSections) {
|
|
if (cs.End != node.DocumentLine)
|
|
newNode.AddDirectlyCollapsed(cs);
|
|
}
|
|
}
|
|
InsertAsRight(node, newNode);
|
|
} else {
|
|
node = node.Right.LeftMost;
|
|
if (node.LineNode.CollapsedSections != null) {
|
|
// we are inserting directly before node - so copy all collapsedSections
|
|
// that do not start at node.
|
|
foreach (var cs in node.LineNode.CollapsedSections) {
|
|
if (cs.Start != node.DocumentLine)
|
|
newNode.AddDirectlyCollapsed(cs);
|
|
}
|
|
}
|
|
InsertAsLeft(node, newNode);
|
|
}
|
|
return newNode;
|
|
}
|
|
#endregion
|
|
|
|
#region Rotation callbacks
|
|
|
|
private enum UpdateAfterChildrenChangeRecursionMode
|
|
{
|
|
None,
|
|
IfRequired,
|
|
WholeBranch
|
|
}
|
|
|
|
private static void UpdateAfterChildrenChange(HeightTreeNode node)
|
|
{
|
|
UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired);
|
|
}
|
|
|
|
private static void UpdateAugmentedData(HeightTreeNode node, UpdateAfterChildrenChangeRecursionMode mode)
|
|
{
|
|
var totalCount = 1;
|
|
var totalHeight = node.LineNode.TotalHeight;
|
|
if (node.Left != null) {
|
|
totalCount += node.Left.TotalCount;
|
|
totalHeight += node.Left.TotalHeight;
|
|
}
|
|
if (node.Right != null) {
|
|
totalCount += node.Right.TotalCount;
|
|
totalHeight += node.Right.TotalHeight;
|
|
}
|
|
if (node.IsDirectlyCollapsed)
|
|
totalHeight = 0;
|
|
if (totalCount != node.TotalCount
|
|
|| !totalHeight.IsClose(node.TotalHeight)
|
|
|| mode == UpdateAfterChildrenChangeRecursionMode.WholeBranch)
|
|
{
|
|
node.TotalCount = totalCount;
|
|
node.TotalHeight = totalHeight;
|
|
if (node.Parent != null && mode != UpdateAfterChildrenChangeRecursionMode.None)
|
|
UpdateAugmentedData(node.Parent, mode);
|
|
}
|
|
}
|
|
|
|
private void UpdateAfterRotateLeft(HeightTreeNode node)
|
|
{
|
|
// node = old parent
|
|
// node.parent = pivot, new parent
|
|
var collapsedP = node.Parent.CollapsedSections;
|
|
var collapsedQ = node.CollapsedSections;
|
|
// move collapsedSections from old parent to new parent
|
|
node.Parent.CollapsedSections = collapsedQ;
|
|
node.CollapsedSections = null;
|
|
// split the collapsedSections from the new parent into its old children:
|
|
if (collapsedP != null) {
|
|
foreach (var cs in collapsedP) {
|
|
if (node.Parent.Right != null)
|
|
node.Parent.Right.AddDirectlyCollapsed(cs);
|
|
node.Parent.LineNode.AddDirectlyCollapsed(cs);
|
|
if (node.Right != null)
|
|
node.Right.AddDirectlyCollapsed(cs);
|
|
}
|
|
}
|
|
MergeCollapsedSectionsIfPossible(node);
|
|
|
|
UpdateAfterChildrenChange(node);
|
|
|
|
// not required: rotations only happen on insertions/deletions
|
|
// -> totalCount changes -> the parent is always updated
|
|
//UpdateAfterChildrenChange(node.parent);
|
|
}
|
|
|
|
private void UpdateAfterRotateRight(HeightTreeNode node)
|
|
{
|
|
// node = old parent
|
|
// node.parent = pivot, new parent
|
|
var collapsedP = node.Parent.CollapsedSections;
|
|
var collapsedQ = node.CollapsedSections;
|
|
// move collapsedSections from old parent to new parent
|
|
node.Parent.CollapsedSections = collapsedQ;
|
|
node.CollapsedSections = null;
|
|
// split the collapsedSections from the new parent into its old children:
|
|
if (collapsedP != null) {
|
|
foreach (var cs in collapsedP) {
|
|
if (node.Parent.Left != null)
|
|
node.Parent.Left.AddDirectlyCollapsed(cs);
|
|
node.Parent.LineNode.AddDirectlyCollapsed(cs);
|
|
if (node.Left != null)
|
|
node.Left.AddDirectlyCollapsed(cs);
|
|
}
|
|
}
|
|
MergeCollapsedSectionsIfPossible(node);
|
|
|
|
UpdateAfterChildrenChange(node);
|
|
|
|
// not required: rotations only happen on insertions/deletions
|
|
// -> totalCount changes -> the parent is always updated
|
|
//UpdateAfterChildrenChange(node.parent);
|
|
}
|
|
|
|
// node removal:
|
|
// a node in the middle of the tree is removed as following:
|
|
// its successor is removed
|
|
// it is replaced with its successor
|
|
|
|
private void BeforeNodeRemove(HeightTreeNode removedNode)
|
|
{
|
|
Debug.Assert(removedNode.Left == null || removedNode.Right == null);
|
|
|
|
var collapsed = removedNode.CollapsedSections;
|
|
if (collapsed != null) {
|
|
var childNode = removedNode.Left ?? removedNode.Right;
|
|
if (childNode != null) {
|
|
foreach (var cs in collapsed)
|
|
childNode.AddDirectlyCollapsed(cs);
|
|
}
|
|
}
|
|
if (removedNode.Parent != null)
|
|
MergeCollapsedSectionsIfPossible(removedNode.Parent);
|
|
}
|
|
|
|
private void BeforeNodeReplace(HeightTreeNode removedNode, HeightTreeNode newNode, HeightTreeNode newNodeOldParent)
|
|
{
|
|
Debug.Assert(removedNode != null);
|
|
Debug.Assert(newNode != null);
|
|
while (newNodeOldParent != removedNode) {
|
|
if (newNodeOldParent.CollapsedSections != null) {
|
|
foreach (var cs in newNodeOldParent.CollapsedSections) {
|
|
newNode.LineNode.AddDirectlyCollapsed(cs);
|
|
}
|
|
}
|
|
newNodeOldParent = newNodeOldParent.Parent;
|
|
}
|
|
if (newNode.CollapsedSections != null) {
|
|
foreach (var cs in newNode.CollapsedSections) {
|
|
newNode.LineNode.AddDirectlyCollapsed(cs);
|
|
}
|
|
}
|
|
newNode.CollapsedSections = removedNode.CollapsedSections;
|
|
MergeCollapsedSectionsIfPossible(newNode);
|
|
}
|
|
|
|
private bool _inRemoval;
|
|
private List<HeightTreeNode> _nodesToCheckForMerging;
|
|
|
|
private void BeginRemoval()
|
|
{
|
|
Debug.Assert(!_inRemoval);
|
|
if (_nodesToCheckForMerging == null) {
|
|
_nodesToCheckForMerging = new List<HeightTreeNode>();
|
|
}
|
|
_inRemoval = true;
|
|
}
|
|
|
|
private void EndRemoval()
|
|
{
|
|
Debug.Assert(_inRemoval);
|
|
_inRemoval = false;
|
|
foreach (var node in _nodesToCheckForMerging) {
|
|
MergeCollapsedSectionsIfPossible(node);
|
|
}
|
|
_nodesToCheckForMerging.Clear();
|
|
}
|
|
|
|
private void MergeCollapsedSectionsIfPossible(HeightTreeNode node)
|
|
{
|
|
Debug.Assert(node != null);
|
|
if (_inRemoval) {
|
|
_nodesToCheckForMerging.Add(node);
|
|
return;
|
|
}
|
|
// now check if we need to merge collapsedSections together
|
|
var merged = false;
|
|
var collapsedL = node.LineNode.CollapsedSections;
|
|
if (collapsedL != null) {
|
|
for (var i = collapsedL.Count - 1; i >= 0; i--) {
|
|
var cs = collapsedL[i];
|
|
if (cs.Start == node.DocumentLine || cs.End == node.DocumentLine)
|
|
continue;
|
|
if (node.Left == null
|
|
|| (node.Left.CollapsedSections != null && node.Left.CollapsedSections.Contains(cs)))
|
|
{
|
|
if (node.Right == null
|
|
|| (node.Right.CollapsedSections != null && node.Right.CollapsedSections.Contains(cs)))
|
|
{
|
|
// all children of node contain cs: -> merge!
|
|
if (node.Left != null) node.Left.RemoveDirectlyCollapsed(cs);
|
|
if (node.Right != null) node.Right.RemoveDirectlyCollapsed(cs);
|
|
collapsedL.RemoveAt(i);
|
|
node.AddDirectlyCollapsed(cs);
|
|
merged = true;
|
|
}
|
|
}
|
|
}
|
|
if (collapsedL.Count == 0)
|
|
node.LineNode.CollapsedSections = null;
|
|
}
|
|
if (merged && node.Parent != null) {
|
|
MergeCollapsedSectionsIfPossible(node.Parent);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region GetNodeBy... / Get...FromNode
|
|
|
|
private HeightTreeNode GetNodeByIndex(int index)
|
|
{
|
|
Debug.Assert(index >= 0);
|
|
Debug.Assert(index < _root.TotalCount);
|
|
var node = _root;
|
|
while (true) {
|
|
if (node.Left != null && index < node.Left.TotalCount) {
|
|
node = node.Left;
|
|
} else {
|
|
if (node.Left != null) {
|
|
index -= node.Left.TotalCount;
|
|
}
|
|
if (index == 0)
|
|
return node;
|
|
index--;
|
|
node = node.Right;
|
|
}
|
|
}
|
|
}
|
|
|
|
private HeightTreeNode GetNodeByVisualPosition(double position)
|
|
{
|
|
var node = _root;
|
|
while (true) {
|
|
var positionAfterLeft = position;
|
|
if (node.Left != null) {
|
|
positionAfterLeft -= node.Left.TotalHeight;
|
|
if (positionAfterLeft < 0) {
|
|
// Descend into left
|
|
node = node.Left;
|
|
continue;
|
|
}
|
|
}
|
|
var positionBeforeRight = positionAfterLeft - node.LineNode.TotalHeight;
|
|
if (positionBeforeRight < 0) {
|
|
// Found the correct node
|
|
return node;
|
|
}
|
|
if (node.Right == null || node.Right.TotalHeight == 0) {
|
|
// Can happen when position>node.totalHeight,
|
|
// i.e. at the end of the document, or due to rounding errors in previous loop iterations.
|
|
|
|
// If node.lineNode isn't collapsed, return that.
|
|
// Also return node.lineNode if there is no previous node that we could return instead.
|
|
if (node.LineNode.TotalHeight > 0 || node.Left == null)
|
|
return node;
|
|
// Otherwise, descend into left (find the last non-collapsed node)
|
|
node = node.Left;
|
|
} else {
|
|
// Descend into right
|
|
position = positionBeforeRight;
|
|
node = node.Right;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static float GetVisualPositionFromNode(HeightTreeNode node)
|
|
{
|
|
var position = (node.Left != null) ? node.Left.TotalHeight : 0;
|
|
while (node.Parent != null) {
|
|
if (node.IsDirectlyCollapsed)
|
|
position = 0;
|
|
if (node == node.Parent.Right) {
|
|
if (node.Parent.Left != null)
|
|
position += node.Parent.Left.TotalHeight;
|
|
position += node.Parent.LineNode.TotalHeight;
|
|
}
|
|
node = node.Parent;
|
|
}
|
|
return position;
|
|
}
|
|
#endregion
|
|
|
|
#region Public methods
|
|
internal DocumentLine GetLineByNumber(int number)
|
|
{
|
|
return GetNodeByIndex(number - 1).DocumentLine;
|
|
}
|
|
|
|
internal DocumentLine GetLineByVisualPosition(double position)
|
|
{
|
|
return GetNodeByVisualPosition(position).DocumentLine;
|
|
}
|
|
|
|
internal float GetVisualPosition(DocumentLine line)
|
|
{
|
|
return GetVisualPositionFromNode(GetNode(line));
|
|
}
|
|
|
|
internal double GetHeight(DocumentLine line)
|
|
{
|
|
return GetNode(line).LineNode.Height;
|
|
}
|
|
|
|
internal void SetHeight(DocumentLine line, float val)
|
|
{
|
|
var node = GetNode(line);
|
|
node.LineNode.Height = val;
|
|
UpdateAfterChildrenChange(node);
|
|
}
|
|
|
|
internal bool GetIsCollapsed(int lineNumber)
|
|
{
|
|
var node = GetNodeByIndex(lineNumber - 1);
|
|
return node.LineNode.IsDirectlyCollapsed || GetIsCollapedFromNode(node);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collapses the specified text section.
|
|
/// Runtime: O(log n)
|
|
/// </summary>
|
|
internal CollapsedLineSection CollapseText(DocumentLine start, DocumentLine end)
|
|
{
|
|
if (!_document.Lines.Contains(start))
|
|
throw new ArgumentException("Line is not part of this document", nameof(start));
|
|
if (!_document.Lines.Contains(end))
|
|
throw new ArgumentException("Line is not part of this document", nameof(end));
|
|
var length = end.LineNumber - start.LineNumber + 1;
|
|
if (length < 0)
|
|
throw new ArgumentException("start must be a line before end");
|
|
var section = new CollapsedLineSection(this, start, end);
|
|
AddCollapsedSection(section, length);
|
|
#if DEBUG
|
|
CheckProperties();
|
|
#endif
|
|
return section;
|
|
}
|
|
#endregion
|
|
|
|
#region LineCount & TotalHeight
|
|
internal int LineCount {
|
|
get {
|
|
return _root.TotalCount;
|
|
}
|
|
}
|
|
|
|
internal float TotalHeight {
|
|
get {
|
|
return _root.TotalHeight;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region GetAllCollapsedSections
|
|
|
|
private IEnumerable<HeightTreeNode> AllNodes {
|
|
get {
|
|
if (_root != null) {
|
|
var node = _root.LeftMost;
|
|
while (node != null) {
|
|
yield return node;
|
|
node = node.Successor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal IEnumerable<CollapsedLineSection> GetAllCollapsedSections()
|
|
{
|
|
var emptyCsList = new List<CollapsedLineSection>();
|
|
return System.Linq.Enumerable.Distinct(
|
|
System.Linq.Enumerable.SelectMany(
|
|
AllNodes, node => System.Linq.Enumerable.Concat(node.LineNode.CollapsedSections ?? emptyCsList,
|
|
node.CollapsedSections ?? emptyCsList)
|
|
));
|
|
}
|
|
#endregion
|
|
|
|
#region CheckProperties
|
|
#if DEBUG
|
|
[Conditional("DATACONSISTENCYTEST")]
|
|
internal void CheckProperties()
|
|
{
|
|
CheckProperties(_root);
|
|
|
|
foreach (var cs in GetAllCollapsedSections()) {
|
|
Debug.Assert(GetNode(cs.Start).LineNode.CollapsedSections.Contains(cs));
|
|
Debug.Assert(GetNode(cs.End).LineNode.CollapsedSections.Contains(cs));
|
|
var endLine = cs.End.LineNumber;
|
|
for (var i = cs.Start.LineNumber; i <= endLine; i++) {
|
|
CheckIsInSection(cs, GetLineByNumber(i));
|
|
}
|
|
}
|
|
|
|
// check red-black property:
|
|
var blackCount = -1;
|
|
CheckNodeProperties(_root, null, Red, 0, ref blackCount);
|
|
}
|
|
|
|
private void CheckIsInSection(CollapsedLineSection cs, DocumentLine line)
|
|
{
|
|
var node = GetNode(line);
|
|
if (node.LineNode.CollapsedSections != null && node.LineNode.CollapsedSections.Contains(cs))
|
|
return;
|
|
while (node != null) {
|
|
if (node.CollapsedSections != null && node.CollapsedSections.Contains(cs))
|
|
return;
|
|
node = node.Parent;
|
|
}
|
|
throw new InvalidOperationException(cs + " not found for line " + line);
|
|
}
|
|
|
|
private void CheckProperties(HeightTreeNode node)
|
|
{
|
|
var totalCount = 1;
|
|
var totalHeight = node.LineNode.TotalHeight;
|
|
if (node.LineNode.IsDirectlyCollapsed)
|
|
Debug.Assert(node.LineNode.CollapsedSections.Count > 0);
|
|
if (node.Left != null) {
|
|
CheckProperties(node.Left);
|
|
totalCount += node.Left.TotalCount;
|
|
totalHeight += node.Left.TotalHeight;
|
|
|
|
CheckAllContainedIn(node.Left.CollapsedSections, node.LineNode.CollapsedSections);
|
|
}
|
|
if (node.Right != null) {
|
|
CheckProperties(node.Right);
|
|
totalCount += node.Right.TotalCount;
|
|
totalHeight += node.Right.TotalHeight;
|
|
|
|
CheckAllContainedIn(node.Right.CollapsedSections, node.LineNode.CollapsedSections);
|
|
}
|
|
if (node.Left != null && node.Right != null) {
|
|
if (node.Left.CollapsedSections != null && node.Right.CollapsedSections != null) {
|
|
var intersection = System.Linq.Enumerable.Intersect(node.Left.CollapsedSections, node.Right.CollapsedSections);
|
|
Debug.Assert(System.Linq.Enumerable.Count(intersection) == 0);
|
|
}
|
|
}
|
|
if (node.IsDirectlyCollapsed) {
|
|
Debug.Assert(node.CollapsedSections.Count > 0);
|
|
totalHeight = 0;
|
|
}
|
|
Debug.Assert(node.TotalCount == totalCount);
|
|
Debug.Assert(node.TotalHeight.IsClose(totalHeight));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks that all elements in list1 are contained in list2.
|
|
/// </summary>
|
|
private static void CheckAllContainedIn(IEnumerable<CollapsedLineSection> list1, ICollection<CollapsedLineSection> list2)
|
|
{
|
|
if (list1 == null) list1 = new List<CollapsedLineSection>();
|
|
if (list2 == null) list2 = new List<CollapsedLineSection>();
|
|
foreach (var cs in list1) {
|
|
Debug.Assert(list2.Contains(cs));
|
|
}
|
|
}
|
|
|
|
/*
|
|
1. A node is either red or black.
|
|
2. The root is black.
|
|
3. All leaves are black. (The leaves are the NIL children.)
|
|
4. Both children of every red node are black. (So every red node must have a black parent.)
|
|
5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.)
|
|
*/
|
|
private void CheckNodeProperties(HeightTreeNode node, HeightTreeNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
|
|
{
|
|
if (node == null) return;
|
|
|
|
Debug.Assert(node.Parent == parentNode);
|
|
|
|
if (parentColor == Red) {
|
|
Debug.Assert(node.Color == Black);
|
|
}
|
|
if (node.Color == Black) {
|
|
blackCount++;
|
|
}
|
|
if (node.Left == null && node.Right == null) {
|
|
// node is a leaf node:
|
|
if (expectedBlackCount == -1)
|
|
expectedBlackCount = blackCount;
|
|
else
|
|
Debug.Assert(expectedBlackCount == blackCount);
|
|
}
|
|
CheckNodeProperties(node.Left, node, node.Color, blackCount, ref expectedBlackCount);
|
|
CheckNodeProperties(node.Right, node, node.Color, blackCount, ref expectedBlackCount);
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
|
internal string GetTreeAsString()
|
|
{
|
|
var b = new StringBuilder();
|
|
AppendTreeToString(_root, b, 0);
|
|
return b.ToString();
|
|
}
|
|
|
|
private static void AppendTreeToString(HeightTreeNode node, StringBuilder b, int indent)
|
|
{
|
|
if (node.Color == Red)
|
|
b.Append("RED ");
|
|
else
|
|
b.Append("BLACK ");
|
|
b.AppendLine(node.ToString());
|
|
indent += 2;
|
|
if (node.Left != null) {
|
|
b.Append(' ', indent);
|
|
b.Append("L: ");
|
|
AppendTreeToString(node.Left, b, indent);
|
|
}
|
|
if (node.Right != null) {
|
|
b.Append(' ', indent);
|
|
b.Append("R: ");
|
|
AppendTreeToString(node.Right, b, indent);
|
|
}
|
|
}
|
|
#endif
|
|
#endregion
|
|
|
|
#region Red/Black Tree
|
|
|
|
private const bool Red = true;
|
|
private const bool Black = false;
|
|
|
|
private void InsertAsLeft(HeightTreeNode parentNode, HeightTreeNode newNode)
|
|
{
|
|
Debug.Assert(parentNode.Left == null);
|
|
parentNode.Left = newNode;
|
|
newNode.Parent = parentNode;
|
|
newNode.Color = Red;
|
|
UpdateAfterChildrenChange(parentNode);
|
|
FixTreeOnInsert(newNode);
|
|
}
|
|
|
|
private void InsertAsRight(HeightTreeNode parentNode, HeightTreeNode newNode)
|
|
{
|
|
Debug.Assert(parentNode.Right == null);
|
|
parentNode.Right = newNode;
|
|
newNode.Parent = parentNode;
|
|
newNode.Color = Red;
|
|
UpdateAfterChildrenChange(parentNode);
|
|
FixTreeOnInsert(newNode);
|
|
}
|
|
|
|
private void FixTreeOnInsert(HeightTreeNode node)
|
|
{
|
|
Debug.Assert(node != null);
|
|
Debug.Assert(node.Color == Red);
|
|
Debug.Assert(node.Left == null || node.Left.Color == Black);
|
|
Debug.Assert(node.Right == null || node.Right.Color == Black);
|
|
|
|
var parentNode = node.Parent;
|
|
if (parentNode == null) {
|
|
// we inserted in the root -> the node must be black
|
|
// since this is a root node, making the node black increments the number of black nodes
|
|
// on all paths by one, so it is still the same for all paths.
|
|
node.Color = Black;
|
|
return;
|
|
}
|
|
if (parentNode.Color == Black) {
|
|
// if the parent node where we inserted was black, our red node is placed correctly.
|
|
// since we inserted a red node, the number of black nodes on each path is unchanged
|
|
// -> the tree is still balanced
|
|
return;
|
|
}
|
|
// parentNode is red, so there is a conflict here!
|
|
|
|
// because the root is black, parentNode is not the root -> there is a grandparent node
|
|
var grandparentNode = parentNode.Parent;
|
|
var uncleNode = Sibling(parentNode);
|
|
if (uncleNode != null && uncleNode.Color == Red) {
|
|
parentNode.Color = Black;
|
|
uncleNode.Color = Black;
|
|
grandparentNode.Color = Red;
|
|
FixTreeOnInsert(grandparentNode);
|
|
return;
|
|
}
|
|
// now we know: parent is red but uncle is black
|
|
// First rotation:
|
|
if (node == parentNode.Right && parentNode == grandparentNode.Left) {
|
|
RotateLeft(parentNode);
|
|
node = node.Left;
|
|
} else if (node == parentNode.Left && parentNode == grandparentNode.Right) {
|
|
RotateRight(parentNode);
|
|
node = node.Right;
|
|
}
|
|
// because node might have changed, reassign variables:
|
|
parentNode = node.Parent;
|
|
grandparentNode = parentNode.Parent;
|
|
|
|
// Now recolor a bit:
|
|
parentNode.Color = Black;
|
|
grandparentNode.Color = Red;
|
|
// Second rotation:
|
|
if (node == parentNode.Left && parentNode == grandparentNode.Left) {
|
|
RotateRight(grandparentNode);
|
|
} else {
|
|
// because of the first rotation, this is guaranteed:
|
|
Debug.Assert(node == parentNode.Right && parentNode == grandparentNode.Right);
|
|
RotateLeft(grandparentNode);
|
|
}
|
|
}
|
|
|
|
private void RemoveNode(HeightTreeNode removedNode)
|
|
{
|
|
if (removedNode.Left != null && removedNode.Right != null) {
|
|
// replace removedNode with it's in-order successor
|
|
|
|
var leftMost = removedNode.Right.LeftMost;
|
|
var parentOfLeftMost = leftMost.Parent;
|
|
RemoveNode(leftMost); // remove leftMost from its current location
|
|
|
|
BeforeNodeReplace(removedNode, leftMost, parentOfLeftMost);
|
|
// and overwrite the removedNode with it
|
|
ReplaceNode(removedNode, leftMost);
|
|
leftMost.Left = removedNode.Left;
|
|
if (leftMost.Left != null) leftMost.Left.Parent = leftMost;
|
|
leftMost.Right = removedNode.Right;
|
|
if (leftMost.Right != null) leftMost.Right.Parent = leftMost;
|
|
leftMost.Color = removedNode.Color;
|
|
|
|
UpdateAfterChildrenChange(leftMost);
|
|
if (leftMost.Parent != null) UpdateAfterChildrenChange(leftMost.Parent);
|
|
return;
|
|
}
|
|
|
|
// now either removedNode.left or removedNode.right is null
|
|
// get the remaining child
|
|
var parentNode = removedNode.Parent;
|
|
var childNode = removedNode.Left ?? removedNode.Right;
|
|
BeforeNodeRemove(removedNode);
|
|
ReplaceNode(removedNode, childNode);
|
|
if (parentNode != null) UpdateAfterChildrenChange(parentNode);
|
|
if (removedNode.Color == Black) {
|
|
if (childNode != null && childNode.Color == Red) {
|
|
childNode.Color = Black;
|
|
} else {
|
|
FixTreeOnDelete(childNode, parentNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void FixTreeOnDelete(HeightTreeNode node, HeightTreeNode parentNode)
|
|
{
|
|
Debug.Assert(node == null || node.Parent == parentNode);
|
|
if (parentNode == null)
|
|
return;
|
|
|
|
// warning: node may be null
|
|
var sibling = Sibling(node, parentNode);
|
|
if (sibling.Color == Red) {
|
|
parentNode.Color = Red;
|
|
sibling.Color = Black;
|
|
if (node == parentNode.Left) {
|
|
RotateLeft(parentNode);
|
|
} else {
|
|
RotateRight(parentNode);
|
|
}
|
|
|
|
sibling = Sibling(node, parentNode); // update value of sibling after rotation
|
|
}
|
|
|
|
if (parentNode.Color == Black
|
|
&& sibling.Color == Black
|
|
&& GetColor(sibling.Left) == Black
|
|
&& GetColor(sibling.Right) == Black)
|
|
{
|
|
sibling.Color = Red;
|
|
FixTreeOnDelete(parentNode, parentNode.Parent);
|
|
return;
|
|
}
|
|
|
|
if (parentNode.Color == Red
|
|
&& sibling.Color == Black
|
|
&& GetColor(sibling.Left) == Black
|
|
&& GetColor(sibling.Right) == Black)
|
|
{
|
|
sibling.Color = Red;
|
|
parentNode.Color = Black;
|
|
return;
|
|
}
|
|
|
|
if (node == parentNode.Left &&
|
|
sibling.Color == Black &&
|
|
GetColor(sibling.Left) == Red &&
|
|
GetColor(sibling.Right) == Black)
|
|
{
|
|
sibling.Color = Red;
|
|
sibling.Left.Color = Black;
|
|
RotateRight(sibling);
|
|
}
|
|
else if (node == parentNode.Right &&
|
|
sibling.Color == Black &&
|
|
GetColor(sibling.Right) == Red &&
|
|
GetColor(sibling.Left) == Black)
|
|
{
|
|
sibling.Color = Red;
|
|
sibling.Right.Color = Black;
|
|
RotateLeft(sibling);
|
|
}
|
|
sibling = Sibling(node, parentNode); // update value of sibling after rotation
|
|
|
|
sibling.Color = parentNode.Color;
|
|
parentNode.Color = Black;
|
|
if (node == parentNode.Left) {
|
|
if (sibling.Right != null) {
|
|
Debug.Assert(sibling.Right.Color == Red);
|
|
sibling.Right.Color = Black;
|
|
}
|
|
RotateLeft(parentNode);
|
|
} else {
|
|
if (sibling.Left != null) {
|
|
Debug.Assert(sibling.Left.Color == Red);
|
|
sibling.Left.Color = Black;
|
|
}
|
|
RotateRight(parentNode);
|
|
}
|
|
}
|
|
|
|
private void ReplaceNode(HeightTreeNode replacedNode, HeightTreeNode newNode)
|
|
{
|
|
if (replacedNode.Parent == null) {
|
|
Debug.Assert(replacedNode == _root);
|
|
_root = newNode;
|
|
} else {
|
|
if (replacedNode.Parent.Left == replacedNode)
|
|
replacedNode.Parent.Left = newNode;
|
|
else
|
|
replacedNode.Parent.Right = newNode;
|
|
}
|
|
if (newNode != null) {
|
|
newNode.Parent = replacedNode.Parent;
|
|
}
|
|
replacedNode.Parent = null;
|
|
}
|
|
|
|
private void RotateLeft(HeightTreeNode p)
|
|
{
|
|
// let q be p's right child
|
|
var q = p.Right;
|
|
Debug.Assert(q != null);
|
|
Debug.Assert(q.Parent == p);
|
|
// set q to be the new root
|
|
ReplaceNode(p, q);
|
|
|
|
// set p's right child to be q's left child
|
|
p.Right = q.Left;
|
|
if (p.Right != null) p.Right.Parent = p;
|
|
// set q's left child to be p
|
|
q.Left = p;
|
|
p.Parent = q;
|
|
UpdateAfterRotateLeft(p);
|
|
}
|
|
|
|
private void RotateRight(HeightTreeNode p)
|
|
{
|
|
// let q be p's left child
|
|
var q = p.Left;
|
|
Debug.Assert(q != null);
|
|
Debug.Assert(q.Parent == p);
|
|
// set q to be the new root
|
|
ReplaceNode(p, q);
|
|
|
|
// set p's left child to be q's right child
|
|
p.Left = q.Right;
|
|
if (p.Left != null) p.Left.Parent = p;
|
|
// set q's right child to be p
|
|
q.Right = p;
|
|
p.Parent = q;
|
|
UpdateAfterRotateRight(p);
|
|
}
|
|
|
|
private static HeightTreeNode Sibling(HeightTreeNode node)
|
|
{
|
|
if (node == node.Parent.Left)
|
|
return node.Parent.Right;
|
|
else
|
|
return node.Parent.Left;
|
|
}
|
|
|
|
private static HeightTreeNode Sibling(HeightTreeNode node, HeightTreeNode parentNode)
|
|
{
|
|
Debug.Assert(node == null || node.Parent == parentNode);
|
|
if (node == parentNode.Left)
|
|
return parentNode.Right;
|
|
else
|
|
return parentNode.Left;
|
|
}
|
|
|
|
private static bool GetColor(HeightTreeNode node)
|
|
{
|
|
return node != null ? node.Color : Black;
|
|
}
|
|
#endregion
|
|
|
|
#region Collapsing support
|
|
|
|
private static bool GetIsCollapedFromNode(HeightTreeNode node)
|
|
{
|
|
while (node != null) {
|
|
if (node.IsDirectlyCollapsed)
|
|
return true;
|
|
node = node.Parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
internal void AddCollapsedSection(CollapsedLineSection section, int sectionLength)
|
|
{
|
|
AddRemoveCollapsedSection(section, sectionLength, true);
|
|
}
|
|
|
|
private void AddRemoveCollapsedSection(CollapsedLineSection section, int sectionLength, bool add)
|
|
{
|
|
Debug.Assert(sectionLength > 0);
|
|
|
|
var node = GetNode(section.Start);
|
|
// Go up in the tree.
|
|
while (true) {
|
|
// Mark all middle nodes as collapsed
|
|
if (add)
|
|
node.LineNode.AddDirectlyCollapsed(section);
|
|
else
|
|
node.LineNode.RemoveDirectlyCollapsed(section);
|
|
sectionLength -= 1;
|
|
if (sectionLength == 0) {
|
|
// we are done!
|
|
Debug.Assert(node.DocumentLine == section.End);
|
|
break;
|
|
}
|
|
// Mark all right subtrees as collapsed.
|
|
if (node.Right != null) {
|
|
if (node.Right.TotalCount < sectionLength) {
|
|
if (add)
|
|
node.Right.AddDirectlyCollapsed(section);
|
|
else
|
|
node.Right.RemoveDirectlyCollapsed(section);
|
|
sectionLength -= node.Right.TotalCount;
|
|
} else {
|
|
// mark partially into the right subtree: go down the right subtree.
|
|
AddRemoveCollapsedSectionDown(section, node.Right, sectionLength, add);
|
|
break;
|
|
}
|
|
}
|
|
// go up to the next node
|
|
var parentNode = node.Parent;
|
|
Debug.Assert(parentNode != null);
|
|
while (parentNode.Right == node) {
|
|
node = parentNode;
|
|
parentNode = node.Parent;
|
|
Debug.Assert(parentNode != null);
|
|
}
|
|
node = parentNode;
|
|
}
|
|
UpdateAugmentedData(GetNode(section.Start), UpdateAfterChildrenChangeRecursionMode.WholeBranch);
|
|
UpdateAugmentedData(GetNode(section.End), UpdateAfterChildrenChangeRecursionMode.WholeBranch);
|
|
}
|
|
|
|
private static void AddRemoveCollapsedSectionDown(CollapsedLineSection section, HeightTreeNode node, int sectionLength, bool add)
|
|
{
|
|
while (true) {
|
|
if (node.Left != null) {
|
|
if (node.Left.TotalCount < sectionLength) {
|
|
// mark left subtree
|
|
if (add)
|
|
node.Left.AddDirectlyCollapsed(section);
|
|
else
|
|
node.Left.RemoveDirectlyCollapsed(section);
|
|
sectionLength -= node.Left.TotalCount;
|
|
} else {
|
|
// mark only inside the left subtree
|
|
node = node.Left;
|
|
Debug.Assert(node != null);
|
|
continue;
|
|
}
|
|
}
|
|
if (add)
|
|
node.LineNode.AddDirectlyCollapsed(section);
|
|
else
|
|
node.LineNode.RemoveDirectlyCollapsed(section);
|
|
sectionLength -= 1;
|
|
if (sectionLength == 0) {
|
|
// done!
|
|
Debug.Assert(node.DocumentLine == section.End);
|
|
break;
|
|
}
|
|
// mark inside right subtree:
|
|
node = node.Right;
|
|
Debug.Assert(node != null);
|
|
}
|
|
}
|
|
|
|
internal void Uncollapse(CollapsedLineSection section)
|
|
{
|
|
var sectionLength = section.End.LineNumber - section.Start.LineNumber + 1;
|
|
AddRemoveCollapsedSection(section, sectionLength, false);
|
|
// do not call CheckProperties() in here - Uncollapse is also called during line removals
|
|
}
|
|
#endregion
|
|
}
|
|
} |