strop_core/history/
validate.rs1use super::{Edit, EditKind, History};
3use ropey::Rope;
4
5#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
6pub enum HistoryError {
7 #[error("invalid history revision graph")]
8 Graph,
9 #[error("history edit range is outside text or splits UTF-8")]
10 Range,
11 #[error("history edit does not match the recorded text")]
12 TextMismatch,
13}
14
15impl History {
16 pub fn validate(&self) -> Result<(), HistoryError> {
17 if self.revisions.is_empty() || self.current >= self.revisions.len() {
18 return Err(HistoryError::Graph);
19 }
20 for (index, revision) in self.revisions.iter().enumerate() {
21 if (index == 0 && revision.parent != 0) || (index > 0 && revision.parent >= index) {
22 return Err(HistoryError::Graph);
23 }
24 if let Some(child) = revision.last_child {
25 if child <= index
26 || child >= self.revisions.len()
27 || self.revisions[child].parent != index
28 {
29 return Err(HistoryError::Graph);
30 }
31 }
32 }
33 Ok(())
34 }
35
36 pub fn validate_for(&self, text: &Rope) -> Result<(), HistoryError> {
37 self.validate()?;
38 if self.pending.is_some() {
39 return Err(HistoryError::Graph);
40 }
41 let mut root = text.clone();
42 let mut current = self.current;
43 while current != 0 {
44 apply(&mut root, self.revisions[current].undo.iter().rev())?;
45 current = self.revisions[current].parent;
46 }
47 let mut states = Vec::with_capacity(self.revisions.len());
48 states.push(root);
49 for revision in self.revisions.iter().skip(1) {
50 let parent = &states[revision.parent];
51 let mut child = parent.clone();
52 apply(&mut child, revision.redo.iter())?;
53 let mut restored = child.clone();
54 apply(&mut restored, revision.undo.iter().rev())?;
55 if &restored != parent {
56 return Err(HistoryError::TextMismatch);
57 }
58 states.push(child);
59 }
60 if states[self.current] != *text {
61 return Err(HistoryError::TextMismatch);
62 }
63 Ok(())
64 }
65}
66
67fn boundary(text: &Rope, byte: usize) -> bool {
68 byte <= text.len_bytes() && text.char_to_byte(text.byte_to_char(byte)) == byte
69}
70
71fn apply<'a>(text: &mut Rope, edits: impl Iterator<Item = &'a Edit>) -> Result<(), HistoryError> {
72 for edit in edits {
73 if !boundary(text, edit.at) {
74 return Err(HistoryError::Range);
75 }
76 match edit.kind {
77 EditKind::Insert => text.insert(text.byte_to_char(edit.at), &edit.text),
78 EditKind::Delete => {
79 let end = edit
80 .at
81 .checked_add(edit.text.len())
82 .ok_or(HistoryError::Range)?;
83 if !boundary(text, end) {
84 return Err(HistoryError::Range);
85 }
86 if text.byte_slice(edit.at..end) != edit.text {
87 return Err(HistoryError::TextMismatch);
88 }
89 text.remove(text.byte_to_char(edit.at)..text.byte_to_char(end));
90 }
91 }
92 }
93 Ok(())
94}