use egui_richedit::Position;
use odox_core::Element;
const DEPTH: usize = 100;
pub type Caret = Position<Vec<usize>>;
struct Snapshot {
content: Element,
caret: Option<Caret>,
}
#[derive(Default)]
pub struct Editing {
pub on: bool,
pub asking: bool,
undo: Vec<Snapshot>,
redo: Vec<Snapshot>,
saved_at: Option<usize>,
revision: u64,
}
impl Editing {
pub fn reset(&mut self) {
self.undo.clear();
self.redo.clear();
self.saved_at = Some(0);
self.touch();
}
pub fn touch(&mut self) {
self.revision += 1;
}
pub fn revision(&self) -> u64 {
self.revision
}
pub fn record(&mut self, content: &Element) {
self.record_snapshot(content.clone(), None);
}
pub fn record_snapshot(&mut self, content: Element, caret: Option<Caret>) {
if self.saved_at.is_some_and(|depth| depth > self.undo.len()) {
self.saved_at = None;
}
self.redo.clear();
self.touch();
self.undo.push(Snapshot { content, caret });
if self.undo.len() > DEPTH {
self.undo.remove(0);
self.saved_at = self.saved_at.and_then(|depth| depth.checked_sub(1));
}
}
pub fn undo(
&mut self,
current: &Element,
caret: Option<Caret>,
) -> Option<(Element, Option<Caret>)> {
let previous = self.undo.pop()?;
self.touch();
self.redo.push(Snapshot {
content: current.clone(),
caret,
});
Some((previous.content, previous.caret))
}
pub fn redo(
&mut self,
current: &Element,
caret: Option<Caret>,
) -> Option<(Element, Option<Caret>)> {
let next = self.redo.pop()?;
self.touch();
self.undo.push(Snapshot {
content: current.clone(),
caret,
});
Some((next.content, next.caret))
}
pub fn can_undo(&self) -> bool {
!self.undo.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo.is_empty()
}
pub fn modified(&self) -> bool {
self.saved_at != Some(self.undo.len())
}
pub fn mark_saved(&mut self) {
self.saved_at = Some(self.undo.len());
}
}
#[cfg(test)]
mod tests {
use super::Editing;
use odox_core::{Element, Ns};
fn tree(text: &str) -> Element {
let mut e = Element::new("text", "p", Ns::Text);
e.children.push(odox_core::Node::Text(text.to_owned()));
e
}
#[test]
fn undo_returns_to_the_saved_state_and_redo_leaves_it() {
let mut editing = Editing::default();
editing.reset();
assert!(!editing.modified());
let mut current = tree("one");
editing.record(¤t);
current = tree("two");
assert!(editing.modified());
current = editing
.undo(¤t, None)
.map(|(tree, _)| tree)
.expect("something to undo");
assert_eq!(current, tree("one"));
assert!(!editing.modified(), "undone to what the file holds");
current = editing
.redo(¤t, None)
.map(|(tree, _)| tree)
.expect("something to redo");
assert_eq!(current, tree("two"));
assert!(editing.modified());
}
#[test]
fn a_new_edit_below_the_saved_state_makes_it_unreachable() {
let mut editing = Editing::default();
editing.reset();
let mut current = tree("one");
editing.record(¤t);
current = tree("two");
editing.mark_saved();
assert!(!editing.modified());
current = editing
.undo(¤t, None)
.map(|(tree, _)| tree)
.expect("something to undo");
assert!(editing.modified());
editing.record(¤t);
assert!(
editing.modified(),
"the same depth is not the same document"
);
assert!(!editing.can_redo(), "the branch the save was on is gone");
}
#[test]
fn undo_and_redo_answer_where_the_caret_was() {
let mut editing = Editing::default();
editing.reset();
let before = super::Caret::new(vec![0], 1);
let after = super::Caret::new(vec![0], 4);
editing.record_snapshot(tree("one"), Some(before.clone()));
let (_, caret) = editing
.undo(&tree("one more"), Some(after.clone()))
.expect("something to undo");
assert_eq!(caret, Some(before), "where the edit began");
let (_, caret) = editing.redo(&tree("one"), None).expect("something to redo");
assert_eq!(caret, Some(after), "where the caret was when it was undone");
}
#[test]
fn the_stack_is_bounded() {
let mut editing = Editing::default();
editing.reset();
for i in 0..(super::DEPTH + 10) {
editing.record(&tree(&i.to_string()));
}
assert!(editing.modified());
let mut undone = 0;
let mut current = tree("last");
while let Some(previous) = editing.undo(¤t, None).map(|(tree, _)| tree) {
current = previous;
undone += 1;
}
assert_eq!(undone, super::DEPTH);
assert!(
editing.modified(),
"the saved state was forgotten with the oldest snapshots"
);
}
}