From 16867cfa7a18cae84301997774330c0e71ca5431 Mon Sep 17 00:00:00 2001 From: Artha Date: Mon, 9 Sep 2024 15:03:55 +0200 Subject: [PATCH 1/4] Make Rope.depth a field --- source/fluid/rope.d | 64 +++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/source/fluid/rope.d b/source/fluid/rope.d index dea9746a..c551e392 100644 --- a/source/fluid/rope.d +++ b/source/fluid/rope.d @@ -30,12 +30,15 @@ struct Rope { /// Start and length of the rope, in UTF-8 bytes. size_t start, length; + /// Depth of the node. + int depth = 1; + /// Create a rope holding given text. this(inout const(char)[] text) inout pure nothrow { // No text, stay with Rope.init if (text == "") - this(null, 0, 0); + this(null, 0, 0, 1); else this(new inout RopeNode(text)); @@ -51,7 +54,7 @@ struct Rope { // Both empty, return .init if (right.length == 0) - this(null, 0, 0); + this(null, 0, 0, 1); // Right node only, clone it else @@ -107,21 +110,25 @@ struct Rope { this.node = node; this.start = 0; this.length = node.length; + this.depth = node.isLeaf + ? 1 + : max(node.left.depth, node.right.depth) + 1; } /// Copy a `Rope`. this(inout const Rope rope) inout pure nothrow { - this(rope.node, rope.start, rope.length); + this(rope.node, rope.start, rope.length, rope.depth); } - private this(inout(RopeNode)* node, size_t start, size_t length) inout pure nothrow { + private this(inout(RopeNode)* node, size_t start, size_t length, int depth) inout pure nothrow { this.node = node; this.start = start; this.length = length; + this.depth = depth; } @@ -228,7 +235,11 @@ struct Rope { } /// True if the node is a leaf. - bool isLeaf() const nothrow { + bool isLeaf() const nothrow + out (r) { + assert(!r || depth == 1, "Leaf nodes must have depth of 1"); + } + do { return node is null || node.isLeaf; @@ -338,7 +349,7 @@ struct Rope { } /// Slice the rope. - Rope opIndex(size_t[2] slice, string caller = __PRETTY_FUNCTION__) const nothrow { + Rope opIndex(size_t[2] slice) const nothrow { assert(slice[0] <= length, format!"Left boundary of slice [%s .. %s] exceeds rope length %s"(slice[0], slice[1], length) @@ -372,7 +383,7 @@ struct Rope { } // Overlap or a leaf: return both as they are - return Rope(node, slice[0], slice[1] - slice[0]); + return Rope(node, slice[0], slice[1] - slice[0], depth); } @@ -408,13 +419,10 @@ struct Rope { } - /// Get the depth of the rope. - size_t depth() const nothrow { + /// Returns: A copy of the rope, optimized to improve reading performance. + Rope rebalance() const { - // Leafs have depth of 1 - if (isLeaf) return 1; - - return max(node.left.depth, node.right.depth) + 1; + assert(false); } @@ -507,9 +515,9 @@ struct Rope { assert(ab[3..$].left.equal("")); assert(ab[4..$].left.equal("")); assert(ab[0..4].left.equal("ABC")); - assert(Rope(ab.node, 0, 3).left.equal("ABC")); - assert(Rope(ab.node, 0, 2).left.equal("AB")); - assert(Rope(ab.node, 0, 1).left.equal("A")); + assert(Rope(ab.node, 0, 3, 1).left.equal("ABC")); + assert(Rope(ab.node, 0, 2, 1).left.equal("AB")); + assert(Rope(ab.node, 0, 1, 1).left.equal("A")); assert(ab[0..0].left.equal("")); assert(ab[1..1].left.equal("")); assert(ab[4..4].left.equal("")); @@ -532,8 +540,8 @@ struct Rope { assert(ab[4..$].left.equal("")); assert(ab[0..4].left.equal("BC")); assert(ab[0..3].left.equal("BC")); - assert(Rope(ab.node, 0, 2).left.equal("BC")); - assert(Rope(ab.node, 0, 1).left.equal("B")); + assert(Rope(ab.node, 0, 2, 1).left.equal("BC")); + assert(Rope(ab.node, 0, 1, 1).left.equal("B")); assert(ab[0..0].left.equal("")); assert(ab[1..1].left.equal("")); assert(ab[4..4].left.equal("")); @@ -566,10 +574,10 @@ struct Rope { assert(ab.right.equal("DEF")); assert(ab[1..$].right.equal("DEF")); - assert(Rope(ab.node, 3, 3).right.equal("DEF")); - assert(Rope(ab.node, 4, 2).right.equal("EF")); - assert(Rope(ab.node, 4, 1).right.equal("E")); - assert(Rope(ab.node, 3, 2).right.equal("DE")); + assert(Rope(ab.node, 3, 3, 1).right.equal("DEF")); + assert(Rope(ab.node, 4, 2, 1).right.equal("EF")); + assert(Rope(ab.node, 4, 1, 1).right.equal("E")); + assert(Rope(ab.node, 3, 2, 1).right.equal("DE")); assert(ab[2..$-1].right.equal("DE")); assert(ab[1..1].right.equal("")); assert(ab[4..4].right.equal("")); @@ -589,11 +597,11 @@ struct Rope { assert(ab.right.equal("EF")); assert(ab[1..$].right.equal("EF")); - assert(Rope(ab.node, 3, 1).right.equal("F")); - assert(Rope(ab.node, 4, 0).right.equal("")); - assert(Rope(ab.node, 1, 2).right.equal("E")); - assert(Rope(ab.node, 2, 1).right.equal("E")); - assert(Rope(ab.node, 3, 0).right.equal("")); + assert(Rope(ab.node, 3, 1, 1).right.equal("F")); + assert(Rope(ab.node, 4, 0, 1).right.equal("")); + assert(Rope(ab.node, 1, 2, 1).right.equal("E")); + assert(Rope(ab.node, 2, 1, 1).right.equal("E")); + assert(Rope(ab.node, 3, 0, 1).right.equal("")); assert(ab[1..1].right.equal("")); assert(ab[4..4].right.equal("")); @@ -1240,8 +1248,6 @@ struct Rope { } - Rope result; - // Perform string comparison const prefix = commonPrefix(this[], other[]).length; const suffix = commonPrefix(this[prefix..$].retro, other[prefix..$].retro).length; From bb73f0e2271da171725b8ede582ea1268414592d Mon Sep 17 00:00:00 2001 From: Artha Date: Mon, 9 Sep 2024 16:23:32 +0200 Subject: [PATCH 2/4] Implement Rope.rebalance --- source/fluid/rope.d | 62 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/source/fluid/rope.d b/source/fluid/rope.d index c551e392..ceb04d69 100644 --- a/source/fluid/rope.d +++ b/source/fluid/rope.d @@ -67,7 +67,7 @@ struct Rope { this(left); // Neither is empty, create a new node - else + else this(new inout RopeNode(left, right)); } @@ -216,26 +216,26 @@ struct Rope { /// Concatenate two ropes together. Rope opBinary(string op : "~")(const Rope that) const nothrow { - return Rope(this, that); + return Rope(this, that).rebalance(); } /// Concatenate with a string. Rope opBinary(string op : "~")(const(char)[] text) const nothrow { - return Rope(this, Rope(text)); + return Rope(this, Rope(text)).rebalance(); } /// ditto Rope opBinaryRight(string op : "~")(const(char)[] text) const nothrow { - return Rope(Rope(text), this); + return Rope(Rope(text), this).rebalance(); } /// True if the node is a leaf. - bool isLeaf() const nothrow + bool isLeaf() const nothrow pure out (r) { assert(!r || depth == 1, "Leaf nodes must have depth of 1"); } @@ -419,10 +419,46 @@ struct Rope { } - /// Returns: A copy of the rope, optimized to improve reading performance. - Rope rebalance() const { + /// Returns: + /// True if the rope is fairly balanced. + /// Params: + /// maxDistance = Maximum allowed `depth` difference + bool isBalanced(int maxDistance = 3) const nothrow { + + // Leaves are always balanced + if (isLeaf) return true; - assert(false); + const depthDifference = node.left.depth - node.right.depth; + + return depthDifference >= -maxDistance + && depthDifference <= -maxDistance; + + } + + /// Returns: + /// If the rope is unbalanced, returns a copy of the rope, optimized to improve reading performance. + /// If the rope is already balanced, returns the original rope unmodified. + /// Params: + /// maxDistance = Maximum allowed `depth` difference before rebalancing happens. + Rope rebalance() const nothrow { + + import std.array; + + if (isBalanced) return this; + + /// Merge the given array of leaf ropes into a single rope. + Rope merge(Rope[] leaves) { + + if (leaves.length == 1) + return leaves[0]; + else if (leaves.length == 2) + return Rope(leaves[0], leaves[1]); + else + return Rope(merge(leaves[0 .. $/2]), merge(leaves[$/2 .. $])); + + } + + return merge(byNode.array); } @@ -772,7 +808,7 @@ struct Rope { auto split = split(index); // Insert the element - return Rope(split.left, Rope(value, split.right)); + return Rope(split.left, Rope(value, split.right)).rebalance(); } @@ -839,7 +875,7 @@ struct Rope { value, rightSplit.right, ), - ); + ).rebalance(); } @@ -967,7 +1003,7 @@ struct Rope { auto left = this; - return this = Rope(left, Rope(value)); + return this = Rope(left, Rope(value)).rebalance; } @@ -976,7 +1012,7 @@ struct Rope { auto left = this; - return this = Rope(left, value); + return this = Rope(left, value).rebalance(); } @@ -996,7 +1032,7 @@ struct Rope { } - /// Perform deep-first search through nodes of the rope. + /// Perform deep-first search through leaf nodes of the rope. auto byNode() inout { import std.container.dlist; From 4ba2b149a89b6719878fd95bae8c3b2c7c344256 Mon Sep 17 00:00:00 2001 From: Artha Date: Mon, 9 Sep 2024 17:13:35 +0200 Subject: [PATCH 3/4] Optimize pasting --- source/fluid/code_input.d | 8 ++--- source/fluid/rope.d | 73 +++++++++++++++++++++++++++++---------- source/fluid/text_input.d | 1 + 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/source/fluid/code_input.d b/source/fluid/code_input.d index dc0a7d06..2f6b72c6 100644 --- a/source/fluid/code_input.d +++ b/source/fluid/code_input.d @@ -1481,12 +1481,12 @@ class CodeInput : TextInput { .walkLength; return a.drop(min(commonIndent, localIndent)); - }); + }) + .map!(a => Rope(a)) + .array; // Push each line - // TODO perhaps reformatting could be done during the same step - foreach (line; outdentedClipboard) - push(line); + push(Rope.merge(outdentedClipboard)); reparse(); diff --git a/source/fluid/rope.d b/source/fluid/rope.d index ceb04d69..5f2915da 100644 --- a/source/fluid/rope.d +++ b/source/fluid/rope.d @@ -235,11 +235,7 @@ struct Rope { } /// True if the node is a leaf. - bool isLeaf() const nothrow pure - out (r) { - assert(!r || depth == 1, "Leaf nodes must have depth of 1"); - } - do { + bool isLeaf() const nothrow pure { return node is null || node.isLeaf; @@ -431,7 +427,7 @@ struct Rope { const depthDifference = node.left.depth - node.right.depth; return depthDifference >= -maxDistance - && depthDifference <= -maxDistance; + && depthDifference <= +maxDistance; } @@ -440,28 +436,35 @@ struct Rope { /// If the rope is already balanced, returns the original rope unmodified. /// Params: /// maxDistance = Maximum allowed `depth` difference before rebalancing happens. - Rope rebalance() const nothrow { + Rope rebalance() const nothrow + out (r) { + assert(r.isBalanced, + format("rebalance(%s) failed. Depth %s (left %s, right %s)", this, depth, left.depth, right.depth) + .assumeWontThrow); + } + do { import std.array; if (isBalanced) return this; - /// Merge the given array of leaf ropes into a single rope. - Rope merge(Rope[] leaves) { + return merge(byNode.array); - if (leaves.length == 1) - return leaves[0]; - else if (leaves.length == 2) - return Rope(leaves[0], leaves[1]); - else - return Rope(merge(leaves[0 .. $/2]), merge(leaves[$/2 .. $])); + } - } + /// Returns: A rope created by concatenating an array of leaves together. + static Rope merge(Rope[] leaves) nothrow { - return merge(byNode.array); + if (leaves.length == 1) + return leaves[0]; + else if (leaves.length == 2) + return Rope(leaves[0], leaves[1]); + else + return Rope(merge(leaves[0 .. $/2]), merge(leaves[$/2 .. $])); } + unittest { auto a = Rope(); @@ -1264,6 +1267,10 @@ struct Rope { /// The return value includes a `start` field which indicates the exact index the resulting range starts with. DiffRegion diff(const Rope other) const { + if (this is other) { + return DiffRegion.init; + } + if (!isLeaf) { // Left side is identical, compare right side only @@ -1285,7 +1292,9 @@ struct Rope { } // Perform string comparison - const prefix = commonPrefix(this[], other[]).length; + const prefix = commonPrefix( + BasicRopeRange(this[]), + BasicRopeRange(other[])).length; const suffix = commonPrefix(this[prefix..$].retro, other[prefix..$].retro).length; const start = prefix; @@ -1602,3 +1611,31 @@ unittest { /// `std.utf.codeLength` implementation for Rope. alias codeLength(T : Rope) = imported!"std.utf".codeLength!char; + +/// A wrapper over Range which disables slicing. Some algorithms assume slicing is faster than regular range access, +/// but it's not the case for `Rope`. +struct BasicRopeRange { + + Rope rope; + + size_t length() const { + return rope.length; + } + + bool empty() const { + return rope.empty; + } + + void popFront() { + rope.popFront; + } + + char front() const { + return rope.front; + } + + BasicRopeRange save() { + return this; + } + +} diff --git a/source/fluid/text_input.d b/source/fluid/text_input.d index bb5fe52c..5770f150 100644 --- a/source/fluid/text_input.d +++ b/source/fluid/text_input.d @@ -1366,6 +1366,7 @@ class TextInput : InputNode!Node, FluidScrollable { // Insert the text by replacing the old node, if present value = value.replace(caretIndex - originalLength, caretIndex, Rope(bufferNode)); + assert(value.isBalanced); // Move the caret caretIndex = caretIndex + ch.length; From 2cfc54b99a9e50a30f5e035fe8fb8d2ca0dfd8cf Mon Sep 17 00:00:00 2001 From: Artha Date: Mon, 9 Sep 2024 17:22:31 +0200 Subject: [PATCH 4/4] Missed an edgecase in Rope.merge --- source/fluid/code_input.d | 2 +- source/fluid/rope.d | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/source/fluid/code_input.d b/source/fluid/code_input.d index 2f6b72c6..8e1f0fa6 100644 --- a/source/fluid/code_input.d +++ b/source/fluid/code_input.d @@ -1485,7 +1485,7 @@ class CodeInput : TextInput { .map!(a => Rope(a)) .array; - // Push each line + // Push the clipboard push(Rope.merge(outdentedClipboard)); reparse(); diff --git a/source/fluid/rope.d b/source/fluid/rope.d index 5f2915da..ce43c482 100644 --- a/source/fluid/rope.d +++ b/source/fluid/rope.d @@ -455,7 +455,9 @@ struct Rope { /// Returns: A rope created by concatenating an array of leaves together. static Rope merge(Rope[] leaves) nothrow { - if (leaves.length == 1) + if (leaves.length == 0) + return Rope.init; + else if (leaves.length == 1) return leaves[0]; else if (leaves.length == 2) return Rope(leaves[0], leaves[1]);