Compare commits
2 Commits
24d266ab34
...
77bc77cd20
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 77bc77cd20 | |
Savanni D'Gerinel | 02af9e4ea7 |
|
@ -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" ]
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
use std::{mem, ops::Deref};
|
||||
|
||||
const CHUNK_SIZE: usize = 10;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct NodeId(usize);
|
||||
|
||||
impl Deref for NodeId {
|
||||
type Target = usize;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[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: Option<NodeId>,
|
||||
right: Option<NodeId>,
|
||||
},
|
||||
Leaf(LeafNode),
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn len(&self) -> usize {
|
||||
match self {
|
||||
Node::Interior { char_count, .. } => 0,
|
||||
Node::Leaf(ln) => ln.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
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<String> 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. 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<Option<Node>>,
|
||||
}
|
||||
|
||||
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) {
|
||||
match self.find_insertion_node_id(loc) {
|
||||
None => {
|
||||
let node = Node::Leaf(LeafNode::from(text));
|
||||
self.node_count += 1;
|
||||
self.contents.push(Some(node));
|
||||
}
|
||||
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, .. }) => {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// 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<NodeId> {
|
||||
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 {
|
||||
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<String> 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);
|
||||
rope.append(first.to_owned());
|
||||
}
|
||||
rope.append(lst.to_owned());
|
||||
rope
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
struct TestCase {
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_creates_a_rope_from_a_string() {
|
||||
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()
|
||||
},
|
||||
];
|
||||
|
||||
for case in test_cases {
|
||||
let rope = Rope::from(case.content.clone());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod doc_rope;
|
||||
pub mod ui;
|
||||
pub mod state;
|
||||
pub mod types;
|
||||
|
|
|
@ -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<F>(test: F)
|
||||
pub fn with_moby_dick<F>(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(),
|
||||
|
|
Loading…
Reference in New Issue