You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
466 lines
13 KiB
466 lines
13 KiB
// Copyright (c) 2020 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;
|
|
|
|
namespace ICSharpCode.TreeView
|
|
{
|
|
// This part of SharpTreeNode controls the 'flat list' data structure, which emulates
|
|
// a big flat list containing the whole tree; allowing access by visible index.
|
|
partial class SharpTreeNode
|
|
{
|
|
/// <summary>The parent in the flat list</summary>
|
|
internal SharpTreeNode listParent;
|
|
/// <summary>Left/right nodes in the flat list</summary>
|
|
SharpTreeNode left, right;
|
|
|
|
internal TreeFlattener treeFlattener;
|
|
|
|
/// <summary>Subtree height in the flat list tree</summary>
|
|
byte height = 1;
|
|
|
|
/// <summary>Length in the flat list, including children (children within the flat list). -1 = invalidated</summary>
|
|
int totalListLength = -1;
|
|
|
|
int Balance {
|
|
get { return Height(right) - Height(left); }
|
|
}
|
|
|
|
static int Height(SharpTreeNode node)
|
|
{
|
|
return node != null ? node.height : 0;
|
|
}
|
|
|
|
internal SharpTreeNode GetListRoot()
|
|
{
|
|
SharpTreeNode node = this;
|
|
while (node.listParent != null)
|
|
node = node.listParent;
|
|
return node;
|
|
}
|
|
|
|
#region Debugging
|
|
[Conditional("DEBUG")]
|
|
void CheckRootInvariants()
|
|
{
|
|
GetListRoot().CheckInvariants();
|
|
}
|
|
|
|
[Conditional("DATACONSISTENCYCHECK")]
|
|
void CheckInvariants()
|
|
{
|
|
Debug.Assert(left == null || left.listParent == this);
|
|
Debug.Assert(right == null || right.listParent == this);
|
|
Debug.Assert(height == 1 + Math.Max(Height(left), Height(right)));
|
|
Debug.Assert(Math.Abs(this.Balance) <= 1);
|
|
Debug.Assert(totalListLength == -1 || totalListLength == (left != null ? left.totalListLength : 0) + (isVisible ? 1 : 0) + (right != null ? right.totalListLength : 0));
|
|
if (left != null)
|
|
left.CheckInvariants();
|
|
if (right != null)
|
|
right.CheckInvariants();
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
static void DumpTree(SharpTreeNode node)
|
|
{
|
|
node.GetListRoot().DumpTree();
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
void DumpTree()
|
|
{
|
|
Debug.Indent();
|
|
if (left != null)
|
|
left.DumpTree();
|
|
Debug.Unindent();
|
|
Debug.WriteLine("{0}, totalListLength={1}, height={2}, Balance={3}, isVisible={4}", ToString(), totalListLength, height, Balance, isVisible);
|
|
Debug.Indent();
|
|
if (right != null)
|
|
right.DumpTree();
|
|
Debug.Unindent();
|
|
}
|
|
#endregion
|
|
|
|
#region GetNodeByVisibleIndex / GetVisibleIndexForNode
|
|
internal static SharpTreeNode GetNodeByVisibleIndex(SharpTreeNode root, int index)
|
|
{
|
|
root.GetTotalListLength(); // ensure all list lengths are calculated
|
|
Debug.Assert(index >= 0);
|
|
Debug.Assert(index < root.totalListLength);
|
|
SharpTreeNode node = root;
|
|
while (true)
|
|
{
|
|
if (node.left != null && index < node.left.totalListLength)
|
|
{
|
|
node = node.left;
|
|
}
|
|
else
|
|
{
|
|
if (node.left != null)
|
|
{
|
|
index -= node.left.totalListLength;
|
|
}
|
|
if (node.isVisible)
|
|
{
|
|
if (index == 0)
|
|
return node;
|
|
index--;
|
|
}
|
|
node = node.right;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static int GetVisibleIndexForNode(SharpTreeNode node)
|
|
{
|
|
int index = node.left != null ? node.left.GetTotalListLength() : 0;
|
|
while (node.listParent != null)
|
|
{
|
|
if (node == node.listParent.right)
|
|
{
|
|
if (node.listParent.left != null)
|
|
index += node.listParent.left.GetTotalListLength();
|
|
if (node.listParent.isVisible)
|
|
index++;
|
|
}
|
|
node = node.listParent;
|
|
}
|
|
return index;
|
|
}
|
|
#endregion
|
|
|
|
#region Balancing
|
|
/// <summary>
|
|
/// Balances the subtree rooted in <paramref name="node"/> and recomputes the 'height' field.
|
|
/// This method assumes that the children of this node are already balanced and have an up-to-date 'height' value.
|
|
/// </summary>
|
|
/// <returns>The new root node</returns>
|
|
static SharpTreeNode Rebalance(SharpTreeNode node)
|
|
{
|
|
Debug.Assert(node.left == null || Math.Abs(node.left.Balance) <= 1);
|
|
Debug.Assert(node.right == null || Math.Abs(node.right.Balance) <= 1);
|
|
// Keep looping until it's balanced. Not sure if this is stricly required; this is based on
|
|
// the Rope code where node merging made this necessary.
|
|
while (Math.Abs(node.Balance) > 1)
|
|
{
|
|
// AVL balancing
|
|
// note: because we don't care about the identity of concat nodes, this works a little different than usual
|
|
// tree rotations: in our implementation, the "this" node will stay at the top, only its children are rearranged
|
|
if (node.Balance > 1)
|
|
{
|
|
if (node.right.Balance < 0)
|
|
{
|
|
node.right = node.right.RotateRight();
|
|
}
|
|
node = node.RotateLeft();
|
|
// If 'node' was unbalanced by more than 2, we've shifted some of the inbalance to the left node; so rebalance that.
|
|
node.left = Rebalance(node.left);
|
|
}
|
|
else if (node.Balance < -1)
|
|
{
|
|
if (node.left.Balance > 0)
|
|
{
|
|
node.left = node.left.RotateLeft();
|
|
}
|
|
node = node.RotateRight();
|
|
// If 'node' was unbalanced by more than 2, we've shifted some of the inbalance to the right node; so rebalance that.
|
|
node.right = Rebalance(node.right);
|
|
}
|
|
}
|
|
Debug.Assert(Math.Abs(node.Balance) <= 1);
|
|
node.height = (byte)(1 + Math.Max(Height(node.left), Height(node.right)));
|
|
node.totalListLength = -1; // mark for recalculation
|
|
// since balancing checks the whole tree up to the root, the whole path will get marked as invalid
|
|
return node;
|
|
}
|
|
|
|
internal int GetTotalListLength()
|
|
{
|
|
if (totalListLength >= 0)
|
|
return totalListLength;
|
|
int length = (isVisible ? 1 : 0);
|
|
if (left != null)
|
|
{
|
|
length += left.GetTotalListLength();
|
|
}
|
|
if (right != null)
|
|
{
|
|
length += right.GetTotalListLength();
|
|
}
|
|
return totalListLength = length;
|
|
}
|
|
|
|
SharpTreeNode RotateLeft()
|
|
{
|
|
/* Rotate tree to the left
|
|
*
|
|
* this right
|
|
* / \ / \
|
|
* A right ===> this C
|
|
* / \ / \
|
|
* B C A B
|
|
*/
|
|
SharpTreeNode b = right.left;
|
|
SharpTreeNode newTop = right;
|
|
|
|
if (b != null)
|
|
b.listParent = this;
|
|
this.right = b;
|
|
newTop.left = this;
|
|
newTop.listParent = this.listParent;
|
|
this.listParent = newTop;
|
|
// rebalance the 'this' node - this is necessary in some bulk insertion cases:
|
|
newTop.left = Rebalance(this);
|
|
return newTop;
|
|
}
|
|
|
|
SharpTreeNode RotateRight()
|
|
{
|
|
/* Rotate tree to the right
|
|
*
|
|
* this left
|
|
* / \ / \
|
|
* left C ===> A this
|
|
* / \ / \
|
|
* A B B C
|
|
*/
|
|
SharpTreeNode b = left.right;
|
|
SharpTreeNode newTop = left;
|
|
|
|
if (b != null)
|
|
b.listParent = this;
|
|
this.left = b;
|
|
newTop.right = this;
|
|
newTop.listParent = this.listParent;
|
|
this.listParent = newTop;
|
|
newTop.right = Rebalance(this);
|
|
return newTop;
|
|
}
|
|
|
|
static void RebalanceUntilRoot(SharpTreeNode pos)
|
|
{
|
|
while (pos.listParent != null)
|
|
{
|
|
if (pos == pos.listParent.left)
|
|
{
|
|
pos = pos.listParent.left = Rebalance(pos);
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(pos == pos.listParent.right);
|
|
pos = pos.listParent.right = Rebalance(pos);
|
|
}
|
|
pos = pos.listParent;
|
|
}
|
|
SharpTreeNode newRoot = Rebalance(pos);
|
|
if (newRoot != pos && pos.treeFlattener != null)
|
|
{
|
|
Debug.Assert(newRoot.treeFlattener == null);
|
|
newRoot.treeFlattener = pos.treeFlattener;
|
|
pos.treeFlattener = null;
|
|
newRoot.treeFlattener.root = newRoot;
|
|
}
|
|
Debug.Assert(newRoot.listParent == null);
|
|
newRoot.CheckInvariants();
|
|
}
|
|
#endregion
|
|
|
|
#region Insertion
|
|
static void InsertNodeAfter(SharpTreeNode pos, SharpTreeNode newNode)
|
|
{
|
|
// newNode might be the model root of a whole subtree, so go to the list root of that subtree:
|
|
newNode = newNode.GetListRoot();
|
|
if (pos.right == null)
|
|
{
|
|
pos.right = newNode;
|
|
newNode.listParent = pos;
|
|
}
|
|
else
|
|
{
|
|
// insert before pos.right's leftmost:
|
|
pos = pos.right;
|
|
while (pos.left != null)
|
|
pos = pos.left;
|
|
Debug.Assert(pos.left == null);
|
|
pos.left = newNode;
|
|
newNode.listParent = pos;
|
|
}
|
|
RebalanceUntilRoot(pos);
|
|
}
|
|
#endregion
|
|
|
|
#region Removal
|
|
void RemoveNodes(SharpTreeNode start, SharpTreeNode end)
|
|
{
|
|
// Removes all nodes from start to end (inclusive)
|
|
// All removed nodes will be reorganized in a separate tree, do not delete
|
|
// regions that don't belong together in the tree model!
|
|
|
|
List<SharpTreeNode> removedSubtrees = new List<SharpTreeNode>();
|
|
SharpTreeNode oldPos;
|
|
SharpTreeNode pos = start;
|
|
do
|
|
{
|
|
// recalculate the endAncestors every time, because the tree might have been rebalanced
|
|
HashSet<SharpTreeNode> endAncestors = new HashSet<SharpTreeNode>();
|
|
for (SharpTreeNode tmp = end; tmp != null; tmp = tmp.listParent)
|
|
endAncestors.Add(tmp);
|
|
|
|
removedSubtrees.Add(pos);
|
|
if (!endAncestors.Contains(pos))
|
|
{
|
|
// we can remove pos' right subtree in a single step:
|
|
if (pos.right != null)
|
|
{
|
|
removedSubtrees.Add(pos.right);
|
|
pos.right.listParent = null;
|
|
pos.right = null;
|
|
}
|
|
}
|
|
SharpTreeNode succ = pos.Successor();
|
|
DeleteNode(pos); // this will also rebalance out the deletion of the right subtree
|
|
|
|
oldPos = pos;
|
|
pos = succ;
|
|
} while (oldPos != end);
|
|
|
|
// merge back together the removed subtrees:
|
|
SharpTreeNode removed = removedSubtrees[0];
|
|
for (int i = 1; i < removedSubtrees.Count; i++)
|
|
{
|
|
removed = ConcatTrees(removed, removedSubtrees[i]);
|
|
}
|
|
}
|
|
|
|
static SharpTreeNode ConcatTrees(SharpTreeNode first, SharpTreeNode second)
|
|
{
|
|
SharpTreeNode tmp = first;
|
|
while (tmp.right != null)
|
|
tmp = tmp.right;
|
|
InsertNodeAfter(tmp, second);
|
|
return tmp.GetListRoot();
|
|
}
|
|
|
|
SharpTreeNode Successor()
|
|
{
|
|
if (right != null)
|
|
{
|
|
SharpTreeNode node = right;
|
|
while (node.left != null)
|
|
node = node.left;
|
|
return node;
|
|
}
|
|
else
|
|
{
|
|
SharpTreeNode node = this;
|
|
SharpTreeNode oldNode;
|
|
do
|
|
{
|
|
oldNode = node;
|
|
node = node.listParent;
|
|
// loop while we are on the way up from the right part
|
|
} while (node != null && node.right == oldNode);
|
|
return node;
|
|
}
|
|
}
|
|
|
|
static void DeleteNode(SharpTreeNode node)
|
|
{
|
|
SharpTreeNode balancingNode;
|
|
if (node.left == null)
|
|
{
|
|
balancingNode = node.listParent;
|
|
node.ReplaceWith(node.right);
|
|
node.right = null;
|
|
}
|
|
else if (node.right == null)
|
|
{
|
|
balancingNode = node.listParent;
|
|
node.ReplaceWith(node.left);
|
|
node.left = null;
|
|
}
|
|
else
|
|
{
|
|
SharpTreeNode tmp = node.right;
|
|
while (tmp.left != null)
|
|
tmp = tmp.left;
|
|
// First replace tmp with tmp.right
|
|
balancingNode = tmp.listParent;
|
|
tmp.ReplaceWith(tmp.right);
|
|
tmp.right = null;
|
|
Debug.Assert(tmp.left == null);
|
|
Debug.Assert(tmp.listParent == null);
|
|
// Now move node's children to tmp:
|
|
tmp.left = node.left;
|
|
node.left = null;
|
|
tmp.right = node.right;
|
|
node.right = null;
|
|
if (tmp.left != null)
|
|
tmp.left.listParent = tmp;
|
|
if (tmp.right != null)
|
|
tmp.right.listParent = tmp;
|
|
// Then replace node with tmp
|
|
node.ReplaceWith(tmp);
|
|
if (balancingNode == node)
|
|
balancingNode = tmp;
|
|
}
|
|
Debug.Assert(node.listParent == null);
|
|
Debug.Assert(node.left == null);
|
|
Debug.Assert(node.right == null);
|
|
node.height = 1;
|
|
node.totalListLength = -1;
|
|
if (balancingNode != null)
|
|
RebalanceUntilRoot(balancingNode);
|
|
}
|
|
|
|
void ReplaceWith(SharpTreeNode node)
|
|
{
|
|
if (listParent != null)
|
|
{
|
|
if (listParent.left == this)
|
|
{
|
|
listParent.left = node;
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(listParent.right == this);
|
|
listParent.right = node;
|
|
}
|
|
if (node != null)
|
|
node.listParent = listParent;
|
|
listParent = null;
|
|
}
|
|
else
|
|
{
|
|
// this was a root node
|
|
Debug.Assert(node != null); // cannot delete the only node in the tree
|
|
node.listParent = null;
|
|
if (treeFlattener != null)
|
|
{
|
|
Debug.Assert(node.treeFlattener == null);
|
|
node.treeFlattener = this.treeFlattener;
|
|
this.treeFlattener = null;
|
|
node.treeFlattener.root = node;
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|