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)); } ///