From 77bc77cd209e9ecf07a4349a7c1f78231d966fd3 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 13 May 2024 10:15:28 -0400 Subject: [PATCH] Get appending at the end of the rope to work --- editor-challenge/src/doc_rope.rs | 207 +++++++++++++++++++------------ 1 file changed, 126 insertions(+), 81 deletions(-) diff --git a/editor-challenge/src/doc_rope.rs b/editor-challenge/src/doc_rope.rs index a44d4e2..6f22461 100644 --- a/editor-challenge/src/doc_rope.rs +++ b/editor-challenge/src/doc_rope.rs @@ -2,7 +2,7 @@ use std::{mem, ops::Deref}; const CHUNK_SIZE: usize = 10; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] struct NodeId(usize); impl Deref for NodeId { @@ -12,14 +12,15 @@ impl Deref for NodeId { } } +#[derive(Debug)] enum Node { Interior { // the number of characters to the left of this node // the number of lines to the left of this node char_count: usize, - left: NodeId, - right: NodeId, + left: Option, + right: Option, }, Leaf(LeafNode), } @@ -33,6 +34,7 @@ impl Node { } } +#[derive(Debug)] struct LeafNode(String); impl LeafNode { @@ -56,97 +58,55 @@ impl From for LeafNode { } } -// a Rope is a regular tree which adds on some extra behavior for dealing with a continuous data -// structure. +/// A Rope is a regular tree which adds on some extra behavior for dealing with a continuous data +/// structure. In this case, the nodes all contain strings, and the rope is arranged such that a +/// depth-first traversal will yield the entire contents of the rope in proper order. +#[derive(Debug)] pub struct Rope { node_count: usize, contents: Vec>, } impl Rope { + /// Insert text at an index within the document. loc is the number of characters from the + /// beginning. pub fn insert_at(&mut self, loc: usize, text: String) { - unimplemented!(); - } - - pub fn append(&mut self, text: String) { - if self.node_count == 0 { - let node = Node::Leaf(LeafNode::from(text)); - self.node_count += 1; - self.contents.push(Some(node)); - } else { - // Let's grab a node. If it's a leaf node, we need to convert it into a interior node. - // In doing so, we need to move the current leaf node into a left-hand child and create - // the right-hand child. - // - // If it's an interior node, we'll dig deeper. Not doing any tree rebalancing at the - // moment. - // - // First thing we need to know is what mode of node we have. That's going to determine - // which branch of operations we go into. We want to do this without borrowing the data - // for anything more than the match. - // let mut node = mem::replace(&mut self.contents[0], None); - match self.contents[0] { - Some(Node::Interior { ref left, ref right, .. }) => { - - } - Some(Node::Leaf(_)) => { - let Some(Node::Leaf(mut ln)) = mem::replace(&mut self.contents[0], None) else { - panic!("Should never leave an empty space in the node list") - }; - let contents = ln.take(); - - let lnode = Node::Leaf(LeafNode::from(contents)); - let rnode = Node::Leaf(LeafNode::from(text)); - - let lnode_id = self.node_count; - let rnode_id = self.node_count + 1; - - let interior_node = Node::Interior { - char_count: lnode.len(), - left: NodeId(lnode_id), - right: NodeId(rnode_id), - }; - - let _ = mem::replace(&mut self.contents[0], Some(interior_node)); - - self.node_count += 2; - self.contents.push(Some(lnode)); - self.contents.push(Some(rnode)); - } - None => panic!("Should never leave an empty space in the node list"), + match self.find_insertion_node_id(loc) { + None => { + let node = Node::Leaf(LeafNode::from(text)); + self.node_count += 1; + self.contents.push(Some(node)); } - /* - let mut node = &mut self.contents[0]; - if let Node::Leaf(s) = node { - let contents = s.take(); - - let lnode = Node::Leaf(LeafNode::from(contents)); - let rnode = Node::Leaf(LeafNode::from(text)); - self.contents.push(lnode); - self.contents.push(rnode); - let lnode_id = self.node_count; - let rnode_id = self.node_count + 1; - self.node_count += 2; - *node = Node::Interior{ - char_count: 0, - left: NodeId(lnode_id), - right: NodeId(rnode_id), - }; + Some(id) => { + self.insert_at_node(id, text); } - */ } } + /// Append text to the end of the document. + pub fn append(&mut self, text: String) { + self.insert_at(self.len(), text); + } + + /// Convert the entire rope back to a continuous String. pub fn to_string(&self) -> String { + if self.contents.is_empty() { + return "".to_owned(); + } + let mut r = String::new(); let mut stack = vec![NodeId(0)]; while let Some(current_id) = stack.pop() { let node = &self.contents[*current_id]; match node { - Some(Node::Interior{ left, right, .. }) => { - stack.push(*right); - stack.push(*left); + Some(Node::Interior { left, right, .. }) => { + if let Some(right_id) = *right { + stack.push(right_id); + } + if let Some(left_id) = *left { + stack.push(left_id); + } } Some(Node::Leaf(ln)) => r.push_str(ln.as_str()), None => panic!("Should never leave an empty space in the node list"), @@ -156,6 +116,7 @@ impl Rope { r } + /// Calculate the length of the stored string. pub fn len(&self) -> usize { // This can be optimized later. Do a traversal of each right node. We already have // character counts of each left tree. Only count the length of the final right leaf. @@ -177,6 +138,74 @@ impl Rope { fn node_count(&self) -> usize { self.node_count } + + // Find the node ID of the insertion point. This is not fully implemented, in that this + // function ignores the offset from the beginning. Because of that, it is also always inserting + // onto the right side, and never traversing down the left. + fn find_insertion_node_id(&self, _loc: usize) -> Option { + let mut current_id = NodeId(0); + loop { + match self.contents.get(*current_id) { + Some(Some(Node::Interior { ref right, .. })) => match right { + Some(id) => current_id = *id, + None => return Some(current_id), + }, + Some(Some(Node::Leaf(_))) => return Some(current_id), + Some(None) => panic!("There should never be an empty node in the tree"), + // This only happens when the list is empty. Otherwise, we're detecting the None in + // advance. + None => return None, + } + } + } + + // Insert text at a particular node location. + // + // This is not a self-balancing operation (yet). Once we know where text needs to be inserted, + // based on the offset from the beginning, we can grab that node and either replace it (if it + // is a Leaf node) or update it (if it is an Interior node). + // + // This function is currently naive, in that it will always assume that text needs to be added + // to the right side, which may not be correct. + fn insert_at_node(&mut self, id: NodeId, text: String) { + match self.contents[*id] { + Some(Node::Interior { ref mut right, .. }) => { + let new_node = Node::Leaf(LeafNode::from(text)); + let new_node_id = NodeId(self.node_count + 1); + + *right = Some(new_node_id); + self.contents.push(Some(new_node)); + + self.node_count += 1; + } + Some(Node::Leaf(_)) => { + let Some(Node::Leaf(mut ln)) = mem::replace(&mut self.contents[*id], None) else { + panic!("Should never leave an empty space in the node list") + }; + let contents = ln.take(); + + let lnode = Node::Leaf(LeafNode::from(contents)); + let rnode = Node::Leaf(LeafNode::from(text)); + + let lnode_id = self.node_count; + let rnode_id = self.node_count + 1; + + let interior_node = Node::Interior { + char_count: lnode.len(), + left: Some(NodeId(lnode_id)), + right: Some(NodeId(rnode_id)), + }; + + let _ = mem::replace(&mut self.contents[*id], Some(interior_node)); + + self.node_count += 2; + self.contents.push(Some(lnode)); + self.contents.push(Some(rnode)); + + } + None => panic!("Should never leave an empty space in the node list"), + } + } } impl Default for Rope { @@ -199,9 +228,9 @@ impl From for Rope { while lst.len() > CHUNK_SIZE { (first, lst) = lst.split_at(CHUNK_SIZE); - println!("[{}] [{}]", first, lst); rope.append(first.to_owned()); } + rope.append(lst.to_owned()); rope } } @@ -210,15 +239,31 @@ impl From for Rope { mod test { use super::*; + struct TestCase { + content: String, + } + #[test] fn it_creates_a_rope_from_a_string() { - let content = + let test_cases = vec![ + TestCase{ content: "".to_owned() }, + TestCase{ content: "This".to_owned() }, + TestCase{ content: "This is some basic".to_owned() }, + TestCase{ content: "This is some basic context which is much smaller".to_owned() }, + TestCase{ content: "This is some basic context which is much smaller than the rope is designed for." - .to_owned(); + .to_owned() + }, + ]; - let rope = Rope::from(content.clone()); + for case in test_cases { + let rope = Rope::from(case.content.clone()); - assert_eq!(rope.to_string(), content); - assert_eq!(rope.len(), content.len()); + for (idx, node) in rope.contents.iter().enumerate() { + } + + assert_eq!(rope.len(), case.content.len(), "{}", case.content); + assert_eq!(rope.to_string(), case.content, "{:?}", case.content); + } } }