diff --git a/editor-challenge/Cargo.toml b/editor-challenge/Cargo.toml index cf173b4..52a178b 100644 --- a/editor-challenge/Cargo.toml +++ b/editor-challenge/Cargo.toml @@ -15,5 +15,5 @@ tui = { version = "0.19", default-features = false, features = [ "crosst [[bin]] name = "bench" -main = "bin/bench.rs" -features = [ "bench" ] +# main = "bin/bench.rs" +# features = [ "bench" ] diff --git a/editor-challenge/src/doc_rope.rs b/editor-challenge/src/doc_rope.rs new file mode 100644 index 0000000..a44d4e2 --- /dev/null +++ b/editor-challenge/src/doc_rope.rs @@ -0,0 +1,224 @@ +use std::{mem, ops::Deref}; + +const CHUNK_SIZE: usize = 10; + +#[derive(Clone, Copy)] +struct NodeId(usize); + +impl Deref for NodeId { + type Target = usize; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +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, + }, + Leaf(LeafNode), +} + +impl Node { + fn len(&self) -> usize { + match self { + Node::Interior { char_count, .. } => 0, + Node::Leaf(ln) => ln.len(), + } + } +} + +struct LeafNode(String); + +impl LeafNode { + fn len(&self) -> usize { + self.0.len() + } + + fn take(&mut self) -> String { + let content = mem::replace(&mut self.0, "".to_owned()); + content + } + + fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for LeafNode { + fn from(s: String) -> Self { + Self(s) + } +} + +// a Rope is a regular tree which adds on some extra behavior for dealing with a continuous data +// structure. +pub struct Rope { + node_count: usize, + contents: Vec>, +} + +impl Rope { + 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"), + } + /* + 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), + }; + } + */ + } + } + + pub fn to_string(&self) -> String { + 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::Leaf(ln)) => r.push_str(ln.as_str()), + None => panic!("Should never leave an empty space in the node list"), + } + } + + r + } + + 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. + self.contents.iter().fold(0, |acc, node| { + if let Some(Node::Leaf(s)) = node { + acc + s.len() + } else { + acc + } + }) + } + + #[cfg(test)] + fn max_depth(&self) -> usize { + unimplemented!(); + } + + #[cfg(test)] + fn node_count(&self) -> usize { + self.node_count + } +} + +impl Default for Rope { + fn default() -> Self { + Self { + node_count: 0, + contents: vec![], + } + } +} + +// Populate the initial rope. The simplest way is to split along lines and turn each line into its +// own leaf node. +impl From for Rope { + fn from(s: String) -> Self { + let mut rope = Rope::default(); + #[allow(unused_assignments)] + let mut first = s.as_str(); + let mut lst = s.as_str(); + + while lst.len() > CHUNK_SIZE { + (first, lst) = lst.split_at(CHUNK_SIZE); + println!("[{}] [{}]", first, lst); + rope.append(first.to_owned()); + } + rope + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn it_creates_a_rope_from_a_string() { + let content = + "This is some basic context which is much smaller than the rope is designed for." + .to_owned(); + + let rope = Rope::from(content.clone()); + + assert_eq!(rope.to_string(), content); + assert_eq!(rope.len(), content.len()); + } +} diff --git a/editor-challenge/src/lib.rs b/editor-challenge/src/lib.rs index 92559af..e6ba8c4 100644 --- a/editor-challenge/src/lib.rs +++ b/editor-challenge/src/lib.rs @@ -1,3 +1,4 @@ +pub mod doc_rope; pub mod ui; pub mod state; pub mod types; diff --git a/editor-challenge/src/types.rs b/editor-challenge/src/types.rs index 17fb9f1..dd9c533 100644 --- a/editor-challenge/src/types.rs +++ b/editor-challenge/src/types.rs @@ -14,6 +14,10 @@ impl Document { Self { rows: contents } } + pub fn line(&self, id: usize) -> Option<&str> { + self.rows.get(id).map(|x| x.as_str()) + } + pub fn contents(&self) -> String { self.rows.join("\n") } @@ -117,7 +121,7 @@ mod test_utils { time::{Duration, Instant}, }; - pub fn with_file(test: F) + pub fn with_moby_dick(test: F) where F: FnOnce(Document), { @@ -163,12 +167,18 @@ mod test { #[test] fn it_inserts_a_line() { - with_file(|mut doc| { + with_moby_dick(|mut doc| { let mut cursor = Cursor::default(); let num_lines = doc.row_count(); + assert_eq!( + doc.line(num_lines - 3), + Some("subscribe to our email newsletter to hear about new eBooks.") + ); + doc.new_line(&mut cursor); assert_eq!(doc.row_count(), num_lines + 1); + assert_eq!(doc.line(0), Some("")); }); } } @@ -177,7 +187,7 @@ pub mod bench { use super::{test_utils::*, *}; pub fn bench_insert_lines() { - with_file(|doc| { + with_moby_dick(|doc| { let performance = benchmark( 1000, || doc.clone(),