From e5e068c28d8f0614cfe0d305dcbe4f4b995930af Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Wed, 24 Aug 2022 22:30:24 +0200 Subject: [PATCH] New implementation of HeightTree The HeightTree is the data structure that maps between document line numbers and visual Y positions. It handles both the varying heights of lines (e.g. due to word-wrapping) and the collapsing of lines (e.g. due to folding). Where the old implementation was based on a red-black tree, the new implementation is inspired by B+ trees. We no longer need a separate memory allocation for every line in the document. This should dramatically reduce the memory consumption: a comment in the old code indicated that each line took 56 bytes of memory. But that was in 32-bit days; in a 64-bit process the old implementation actually needed 88 bytes per line. The new implementation should need 168 bytes per leaf node which holds 8-16 lines, i.e. we need 10.5-21 bytes per line for the leaf nodes. It additionally needs 456 bytes per inner node (for the inner node and its children array). The level just above the leafs holds 64-256 lines per inner node, i.e. we need 1.8-7.2 bytes per line for the second level. Thus the total memory consumption of the new tree should in the worst case still be below 29.2 bytes per line (`168/8 + sum(456/(8**lvl) for lvl in range(2,100))`). However after freshly loading a large document from disk we should be close to the best case which in total is just 12.4 bytes per line (`168/16 + sum(456/(16**lvl) for lvl in range(2,100))`). Apart from the reduced memory usage, the behavior of the new tree should be identical to the old behavior. In particular, all operations (line insertion, line deletion, collapsing, uncollapsing, line<->Y queries) maintain their externally-visible behavior and their O(lg N) run-time. set_DefaultLineHeight runs in O(N) now (previously could take O(N lg N)). --- .../Document/CollapsingTests.cs | 4 +- .../Document/HeightTests.cs | 14 +- .../Document/RandomizedLineManagerTest.cs | 53 +- .../ICSharpCode.AvalonEdit.csproj | 2 + .../Rendering/CollapsedLineSection.cs | 76 +- .../Rendering/HeightTree.cs | 1053 ++--------------- .../Rendering/HeightTreeInnerNode.cs | 607 ++++++++++ .../Rendering/HeightTreeLeafNode.cs | 408 +++++++ .../Rendering/HeightTreeLineNode.cs | 63 - .../Rendering/HeightTreeNode.cs | 363 ++++-- ICSharpCode.AvalonEdit/Rendering/TextView.cs | 17 +- 11 files changed, 1510 insertions(+), 1150 deletions(-) create mode 100644 ICSharpCode.AvalonEdit/Rendering/HeightTreeInnerNode.cs create mode 100644 ICSharpCode.AvalonEdit/Rendering/HeightTreeLeafNode.cs delete mode 100644 ICSharpCode.AvalonEdit/Rendering/HeightTreeLineNode.cs diff --git a/ICSharpCode.AvalonEdit.Tests/Document/CollapsingTests.cs b/ICSharpCode.AvalonEdit.Tests/Document/CollapsingTests.cs index 629295e8..b2ec2396 100644 --- a/ICSharpCode.AvalonEdit.Tests/Document/CollapsingTests.cs +++ b/ICSharpCode.AvalonEdit.Tests/Document/CollapsingTests.cs @@ -35,7 +35,7 @@ public void Setup() document.Text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"; heightTree = new HeightTree(document, 10); foreach (DocumentLine line in document.Lines) { - heightTree.SetHeight(line, line.LineNumber); + heightTree.SetHeight(line.LineNumber, line.LineNumber); } } @@ -82,7 +82,7 @@ public void FullCheck() for (int i = 1; i <= 10; i++) { Assert.IsFalse(heightTree.GetIsCollapsed(i)); } - CheckHeights(); + CheckHeights(); } catch { Console.WriteLine("from = " + from + ", to = " + to); throw; diff --git a/ICSharpCode.AvalonEdit.Tests/Document/HeightTests.cs b/ICSharpCode.AvalonEdit.Tests/Document/HeightTests.cs index b39e914f..4fb3687d 100644 --- a/ICSharpCode.AvalonEdit.Tests/Document/HeightTests.cs +++ b/ICSharpCode.AvalonEdit.Tests/Document/HeightTests.cs @@ -36,7 +36,7 @@ public void Setup() document.Text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"; heightTree = new HeightTree(document, 10); foreach (DocumentLine line in document.Lines) { - heightTree.SetHeight(line, line.LineNumber); + heightTree.SetHeight(line.LineNumber, line.LineNumber); } } @@ -56,7 +56,7 @@ public void TestLinesRemoved() [Test] public void TestHeightChanged() { - heightTree.SetHeight(document.GetLineByNumber(4), 100); + heightTree.SetHeight(4, 100); CheckHeights(); } @@ -64,9 +64,9 @@ public void TestHeightChanged() public void TestLinesInserted() { document.Insert(0, "x\ny\n"); - heightTree.SetHeight(document.Lines[0], 100); - heightTree.SetHeight(document.Lines[1], 1000); - heightTree.SetHeight(document.Lines[2], 10000); + heightTree.SetHeight(1, 100); + heightTree.SetHeight(2, 1000); + heightTree.SetHeight(3, 10000); CheckHeights(); } @@ -77,13 +77,13 @@ void CheckHeights() internal static void CheckHeights(TextDocument document, HeightTree heightTree) { - double[] heights = document.Lines.Select(l => heightTree.GetIsCollapsed(l.LineNumber) ? 0 : heightTree.GetHeight(l)).ToArray(); + double[] heights = document.Lines.Select(l => heightTree.GetIsCollapsed(l.LineNumber) ? 0 : heightTree.GetHeight(l.LineNumber)).ToArray(); double[] visualPositions = new double[document.LineCount+1]; for (int i = 0; i < heights.Length; i++) { visualPositions[i+1]=visualPositions[i]+heights[i]; } foreach (DocumentLine ls in document.Lines) { - Assert.AreEqual(visualPositions[ls.LineNumber-1], heightTree.GetVisualPosition(ls)); + Assert.AreEqual(visualPositions[ls.LineNumber-1], heightTree.GetVisualPosition(ls.LineNumber)); } Assert.AreEqual(visualPositions[document.LineCount], heightTree.TotalHeight); } diff --git a/ICSharpCode.AvalonEdit.Tests/Document/RandomizedLineManagerTest.cs b/ICSharpCode.AvalonEdit.Tests/Document/RandomizedLineManagerTest.cs index c4262aae..01e38706 100644 --- a/ICSharpCode.AvalonEdit.Tests/Document/RandomizedLineManagerTest.cs +++ b/ICSharpCode.AvalonEdit.Tests/Document/RandomizedLineManagerTest.cs @@ -18,7 +18,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; + using ICSharpCode.AvalonEdit.Rendering; + using NUnit.Framework; namespace ICSharpCode.AvalonEdit.Document @@ -31,7 +34,7 @@ public class RandomizedLineManagerTest { TextDocument document; Random rnd; - + [OneTimeSetUp] public void FixtureSetup() { @@ -39,13 +42,13 @@ public void FixtureSetup() Console.WriteLine("RandomizedLineManagerTest Seed: " + seed); rnd = new Random(seed); } - + [SetUp] public void Setup() { document = new TextDocument(); } - + [Test] public void ShortReplacements() { @@ -58,12 +61,12 @@ public void ShortReplacements() for (int j = 0; j < newTextLength; j++) { buffer[j] = chars[rnd.Next(0, chars.Length)]; } - + document.Replace(offset, length, new string(buffer, 0, newTextLength)); CheckLines(); } } - + [Test] public void LargeReplacements() { @@ -76,7 +79,7 @@ public void LargeReplacements() for (int j = 0; j < newTextLength; j++) { buffer[j] = chars[rnd.Next(0, chars.Length)]; } - + string newText = new string(buffer, 0, newTextLength); string expectedText = document.Text.Remove(offset, length).Insert(offset, newText); document.Replace(offset, length, newText); @@ -84,7 +87,7 @@ public void LargeReplacements() CheckLines(); } } - + void CheckLines() { string text = document.Text; @@ -100,7 +103,7 @@ void CheckLines() Assert.AreEqual(i - lineStart, line.Length); i++; // consume \n lineNumber++; - lineStart = i+1; + lineStart = i + 1; } else if (c == '\r' || c == '\n') { DocumentLine line = document.GetLineByNumber(lineNumber); Assert.AreEqual(lineNumber, line.LineNumber); @@ -108,12 +111,12 @@ void CheckLines() Assert.AreEqual(lineStart, line.Offset); Assert.AreEqual(i - lineStart, line.Length); lineNumber++; - lineStart = i+1; + lineStart = i + 1; } } Assert.AreEqual(lineNumber, document.LineCount); } - + [Test] public void CollapsingTest() { @@ -121,14 +124,15 @@ public void CollapsingTest() char[] buffer = new char[20]; HeightTree heightTree = new HeightTree(document, 10); List collapsedSections = new List(); - for (int i = 0; i < 2500; i++) { -// Console.WriteLine("Iteration " + i); -// Console.WriteLine(heightTree.GetTreeAsString()); -// foreach (CollapsedLineSection cs in collapsedSections) { -// Console.WriteLine(cs); -// } - - switch (rnd.Next(0, 10)) { + for (int i = 0; i < 25000; i++) { + // Debug.WriteLine("Iteration " + i); + // Debug.WriteLine(heightTree.GetTreeAsString()); + // foreach (CollapsedLineSection cs in collapsedSections) { + // Debug.WriteLine(cs); + // } + + int command = rnd.Next(0, 10); + switch (command) { case 0: case 1: case 2: @@ -136,12 +140,19 @@ public void CollapsingTest() case 4: case 5: int offset = rnd.Next(0, document.TextLength); - int length = rnd.Next(0, document.TextLength - offset); + int length; + if (command == 0) { + length = rnd.Next(0, document.TextLength - offset); + } else if (command == 1) { + length = 0; + } else { + length = rnd.Next(0, Math.Min(15, document.TextLength - offset)); + } int newTextLength = rnd.Next(0, 20); for (int j = 0; j < newTextLength; j++) { buffer[j] = chars[rnd.Next(0, chars.Length)]; } - + document.Replace(offset, length, new string(buffer, 0, newTextLength)); break; case 6: @@ -162,7 +173,7 @@ public void CollapsingTest() break; case 9: foreach (DocumentLine ls in document.Lines) { - heightTree.SetHeight(ls, ls.LineNumber); + heightTree.SetHeight(ls.LineNumber, ls.LineNumber); } break; } diff --git a/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index f7ceb98e..6e12e7e6 100644 --- a/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -14,6 +14,7 @@ 6.1.0.0 6.1.0.0 True + 10 @@ -29,6 +30,7 @@ images\AvalonEditNuGetPackageIcon.png WPF Text Editor SharpDevelop AvalonEdit Changes are detailed at https://github.com/icsharpcode/AvalonEdit/blob/master/ChangeLog.md + True diff --git a/ICSharpCode.AvalonEdit/Rendering/CollapsedLineSection.cs b/ICSharpCode.AvalonEdit/Rendering/CollapsedLineSection.cs index 8dae0fd2..f927a5a5 100644 --- a/ICSharpCode.AvalonEdit/Rendering/CollapsedLineSection.cs +++ b/ICSharpCode.AvalonEdit/Rendering/CollapsedLineSection.cs @@ -15,6 +15,8 @@ // 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. +#nullable enable +using System; using ICSharpCode.AvalonEdit.Document; @@ -26,8 +28,14 @@ namespace ICSharpCode.AvalonEdit.Rendering /// public sealed class CollapsedLineSection { - DocumentLine start, end; - HeightTree heightTree; + // note: we don't need to store start/end, we could recompute them from + // the height tree if that had parent pointers. + DocumentLine? start, end; + internal readonly HeightTree heightTree; + internal HeightTreeLeafNode? startLeaf, endLeaf; + // tree nodes that contains the start/end of the collapsed section + internal byte startIndexInLeaf, endIndexInLeaf; + // start/end line within the HeightTreeLeafNode #if DEBUG internal string ID; @@ -61,7 +69,7 @@ public bool IsCollapsed { /// When the section is uncollapsed or the text containing it is deleted, /// this property returns null. /// - public DocumentLine Start { + public DocumentLine? Start { get { return start; } internal set { start = value; } } @@ -71,11 +79,17 @@ public DocumentLine Start { /// When the section is uncollapsed or the text containing it is deleted, /// this property returns null. /// - public DocumentLine End { + public DocumentLine? End { get { return end; } internal set { end = value; } } + internal void Reset() + { + start = end = null; + startLeaf = endLeaf = null; + } + /// /// Uncollapses the section. /// This causes the Start and End properties to be set to null! @@ -83,18 +97,28 @@ public DocumentLine End { /// public void Uncollapse() { - if (start == null) + if (startLeaf == null || endLeaf == null) return; - - if (!heightTree.IsDisposed) { - heightTree.Uncollapse(this); -#if DEBUG - heightTree.CheckProperties(); -#endif + HeightTreeNode startNode = startLeaf; + HeightTreeNode endNode = endLeaf; + while (startNode != endNode) { + startNode.RemoveEvent(this, HeightTreeNode.EventKind.Start); + endNode.RemoveEvent(this, HeightTreeNode.EventKind.End); + startNode.parent!.UpdateHeight(startNode.indexInParent); + endNode.parent!.UpdateHeight(endNode.indexInParent); + startNode = startNode.parent; + endNode = endNode.parent; + } + // Now we have arrived at the node which has both events. + startNode.RemoveEvent(this, HeightTreeNode.EventKind.Start); + startNode.RemoveEvent(this, HeightTreeNode.EventKind.End); + // Propagate the new height up to the root node. + while (startNode.parent != null) { + startNode.parent.UpdateHeight(startNode.indexInParent); + startNode = startNode.parent; } - start = null; - end = null; + Reset(); } /// @@ -106,5 +130,31 @@ public override string ToString() return "[CollapsedSection" + ID + " Start=" + (start != null ? start.LineNumber.ToString() : "null") + " End=" + (end != null ? end.LineNumber.ToString() : "null") + "]"; } + + internal bool StartIsWithin(HeightTreeNode heightTreeNode, out int index) + { + index = startIndexInLeaf; + HeightTreeNode? node = startLeaf; + while (node != null) { + if (node == heightTreeNode) + return true; + index = node.indexInParent; + node = node.parent; + } + return false; + } + + internal bool EndIsWithin(HeightTreeNode heightTreeNode, out int index) + { + index = endIndexInLeaf; + HeightTreeNode? node = endLeaf; + while (node != null) { + if (node == heightTreeNode) + return true; + index = node.indexInParent; + node = node.parent; + } + return false; + } } } diff --git a/ICSharpCode.AvalonEdit/Rendering/HeightTree.cs b/ICSharpCode.AvalonEdit/Rendering/HeightTree.cs index ea5e992c..9a7f6af0 100644 --- a/ICSharpCode.AvalonEdit/Rendering/HeightTree.cs +++ b/ICSharpCode.AvalonEdit/Rendering/HeightTree.cs @@ -16,42 +16,27 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +// Enable this define to use expensive consistency checks in debug builds. +// (will cause performance to degrade from O(lg N) to O(N), which may cause some operations +// that are already linear to go quadratic) +//#define DATACONSISTENCYTEST + using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text; using ICSharpCode.AvalonEdit.Document; -using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Rendering { /// - /// Red-black tree similar to DocumentLineTree, augmented with collapsing and height data. + /// A tree that maps line numbers to visual positions. + /// The balancing of the tree work as in a B+ tree. /// 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 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 readonly TextDocument document; HeightTreeNode root; @@ -89,24 +74,18 @@ public double DefaultLineHeight { 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); - } - } + root?.UpdateHeight(oldValue, value); } } - - HeightTreeNode GetNode(DocumentLine ls) - { - return GetNodeByIndex(ls.LineNumber - 1); - } #endregion #region RebuildDocument void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) { +#if DEBUG + //Debug.WriteLine(GetTreeAsString()); + CheckProperties(); +#endif } void ILineTracker.SetLineLength(DocumentLine ls, int newTotalLength) @@ -119,420 +98,148 @@ void ILineTracker.SetLineLength(DocumentLine ls, int newTotalLength) public void RebuildDocument() { foreach (CollapsedLineSection s in GetAllCollapsedSections()) { - s.Start = null; - s.End = null; + s.Reset(); + } + int lineCount = document.LineCount; + // List of inner nodes that are not yet full and not yet connected to their parent + var innerNodes = new List(); + innerNodes.Add(new HeightTreeInnerNode()); + // Create leaf nodes + int pos = 0; + while (pos < lineCount) { + int linesInThisNode = Math.Min(lineCount - pos, HeightTreeLeafNode.MaxLineCount); + HeightTreeLeafNode leafNode = HeightTreeLeafNode.Create(linesInThisNode, defaultLineHeight); + innerNodes[0].InsertChild(innerNodes[0].childCount, leafNode); + pos += linesInThisNode; + // Restore invariant that innerNodes are not yet full + int level = 0; + while (innerNodes[level].childCount == HeightTreeInnerNode.MaxChildCount) { + if (level + 1 == innerNodes.Count) { + innerNodes.Add(new HeightTreeInnerNode()); + } + innerNodes[level + 1].InsertChild(innerNodes[level + 1].childCount, innerNodes[level]); + innerNodes[level] = new HeightTreeInnerNode(); + level++; + } } - - HeightTreeNode[] nodes = new HeightTreeNode[document.LineCount]; - int lineNumber = 0; - foreach (DocumentLine ls in document.Lines) { - nodes[lineNumber++] = new HeightTreeNode(ls, defaultLineHeight); + // Connect inner nodes + for (int level = 0; level < innerNodes.Count - 1; level++) { + innerNodes[level + 1].InsertChild(innerNodes[level + 1].childCount, innerNodes[level]); + } + if (innerNodes[innerNodes.Count - 1].childCount == 1) { + // The root node is a leaf node + root = innerNodes[innerNodes.Count - 1].children[0]; + root.parent = null; + } else { + root = innerNodes[innerNodes.Count - 1]; } - Debug.Assert(nodes.Length > 0); - // now build the corresponding balanced tree - int height = DocumentLineTree.GetTreeHeight(nodes.Length); - Debug.WriteLine("HeightTree will have height: " + height); - root = BuildTree(nodes, 0, nodes.Length, height); - root.color = BLACK; + Debug.Assert(root.LineCount == lineCount); + + // All nodes except for the last in each layer are completely full, + // but the last nodes may be nearly empty, requiring a rebalancing + // to establish the B+ tree invariant. + (root as HeightTreeInnerNode)?.RebalanceLastChild(); #if DEBUG + //Debug.WriteLine(GetTreeAsString()); CheckProperties(); #endif } - - /// - /// build a tree from a list of nodes - /// - HeightTreeNode BuildTree(HeightTreeNode[] nodes, int start, int end, int subtreeHeight) - { - Debug.Assert(start <= end); - if (start == end) { - return null; - } - int middle = (start + end) / 2; - HeightTreeNode 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 + static int opId = 0; + void ILineTracker.BeforeRemoveLine(DocumentLine line) { - HeightTreeNode node = GetNode(line); - if (node.lineNode.collapsedSections != null) { - foreach (CollapsedLineSection 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); - } - } + //Debug.WriteLine($"#{++opId} BeforeRemoveLine " + line.LineNumber); + //Debug.WriteLine(GetTreeAsString()); + root.DeleteLine(line.LineNumber - 1, null, null); + if (root is HeightTreeInnerNode { childCount: 1 } innerNode) { + // Reduce the height of the tree by one level + root = innerNode.children[0]; + root.parent = null; } - BeginRemoval(); - RemoveNode(node); - // clear collapsedSections from removed line: prevent damage if removed line is in "nodesToCheckForMerging" - node.lineNode.collapsedSections = null; - EndRemoval(); + //Debug.WriteLine(GetTreeAsString()); + // CheckProperties would fail here because the line numbers are not updated yet + // We will call it in ChangeComplete. } - -// void ILineTracker.AfterRemoveLine(DocumentLine line) -// { -// -// } - + void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine) { - InsertAfter(GetNode(insertionPos), newLine); + //Debug.WriteLine($"#{++opId} LineInserted " + newLine.LineNumber); + //Debug.WriteLine(GetTreeAsString()); + var newSibling = root.InsertLine(newLine.LineNumber - 1, defaultLineHeight); + if (newSibling != null) { + // Increase the height of the tree by one level + root = HeightTreeInnerNode.NewRoot(root, newSibling); + } #if DEBUG + //Debug.WriteLine(GetTreeAsString()); CheckProperties(); #endif } - - HeightTreeNode InsertAfter(HeightTreeNode node, DocumentLine newLine) - { - HeightTreeNode 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 (CollapsedLineSection 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 (CollapsedLineSection cs in node.lineNode.collapsedSections) { - if (cs.Start != node.documentLine) - newNode.AddDirectlyCollapsed(cs); - } - } - InsertAsLeft(node, newNode); - } - return newNode; - } #endregion - #region Rotation callbacks - enum UpdateAfterChildrenChangeRecursionMode - { - None, - IfRequired, - WholeBranch - } - - static void UpdateAfterChildrenChange(HeightTreeNode node) - { - UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired); - } - - static void UpdateAugmentedData(HeightTreeNode node, UpdateAfterChildrenChangeRecursionMode mode) - { - int totalCount = 1; - double 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); - } - } - - 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 (CollapsedLineSection 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); - } - - 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 (CollapsedLineSection 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 - - void BeforeNodeRemove(HeightTreeNode removedNode) + #region GetLeafForLineNumber + HeightTreeLeafNode GetLeafForLineNumber(int lineNumber, out int indexInLeaf) { - Debug.Assert(removedNode.left == null || removedNode.right == null); - - var collapsed = removedNode.collapsedSections; - if (collapsed != null) { - HeightTreeNode childNode = removedNode.left ?? removedNode.right; - if (childNode != null) { - foreach (CollapsedLineSection cs in collapsed) - childNode.AddDirectlyCollapsed(cs); - } - } - if (removedNode.parent != null) - MergeCollapsedSectionsIfPossible(removedNode.parent); - } - - void BeforeNodeReplace(HeightTreeNode removedNode, HeightTreeNode newNode, HeightTreeNode newNodeOldParent) - { - Debug.Assert(removedNode != null); - Debug.Assert(newNode != null); - while (newNodeOldParent != removedNode) { - if (newNodeOldParent.collapsedSections != null) { - foreach (CollapsedLineSection cs in newNodeOldParent.collapsedSections) { - newNode.lineNode.AddDirectlyCollapsed(cs); - } - } - newNodeOldParent = newNodeOldParent.parent; - } - if (newNode.collapsedSections != null) { - foreach (CollapsedLineSection cs in newNode.collapsedSections) { - newNode.lineNode.AddDirectlyCollapsed(cs); - } - } - newNode.collapsedSections = removedNode.collapsedSections; - MergeCollapsedSectionsIfPossible(newNode); - } - - bool inRemoval; - List nodesToCheckForMerging; - - void BeginRemoval() - { - Debug.Assert(!inRemoval); - if (nodesToCheckForMerging == null) { - nodesToCheckForMerging = new List(); - } - inRemoval = true; - } - - void EndRemoval() - { - Debug.Assert(inRemoval); - inRemoval = false; - foreach (HeightTreeNode node in nodesToCheckForMerging) { - MergeCollapsedSectionsIfPossible(node); - } - nodesToCheckForMerging.Clear(); - } - - void MergeCollapsedSectionsIfPossible(HeightTreeNode node) - { - Debug.Assert(node != null); - if (inRemoval) { - nodesToCheckForMerging.Add(node); - return; - } - // now check if we need to merge collapsedSections together - bool merged = false; - var collapsedL = node.lineNode.collapsedSections; - if (collapsedL != null) { - for (int i = collapsedL.Count - 1; i >= 0; i--) { - CollapsedLineSection 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); + HeightTreeNode node = root; + int line = lineNumber - 1; + while (node is HeightTreeInnerNode inner) { + int childIndex = inner.FindChildForLine(line, out line); + node = inner.children[childIndex]; } + indexInLeaf = line; + return (HeightTreeLeafNode)node; } #endregion - #region GetNodeBy... / Get...FromNode - HeightTreeNode GetNodeByIndex(int index) + #region Public methods + public int GetLineByVisualPosition(double position) { - Debug.Assert(index >= 0); - Debug.Assert(index < root.totalCount); + int result = 1; HeightTreeNode 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; - } + while (node is HeightTreeInnerNode inner) { + int childIndex = inner.FindChildForVisualPosition(position, out position); + result += inner.GetTotalLineCountUntilChildIndex(childIndex); + node = inner.children[childIndex]; } + result += ((HeightTreeLeafNode)node).FindChildForVisualPosition(position); + return result; } - HeightTreeNode GetNodeByVisualPosition(double position) + public double GetVisualPosition(int lineNumber) { + double result = 0; HeightTreeNode node = root; - while (true) { - double positionAfterLeft = position; - if (node.left != null) { - positionAfterLeft -= node.left.totalHeight; - if (positionAfterLeft < 0) { - // Descend into left - node = node.left; - continue; - } + int line = lineNumber - 1; + while (node is HeightTreeInnerNode inner) { + int childIndex = inner.FindChildForLine(line, out line); + result += inner.GetTotalHeightUntilChildIndex(childIndex); + if ((inner.collapsed & (1 << childIndex)) != 0) { + // The child is collapsed, so we can skip the rest of the tree + return result; } - double 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; - } - } - } - - static double GetVisualPositionFromNode(HeightTreeNode node) - { - double 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; + node = inner.children[childIndex]; } - return position; - } - #endregion - - #region Public methods - public DocumentLine GetLineByNumber(int number) - { - return GetNodeByIndex(number - 1).documentLine; - } - - public DocumentLine GetLineByVisualPosition(double position) - { - return GetNodeByVisualPosition(position).documentLine; + result += ((HeightTreeLeafNode)node).GetTotalHeightUntilChildIndex(line); + return result; } - public double GetVisualPosition(DocumentLine line) + public double GetHeight(int lineNumber) { - return GetVisualPositionFromNode(GetNode(line)); + var leaf = GetLeafForLineNumber(lineNumber, out int indexInLeaf); + return leaf.GetHeight(indexInLeaf); } - public double GetHeight(DocumentLine line) + public void SetHeight(int lineNumber, double val) { - return GetNode(line).lineNode.height; - } - - public void SetHeight(DocumentLine line, double val) - { - var node = GetNode(line); - node.lineNode.height = val; - UpdateAfterChildrenChange(node); + root.SetHeight(lineNumber - 1, val); } public bool GetIsCollapsed(int lineNumber) { - var node = GetNodeByIndex(lineNumber - 1); - return node.lineNode.IsDirectlyCollapsed || GetIsCollapedFromNode(node); + return root.GetIsCollapsed(lineNumber - 1); } /// @@ -545,11 +252,14 @@ public CollapsedLineSection CollapseText(DocumentLine start, DocumentLine end) throw new ArgumentException("Line is not part of this document", "start"); if (!document.Lines.Contains(end)) throw new ArgumentException("Line is not part of this document", "end"); - int length = end.LineNumber - start.LineNumber + 1; - if (length < 0) + // Our start/end parameters are both inclusive + int startLineNumber = start.LineNumber; + int endLineNumber = end.LineNumber; + if (startLineNumber > endLineNumber) throw new ArgumentException("start must be a line before end"); CollapsedLineSection section = new CollapsedLineSection(this, start, end); - AddCollapsedSection(section, length); + root.AddCollapsedSection(startLineNumber - 1, endLineNumber - 1, section); + //Debug.WriteLine(GetTreeAsString()); #if DEBUG CheckProperties(); #endif @@ -560,558 +270,41 @@ public CollapsedLineSection CollapseText(DocumentLine start, DocumentLine end) #region LineCount & TotalHeight public int LineCount { get { - return root.totalCount; + return root.LineCount; } } public double TotalHeight { get { - return root.totalHeight; + return root.TotalHeight; } } #endregion #region GetAllCollapsedSections - IEnumerable AllNodes { - get { - if (root != null) { - HeightTreeNode node = root.LeftMost; - while (node != null) { - yield return node; - node = node.Successor; - } - } - } - } - internal IEnumerable GetAllCollapsedSections() { - List emptyCSList = new List(); - return System.Linq.Enumerable.Distinct( - System.Linq.Enumerable.SelectMany( - AllNodes, node => System.Linq.Enumerable.Concat(node.lineNode.collapsedSections ?? emptyCSList, - node.collapsedSections ?? emptyCSList) - )); + return root?.GetAllCollapsedSections(HeightTreeNode.EventKind.Start) ?? Enumerable.Empty(); } #endregion #region CheckProperties #if DEBUG [Conditional("DATACONSISTENCYTEST")] - internal void CheckProperties() - { - CheckProperties(root); - - foreach (CollapsedLineSection cs in GetAllCollapsedSections()) { - Debug.Assert(GetNode(cs.Start).lineNode.collapsedSections.Contains(cs)); - Debug.Assert(GetNode(cs.End).lineNode.collapsedSections.Contains(cs)); - int endLine = cs.End.LineNumber; - for (int i = cs.Start.LineNumber; i <= endLine; i++) { - CheckIsInSection(cs, GetLineByNumber(i)); - } - } - - // check red-black property: - int blackCount = -1; - CheckNodeProperties(root, null, RED, 0, ref blackCount); - } - - void CheckIsInSection(CollapsedLineSection cs, DocumentLine line) + void CheckProperties() { - HeightTreeNode 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); + root?.CheckInvariant(true, 1); } - void CheckProperties(HeightTreeNode node) - { - int totalCount = 1; - double 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)); - } - - /// - /// Checks that all elements in list1 are contained in list2. - /// - static void CheckAllContainedIn(IEnumerable list1, ICollection list2) - { - if (list1 == null) list1 = new List(); - if (list2 == null) list2 = new List(); - foreach (CollapsedLineSection 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.) - */ - 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")] - public string GetTreeAsString() + internal string GetTreeAsString() { StringBuilder b = new StringBuilder(); - AppendTreeToString(root, b, 0); + root?.AppendTreeToString(b, 0, 1); return b.ToString(); } - - 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 - const bool RED = true; - const bool BLACK = false; - - void InsertAsLeft(HeightTreeNode parentNode, HeightTreeNode newNode) - { - Debug.Assert(parentNode.left == null); - parentNode.left = newNode; - newNode.parent = parentNode; - newNode.color = RED; - UpdateAfterChildrenChange(parentNode); - FixTreeOnInsert(newNode); - } - - void InsertAsRight(HeightTreeNode parentNode, HeightTreeNode newNode) - { - Debug.Assert(parentNode.right == null); - parentNode.right = newNode; - newNode.parent = parentNode; - newNode.color = RED; - UpdateAfterChildrenChange(parentNode); - FixTreeOnInsert(newNode); - } - - 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); - - HeightTreeNode 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 - HeightTreeNode grandparentNode = parentNode.parent; - HeightTreeNode 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); - } - } - - void RemoveNode(HeightTreeNode removedNode) - { - if (removedNode.left != null && removedNode.right != null) { - // replace removedNode with it's in-order successor - - HeightTreeNode leftMost = removedNode.right.LeftMost; - HeightTreeNode 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 - HeightTreeNode parentNode = removedNode.parent; - HeightTreeNode 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); - } - } - } - - void FixTreeOnDelete(HeightTreeNode node, HeightTreeNode parentNode) - { - Debug.Assert(node == null || node.parent == parentNode); - if (parentNode == null) - return; - - // warning: node may be null - HeightTreeNode 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); - } - } - - 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; - } - - void RotateLeft(HeightTreeNode p) - { - // let q be p's right child - HeightTreeNode 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); - } - - void RotateRight(HeightTreeNode p) - { - // let q be p's left child - HeightTreeNode 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); - } - - static HeightTreeNode Sibling(HeightTreeNode node) - { - if (node == node.parent.left) - return node.parent.right; - else - return node.parent.left; - } - - 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; - } - - static bool GetColor(HeightTreeNode node) - { - return node != null ? node.color : BLACK; - } - #endregion - - #region Collapsing support - 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); - } - - void AddRemoveCollapsedSection(CollapsedLineSection section, int sectionLength, bool add) - { - Debug.Assert(sectionLength > 0); - - HeightTreeNode 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 - HeightTreeNode 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); - } - - 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); - } - } - - public void Uncollapse(CollapsedLineSection section) - { - int 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 } } diff --git a/ICSharpCode.AvalonEdit/Rendering/HeightTreeInnerNode.cs b/ICSharpCode.AvalonEdit/Rendering/HeightTreeInnerNode.cs new file mode 100644 index 00000000..a432320a --- /dev/null +++ b/ICSharpCode.AvalonEdit/Rendering/HeightTreeInnerNode.cs @@ -0,0 +1,607 @@ +// 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. +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + sealed class HeightTreeInnerNode : HeightTreeNode + { + internal const int MaxChildCount = 16; + // Must be at least 3 (it must be more than MinChildCount) + // Must be at most 16 due to the 'collapsed' bitfield. + // Must be at most 255 because we use type byte in various places. + // Must be even to simplify the insertion code (splitting evenly) + + internal const int MinChildCount = (MaxChildCount + 1) / 2; + // Must be at least 2 to avoid the degenerate trees + // Must be at most MaxChildCount/2 (rounded up) to allow node merging + + struct AggregatedData + { + // C# requires fixed-size arrays to appear only in structs + internal unsafe fixed double totalHeights[MaxChildCount]; + internal unsafe fixed int lineCounts[MaxChildCount]; + // only indices 0..childCount-1 are valid + } + AggregatedData data; + internal readonly HeightTreeNode?[] children = new HeightTreeNode?[MaxChildCount]; + // invariant: children[0..childCount-1] are non-null + + public static HeightTreeInnerNode NewRoot(HeightTreeNode a, HeightTreeNode b) + { + var root = new HeightTreeInnerNode(); + root.children[0] = a; + root.children[1] = b; + root.childCount = 2; + a.parent = root; + a.indexInParent = 0; + b.parent = root; + b.indexInParent = 1; + root.UpdateChild(0); + root.UpdateChild(1); + root.collapsed = root.RecomputeCollapsedBits(); + return root; + } + + // Gets the index of the child that contains the specified line. + internal int FindChildForLine(int line, out int lineInChild) + { + Debug.Assert(line >= 0); + unsafe { + for (int i = 0; i < childCount; i++) { + if (line < data.lineCounts[i]) { + lineInChild = line; + return i; + } + line -= data.lineCounts[i]; + } + // line is out of range, this might happen when inserting a line at the end of the document + Debug.Assert(line == 0); + if (childCount == 0) { + throw new InvalidOperationException("childCount==0"); + } + // return the last child + lineInChild = line + data.lineCounts[childCount - 1]; + return childCount - 1; + } + } + + // Gets the index of the child that contains the specified visual position. + // Post-condition: 0 <= result < childCount + internal int FindChildForVisualPosition(double position, out double positionInChild) + { + Debug.Assert(childCount >= 1); + double totalHeight = 0; + unsafe { // using the safety invariant that childCount<=MaxChildCount + for (int i = 0; i < childCount; i++) { + if ((collapsed & (1 << i)) != 0) + continue; + double newTotalHeight = totalHeight + data.totalHeights[i]; + if (position < newTotalHeight) { + positionInChild = position - totalHeight; + return i; + } + totalHeight = newTotalHeight; + } + // Not Found: Can happen when position>totalHeight, + // i.e. at the end of the document, or due to rounding errors. + // In this case, return the last non-collapsed child. + for (int i = childCount - 1; i >= 0; i--) { + if (data.totalHeights[i] > 0 && (collapsed & (1 << i)) == 0) { + positionInChild = data.totalHeights[i]; + return i; + } + } + // If all children are collapsed, return the first child. + positionInChild = data.totalHeights[0]; + return 0; + } + } + + internal override int LineCount => GetTotalLineCountUntilChildIndex(childCount); + + internal int GetTotalLineCountUntilChildIndex(int childIndex) + { + // To avoid memory unsafety this is not just an assertion, but a runtime check. + if (childIndex < 0 || childIndex > childCount) + throw new ArgumentOutOfRangeException(nameof(childIndex)); + int totalCount = 0; + unsafe { // using the safety invariant that childCount<=MaxChildCount + for (int i = 0; i < childIndex; i++) { + totalCount += data.lineCounts[i]; + } + } + return totalCount; + } + + internal override double TotalHeight => GetTotalHeightUntilChildIndex(childCount); + + internal double GetTotalHeightUntilChildIndex(int childIndex) + { + // To avoid memory unsafety this is not just an assertion, but a runtime check. + if (childIndex < 0 || childIndex > childCount) + throw new ArgumentOutOfRangeException(nameof(childIndex)); + double totalHeight = 0; + unsafe { + for (int i = 0; i < childIndex; i++) { + if ((collapsed & (1 << i)) == 0) { + totalHeight += data.totalHeights[i]; + } + } + } + return totalHeight; + } + + internal override void SetHeight(int line, double val) + { + int lineInChild; + int childIndex = FindChildForLine(line, out lineInChild); + children[childIndex]!.SetHeight(lineInChild, val); + unsafe { // index already validated by array access above + data.totalHeights[childIndex] = children[childIndex]!.TotalHeight; + } + } + + internal override void UpdateHeight(double oldValue, double newValue) + { + for (int i = 0; i < childCount; i++) { + children[i]!.UpdateHeight(oldValue, newValue); + unsafe { // index already validated by array access above + data.totalHeights[i] = children[i]!.TotalHeight; + } + } + } + + internal override bool GetIsCollapsed(int line) + { + int childIndex = FindChildForLine(line, out int lineInChild); + if ((collapsed & (1 << childIndex)) != 0) { + return true; + } else { + return children[childIndex]!.GetIsCollapsed(lineInChild); + } + } + + internal override HeightTreeNode? InsertLine(int line, double height) + { + int childIndex = FindChildForLine(line, out int lineInChild); + HeightTreeNode? newChild = children[childIndex]!.InsertLine(lineInChild, height); + bool needRecomputeCollapsed = UpdateChild(childIndex); + HeightTreeInnerNode? newSibling = null; + if (newChild != null) { + // child was split, insert newChild into this node + if (childCount == MaxChildCount) { + // this node is full, split it + newSibling = new HeightTreeInnerNode(); + int splitIndex = MaxChildCount / 2; + newSibling.StealFromPredecessor(this, childCount - splitIndex); + // insert newChild into this node or newSibling + if (childIndex < splitIndex) { + needRecomputeCollapsed |= InsertChild(childIndex + 1, newChild); + } else { + newSibling.InsertChild(childIndex + 1 - splitIndex, newChild); + newSibling.collapsed = newSibling.RecomputeCollapsedBits(); + } + } else { + // this node is not full, insert newChild + needRecomputeCollapsed |= InsertChild(childIndex + 1, newChild); + } + } + if (needRecomputeCollapsed) { + collapsed = RecomputeCollapsedBits(); + } + return newSibling; + } + + // Updates information cached from the child node. + // Returns whether the collapsed bits will need to be recomputed. + bool UpdateChild(int childIndex) + { + Debug.Assert(0 <= childIndex && childIndex < childCount); + var child = children[childIndex]!; + Debug.Assert(child.parent == this && child.indexInParent == childIndex); + unsafe { // index already validated by array access above + data.totalHeights[childIndex] = child.TotalHeight; + data.lineCounts[childIndex] = child.LineCount; + } + // First, clear out the events belonging to this child so that we can propagate + // them again from scratch. + bool needRecomputeCollapsed = false; + int remainingEvents = 0; + if (events != null) { + for (int i = 0; i < events.Length; i++) { + if (events[i].Position == childIndex) { + events[i].Section = null; + needRecomputeCollapsed = true; + } else if (events[i].Section != null) { + remainingEvents++; + } + } + } + // Propagate events from child to this node. + if (child.events != null) { + int outputIndex = 0; + foreach (Event e in child.events) { + if (e.Section == null) + continue; + // Ignore events if the partner event is contained in the same child. + if (e.Kind == EventKind.Start) { + if (e.Section.EndIsWithin(child, out _)) + continue; + } else { + Debug.Assert(e.Kind == EventKind.End); + if (e.Section.StartIsWithin(child, out _)) + continue; + } + InsertEvent(ref outputIndex, new Event { + Kind = e.Kind, + Section = e.Section, + Position = (byte)childIndex + }); + needRecomputeCollapsed = true; + remainingEvents++; + } + } + CompactifyEvents(remainingEvents); + return needRecomputeCollapsed; + } + + internal void UpdateHeight(int childIndex) + { + Debug.Assert(0 <= childIndex && childIndex < childCount); + var child = children[childIndex]!; + Debug.Assert(child.parent == this && child.indexInParent == childIndex); + unsafe { // index already validated by array access above + data.totalHeights[childIndex] = child.TotalHeight; + } + } + + internal bool InsertChild(int childIndex, HeightTreeNode newChild) + { + MakeGapForInsertion(childIndex, 1); + children[childIndex] = newChild; + newChild.parent = this; + newChild.indexInParent = (byte)childIndex; + return UpdateChild(childIndex); + } + + void MakeGapForInsertion(int childIndex, int amount) + { + // Move all children >=childIndex up by amount. + Debug.Assert(0 <= childIndex && childIndex <= childCount); + Debug.Assert(childCount + amount <= MaxChildCount); + for (int i = childCount - 1; i >= childIndex; i--) { + children[i + amount] = children[i]; + unsafe { // index already validated by array access above + data.totalHeights[i + amount] = data.totalHeights[i]; + data.lineCounts[i + amount] = data.lineCounts[i]; + } + children[i + amount]!.indexInParent = (byte)(i + amount); + } + childCount += (byte)amount; + AdjustEventPositions(childIndex, amount, deleteAffectedEvents: false); + } + + internal override DeletionResults DeleteLine(int line, HeightTreeNode? predecessor, HeightTreeNode? successor) + { + int childIndex = FindChildForLine(line, out int lineInChild); + HeightTreeInnerNode? predecessorParent; + int predecessorChildIndex; + if (childIndex > 0) { + predecessorParent = this; + predecessorChildIndex = childIndex - 1; + } else { + predecessorParent = (HeightTreeInnerNode?)predecessor; + predecessorChildIndex = predecessorParent?.childCount - 1 ?? 0; + } + HeightTreeInnerNode? successorParent; + int successorChildIndex; + if (childIndex < childCount - 1) { + successorParent = this; + successorChildIndex = childIndex + 1; + } else { + successorParent = (HeightTreeInnerNode?)successor; + successorChildIndex = 0; + } + DeletionResults childResults = children[childIndex]!.DeleteLine(lineInChild, predecessorParent?.children[predecessorChildIndex], successorParent?.children[successorChildIndex]); + DeletionResults results = DeletionResults.None; + bool needsRecomputeCollapsed = false; + if ((childResults & DeletionResults.PredecessorChanged) != 0) { + bool predRecomputeCollapsed = predecessorParent!.UpdateChild(predecessorChildIndex); + if (predecessorParent == predecessor) { + results |= DeletionResults.PredecessorChanged; + if (predRecomputeCollapsed) { + predecessor.collapsed = predecessor.RecomputeCollapsedBits(); + } + } else { + needsRecomputeCollapsed |= predRecomputeCollapsed; + } + } + if ((childResults & DeletionResults.SuccessorChanged) != 0) { + bool succRecomputeCollapsed = successorParent!.UpdateChild(successorChildIndex); + if (successorParent == successor) { + results |= DeletionResults.SuccessorChanged; + if (succRecomputeCollapsed) { + successor.collapsed = successor.RecomputeCollapsedBits(); + } + } else { + needsRecomputeCollapsed |= succRecomputeCollapsed; + } + } + if ((childResults & DeletionResults.NodeDeleted) != 0) { + Debug.Assert(children[childIndex]!.LineCount == 0); // child must be empty + PerformDeletion(childIndex, childIndex + 1); + needsRecomputeCollapsed = true; + // After removing a child from this inner node, it is possible that we need to rebalance the inner nodes + if (childCount < MinChildCount) { + var prev = (HeightTreeInnerNode?)predecessor; + var next = (HeightTreeInnerNode?)successor; + // Try to steal lines from our siblings + if (prev != null && prev.childCount > MinChildCount && prev.childCount > (next?.childCount ?? 0)) { + StealFromPredecessor(prev, (prev.childCount - MinChildCount + 1) / 2); + results |= DeletionResults.PredecessorChanged; + Debug.Assert(childCount >= MinChildCount); + Debug.Assert(prev.childCount >= MinChildCount); + // StealFromPredecessor already recomputed 'collapsed' + needsRecomputeCollapsed = false; + } else if (next != null && next.childCount > MinChildCount) { + StealFromSuccessor(next, (next.childCount - MinChildCount + 1) / 2); + results |= DeletionResults.SuccessorChanged; + Debug.Assert(childCount >= MinChildCount); + Debug.Assert(next.childCount >= MinChildCount); + // StealFromPredecessor already recomputed 'collapsed' + needsRecomputeCollapsed = false; + } else if (prev != null) { + // Merge into predecessor + prev.StealFromSuccessor(this, childCount); + results |= DeletionResults.PredecessorChanged | DeletionResults.NodeDeleted; + // Don't need to recompute collapsed for a node about to be deleted + needsRecomputeCollapsed = false; + } else if (next != null) { + // Merge into successor + next.StealFromPredecessor(this, childCount); + results |= DeletionResults.SuccessorChanged | DeletionResults.NodeDeleted; + // Don't need to recompute collapsed for a node about to be deleted + needsRecomputeCollapsed = false; + } + } + } else { + needsRecomputeCollapsed |= UpdateChild(childIndex); + } + if (needsRecomputeCollapsed) { + collapsed = RecomputeCollapsedBits(); + } + return results; + } + + private void PerformDeletion(int start, int end) + { + Debug.Assert(0 <= start && start <= end && end <= childCount); + int length = end - start; + childCount -= (byte)length; + for (int i = start; i < childCount; i++) { + children[i] = children[i + length]; + unsafe { // index already validated by array access above + data.totalHeights[i] = data.totalHeights[i + length]; + data.lineCounts[i] = data.lineCounts[i + length]; + } + children[i]!.indexInParent = (byte)i; + } + for (int i = 0; i < length; i++) { + children[childCount + i] = null; + } + AdjustEventPositions(start, -length, deleteAffectedEvents: true); + } + + void StealFromPredecessor(HeightTreeInnerNode prev, int childrenToMove) + { + if (childrenToMove > prev.childCount || childCount + childrenToMove > MaxChildCount) + throw new ArgumentOutOfRangeException(nameof(childrenToMove)); + MakeGapForInsertion(0, childrenToMove); + // steal children + for (int i = 0; i < childrenToMove; i++) { + children[i] = prev.children[prev.childCount - childrenToMove + i]; + unsafe { // index already validated by array access above + data.totalHeights[i] = prev.data.totalHeights[prev.childCount - childrenToMove + i]; + data.lineCounts[i] = prev.data.lineCounts[prev.childCount - childrenToMove + i]; + } + children[i]!.parent = this; + children[i]!.indexInParent = (byte)i; + } + StealEvents(prev, prev.childCount - childrenToMove, prev.childCount, 0); + // update child count + prev.childCount -= (byte)childrenToMove; + for (int i = 0; i < childrenToMove; i++) { + prev.children[prev.childCount + i] = null; + } + // Because 'collapsed' only considers events local to the node, and we might + // have moved a collapsed section start from the predecessor to this node, + // this might also change the 'collapsed' status of the existing lines within + // this node. So fully recompute to be safe. + collapsed = RecomputeCollapsedBits(); + prev.collapsed = prev.RecomputeCollapsedBits(); + } + + void StealFromSuccessor(HeightTreeInnerNode next, int childrenToMove) + { + if (childrenToMove > next.childCount || childCount + childrenToMove > MaxChildCount) + throw new ArgumentOutOfRangeException(nameof(childrenToMove)); + // steal children + for (int i = 0; i < childrenToMove; i++) { + children[childCount + i] = next.children[i]; + unsafe { // index already validated by array access above + data.totalHeights[childCount + i] = next.data.totalHeights[i]; + data.lineCounts[childCount + i] = next.data.lineCounts[i]; + } + children[childCount + i]!.parent = this; + children[childCount + i]!.indexInParent = (byte)(childCount + i); + } + StealEvents(next, 0, childrenToMove, childCount); + // update child count + childCount += (byte)childrenToMove; + next.PerformDeletion(0, childrenToMove); + collapsed = RecomputeCollapsedBits(); + next.collapsed = next.RecomputeCollapsedBits(); + } + + internal void RebalanceLastChild() + { + // special rebalancing when building a new tree in RebuildDocument + // all nodes except the last are guaranteed to be full + // the last node itself might be empty + Debug.Assert(childCount >= 2); + var lastChild = children[childCount - 1]!; + if (lastChild is HeightTreeInnerNode lastInner) { + if (lastInner.childCount < HeightTreeInnerNode.MinChildCount) { + var prevInner = (HeightTreeInnerNode)children[childCount - 2]!; + int balancedCount = (prevInner.childCount + lastInner.childCount) / 2; + lastInner.StealFromPredecessor(prevInner, balancedCount - lastInner.childCount); + UpdateChild(childCount - 2); + } + lastInner.RebalanceLastChild(); + UpdateChild(childCount - 1); + } else { + var lastLeaf = (HeightTreeLeafNode)lastChild; + if (lastLeaf.LineCount < HeightTreeLeafNode.MinLineCount) { + var prevLeaf = (HeightTreeLeafNode)children[childCount - 2]!; + int balancedCount = (prevLeaf.LineCount + lastLeaf.LineCount) / 2; + lastLeaf.StealFromPredecessor(prevLeaf, balancedCount - lastLeaf.LineCount); + UpdateChild(childCount - 2); + UpdateChild(childCount - 1); + } + } + collapsed = RecomputeCollapsedBits(); + } + + internal override void AddCollapsedSection(int start, int end, CollapsedLineSection section) + { + Debug.Assert(start <= end); // start+end are both inclusive + int lineCount = this.LineCount; + bool startsHere = (0 <= start && start < lineCount); + bool endsHere = (0 <= end && end < lineCount); + Debug.Assert(startsHere || endsHere); + int outputIndex = 0; + if (startsHere && endsHere) { + int startIndex = FindChildForLine(start, out int startInChild); + int endIndex = FindChildForLine(end, out int endInChild); + if (startIndex == endIndex) { + // collapsed section can be fully handled by our child node + children[startIndex]!.AddCollapsedSection(startInChild, endInChild, section); + UpdateHeight(startIndex); + return; + } + Debug.Assert(startIndex < endIndex); + children[startIndex]!.AddCollapsedSection(startInChild, end + (startInChild - start), section); + UpdateHeight(startIndex); + InsertEvent(ref outputIndex, new Event { + Section = section, + Kind = EventKind.Start, + Position = (byte)startIndex + }); + children[endIndex]!.AddCollapsedSection(start + (endInChild - end), endInChild, section); + UpdateHeight(endIndex); + InsertEvent(ref outputIndex, new Event { + Section = section, + Kind = EventKind.End, + Position = (byte)endIndex + }); + } else if (startsHere) { + int startIndex = FindChildForLine(start, out int startInChild); + children[startIndex]!.AddCollapsedSection(startInChild, end + (startInChild - start), section); + UpdateHeight(startIndex); + InsertEvent(ref outputIndex, new Event { + Section = section, + Kind = EventKind.Start, + Position = (byte)startIndex + }); + } else { + int endIndex = FindChildForLine(end, out int endInChild); + children[endIndex]!.AddCollapsedSection(start + (endInChild - end), endInChild, section); + UpdateHeight(endIndex); + InsertEvent(ref outputIndex, new Event { + Section = section, + Kind = EventKind.End, + Position = (byte)endIndex + }); + } + collapsed = RecomputeCollapsedBits(); + } + + internal override IEnumerable GetAllCollapsedSections(EventKind kind) + { + for (int i = 0; i < childCount; i++) { + foreach (var section in children[i]!.GetAllCollapsedSections(kind)) + yield return section; + } + } + +#if DEBUG + internal override void AppendTreeToString(System.Text.StringBuilder b, int indent, int lineNumber) + { + b.AppendFormat("inner (childCount={0}, LineCount={1}, TotalHeight={2}, collapsed={3:x})", childCount, LineCount, TotalHeight, collapsed); + b.AppendLine(); + indent += 2; + unsafe { + for (int i = 0; i < childCount; i++) { + b.Append(' ', indent); + b.Append($"[{i}] "); + if (children[i] == null) { + b.AppendLine("null"); + } else { + children[i]!.AppendTreeToString(b, indent, lineNumber); + } + lineNumber += data.lineCounts[i]; + } + } + AppendEventsToString(b, indent); + } + + internal override void CheckInvariant(bool isRoot, int lineNumber) + { + base.CheckInvariant(isRoot, lineNumber); + Debug.Assert(childCount <= MaxChildCount); + if (isRoot) { + Debug.Assert(childCount >= 2); + } else { + Debug.Assert(childCount >= MinChildCount); + } + int lineNumberInChild = lineNumber; + for (int i = 0; i < childCount; i++) { + children[i]!.CheckInvariant(false, lineNumberInChild); + unsafe { + lineNumberInChild += data.lineCounts[i]; + Debug.Assert(children[i]!.TotalHeight == data.totalHeights[i]); + Debug.Assert(children[i]!.LineCount == data.lineCounts[i]); + } + } + foreach (var e in events ?? Array.Empty()) { + if (e.Section != null) { + Debug.Assert(e.Position < childCount); + var eventLine = e.Kind == EventKind.Start ? e.Section.Start : e.Section.End; + int start = lineNumber + GetTotalLineCountUntilChildIndex(e.Position); + int end = lineNumber + GetTotalLineCountUntilChildIndex(e.Position + 1); + Debug.Assert(start <= eventLine!.LineNumber && eventLine!.LineNumber < end); + } + } + } +#endif + } +} diff --git a/ICSharpCode.AvalonEdit/Rendering/HeightTreeLeafNode.cs b/ICSharpCode.AvalonEdit/Rendering/HeightTreeLeafNode.cs new file mode 100644 index 00000000..8cf74c6c --- /dev/null +++ b/ICSharpCode.AvalonEdit/Rendering/HeightTreeLeafNode.cs @@ -0,0 +1,408 @@ +// 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. +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + sealed class HeightTreeLeafNode : HeightTreeNode + { + internal const int MaxLineCount = 16; + // Must be at least 3 (it must be more than MinChildCount) + // Must be at most 16 due to the 'collapsed' bitfield. + // Must be at most 255 because we use type byte in various places. + // Must be even to simplify the insertion code (splitting evenly) + + internal const int MinLineCount = (MaxLineCount + 1) / 2; + // Must be at least 2 to avoid the degenerate trees + // Must be at most MaxChildCount/2 (rounded up) to allow node merging + + struct LineData + { + // C# requires fixed-size arrays to appear only in structs + internal unsafe fixed double heights[MaxLineCount]; + // only indices 0..lineCount-1 are valid + } + LineData data; + + internal static HeightTreeLeafNode Create(int lineCount, double defaultLineHeight) + { + if (lineCount > MaxLineCount) + throw new ArgumentOutOfRangeException(nameof(lineCount)); + HeightTreeLeafNode leaf = new HeightTreeLeafNode(); + unsafe { + for (int i = 0; i < lineCount; i++) { + leaf.data.heights[i] = defaultLineHeight; + } + leaf.childCount = (byte)lineCount; + } + return leaf; + } + + internal override int LineCount => childCount; + + internal override double TotalHeight => GetTotalHeightUntilChildIndex(childCount); + + internal double GetTotalHeightUntilChildIndex(int childIndex) + { + // To avoid memory unsafety this is not just an assertion, but a runtime check. + if (childIndex < 0 || childIndex > childCount) + throw new ArgumentOutOfRangeException(nameof(childIndex)); + double totalHeight = 0; + unsafe { // using the safety invariant that childIndex<=lineCount<=MaxLineCount + for (int i = 0; i < childIndex; i++) { + if ((collapsed & (1 << i)) == 0) { + totalHeight += data.heights[i]; + } + } + } + return totalHeight; + } + + internal int FindChildForVisualPosition(double position) + { + double totalHeight = 0; + unsafe { // using the safety invariant that lineCount<=MaxLineCount + for (int i = 0; i < childCount; i++) { + if ((collapsed & (1 << i)) == 0) { + totalHeight += data.heights[i]; + if (position < totalHeight) + return i; + } + } + } + // Not Found: Can happen when position>totalHeight, + // i.e. at the end of the document, or due to rounding errors. + // In this case, return the last non-collapsed child. + for (int i = childCount - 1; i >= 0; i--) { + if ((collapsed & (1 << i)) == 0) { + return i; + } + } + // If all children are collapsed, return the first child. + return 0; + } + + public double GetHeight(int line) + { + // To avoid memory unsafety this is not just an assertion, but a runtime check. + if (line < 0 || line >= childCount) + throw new ArgumentOutOfRangeException(nameof(line)); + unsafe { + return data.heights[line]; + } + } + + internal override void SetHeight(int line, double val) + { + // To avoid memory unsafety this is not just an assertion, but a runtime check. + if (line < 0 || line >= childCount) + throw new ArgumentOutOfRangeException(nameof(line)); + unsafe { + data.heights[line] = val; + } + } + + internal override void UpdateHeight(double oldValue, double newValue) + { + unsafe { // using the safety invariant that lineCount<=MaxLineCount + for (int i = 0; i < childCount; i++) { + if (data.heights[i] == oldValue) { + data.heights[i] = newValue; + } + } + } + } + + internal override bool GetIsCollapsed(int line) + { + Debug.Assert(line >= 0 && line < childCount); + return (collapsed & (1 << line)) != 0; + } + + internal unsafe override HeightTreeNode? InsertLine(int line, double height) + { + // To avoid memory unsafety this is not just an assertion, but a runtime check. + if (line < 0 || line > childCount) + throw new ArgumentOutOfRangeException(nameof(line)); + HeightTreeLeafNode? newLeaf = null; + if (childCount == MaxLineCount) { + // split leaf node + newLeaf = new HeightTreeLeafNode(); + int splitIndex = MaxLineCount / 2; + newLeaf.StealFromPredecessor(this, childCount - splitIndex); + // now an insertion will be possible without splitting + if (line >= splitIndex) { + newLeaf.InsertLine(line - splitIndex, height); + return newLeaf; + } + } + MakeGapForInsertion(line, 1); + // insert new line + data.heights[line] = height; + return newLeaf; + } + + private void MakeGapForInsertion(int line, int amount) + { + // Move all elements >=line up by amount. + Debug.Assert(line >= 0 && line <= childCount); + if (childCount + amount > MaxLineCount) { + throw new ArgumentOutOfRangeException(nameof(amount)); + } + unsafe { // checked in if above + for (int i = childCount - 1; i >= line; i--) { + data.heights[i + amount] = data.heights[i]; + } + } + childCount += (byte)amount; + AdjustEventPositions(line, amount, deleteAffectedEvents: false); + } + + internal override DeletionResults DeleteLine(int line, HeightTreeNode? predecessor, HeightTreeNode? successor) + { + // To avoid memory unsafety this is not just an assertion, but a runtime check. + if (line < 0 || line >= childCount) + throw new ArgumentOutOfRangeException(nameof(line)); + DeletionResults results = AdjustEventsOnLine(line, predecessor, successor); + PerformDeletion(line, line + 1); + if (childCount >= MinLineCount) { + return results; + } + var prev = (HeightTreeLeafNode?)predecessor; + var next = (HeightTreeLeafNode?)successor; + // Try to steal lines from our siblings + if (prev != null && prev.childCount > MinLineCount && prev.childCount > (next?.childCount ?? 0)) { + StealFromPredecessor(prev, (prev.childCount - MinLineCount + 1) / 2); + Debug.Assert(childCount >= MinLineCount); + Debug.Assert(prev.childCount >= MinLineCount); + results |= DeletionResults.PredecessorChanged; + } else if (next != null && next.childCount > MinLineCount) { + StealFromSuccessor(next, (next.childCount - MinLineCount + 1) / 2); + Debug.Assert(childCount >= MinLineCount); + Debug.Assert(next.childCount >= MinLineCount); + results |= DeletionResults.SuccessorChanged; + } else if (prev != null) { + // Merge into predecessor + prev.StealFromSuccessor(this, childCount); + results |= DeletionResults.PredecessorChanged | DeletionResults.NodeDeleted; + } else if (next != null) { + // Merge into successor + next.StealFromPredecessor(this, childCount); + results |= DeletionResults.SuccessorChanged | DeletionResults.NodeDeleted; + } + return results; + } + + private DeletionResults AdjustEventsOnLine(int line, HeightTreeNode? predecessor, HeightTreeNode? successor) + { + // Handle sections that start or end directly on line + if (events == null) + return DeletionResults.None; + DeletionResults results = DeletionResults.None; + List? removedSections = null; + int successorOutputIndex = 0; + int predecessorOutputIndex = 0; + for (int i = 0; i < events.Length; i++) { + ref var ev = ref events[i]; + if (ev.Position != line || ev.Section == null) + continue; + // This section starts or ends directly on line, so we need to move it. + var section = ev.Section; + if (section.Start == section.End) { + // This is a single-line section, so we uncollapse it completely. + // Because both start and end are local to this node, we can just remove the section + // without going through the full Uncollapse() logic. + ev.Section = null; + // We can't call section.Reset() yet because we first need to delete the other event. + removedSections ??= new List(); + removedSections.Add(section); + } else if (ev.Kind == EventKind.Start) { + section.Start = section.Start!.NextLine; + if (line + 1 < childCount) { + ev.Position++; + section.startIndexInLeaf = ev.Position; + } else { + // Move event to successor + successor!.InsertEvent(ref successorOutputIndex, new Event { + Position = 0, + Kind = EventKind.Start, + Section = section + }); + section.startLeaf = (HeightTreeLeafNode)successor; + section.startIndexInLeaf = 0; + ev.Section = null; + successor.collapsed = successor.RecomputeCollapsedBits(); + results |= DeletionResults.SuccessorChanged; + } + } else { + section.End = section.End!.PreviousLine; + if (line > 0) { + ev.Position--; + section.endIndexInLeaf = ev.Position; + } else { + // Move event to predecessor + predecessor!.InsertEvent(ref predecessorOutputIndex, new Event { + Position = (byte)(predecessor.childCount - 1), + Kind = EventKind.End, + Section = section + }); + section.endLeaf = (HeightTreeLeafNode)predecessor; + section.endIndexInLeaf = (byte)(predecessor.childCount - 1); + ev.Section = null; + predecessor.collapsed = predecessor.RecomputeCollapsedBits(); + results |= DeletionResults.PredecessorChanged; + } + } + } + // Note: we don't need to update the collapsed bits of this node, because the changed line + // is about to be deleted. + if (removedSections != null) { + foreach (var section in removedSections) { + section.Reset(); + } + } + return results; + } + + private void PerformDeletion(int start, int end) + { + Debug.Assert(0 <= start && start <= end && end <= childCount); + // shift data to remove the lines + byte length = (byte)(end - start); + childCount -= length; + unsafe { + for (int i = start; i < childCount; i++) { + data.heights[i] = data.heights[i + length]; + } + } + AdjustEventPositions(start, -length, deleteAffectedEvents: false); + } + + internal unsafe void StealFromPredecessor(HeightTreeLeafNode prev, int linesToMove) + { + if (linesToMove > prev.childCount || childCount + linesToMove > MaxLineCount) + throw new ArgumentOutOfRangeException(nameof(linesToMove)); + MakeGapForInsertion(0, linesToMove); + // steal lines + for (int i = 0; i < linesToMove; i++) { + data.heights[i] = prev.data.heights[prev.childCount - linesToMove + i]; + } + StealEvents(prev, prev.childCount - linesToMove, prev.childCount, 0); + // update line count + prev.childCount -= (byte)linesToMove; + // Because 'collapsed' only considers events local to the node, and we might + // have moved a collapsed section start from the predecessor to this node, + // this might also change the 'collapsed' status of the existing lines within + // this node. So fully recompute to be safe. + collapsed = RecomputeCollapsedBits(); + prev.collapsed = prev.RecomputeCollapsedBits(); + } + + internal unsafe void StealFromSuccessor(HeightTreeLeafNode next, int linesToMove) + { + if (linesToMove > next.childCount || childCount + linesToMove > MaxLineCount) + throw new ArgumentOutOfRangeException(nameof(linesToMove)); + // steal lines + for (int i = 0; i < linesToMove; i++) { + data.heights[childCount + i] = next.data.heights[i]; + } + StealEvents(next, 0, linesToMove, childCount); + // update line count + childCount += (byte)linesToMove; + next.PerformDeletion(0, linesToMove); + collapsed = RecomputeCollapsedBits(); + next.collapsed = next.RecomputeCollapsedBits(); + } + + internal override void AddCollapsedSection(int start, int end, CollapsedLineSection section) + { + Debug.Assert(start <= end); // start+end are both inclusive + bool startsHere = (0 <= start && start < childCount); + bool endsHere = (0 <= end && end < childCount); + Debug.Assert(startsHere || endsHere); + events ??= new Event[2]; + int outputIndex = 0; + // prepend to linked lists + if (startsHere) { + section.startLeaf = this; + section.startIndexInLeaf = (byte)start; + InsertEvent(ref outputIndex, new Event { + Section = section, + Kind = EventKind.Start, + Position = (byte)start + }); + } + if (endsHere) { + section.endLeaf = this; + section.endIndexInLeaf = (byte)end; + InsertEvent(ref outputIndex, new Event { + Section = section, + Kind = EventKind.End, + Position = (byte)end + }); + } + collapsed = RecomputeCollapsedBits(); + } + + internal override IEnumerable GetAllCollapsedSections(EventKind kind) + { + if (events == null) + yield break; + foreach (Event e in events) { + if (e.Kind == kind && e.Section != null) + yield return e.Section; + } + } + +#if DEBUG + internal override void AppendTreeToString(System.Text.StringBuilder b, int indent, int lineNumber) + { + b.AppendFormat("leaf (LineCount={0}, TotalHeight={1})", childCount, TotalHeight); + b.AppendLine(); + unsafe { + for (int i = 0; i < childCount; i++) { + b.Append(' ', indent + 2); + b.AppendFormat("[{0}] @{1} height={2}, collapsed={3}", + i, lineNumber + i, data.heights[i], (collapsed & (1 << i)) != 0); + b.AppendLine(); + } + } + AppendEventsToString(b, indent + 2); + } + + internal override void CheckInvariant(bool isRoot, int lineNumber) + { + Debug.Assert(childCount <= MaxLineCount); + if (!isRoot) { + Debug.Assert(childCount >= MinLineCount); + } + base.CheckInvariant(isRoot, lineNumber); + foreach (var e in events ?? Array.Empty()) { + if (e.Section != null) { + Debug.Assert(e.Position < childCount); + var line = e.Kind == EventKind.Start ? e.Section.Start : e.Section.End; + Debug.Assert(line!.LineNumber == lineNumber + e.Position); + } + } + } +#endif + } +} \ No newline at end of file diff --git a/ICSharpCode.AvalonEdit/Rendering/HeightTreeLineNode.cs b/ICSharpCode.AvalonEdit/Rendering/HeightTreeLineNode.cs deleted file mode 100644 index a0150231..00000000 --- a/ICSharpCode.AvalonEdit/Rendering/HeightTreeLineNode.cs +++ /dev/null @@ -1,63 +0,0 @@ -// 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.Collections.Generic; -using System.Diagnostics; - -namespace ICSharpCode.AvalonEdit.Rendering -{ - struct HeightTreeLineNode - { - internal HeightTreeLineNode(double height) - { - this.collapsedSections = null; - this.height = height; - } - - internal double height; - internal List collapsedSections; - - internal bool IsDirectlyCollapsed { - get { return collapsedSections != null; } - } - - internal void AddDirectlyCollapsed(CollapsedLineSection section) - { - if (collapsedSections == null) - collapsedSections = new List(); - collapsedSections.Add(section); - } - - internal void RemoveDirectlyCollapsed(CollapsedLineSection section) - { - Debug.Assert(collapsedSections.Contains(section)); - collapsedSections.Remove(section); - if (collapsedSections.Count == 0) - collapsedSections = null; - } - - /// - /// Returns 0 if the line is directly collapsed, otherwise, returns . - /// - internal double TotalHeight { - get { - return IsDirectlyCollapsed ? 0 : height; - } - } - } -} diff --git a/ICSharpCode.AvalonEdit/Rendering/HeightTreeNode.cs b/ICSharpCode.AvalonEdit/Rendering/HeightTreeNode.cs index 8f752e0f..14be7e5d 100644 --- a/ICSharpCode.AvalonEdit/Rendering/HeightTreeNode.cs +++ b/ICSharpCode.AvalonEdit/Rendering/HeightTreeNode.cs @@ -15,155 +15,306 @@ // 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. +#nullable enable +using System; using System.Collections.Generic; using System.Diagnostics; - -using ICSharpCode.AvalonEdit.Document; +using System.Text; namespace ICSharpCode.AvalonEdit.Rendering { /// /// A node in the text view's height tree. /// - sealed class HeightTreeNode + abstract class HeightTreeNode { - internal readonly DocumentLine documentLine; - internal HeightTreeLineNode lineNode; + internal enum EventKind : byte { Start, End }; + internal struct Event + { + public CollapsedLineSection? Section; + public EventKind Kind; + public byte Position; + // In leaf nodes, Position is actual (inclusive) starts/end index. + // In inner nodes, Position is the child index of the leaf that contains the actual start/end. + // This means for the purpose of computing the `collapsed` bitfield, Position acts as + // exclusive start/end within inner nodes. + // In all cases, Position is the index of the node with which the event is associated + // for the purpose of moving when lines are inserted or deleted. + } + + internal HeightTreeInnerNode? parent; + + internal Event[]? events; + // Holds start/end events for collapsed sections. + // Inside leaf nodes, each collapsed section has exactly one start event and one end event. + // If the collapsed section starts and ends in different leaf nodes, + // it additionally has start+end events in the parent level of inner nodes. + // As long as these events are in different inner nodes, the section also has + // start+end events in the grandparent level, and so on. + + internal ushort collapsed; + // bit (1 << 0) = child 0, bit (1 << 1) = child 1, etc. + // Used to determine if the child should be excluded when computing the total height. + // Only considers CollapsedLineSections that start or end within this node. + + internal byte childCount; + // safety-critical invariant: 0 <= childCount <= Max...Count + // Usually childCount>=Min..Count, but it can be less during a deletion operation or for the root node. + // For leaf nodes, childCount is the number of lines stored in the node. + + internal byte indexInParent; + // invariant: parent is null or parent.children[indexInParent] == this + + /// + /// The total height of all lines in the node. + /// The height of a line is not counted if it is collapsed by a section + /// starting or ending in this node. + /// + internal abstract double TotalHeight { get; } + + /// + /// The number of lines represented by this node. + /// Collapsed lines are included in the count. + /// + internal abstract int LineCount { get; } - internal HeightTreeNode left, right, parent; - internal bool color; + internal abstract void SetHeight(int line, double val); + internal abstract void UpdateHeight(double oldValue, double newValue); - internal HeightTreeNode() + /// + /// Inserts a new line into the tree. + /// Returns null if the insertion was possible without splitting this node. + /// Otherwise, returns a new node that contains the lines that were split off. + /// + internal abstract HeightTreeNode? InsertLine(int line, double height); + [Flags] + internal enum DeletionResults : byte { + None = 0, + NodeDeleted = 1, // this node now is empty and needs to be deleted from its parent + PredecessorChanged = 2, // lines were moved from/to the predecessor node, parent needs to update aggregated information + SuccessorChanged = 4 // lines were moved from/to the successor node, parent needs to update aggregated information } + internal abstract DeletionResults DeleteLine(int line, HeightTreeNode? predecessor, HeightTreeNode? successor); + internal abstract bool GetIsCollapsed(int line); + internal abstract void AddCollapsedSection(int v, int length, CollapsedLineSection section); + internal abstract IEnumerable GetAllCollapsedSections(EventKind kind); - internal HeightTreeNode(DocumentLine documentLine, double height) +#if DEBUG + internal abstract void AppendTreeToString(StringBuilder b, int indent, int lineNumber); + protected void AppendEventsToString(StringBuilder b, int indent) { - this.documentLine = documentLine; - this.totalCount = 1; - this.lineNode = new HeightTreeLineNode(height); - this.totalHeight = height; + if (events != null) { + for (int i = 0; i < events.Length; i++) { + if (events[i].Section == null) + continue; + b.Append(' ', indent); + b.Append(events[i].Kind); + b.Append(' '); + b.Append(events[i].Position); + b.Append(' '); + b.Append(events[i].Section); + b.AppendLine(); + } + } } - internal HeightTreeNode LeftMost { - get { - HeightTreeNode node = this; - while (node.left != null) - node = node.left; - return node; - } + public override string ToString() + { + var b = new StringBuilder(); + AppendTreeToString(b, 0, 0); + return b.ToString(); } - internal HeightTreeNode RightMost { - get { - HeightTreeNode node = this; - while (node.right != null) - node = node.right; - return node; + internal virtual void CheckInvariant(bool isRoot, int lineNumber) + { + if (isRoot) { + Debug.Assert(parent == null); + } else { + Debug.Assert(parent != null && parent.children[indexInParent] == this); } + Debug.Assert(collapsed == RecomputeCollapsedBits()); } +#endif - /// - /// Gets the inorder successor of the node. - /// - internal HeightTreeNode Successor { - get { - if (right != null) { - return right.LeftMost; - } else { - HeightTreeNode node = this; - HeightTreeNode oldNode; - do { - oldNode = node; - node = node.parent; - // go up until we are coming out of a left subtree - } while (node != null && node.right == oldNode); - return node; + protected void AdjustEventPositions(int index, int delta, bool deleteAffectedEvents) + { + // Adjust the positions of all events that are after index. + // Positive delta means insertion, negative delta means deletion. + if (events == null) + return; + bool inLeaf = this is HeightTreeLeafNode; + for (int i = 0; i < events.Length; i++) { + ref Event e = ref events[i]; + if (e.Position >= index && e.Section != null) { + if (e.Position < index - delta) { + // can only happen for negative delta (=deletion) + // when the event is in the deleted region + Debug.Assert(deleteAffectedEvents); + e.Section = null; + continue; + } + byte newPosition = (byte)(e.Position + delta); + if (inLeaf) { + if (e.Kind == EventKind.Start) { + Debug.Assert(e.Section.startLeaf == this); + Debug.Assert(e.Section.startIndexInLeaf == e.Position); + e.Section.startIndexInLeaf = newPosition; + } else { + Debug.Assert(e.Section.endLeaf == this); + Debug.Assert(e.Section.endIndexInLeaf == e.Position); + e.Section.endIndexInLeaf = newPosition; + } + } + e.Position = newPosition; } } + // Update collapsed + if (delta < 0) { + ushort maskBefore = (ushort)((1 << index) - 1); + ushort maskAfter = (ushort)~((1 << (index - delta)) - 1); + collapsed = (ushort)((collapsed & maskBefore) | ((collapsed & maskAfter) >> -delta)); + // cannot assert collapsed == RecomputeCollapsedBits() here because we might + // be inside InnerNode.DeleteLine() with an outstanding needsRecomputeCollapsed. + } else { + // We need to check the individual sections to see if the new line is collapsed, + // so we might as well recompute all the collapsed bits. + collapsed = RecomputeCollapsedBits(); + } } - /// - /// The number of lines in this node and its child nodes. - /// Invariant: - /// totalCount = 1 + left.totalCount + right.totalCount - /// - internal int totalCount; - - /// - /// The total height of this node and its child nodes, excluding directly collapsed nodes. - /// Invariant: - /// totalHeight = left.IsDirectlyCollapsed ? 0 : left.totalHeight - /// + lineNode.IsDirectlyCollapsed ? 0 : lineNode.Height - /// + right.IsDirectlyCollapsed ? 0 : right.totalHeight - /// - internal double totalHeight; - - /// - /// List of the sections that hold this node collapsed. - /// Invariant 1: - /// For each document line in the range described by a CollapsedSection, exactly one ancestor - /// contains that CollapsedSection. - /// Invariant 2: - /// A CollapsedSection is contained either in left+middle or middle+right or just middle. - /// Invariant 3: - /// Start and end of a CollapsedSection always contain the collapsedSection in their - /// documentLine (middle node). - /// - internal List collapsedSections; + internal void InsertEvent(ref int outputIndex, Event e) + { + events ??= new Event[2]; + while (outputIndex < events.Length && events[outputIndex].Section != null) + outputIndex++; + if (outputIndex == events.Length) + Array.Resize(ref events, events.Length * 2); + events[outputIndex++] = e; + } - internal bool IsDirectlyCollapsed { - get { - return collapsedSections != null; + protected void StealEvents(HeightTreeNode sibling, int start, int end, int startHere) + { + Debug.Assert(0 <= start && start <= end && end <= sibling.childCount); + // Move all collapsed sections starting/ending at a child between start and end + // from sibling to this node. + if (sibling.events == null) + return; + var thisAsLeaf = this as HeightTreeLeafNode; + int outputIndex = 0; + int remainingEventsInSibling = 0; + for (int i = 0; i < sibling.events.Length; i++) { + Event e = sibling.events[i]; + if (e.Section == null) + continue; + Debug.Assert(e.Position < sibling.childCount); + if (start <= e.Position && e.Position < end) { + // move e to this node. + byte newPosition = (byte)(e.Position - start + startHere); + if (thisAsLeaf != null) { + // update the section's leaf references + if (e.Kind == EventKind.Start) { + Debug.Assert(e.Section.startLeaf == sibling); + Debug.Assert(e.Section.startIndexInLeaf == e.Position); + e.Section.startLeaf = thisAsLeaf; + e.Section.startIndexInLeaf = newPosition; + } else { + Debug.Assert(e.Section.endLeaf == sibling); + Debug.Assert(e.Section.endIndexInLeaf == e.Position); + e.Section.endLeaf = thisAsLeaf; + e.Section.endIndexInLeaf = newPosition; + } + } + // Write event to output array + InsertEvent(ref outputIndex, new Event { + Section = e.Section, + Kind = e.Kind, + Position = newPosition + }); + sibling.events[i].Section = null; + } else { + remainingEventsInSibling++; + } } + sibling.CompactifyEvents(remainingEventsInSibling); + // Note: we expect the caller to update `collapsed` after childCount is also updated. } - internal void AddDirectlyCollapsed(CollapsedLineSection section) + protected void CompactifyEvents(int remainingEvents) { - if (collapsedSections == null) { - collapsedSections = new List(); - totalHeight = 0; + if (events == null) + return; + if (remainingEvents == 0) { + events = null; + } else if (remainingEvents < events.Length / 4) { + int outputIndex = 0; + for (int i = 0; i < events.Length; i++) { + if (events[i].Section != null) { + events[outputIndex] = events[i]; + outputIndex++; + } + } + if (outputIndex < events.Length) + Array.Resize(ref events, outputIndex); } - Debug.Assert(!collapsedSections.Contains(section)); - collapsedSections.Add(section); } - - internal void RemoveDirectlyCollapsed(CollapsedLineSection section) + internal void RemoveEvent(CollapsedLineSection section, EventKind kind) { - Debug.Assert(collapsedSections.Contains(section)); - collapsedSections.Remove(section); - if (collapsedSections.Count == 0) { - collapsedSections = null; - totalHeight = lineNode.TotalHeight; - if (left != null) - totalHeight += left.totalHeight; - if (right != null) - totalHeight += right.totalHeight; + if (events == null) + return; + int remainingEvents = 0; + for (int i = 0; i < events.Length; i++) { + if (events[i].Section == section && events[i].Kind == kind) { + events[i].Section = null; + } else if (events[i].Section != null) { + remainingEvents++; + } } + CompactifyEvents(remainingEvents); + collapsed = RecomputeCollapsedBits(); } -#if DEBUG - public override string ToString() + internal ushort RecomputeCollapsedBits() { - return "[HeightTreeNode " - + documentLine.LineNumber + " CS=" + GetCollapsedSections(collapsedSections) - + " Line.CS=" + GetCollapsedSections(lineNode.collapsedSections) - + " Line.Height=" + lineNode.height - + " TotalHeight=" + totalHeight - + "]"; + if (events == null) + return 0; + bool inLeaf = this is HeightTreeLeafNode; + int startOffset = inLeaf ? 0 : 1; + int endOffset = inLeaf ? 1 : 0; + ushort result = 0; + foreach (var e in events) { + var section = e.Section; + if (section == null) + continue; + if (e.Kind == EventKind.Start) { + Debug.Assert(section.StartIsWithin(this, out int start) && start == e.Position); + Debug.Assert(e.Position < childCount); + result |= BitsBetween( + e.Position + startOffset, + section.EndIsWithin(this, out int end) ? end + endOffset : childCount + ); + } else { + Debug.Assert(section.EndIsWithin(this, out int end) && end == e.Position); + Debug.Assert(e.Position < childCount); + result |= BitsBetween( + section.StartIsWithin(this, out int start) ? start + startOffset : 0, + e.Position + endOffset + ); + } + } + return result; } - static string GetCollapsedSections(List list) + /// + /// Generate mask where all bits between start (inclusive) and end (exclusive) are 1. + /// + private static ushort BitsBetween(int start, int end) { - if (list == null) - return "{}"; - return "{" + - string.Join(",", - list.ConvertAll(cs => cs.ID).ToArray()) - + "}"; + Debug.Assert(0 <= start && start <= end && end <= 16); + return (ushort)(((1 << (end - start)) - 1) << start); } -#endif } } + diff --git a/ICSharpCode.AvalonEdit/Rendering/TextView.cs b/ICSharpCode.AvalonEdit/Rendering/TextView.cs index ae617b26..22a8d797 100644 --- a/ICSharpCode.AvalonEdit/Rendering/TextView.cs +++ b/ICSharpCode.AvalonEdit/Rendering/TextView.cs @@ -811,7 +811,7 @@ public VisualLine GetOrConstructVisualLine(DocumentLine documentLine) allVisualLines.Add(l); // update all visual top values (building the line might have changed visual top of other lines due to word wrapping) foreach (var line in allVisualLines) { - line.VisualTop = heightTree.GetVisualPosition(line.FirstDocumentLine); + line.VisualTop = heightTree.GetVisualPosition(line.FirstDocumentLine.LineNumber); } } return l; @@ -974,10 +974,11 @@ double CreateAndMeasureVisualLines(Size availableSize) VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties); Debug.WriteLine("Measure availableSize=" + availableSize + ", scrollOffset=" + scrollOffset); - var firstLineInView = heightTree.GetLineByVisualPosition(scrollOffset.Y); + var firstLineNumberInView = heightTree.GetLineByVisualPosition(scrollOffset.Y); + var firstLineInView = document.GetLineByNumber(firstLineNumberInView); // number of pixels clipped from the first visual line(s) - clippedPixelsOnTop = scrollOffset.Y - heightTree.GetVisualPosition(firstLineInView); + clippedPixelsOnTop = scrollOffset.Y - heightTree.GetVisualPosition(firstLineNumberInView); // clippedPixelsOnTop should be >= 0, except for floating point inaccurracy. Debug.Assert(clippedPixelsOnTop >= -ExtensionMethods.Epsilon); @@ -1081,8 +1082,8 @@ VisualLine BuildVisualLine(DocumentLine documentLine, if (visualLine.FirstDocumentLine != visualLine.LastDocumentLine) { // Check whether the lines are collapsed correctly: - double firstLinePos = heightTree.GetVisualPosition(visualLine.FirstDocumentLine.NextLine); - double lastLinePos = heightTree.GetVisualPosition(visualLine.LastDocumentLine.NextLine ?? visualLine.LastDocumentLine); + double firstLinePos = heightTree.GetVisualPosition(visualLine.FirstDocumentLine.LineNumber + 1); + double lastLinePos = heightTree.GetVisualPosition(visualLine.LastDocumentLine.LineNumber + 1); if (!firstLinePos.IsClose(lastLinePos)) { for (int i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++) { if (!heightTree.GetIsCollapsed(i)) @@ -1135,7 +1136,7 @@ VisualLine BuildVisualLine(DocumentLine documentLine, lastLineBreak = textLine.GetTextLineBreak(); } visualLine.SetTextLines(textLines); - heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height); + heightTree.SetHeight(visualLine.FirstDocumentLine.LineNumber, visualLine.Height); return visualLine; } @@ -1756,7 +1757,7 @@ public double GetVisualTopByDocumentLine(int line) VerifyAccess(); if (heightTree == null) throw ThrowUtil.NoDocumentAssigned(); - return heightTree.GetVisualPosition(heightTree.GetLineByNumber(line)); + return heightTree.GetVisualPosition(line); } VisualLineElement GetVisualLineElementFromPosition(Point visualPosition) @@ -2002,7 +2003,7 @@ public DocumentLine GetDocumentLineByVisualTop(double visualTop) VerifyAccess(); if (heightTree == null) throw ThrowUtil.NoDocumentAssigned(); - return heightTree.GetLineByVisualPosition(visualTop); + return document.GetLineByNumber(heightTree.GetLineByVisualPosition(visualTop)); } ///