use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct UndoEntry {
pub rope: ropey::Rope,
pub cursor: (usize, usize),
pub timestamp: SystemTime,
pub marks: MarkSnapshot,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarkSnapshot {
pub local_marks: BTreeMap<char, (usize, usize)>,
pub jump_back: Vec<(usize, usize)>,
pub jump_fwd: Vec<(usize, usize)>,
pub change_last_edit: Option<(usize, usize)>,
pub change_list: Vec<(usize, usize)>,
pub change_cursor: Option<usize>,
pub global_marks: BTreeMap<char, (usize, usize)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Delta {
pub start: usize,
pub old: String,
pub new: String,
}
fn is_char_boundary(r: &ropey::Rope, byte_idx: usize) -> bool {
if byte_idx == 0 || byte_idx == r.len_bytes() {
return true;
}
let (chunk, chunk_start, _, _) = r.chunk_at_byte(byte_idx);
chunk.is_char_boundary(byte_idx - chunk_start)
}
fn common_prefix_bytes(a: &ropey::Rope, b: &ropey::Rope, max: usize) -> usize {
let mut a_chunks = a.chunks();
let mut b_chunks = b.chunks();
let mut at: &[u8] = &[];
let mut bt: &[u8] = &[];
let mut n = 0;
while n < max {
if at.is_empty() {
match a_chunks.next() {
Some(c) => at = c.as_bytes(),
None => break,
}
continue;
}
if bt.is_empty() {
match b_chunks.next() {
Some(c) => bt = c.as_bytes(),
None => break,
}
continue;
}
if at.as_ptr() == bt.as_ptr() && at.len() == bt.len() && at.len() <= max - n {
n += at.len();
at = &[];
bt = &[];
continue;
}
let m = at.len().min(bt.len()).min(max - n);
if at[..m] == bt[..m] {
n += m;
at = &at[m..];
bt = &bt[m..];
} else {
let mut i = 0;
while at[i] == bt[i] {
i += 1;
}
n += i;
break;
}
}
n
}
fn common_suffix_bytes(a: &ropey::Rope, b: &ropey::Rope, max: usize) -> usize {
let mut a_chunks = a.chunks_at_byte(a.len_bytes()).0;
let mut b_chunks = b.chunks_at_byte(b.len_bytes()).0;
let mut at: &[u8] = &[];
let mut bt: &[u8] = &[];
let mut n = 0;
while n < max {
if at.is_empty() {
match a_chunks.prev() {
Some(c) => at = c.as_bytes(),
None => break,
}
continue;
}
if bt.is_empty() {
match b_chunks.prev() {
Some(c) => bt = c.as_bytes(),
None => break,
}
continue;
}
if at.as_ptr() == bt.as_ptr() && at.len() == bt.len() && at.len() <= max - n {
n += at.len();
at = &[];
bt = &[];
continue;
}
let m = at.len().min(bt.len()).min(max - n);
let (a_tail, b_tail) = (&at[at.len() - m..], &bt[bt.len() - m..]);
if a_tail == b_tail {
n += m;
at = &at[..at.len() - m];
bt = &bt[..bt.len() - m];
} else {
let mut i = 0;
while a_tail[m - 1 - i] == b_tail[m - 1 - i] {
i += 1;
}
n += i;
break;
}
}
n
}
fn diff(parent: &ropey::Rope, child: &ropey::Rope) -> Delta {
let a_len = parent.len_bytes();
let b_len = child.len_bytes();
let max_pre = a_len.min(b_len);
let mut pre = common_prefix_bytes(parent, child, max_pre);
while pre > 0 && !is_char_boundary(parent, pre) {
pre -= 1;
}
let suf = common_suffix_bytes(parent, child, max_pre - pre);
let mut a_end = a_len - suf;
while a_end < a_len && !is_char_boundary(parent, a_end) {
a_end += 1;
}
let b_end = b_len - (a_len - a_end);
Delta {
start: parent.byte_to_char(pre),
old: parent.byte_slice(pre..a_end).to_string(),
new: child.byte_slice(pre..b_end).to_string(),
}
}
fn apply_forward(parent: &ropey::Rope, d: &Delta) -> ropey::Rope {
let mut r = parent.clone();
let old_chars = d.old.chars().count();
r.remove(d.start..d.start + old_chars);
r.insert(d.start, &d.new);
r
}
fn apply_inverse(child: &ropey::Rope, d: &Delta) -> ropey::Rope {
let mut r = child.clone();
let new_chars = d.new.chars().count();
r.remove(d.start..d.start + new_chars);
r.insert(d.start, &d.old);
r
}
const KEYFRAME_INTERVAL: usize = 16;
const KEYFRAME_CAP: usize = 512;
pub type NodeId = usize;
const WARM_CAP: usize = 32;
#[derive(Debug, Clone)]
pub struct UndoNode {
pub parent: Option<NodeId>,
pub children: Vec<NodeId>,
pub last_child: Option<NodeId>,
pub delta: Option<Delta>,
pub base: Option<ropey::Rope>,
pub rope_cache: Option<ropey::Rope>,
pub depth: usize,
pub cursor: (usize, usize),
pub timestamp: SystemTime,
pub marks: Arc<MarkSnapshot>,
pub seq: u64,
}
#[derive(Debug)]
pub struct UndoTree {
nodes: Vec<Option<UndoNode>>,
free: Vec<NodeId>,
warm: Vec<NodeId>,
keyframes: Vec<NodeId>,
root: NodeId,
current: NodeId,
next_seq: u64,
}
fn evict_to(list: &mut Vec<NodeId>, nodes: &mut [Option<UndoNode>], current: NodeId, cap: usize) {
while list.len() > cap {
let Some(pos) = list.iter().position(|&n| n != current) else {
break;
};
let victim = list.remove(pos);
if let Some(node) = nodes[victim].as_mut() {
node.rope_cache = None;
}
}
}
impl UndoTree {
pub(crate) fn new(rope: ropey::Rope) -> Self {
let root = UndoNode {
parent: None,
children: Vec::new(),
last_child: None,
delta: None,
base: Some(rope),
rope_cache: None,
depth: 0,
cursor: (0, 0),
timestamp: SystemTime::now(),
marks: Arc::default(),
seq: 0,
};
Self {
nodes: vec![Some(root)],
free: Vec::new(),
warm: Vec::new(),
keyframes: Vec::new(),
root: 0,
current: 0,
next_seq: 1,
}
}
fn get(&self, id: NodeId) -> &UndoNode {
self.nodes[id].as_ref().expect("live NodeId")
}
fn get_mut(&mut self, id: NodeId) -> &mut UndoNode {
self.nodes[id].as_mut().expect("live NodeId")
}
fn alloc(&mut self, node: UndoNode) -> NodeId {
if let Some(id) = self.free.pop() {
self.nodes[id] = Some(node);
id
} else {
self.nodes.push(Some(node));
self.nodes.len() - 1
}
}
fn free(&mut self, id: NodeId) {
self.nodes[id] = None;
self.free.push(id);
self.warm.retain(|&n| n != id);
self.keyframes.retain(|&n| n != id);
}
fn is_keyframe(&self, id: NodeId) -> bool {
id != self.root && self.get(id).depth.is_multiple_of(KEYFRAME_INTERVAL)
}
fn touch_warm(&mut self, id: NodeId) {
if id == self.root {
return;
}
let (list, cap) = if self.is_keyframe(id) {
(&mut self.keyframes, KEYFRAME_CAP)
} else {
(&mut self.warm, WARM_CAP)
};
list.retain(|&n| n != id);
list.push(id);
evict_to(list, &mut self.nodes, self.current, cap);
}
fn materialize(&mut self, id: NodeId) -> ropey::Rope {
if let Some(r) = &self.get(id).rope_cache {
return r.clone();
}
if let Some(base) = &self.get(id).base {
return base.clone();
}
let mut path = Vec::new();
let base_rope;
let mut anchor = id;
loop {
path.push(anchor);
let par = self
.get(anchor)
.parent
.expect("a non-root, non-based node always has a parent");
if let Some(r) = &self.get(par).rope_cache {
base_rope = r.clone();
break;
}
if let Some(b) = &self.get(par).base {
base_rope = b.clone();
break;
}
anchor = par;
}
let mut rope = base_rope;
for &node in path.iter().rev() {
let d = self
.get(node)
.delta
.as_ref()
.expect("a non-root node always carries its edge delta");
rope = apply_forward(&rope, d);
self.get_mut(node).rope_cache = Some(rope.clone());
self.touch_warm(node);
}
rope
}
fn entry_of(&mut self, id: NodeId) -> UndoEntry {
let rope = self.materialize(id);
let n = self.get(id);
UndoEntry {
rope,
cursor: n.cursor,
timestamp: n.timestamp,
marks: (*n.marks).clone(),
}
}
fn set_node_state(
&mut self,
id: NodeId,
rope: ropey::Rope,
cursor: (usize, usize),
timestamp: SystemTime,
marks: Arc<MarkSnapshot>,
) {
let is_root = self.get(id).parent.is_none();
let unchanged = self.get(id).rope_cache.as_ref() == Some(&rope)
|| (is_root && self.get(id).base.as_ref() == Some(&rope));
{
let node = self.get_mut(id);
node.cursor = cursor;
node.timestamp = timestamp;
node.marks = marks;
}
if unchanged {
return;
}
if is_root {
self.get_mut(id).base = Some(rope);
self.get_mut(id).rope_cache = None;
self.warm.retain(|&n| n != id);
self.keyframes.retain(|&n| n != id);
} else {
let par = self.get(id).parent.expect("non-root has a parent");
let par_rope = self.materialize(par);
let d = diff(&par_rope, &rope);
let node = self.get_mut(id);
node.delta = Some(d);
node.rope_cache = Some(rope);
self.touch_warm(id);
}
}
fn free_subtree(&mut self, id: NodeId) {
let mut stack = vec![id];
while let Some(n) = stack.pop() {
let kids = std::mem::take(&mut self.get_mut(n).children);
stack.extend(kids);
self.free(n);
}
}
pub(crate) fn is_at_root(&self) -> bool {
self.get(self.current).parent.is_none()
}
pub(crate) fn has_redo(&self) -> bool {
self.get(self.current).last_child.is_some()
}
pub(crate) fn depth(&self) -> usize {
let mut d = 0;
let mut n = self.get(self.current).parent;
while let Some(p) = n {
d += 1;
n = self.get(p).parent;
}
d
}
pub(crate) fn parent_timestamp(&self) -> Option<SystemTime> {
self.get(self.current).parent.map(|p| self.get(p).timestamp)
}
pub(crate) fn child_timestamp(&self) -> Option<SystemTime> {
self.get(self.current)
.last_child
.map(|c| self.get(c).timestamp)
}
pub(crate) fn push(&mut self, entry: UndoEntry) {
let cur = self.current;
let marks = Arc::new(entry.marks);
self.set_node_state(
cur,
entry.rope.clone(),
entry.cursor,
entry.timestamp,
Arc::clone(&marks),
);
let seq = self.next_seq;
self.next_seq += 1;
let child_depth = self.get(cur).depth + 1;
let child = self.alloc(UndoNode {
parent: Some(cur),
children: Vec::new(),
last_child: None,
delta: Some(Delta::default()),
base: None,
rope_cache: Some(entry.rope),
depth: child_depth,
cursor: entry.cursor,
timestamp: entry.timestamp,
marks,
seq,
});
let cur_node = self.get_mut(cur);
cur_node.children.push(child);
cur_node.last_child = Some(child);
self.current = child;
self.touch_warm(child);
}
pub(crate) fn undo_step(
&mut self,
rope: ropey::Rope,
cursor: (usize, usize),
marks: MarkSnapshot,
) -> Option<UndoEntry> {
let cur = self.current;
let par = self.get(cur).parent?;
let dest_ts = self.get(par).timestamp;
self.set_node_state(cur, rope, cursor, dest_ts, Arc::new(marks));
self.get_mut(par).last_child = Some(cur);
self.current = par;
if self.get(par).rope_cache.is_none() && self.get(par).base.is_none() {
let child_rope = self.get(cur).rope_cache.clone();
let child_delta = self.get(cur).delta.clone();
if let (Some(cr), Some(d)) = (child_rope, child_delta) {
let par_rope = apply_inverse(&cr, &d);
self.get_mut(par).rope_cache = Some(par_rope);
self.touch_warm(par);
}
}
Some(self.entry_of(par))
}
pub(crate) fn redo_step(
&mut self,
rope: ropey::Rope,
cursor: (usize, usize),
marks: MarkSnapshot,
) -> Option<UndoEntry> {
let cur = self.current;
let child = self.get(cur).last_child?;
let dest_ts = self.get(child).timestamp;
self.set_node_state(cur, rope, cursor, dest_ts, Arc::new(marks));
self.current = child;
Some(self.entry_of(child))
}
fn current_seq(&self) -> u64 {
self.get(self.current).seq
}
fn node_below(&self, s: u64) -> Option<NodeId> {
let mut best: Option<(u64, NodeId)> = None;
for (id, slot) in self.nodes.iter().enumerate() {
if let Some(n) = slot
&& n.seq < s
&& best.is_none_or(|(bs, _)| n.seq > bs)
{
best = Some((n.seq, id));
}
}
best.map(|(_, id)| id)
}
fn node_above(&self, s: u64) -> Option<NodeId> {
let mut best: Option<(u64, NodeId)> = None;
for (id, slot) in self.nodes.iter().enumerate() {
if let Some(n) = slot
&& n.seq > s
&& best.is_none_or(|(bs, _)| n.seq < bs)
{
best = Some((n.seq, id));
}
}
best.map(|(_, id)| id)
}
fn retarget_current(&mut self, target: NodeId) {
self.current = target;
let mut node = target;
while let Some(p) = self.get(node).parent {
self.get_mut(p).last_child = Some(node);
node = p;
}
}
fn stash_and_move(
&mut self,
target: NodeId,
rope: ropey::Rope,
cursor: (usize, usize),
marks: MarkSnapshot,
) {
let cur = self.current;
let ts = self.get(cur).timestamp;
self.set_node_state(cur, rope, cursor, ts, Arc::new(marks));
self.retarget_current(target);
}
pub(crate) fn seq_earlier_step(
&mut self,
rope: ropey::Rope,
cursor: (usize, usize),
marks: MarkSnapshot,
) -> Option<UndoEntry> {
let target = self.node_below(self.current_seq())?;
self.stash_and_move(target, rope, cursor, marks);
Some(self.entry_of(target))
}
pub(crate) fn seq_later_step(
&mut self,
rope: ropey::Rope,
cursor: (usize, usize),
marks: MarkSnapshot,
) -> Option<UndoEntry> {
let target = self.node_above(self.current_seq())?;
self.stash_and_move(target, rope, cursor, marks);
Some(self.entry_of(target))
}
pub(crate) fn seq_earlier_timestamp(&self) -> Option<SystemTime> {
self.node_below(self.current_seq())
.map(|id| self.get(id).timestamp)
}
pub(crate) fn seq_later_timestamp(&self) -> Option<SystemTime> {
self.node_above(self.current_seq())
.map(|id| self.get(id).timestamp)
}
pub(crate) fn leaves(&self) -> Vec<(u64, usize, SystemTime, bool)> {
let mut out: Vec<(u64, usize, SystemTime, bool)> = Vec::new();
for (id, slot) in self.nodes.iter().enumerate() {
let Some(n) = slot else { continue };
if id == self.root || !n.children.is_empty() {
continue;
}
let mut depth = 0;
let mut p = n.parent;
while let Some(pid) = p {
depth += 1;
p = self.get(pid).parent;
}
out.push((n.seq, depth, n.timestamp, id == self.current));
}
out.sort_by_key(|&(seq, ..)| seq);
out
}
fn live_count(&self) -> usize {
self.nodes.iter().filter(|n| n.is_some()).count()
}
pub(crate) fn pop_committed(&mut self) -> bool {
let cur = self.current;
if !self.get(cur).children.is_empty() {
return false;
}
let Some(par) = self.get(cur).parent else {
return false;
};
let par_node = self.get_mut(par);
par_node.children.retain(|&c| c != cur);
par_node.last_child = par_node.children.last().copied();
self.current = par;
if self.get(cur).seq + 1 == self.next_seq {
self.next_seq -= 1;
}
self.free(cur);
true
}
pub(crate) fn cap(&mut self, cap: usize) {
if cap == 0 {
return;
}
let mut budget_iters = self.live_count() + 1;
while self.live_count().saturating_sub(1) > cap && budget_iters > 0 {
budget_iters -= 1;
if let Some(leaf) = self.lowest_offpath_leaf() {
self.detach_leaf(leaf);
} else if !self.prune_root_side() {
break;
}
}
}
fn current_path(&self) -> Vec<NodeId> {
let mut path = Vec::new();
let mut n = Some(self.current);
while let Some(id) = n {
path.push(id);
n = self.get(id).parent;
}
path
}
fn lowest_offpath_leaf(&self) -> Option<NodeId> {
let path = self.current_path();
let mut best: Option<(u64, NodeId)> = None;
for (id, slot) in self.nodes.iter().enumerate() {
if let Some(n) = slot
&& n.children.is_empty()
&& !path.contains(&id)
&& best.is_none_or(|(bs, _)| n.seq < bs)
{
best = Some((n.seq, id));
}
}
best.map(|(_, id)| id)
}
fn detach_leaf(&mut self, leaf: NodeId) {
if let Some(par) = self.get(leaf).parent {
let par_node = self.get_mut(par);
par_node.children.retain(|&c| c != leaf);
if par_node.last_child == Some(leaf) {
par_node.last_child = par_node.children.last().copied();
}
}
self.free(leaf);
}
fn prune_root_side(&mut self) -> bool {
let root = self.root;
if root == self.current {
return false;
}
let path = self.current_path();
let Some(&child) = self.get(root).children.iter().find(|c| path.contains(c)) else {
return false;
};
let others: Vec<NodeId> = self
.get(root)
.children
.iter()
.copied()
.filter(|&c| c != child)
.collect();
for c in others {
self.free_subtree(c);
}
let base = self.materialize(child);
{
let node = self.get_mut(child);
node.parent = None;
node.base = Some(base);
node.delta = None;
node.rope_cache = None;
}
self.warm.retain(|&n| n != child);
self.keyframes.retain(|&n| n != child);
self.root = child;
self.free(root);
true
}
pub(crate) fn clear_redo(&mut self) {
let cur = self.current;
let kids = std::mem::take(&mut self.get_mut(cur).children);
self.get_mut(cur).last_child = None;
for c in kids {
self.free_subtree(c);
}
}
pub(crate) fn clear_all(&mut self) {
let cur = self.current;
let base = self.materialize(cur);
for id in 0..self.nodes.len() {
if id != cur && self.nodes[id].is_some() {
self.nodes[id] = None;
self.free.push(id);
}
}
self.warm.clear();
self.keyframes.clear();
let node = self.get_mut(cur);
node.parent = None;
node.children.clear();
node.last_child = None;
node.delta = None;
node.base = Some(base);
node.rope_cache = None;
node.depth = 0;
self.root = cur;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerNode {
pub parent: Option<u32>,
pub children: Vec<u32>,
pub last_child: Option<u32>,
pub delta: Option<Delta>,
pub cursor: (u32, u32),
pub timestamp_unix_ms: u64,
pub marks: MarkSnapshot,
pub seq: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerTree {
pub base: String,
pub nodes: Vec<SerNode>,
pub root: u32,
pub current: u32,
pub next_seq: u64,
}
fn system_time_to_unix_ms(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_millis() as u64)
}
fn unix_ms_to_system_time(ms: u64) -> SystemTime {
UNIX_EPOCH + Duration::from_millis(ms)
}
impl UndoTree {
pub(crate) fn current_node_seq(&self) -> u64 {
self.get(self.current).seq
}
pub(crate) fn current_content(&mut self) -> ropey::Rope {
let cur = self.current;
self.materialize(cur)
}
pub(crate) fn sync_current(&mut self, rope: ropey::Rope) {
let cur = self.current;
let (cursor, ts, marks) = {
let n = self.get(cur);
(n.cursor, n.timestamp, n.marks.clone())
};
self.set_node_state(cur, rope, cursor, ts, marks);
}
pub(crate) fn to_serializable(&self) -> SerTree {
let mut map: Vec<Option<u32>> = vec![None; self.nodes.len()];
let mut order: Vec<NodeId> = Vec::new();
for (id, slot) in self.nodes.iter().enumerate() {
if slot.is_some() {
map[id] = Some(order.len() as u32);
order.push(id);
}
}
let remap = |id: NodeId| map[id].expect("live link points at a live node");
let nodes = order
.iter()
.map(|&id| {
let n = self.get(id);
SerNode {
parent: n.parent.map(remap),
children: n.children.iter().map(|&c| remap(c)).collect(),
last_child: n.last_child.map(remap),
delta: n.delta.clone(),
cursor: (n.cursor.0 as u32, n.cursor.1 as u32),
timestamp_unix_ms: system_time_to_unix_ms(n.timestamp),
marks: (*n.marks).clone(),
seq: n.seq,
}
})
.collect();
let base = self
.get(self.root)
.base
.as_ref()
.map(|r| r.to_string())
.unwrap_or_default();
SerTree {
base,
nodes,
root: remap(self.root),
current: remap(self.current),
next_seq: self.next_seq,
}
}
pub(crate) fn from_serializable(s: &SerTree) -> Option<Self> {
let len = s.nodes.len();
if len == 0 || s.root as usize >= len || s.current as usize >= len {
return None;
}
for (i, n) in s.nodes.iter().enumerate() {
let is_root = i as u32 == s.root;
match (is_root, &n.delta, &n.parent) {
(true, None, None) => {}
(false, Some(_), Some(_)) => {}
_ => return None,
}
if let Some(p) = n.parent
&& p as usize >= len
{
return None;
}
if n.children.iter().any(|&c| c as usize >= len) {
return None;
}
if let Some(c) = n.last_child
&& c as usize >= len
{
return None;
}
}
let base = ropey::Rope::from_str(&s.base);
let depths = depths_from_root(s);
let nodes: Vec<Option<UndoNode>> = s
.nodes
.iter()
.enumerate()
.map(|(i, n)| {
let is_root = i as u32 == s.root;
Some(UndoNode {
parent: n.parent.map(|p| p as NodeId),
children: n.children.iter().map(|&c| c as NodeId).collect(),
last_child: n.last_child.map(|c| c as NodeId),
delta: n.delta.clone(),
base: if is_root { Some(base.clone()) } else { None },
rope_cache: None,
depth: depths[i],
cursor: (n.cursor.0 as usize, n.cursor.1 as usize),
timestamp: unix_ms_to_system_time(n.timestamp_unix_ms),
marks: Arc::new(n.marks.clone()),
seq: n.seq,
})
})
.collect();
Some(Self {
nodes,
free: Vec::new(),
warm: Vec::new(),
keyframes: Vec::new(),
root: s.root as NodeId,
current: s.current as NodeId,
next_seq: s.next_seq,
})
}
}
fn depths_from_root(s: &SerTree) -> Vec<usize> {
let mut depths = vec![0usize; s.nodes.len()];
let mut seen = vec![false; s.nodes.len()];
let mut queue = std::collections::VecDeque::new();
seen[s.root as usize] = true;
queue.push_back(s.root as usize);
while let Some(i) = queue.pop_front() {
for &c in &s.nodes[i].children {
let c = c as usize;
if !seen[c] {
seen[c] = true;
depths[c] = depths[i] + 1;
queue.push_back(c);
}
}
}
depths
}
#[cfg(test)]
impl UndoTree {
fn live_ids(&self) -> Vec<NodeId> {
(0..self.nodes.len())
.filter(|&i| self.nodes[i].is_some())
.collect()
}
fn materialize_for_test(&mut self, id: NodeId) -> ropey::Rope {
self.materialize(id)
}
fn drop_all_caches(&mut self) {
for n in self.nodes.iter_mut().flatten() {
n.rope_cache = None;
}
self.warm.clear();
self.keyframes.clear();
}
fn drop_warm_caches(&mut self) {
for id in std::mem::take(&mut self.warm) {
if let Some(n) = self.nodes[id].as_mut() {
n.rope_cache = None;
}
}
}
fn replay_distance(&self, id: NodeId) -> usize {
let mut n = 0;
let mut cur = id;
loop {
let node = self.get(cur);
if node.rope_cache.is_some() || node.base.is_some() {
return n;
}
n += 1;
match node.parent {
Some(p) => cur = p,
None => return n,
}
}
}
fn materialize_naive(&self, id: NodeId) -> ropey::Rope {
let mut path = vec![id];
let mut cur = id;
while let Some(p) = self.get(cur).parent {
path.push(p);
cur = p;
}
let mut rope = self
.get(cur)
.base
.clone()
.expect("the root always carries a base");
for &node in path.iter().rev().skip(1) {
let d = self
.get(node)
.delta
.as_ref()
.expect("a non-root node always carries its edge delta");
rope = apply_forward(&rope, d);
}
rope
}
}
#[cfg(test)]
mod tree_tests {
use super::*;
fn entry(text: &str) -> UndoEntry {
UndoEntry {
rope: ropey::Rope::from_str(text),
cursor: (0, 0),
timestamp: SystemTime::now(),
marks: MarkSnapshot::default(),
}
}
fn live(text: &str) -> (ropey::Rope, (usize, usize), MarkSnapshot) {
(ropey::Rope::from_str(text), (0, 0), MarkSnapshot::default())
}
#[test]
fn fresh_tree_is_root_current_empty() {
let t = UndoTree::new(ropey::Rope::from_str("hello"));
assert!(t.is_at_root());
assert!(!t.has_redo());
assert_eq!(t.depth(), 0);
assert_eq!(t.root, t.current);
}
#[test]
fn push_links_child_and_advances_current() {
let mut t = UndoTree::new(ropey::Rope::from_str("hello"));
let root = t.current;
t.push(entry("hello"));
assert_eq!(t.get(t.current).parent, Some(root));
assert_eq!(t.get(root).last_child, Some(t.current));
assert_eq!(t.get(root).children, vec![t.current]);
assert_eq!(t.depth(), 1);
assert!(!t.has_redo());
assert!(!t.is_at_root());
}
#[test]
fn undo_then_redo_round_trips_links() {
let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
t.push(entry("s0")); let n0 = t.root;
let n1 = t.current;
let (r, c, m) = live("s1");
let restored = t.undo_step(r, c, m).unwrap();
assert_eq!(restored.rope.to_string(), "s0");
assert_eq!(t.current, n0);
assert!(t.has_redo());
assert_eq!(t.get(n0).last_child, Some(n1));
let (r, c, m) = live("s0");
let restored = t.redo_step(r, c, m).unwrap();
assert_eq!(restored.rope.to_string(), "s1");
assert_eq!(t.current, n1);
assert!(!t.has_redo());
}
#[test]
fn undo_at_root_and_redo_at_leaf_are_noops() {
let mut t = UndoTree::new(ropey::Rope::from_str("x"));
let (r, c, m) = live("x");
assert!(t.undo_step(r, c, m).is_none());
let (r, c, m) = live("x");
assert!(t.redo_step(r, c, m).is_none());
assert_eq!(t.depth(), 0);
}
#[test]
fn push_retains_forward_branch() {
let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
t.push(entry("A")); let root = t.root;
let na = t.current;
let (r, c, m) = live("A");
t.undo_step(r, c, m); assert!(t.has_redo());
t.push(entry("B"));
let nb = t.current;
assert_ne!(nb, na);
assert_eq!(t.get(root).children.len(), 2);
assert!(t.get(root).children.contains(&na));
assert!(t.get(root).children.contains(&nb));
assert_eq!(t.get(root).last_child, Some(nb));
let live = t.nodes.iter().filter(|n| n.is_some()).count();
assert_eq!(live, 3);
}
#[test]
fn seq_walk_crosses_branches() {
let mut t = UndoTree::new(ropey::Rope::from_str(""));
t.push(entry("")); let (r, c, m) = live("A");
t.undo_step(r, c, m); t.push(entry("")); let nb = t.current;
let (r, c, m) = live("B");
let a = t.seq_earlier_step(r, c, m).unwrap();
assert_eq!(a.rope.to_string(), "A");
let (r, c, m) = live("A");
let root_snap = t.seq_earlier_step(r, c, m).unwrap();
assert_eq!(root_snap.rope.to_string(), "");
let (r, c, m) = live("");
let a2 = t.seq_later_step(r, c, m).unwrap();
assert_eq!(a2.rope.to_string(), "A");
let (r, c, m) = live("A");
let b = t.seq_later_step(r, c, m).unwrap();
assert_eq!(b.rope.to_string(), "B");
assert_eq!(t.current, nb);
let (r, c, m) = live("B");
assert!(t.seq_later_step(r, c, m).is_none());
}
#[test]
fn seq_walk_updates_retrace_path() {
let mut t = UndoTree::new(ropey::Rope::from_str("R"));
t.push(entry("R")); t.push(entry("X")); let (r, c, m) = live("Y");
t.undo_step(r, c, m); t.push(entry("X")); let (r, c, m) = live("Z");
let y = t.seq_earlier_step(r, c, m).unwrap();
assert_eq!(y.rope.to_string(), "Y");
let (r, c, m) = live("Y");
t.undo_step(r, c, m);
let (r, c, m) = live("X");
t.undo_step(r, c, m);
assert!(t.is_at_root());
let (r, c, m) = live("R");
let x = t.redo_step(r, c, m).unwrap();
assert_eq!(x.rope.to_string(), "X");
let (r, c, m) = live("X");
let y2 = t.redo_step(r, c, m).unwrap();
assert_eq!(y2.rope.to_string(), "Y");
}
#[test]
fn leaves_lists_branch_tips_by_seq() {
let mut t = UndoTree::new(ropey::Rope::from_str(""));
t.push(entry("X"));
t.push(entry("Y"));
t.push(entry("W"));
let (r, c, m) = live("W");
t.undo_step(r, c, m);
let (r, c, m) = live("Y");
t.undo_step(r, c, m); t.push(entry("Z")); let leaves = t.leaves();
let dims: Vec<(u64, usize, bool)> =
leaves.iter().map(|&(s, d, _, cur)| (s, d, cur)).collect();
assert_eq!(dims, vec![(3, 3, false), (4, 2, true)]);
}
#[test]
fn cap_prunes_oldest_from_root_side() {
let mut t = UndoTree::new(ropey::Rope::from_str("s"));
for _ in 0..5 {
t.push(entry("s"));
}
assert_eq!(t.depth(), 5);
t.cap(3);
assert_eq!(t.depth(), 3);
assert!(!t.has_redo());
assert_eq!(t.free.len(), 2);
}
#[test]
fn cap_drops_offpath_leaf_before_main_line() {
let mut t = UndoTree::new(ropey::Rope::from_str(""));
t.push(entry("A")); let na = t.current;
let (r, c, m) = live("A");
t.undo_step(r, c, m);
t.push(entry("B")); let nb = t.current;
let (r, c, m) = live("B");
t.undo_step(r, c, m);
t.push(entry("C")); let nc = t.current;
assert_eq!(t.leaves().len(), 3);
t.cap(2);
assert!(t.nodes[na].is_none());
assert!(t.nodes[nb].is_some());
assert_eq!(t.current, nc);
assert!(!t.is_at_root());
assert!(t.get(t.root).children.contains(&nb));
assert!(t.get(t.root).children.contains(&nc));
}
#[test]
fn pop_committed_reverses_last_push() {
let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
t.push(entry("s0")); assert_eq!(t.depth(), 1);
assert!(t.pop_committed());
assert_eq!(t.depth(), 0);
assert!(t.is_at_root());
assert_eq!(t.free.len(), 1);
assert_eq!(t.next_seq, 1);
}
#[test]
fn pop_committed_retains_sibling_branches() {
let mut t = UndoTree::new(ropey::Rope::from_str(""));
t.push(entry("A")); let na = t.current;
let (r, c, m) = live("A");
t.undo_step(r, c, m); t.push(entry("B")); let root = t.root;
assert!(t.pop_committed());
assert!(t.get(root).children.contains(&na));
assert_eq!(t.get(root).children.len(), 1);
assert_eq!(t.current, root);
let live = t.nodes.iter().filter(|n| n.is_some()).count();
assert_eq!(live, 2); }
#[test]
fn pop_committed_at_root_is_false() {
let mut t = UndoTree::new(ropey::Rope::from_str("s"));
assert!(!t.pop_committed());
}
#[test]
fn clear_redo_drops_forward_only() {
let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
t.push(entry("s0"));
let (r, c, m) = live("s1");
t.undo_step(r, c, m);
assert!(t.has_redo());
assert_eq!(t.depth(), 0);
t.clear_redo();
assert!(!t.has_redo());
assert_eq!(t.depth(), 0);
}
#[test]
fn clear_all_collapses_to_single_node() {
let mut t = UndoTree::new(ropey::Rope::from_str("s"));
for _ in 0..3 {
t.push(entry("s"));
}
t.clear_all();
assert!(t.is_at_root());
assert!(!t.has_redo());
assert_eq!(t.depth(), 0);
assert_eq!(t.root, t.current);
}
}
#[cfg(test)]
mod delta_tests {
use super::*;
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(if seed == 0 {
0x9E37_79B9_7F4A_7C15
} else {
seed
})
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: usize) -> usize {
(self.next_u64() % n as u64) as usize
}
}
fn mutate(s: &str, rng: &mut Rng) -> String {
const ALPHABET: [char; 10] = ['a', 'b', '\n', 'é', '日', '本', '🎉', '語', 'x', 'z'];
let chars: Vec<char> = s.chars().collect();
let pick = |rng: &mut Rng| ALPHABET[rng.below(ALPHABET.len())];
match rng.below(3) {
0 => {
let pos = rng.below(chars.len() + 1);
let mut v = chars.clone();
v.insert(pos, pick(rng));
v.into_iter().collect()
}
1 if !chars.is_empty() => {
let pos = rng.below(chars.len());
let mut v = chars.clone();
v.remove(pos);
v.into_iter().collect()
}
_ => {
if chars.is_empty() {
return pick(rng).to_string();
}
let a = rng.below(chars.len());
let b = (a + rng.below(chars.len() - a + 1)).min(chars.len());
let mut v = chars[..a].to_vec();
v.push(pick(rng));
v.extend_from_slice(&chars[b..]);
v.into_iter().collect()
}
}
}
fn entry_str(s: &str) -> UndoEntry {
UndoEntry {
rope: ropey::Rope::from_str(s),
cursor: (0, 0),
timestamp: SystemTime::now(),
marks: MarkSnapshot::default(),
}
}
fn diff_reference(parent: &ropey::Rope, child: &ropey::Rope) -> Delta {
let a = parent.to_string();
let b = child.to_string();
let ab = a.as_bytes();
let bb = b.as_bytes();
let max_pre = ab.len().min(bb.len());
let mut pre = 0;
while pre < max_pre && ab[pre] == bb[pre] {
pre += 1;
}
while pre > 0 && !a.is_char_boundary(pre) {
pre -= 1;
}
let max_suf = max_pre - pre;
let mut suf = 0;
while suf < max_suf && ab[ab.len() - 1 - suf] == bb[bb.len() - 1 - suf] {
suf += 1;
}
let mut a_end = ab.len() - suf;
while a_end < ab.len() && !a.is_char_boundary(a_end) {
a_end += 1;
}
let b_end = bb.len() - (ab.len() - a_end);
Delta {
start: a[..pre].chars().count(),
old: a[pre..a_end].to_string(),
new: b[pre..b_end].to_string(),
}
}
#[track_caller]
fn assert_diff_matches_reference(sa: &str, sb: &str) {
let a = ropey::Rope::from_str(sa);
let b = ropey::Rope::from_str(sb);
assert_eq!(
diff(&a, &b),
diff_reference(&a, &b),
"diff != reference for {sa:?} -> {sb:?}"
);
let mut a2 = ropey::Rope::new();
a2.insert(0, sa);
let mut b2 = ropey::Rope::new();
for (i, c) in sb.chars().enumerate() {
b2.insert_char(i, c);
}
assert_eq!(
diff(&a2, &b2),
diff_reference(&a2, &b2),
"diff != reference (misaligned chunks) for {sa:?} -> {sb:?}"
);
}
#[test]
fn diff_matches_reference_on_edge_cases() {
let cases: &[(&str, &str)] = &[
("", ""),
("", "a"),
("a", ""),
("abc", "abc"),
("café🎉", "café🎉"),
("abcdef", "abcdefXY"),
("abcdefXY", "abcdef"),
("Xabcdef", "abcdef"),
("abcdef", "Xabcdef"),
("abcdef", "Zbcdef"),
("abcdef", "abcdeZ"),
("abcabc", "abc"),
("abc", "abcabc"),
("aaaa", "aa"),
("aa", "aaaa"),
("abab", "ababab"),
("xyxyxy", "xyxy"),
("café", "cafés"),
("cafés", "café"),
("café", "cafè"),
("日本語", "日語"),
("日本語", "日本本語"),
("🎉🎉🎉", "🎉🎉"),
("🎉🎉", "🎉🎉🎉"),
("🎉x🎉", "🎉y🎉"),
("a🎉b", "a🎊b"),
("é", "e"),
("e", "é"),
("🎉", ""),
("", "🎉"),
("日", "旦"),
("x日y", "x旦y"),
("語", "誤"),
(
&"the quick brown fox ".repeat(400),
&"the quick brown fox ".repeat(400),
),
];
for (sa, sb) in cases {
assert_diff_matches_reference(sa, sb);
}
let big: String = "the quick brown fox jumps over the lazy dog\n".repeat(200);
let mid = big.len() / 2;
let mut edited = big.clone();
edited.insert(mid, 'Z');
assert_diff_matches_reference(&big, &edited);
assert_diff_matches_reference(&edited, &big);
assert_diff_matches_reference(&big, &format!("Z{big}"));
assert_diff_matches_reference(&big, &format!("{big}Z"));
assert_diff_matches_reference(&big, &big.repeat(2));
let uni: String = "café 日本語 🎉 αβγ\n".repeat(200);
let umid = uni.len() / 2;
let umid = (0..=umid).rev().find(|i| uni.is_char_boundary(*i)).unwrap();
let mut uedited = uni.clone();
uedited.insert(umid, '🎊');
assert_diff_matches_reference(&uni, &uedited);
assert_diff_matches_reference(&uedited, &uni);
}
#[test]
fn diff_matches_reference_over_random_evolving_content() {
let mut rng = Rng::new(0x0BAD_F00D_1234_5678);
let mut s = String::from("seed café 日本語\n🎉");
for _ in 0..4000 {
let t = mutate(&s, &mut rng);
let a = ropey::Rope::from_str(&s);
let b = ropey::Rope::from_str(&t);
assert_eq!(diff(&a, &b), diff_reference(&a, &b), "{s:?} -> {t:?}");
assert_eq!(diff(&b, &a), diff_reference(&b, &a), "{t:?} -> {s:?}");
s = t;
}
}
#[test]
fn diff_matches_reference_on_shared_leaf_clones() {
let base: String = "the quick brown fox jumps over the lazy dog\n".repeat(300);
let parent = ropey::Rope::from_str(&base);
assert_eq!(
diff(&parent, &parent.clone()),
diff_reference(&parent, &parent.clone())
);
let n = parent.len_chars();
for at in [0, 1, n / 4, n / 2, n - 1, n] {
let mut child = parent.clone();
child.insert_char(at, '𝄞');
assert_eq!(
diff(&parent, &child),
diff_reference(&parent, &child),
"@{at}"
);
assert_eq!(
diff(&child, &parent),
diff_reference(&child, &parent),
"@{at}"
);
}
for at in [0, n / 3, n - 10] {
let mut child = parent.clone();
child.remove(at..at + 5);
assert_eq!(
diff(&parent, &child),
diff_reference(&parent, &child),
"-{at}"
);
assert_eq!(
diff(&child, &parent),
diff_reference(&child, &parent),
"-{at}"
);
}
}
#[test]
fn diff_matches_reference_over_random_multi_chunk_pairs() {
let mut rng = Rng::new(0xF00D_BEEF_0BAD_C0DE);
let units = ["ab", "café ", "日本語", "🎉", "\n", "x", "語日", "é"];
let build = |rng: &mut Rng| -> String {
let mut s = String::new();
for _ in 0..rng.below(400) {
s.push_str(units[rng.below(units.len())]);
}
s
};
for _ in 0..300 {
let sa = build(&mut rng);
let sb = if rng.below(2) == 0 {
build(&mut rng)
} else {
let mut t = sa.clone();
if !t.is_empty() {
let cut = rng.below(t.chars().count() + 1);
let byte = t.char_indices().nth(cut).map_or(t.len(), |(i, _)| i);
t.insert_str(byte, "🎊zz");
}
t
};
let a = ropey::Rope::from_str(&sa);
let b = ropey::Rope::from_str(&sb);
assert_eq!(diff(&a, &b), diff_reference(&a, &b));
assert_eq!(diff(&b, &a), diff_reference(&b, &a));
}
}
#[test]
fn diff_round_trips_over_random_evolving_content() {
let mut rng = Rng::new(0x1234_5678_9ABC_DEF0);
let mut s = String::from("seed café 日本語\n🎉");
for _ in 0..4000 {
let t = mutate(&s, &mut rng);
let a = ropey::Rope::from_str(&s);
let b = ropey::Rope::from_str(&t);
let d = diff(&a, &b);
assert_eq!(
apply_forward(&a, &d).to_string(),
t,
"forward a->b failed (start={}, old={:?}, new={:?})",
d.start,
d.old,
d.new
);
assert_eq!(
apply_inverse(&b, &d).to_string(),
s,
"inverse b->a failed (start={}, old={:?}, new={:?})",
d.start,
d.old,
d.new
);
s = t;
}
}
#[test]
fn diff_round_trips_over_unrelated_pairs() {
let corpus = [
"",
"a",
"café\n日本語\n",
"🎉🎉🎉",
"abcdef",
"日本",
"x\ny\nz\n",
"aXb",
"café",
"語日本",
"\n\n\n",
"🎉x🎉y🎉",
];
let mut rng = Rng::new(0xDEAD_BEEF_CAFE_1234);
for _ in 0..3000 {
let sa = corpus[rng.below(corpus.len())];
let sb = corpus[rng.below(corpus.len())];
let a = ropey::Rope::from_str(sa);
let b = ropey::Rope::from_str(sb);
let d = diff(&a, &b);
assert_eq!(apply_forward(&a, &d).to_string(), sb);
assert_eq!(apply_inverse(&b, &d).to_string(), sa);
}
}
#[test]
fn non_ascii_edit_undo_redo_round_trip() {
let mut d = Driver::new("café\n日本語\n");
d.edit("cafés\n日本語\n");
d.edit("cafés\n日本語です\n");
d.edit("cafés\n日本語です🎉\n");
assert_eq!(d.undo().as_deref(), Some("cafés\n日本語です\n"));
assert_eq!(d.undo().as_deref(), Some("cafés\n日本語\n"));
assert_eq!(d.undo().as_deref(), Some("café\n日本語\n"));
assert_eq!(d.redo().as_deref(), Some("cafés\n日本語\n"));
assert_eq!(d.redo().as_deref(), Some("cafés\n日本語です\n"));
assert_eq!(d.redo().as_deref(), Some("cafés\n日本語です🎉\n"));
assert_warm_equals_cold(&mut d.t);
}
#[test]
fn tree_matches_full_snapshot_reference_over_random_ops() {
let mut rng = Rng::new(0x9E37_79B9_7F4A_7C15);
let start = "α\nβγ\n日本🎉\n";
let mut real = UndoTree::new(ropey::Rope::from_str(start));
let mut refr = RefTree::new(start);
let mut live = start.to_string();
for step in 0..6000 {
assert_eq!(real.is_at_root(), refr.is_at_root(), "is_at_root @ {step}");
assert_eq!(real.has_redo(), refr.has_redo(), "has_redo @ {step}");
assert_eq!(real.depth(), refr.depth(), "depth @ {step}");
match rng.below(6) {
0 | 1 => {
let pre = live.clone();
real.push(entry_str(&pre));
refr.push(&pre);
live = mutate(&live, &mut rng);
}
2 => {
let got = real
.undo_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
)
.map(|e| e.rope.to_string());
let want = refr.undo_step(&live);
assert_eq!(got, want, "undo @ {step}");
if let Some(c) = got {
live = c;
}
}
3 => {
let got = real
.redo_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
)
.map(|e| e.rope.to_string());
let want = refr.redo_step(&live);
assert_eq!(got, want, "redo @ {step}");
if let Some(c) = got {
live = c;
}
}
4 => {
let got = real
.seq_earlier_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
)
.map(|e| e.rope.to_string());
let want = refr.seq_earlier_step(&live);
assert_eq!(got, want, "g- @ {step}");
if let Some(c) = got {
live = c;
}
}
_ => {
let got = real
.seq_later_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
)
.map(|e| e.rope.to_string());
let want = refr.seq_later_step(&live);
assert_eq!(got, want, "g+ @ {step}");
if let Some(c) = got {
live = c;
}
}
}
if step % 200 == 0 {
assert_warm_equals_cold(&mut real);
}
}
assert_warm_equals_cold(&mut real);
}
#[track_caller]
fn assert_materialize_matches_naive(t: &mut UndoTree) {
for id in t.live_ids() {
let naive = t.materialize_naive(id).to_string();
let got = t.materialize_for_test(id).to_string();
assert_eq!(got, naive, "accelerated != naive root replay for node {id}");
}
}
fn deep_linear_history(n: usize) -> (UndoTree, Vec<String>) {
let base: String =
"the quick brown fox\njumps over the lazy dog\ncafé 日本語 🎉\n".repeat(20);
let mut t = UndoTree::new(ropey::Rope::from_str(&base));
let mut states = vec![base.clone()];
let mut live = base;
for i in 0..n {
t.push(entry_str(&live));
live = format!("e{i} {live}");
states.push(live.clone());
}
t.sync_current(ropey::Rope::from_str(&live));
(t, states)
}
#[test]
fn deep_history_walks_back_and_forward_exactly() {
let n = 200;
assert!(n > 4 * KEYFRAME_INTERVAL);
let (mut t, states) = deep_linear_history(n);
let mut live = states[n].clone();
for want in (0..n).rev() {
let got = t
.seq_earlier_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
)
.expect("history is deeper than the walk");
live = got.rope.to_string();
assert_eq!(live, states[want], "g- onto seq {want}");
}
assert!(
t.seq_earlier_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default()
)
.is_none(),
"walk ended at the oldest state"
);
for (seq, want) in states.iter().enumerate().skip(1) {
let got = t
.seq_later_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
)
.expect("history is deeper than the walk");
live = got.rope.to_string();
assert_eq!(&live, want, "g+ onto seq {seq}");
}
assert_materialize_matches_naive(&mut t);
assert_warm_equals_cold(&mut t);
}
#[test]
fn keyframes_bound_the_cold_replay_distance() {
let n = 200;
let (mut t, _) = deep_linear_history(n);
t.drop_warm_caches();
for id in t.live_ids() {
let d = t.replay_distance(id);
assert!(
d < KEYFRAME_INTERVAL,
"node {id} (depth {}) replays {d} deltas, over the keyframe bound",
t.get(id).depth
);
}
let deepest = *t
.live_ids()
.iter()
.max_by_key(|&&id| t.get(id).depth)
.unwrap();
t.drop_all_caches();
assert!(t.replay_distance(deepest) > KEYFRAME_INTERVAL);
t.materialize_for_test(deepest);
t.drop_warm_caches();
for id in t.live_ids() {
assert!(t.replay_distance(id) < KEYFRAME_INTERVAL, "node {id}");
}
}
#[test]
fn keyframe_materialize_matches_naive_over_random_ops() {
let mut rng = Rng::new(0x0FF1_CE00_D15E_A5E5);
let start = "α\nβγ\n日本🎉\nthe quick brown fox\n";
let mut t = UndoTree::new(ropey::Rope::from_str(start));
let mut live = start.to_string();
for step in 0..5000 {
match rng.below(10) {
0..=5 => {
let pre = live.clone();
t.push(entry_str(&pre));
live = mutate(&live, &mut rng);
}
6 => {
if let Some(e) = t.undo_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
) {
live = e.rope.to_string();
}
}
7 => {
if let Some(e) = t.redo_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
) {
live = e.rope.to_string();
}
}
8 => {
if let Some(e) = t.seq_earlier_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
) {
live = e.rope.to_string();
}
}
_ => {
if let Some(e) = t.seq_later_step(
ropey::Rope::from_str(&live),
(0, 0),
MarkSnapshot::default(),
) {
live = e.rope.to_string();
}
}
}
let cur = t.current;
assert_eq!(
t.materialize_for_test(cur).to_string(),
t.materialize_naive(cur).to_string(),
"current node diverged @ {step}"
);
if step % 250 == 0 {
assert_materialize_matches_naive(&mut t);
}
if step % 700 == 0 {
t.cap(60);
}
}
assert_materialize_matches_naive(&mut t);
assert_warm_equals_cold(&mut t);
}
#[test]
fn deserialized_deep_tree_rebuilds_the_keyframe_ladder() {
let n = 100;
let (t, states) = deep_linear_history(n);
let ser = t.to_serializable();
let mut back = UndoTree::from_serializable(&ser).expect("valid projection");
let deepest = *back
.live_ids()
.iter()
.max_by_key(|&&id| back.get(id).depth)
.unwrap();
assert_eq!(back.get(deepest).depth, n, "depths recomputed on load");
assert_eq!(back.materialize_for_test(deepest).to_string(), states[n]);
assert_materialize_matches_naive(&mut back);
back.drop_warm_caches();
for id in back.live_ids() {
assert!(back.replay_distance(id) < KEYFRAME_INTERVAL, "node {id}");
}
}
fn assert_warm_equals_cold(t: &mut UndoTree) {
let ids = t.live_ids();
let warm: Vec<String> = ids
.iter()
.map(|&id| t.materialize_for_test(id).to_string())
.collect();
t.drop_all_caches();
for (i, &id) in ids.iter().enumerate() {
let cold = t.materialize_for_test(id).to_string();
assert_eq!(cold, warm[i], "warm != cold for node {id}");
}
}
struct Driver {
t: UndoTree,
live: String,
}
impl Driver {
fn new(s: &str) -> Self {
Self {
t: UndoTree::new(ropey::Rope::from_str(s)),
live: s.to_string(),
}
}
fn edit(&mut self, new: &str) {
self.t.push(entry_str(&self.live));
self.live = new.to_string();
}
fn undo(&mut self) -> Option<String> {
let e = self.t.undo_step(
ropey::Rope::from_str(&self.live),
(0, 0),
MarkSnapshot::default(),
)?;
self.live = e.rope.to_string();
Some(self.live.clone())
}
fn redo(&mut self) -> Option<String> {
let e = self.t.redo_step(
ropey::Rope::from_str(&self.live),
(0, 0),
MarkSnapshot::default(),
)?;
self.live = e.rope.to_string();
Some(self.live.clone())
}
}
struct RefNode {
parent: Option<usize>,
children: Vec<usize>,
last_child: Option<usize>,
content: String,
seq: u64,
}
struct RefTree {
nodes: Vec<Option<RefNode>>,
current: usize,
next_seq: u64,
}
impl RefTree {
fn new(s: &str) -> Self {
let root = RefNode {
parent: None,
children: Vec::new(),
last_child: None,
content: s.to_string(),
seq: 0,
};
Self {
nodes: vec![Some(root)],
current: 0,
next_seq: 1,
}
}
fn get(&self, id: usize) -> &RefNode {
self.nodes[id].as_ref().unwrap()
}
fn get_mut(&mut self, id: usize) -> &mut RefNode {
self.nodes[id].as_mut().unwrap()
}
fn alloc(&mut self, n: RefNode) -> usize {
self.nodes.push(Some(n));
self.nodes.len() - 1
}
fn is_at_root(&self) -> bool {
self.get(self.current).parent.is_none()
}
fn has_redo(&self) -> bool {
self.get(self.current).last_child.is_some()
}
fn depth(&self) -> usize {
let mut d = 0;
let mut n = self.get(self.current).parent;
while let Some(p) = n {
d += 1;
n = self.get(p).parent;
}
d
}
fn push(&mut self, pre: &str) {
let cur = self.current;
self.get_mut(cur).content = pre.to_string();
let seq = self.next_seq;
self.next_seq += 1;
let child = self.alloc(RefNode {
parent: Some(cur),
children: Vec::new(),
last_child: None,
content: pre.to_string(),
seq,
});
let c = self.get_mut(cur);
c.children.push(child);
c.last_child = Some(child);
self.current = child;
}
fn undo_step(&mut self, live: &str) -> Option<String> {
let cur = self.current;
let par = self.get(cur).parent?;
self.get_mut(cur).content = live.to_string();
self.get_mut(par).last_child = Some(cur);
self.current = par;
Some(self.get(par).content.clone())
}
fn redo_step(&mut self, live: &str) -> Option<String> {
let cur = self.current;
let child = self.get(cur).last_child?;
self.get_mut(cur).content = live.to_string();
self.current = child;
Some(self.get(child).content.clone())
}
fn current_seq(&self) -> u64 {
self.get(self.current).seq
}
fn node_below(&self, s: u64) -> Option<usize> {
let mut best: Option<(u64, usize)> = None;
for (id, slot) in self.nodes.iter().enumerate() {
if let Some(n) = slot
&& n.seq < s
&& best.is_none_or(|(bs, _)| n.seq > bs)
{
best = Some((n.seq, id));
}
}
best.map(|(_, id)| id)
}
fn node_above(&self, s: u64) -> Option<usize> {
let mut best: Option<(u64, usize)> = None;
for (id, slot) in self.nodes.iter().enumerate() {
if let Some(n) = slot
&& n.seq > s
&& best.is_none_or(|(bs, _)| n.seq < bs)
{
best = Some((n.seq, id));
}
}
best.map(|(_, id)| id)
}
fn retarget(&mut self, target: usize) {
self.current = target;
let mut node = target;
while let Some(p) = self.get(node).parent {
self.get_mut(p).last_child = Some(node);
node = p;
}
}
fn stash_and_move(&mut self, target: usize, live: &str) {
let cur = self.current;
self.get_mut(cur).content = live.to_string();
self.retarget(target);
}
fn seq_earlier_step(&mut self, live: &str) -> Option<String> {
let target = self.node_below(self.current_seq())?;
self.stash_and_move(target, live);
Some(self.get(target).content.clone())
}
fn seq_later_step(&mut self, live: &str) -> Option<String> {
let target = self.node_above(self.current_seq())?;
self.stash_and_move(target, live);
Some(self.get(target).content.clone())
}
}
}
#[cfg(test)]
mod serialize_tests {
use super::*;
fn e(text: &str) -> UndoEntry {
UndoEntry {
rope: ropey::Rope::from_str(text),
cursor: (0, 0),
timestamp: SystemTime::now(),
marks: MarkSnapshot::default(),
}
}
fn l(text: &str) -> (ropey::Rope, (usize, usize), MarkSnapshot) {
(ropey::Rope::from_str(text), (0, 0), MarkSnapshot::default())
}
fn headline_tree() -> UndoTree {
let mut t = UndoTree::new(ropey::Rope::from_str("s0"));
for pre in ["s0", "s1", "s2", "s3", "s4"] {
t.push(e(pre)); }
let (r, c, m) = l("s5");
t.undo_step(r, c, m); let (r, c, m) = l("s4");
t.undo_step(r, c, m); t.sync_current(ropey::Rope::from_str("s3")); t
}
fn content_by_seq(t: &mut UndoTree) -> std::collections::BTreeMap<u64, String> {
t.live_ids()
.into_iter()
.map(|id| {
let seq = t.get(id).seq;
(seq, t.materialize_for_test(id).to_string())
})
.collect()
}
#[test]
fn round_trip_reproduces_structure_and_content() {
let mut orig = headline_tree();
let cur_seq = orig.current_node_seq();
let ser = orig.to_serializable();
let orig_content = content_by_seq(&mut orig);
let mut back = UndoTree::from_serializable(&ser).expect("valid projection");
assert_eq!(back.current_node_seq(), cur_seq, "current preserved");
assert_eq!(back.next_seq, orig.next_seq, "next_seq preserved");
assert_eq!(
content_by_seq(&mut back),
orig_content,
"content at every node reproduced"
);
assert_eq!(orig_content.len(), 6);
assert_eq!(orig_content[&3], "s3");
assert_eq!(orig_content[&5], "s5");
}
#[test]
fn deserialized_tree_walks_forward_and_back() {
let ser = headline_tree().to_serializable();
let mut t = UndoTree::from_serializable(&ser).unwrap();
let (r, c, m) = l("s3");
assert_eq!(t.redo_step(r, c, m).unwrap().rope.to_string(), "s4");
let (r, c, m) = l("s4");
assert_eq!(t.redo_step(r, c, m).unwrap().rope.to_string(), "s5");
let mut live = "s5".to_string();
for want in ["s4", "s3", "s2", "s1", "s0"] {
let (r, c, m) = l(&live);
assert_eq!(t.undo_step(r, c, m).unwrap().rope.to_string(), want);
live = want.to_string();
}
assert!(t.is_at_root());
}
#[test]
fn from_serializable_rejects_out_of_range_current() {
let mut ser = headline_tree().to_serializable();
ser.current = ser.nodes.len() as u32; assert!(UndoTree::from_serializable(&ser).is_none());
}
#[test]
fn from_serializable_rejects_non_root_missing_delta() {
let mut ser = headline_tree().to_serializable();
let victim = if ser.root == 0 { 1 } else { 0 };
ser.nodes[victim].delta = None;
assert!(UndoTree::from_serializable(&ser).is_none());
}
#[test]
fn multibyte_content_survives_round_trip() {
let mut t = UndoTree::new(ropey::Rope::from_str("café\n日本語"));
t.push(e("café\n日本語"));
t.push(e("cafés\n日本語"));
t.sync_current(ropey::Rope::from_str("cafés\n日本語です🎉"));
let want = content_by_seq(&mut t);
let ser = t.to_serializable();
let mut back = UndoTree::from_serializable(&ser).unwrap();
assert_eq!(content_by_seq(&mut back), want);
}
}