Skip to main content

strop_core/
history.rs

1//! Undo history (Helix `helix-core/history.rs` lineage, ported):
2//! revisions form a tree — every committed transaction is a node holding
3//! both its undo and redo edit sets; `u` walks to the parent, `Ctrl-r`
4//! descends to the last-visited child. Editing after an undo forks a new
5//! branch; the tree keeps the old one (0001 pillar 4: Neovim users
6//! expect branches).
7
8mod validate;
9pub use validate::HistoryError;
10/// One buffer mutation as seen by history. Both directions are stored so
11/// redo replays exactly what undo undid — no re-derivation.
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13pub struct Edit {
14    pub at: usize,
15    /// Text inserted (for Insert) or removed (for Delete) by this edit.
16    pub text: String,
17    pub kind: EditKind,
18}
19
20/// A borrowed payload location in this buffer's immutable history tree (or its
21/// current pending node). It carries no mutation authority.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
23pub struct EditRef {
24    pub revision: usize,
25    pub index: usize,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub enum EditKind {
30    Insert,
31    Delete,
32}
33
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35struct Revision {
36    parent: usize,
37    last_child: Option<usize>,
38    /// Applied in reverse order on undo.
39    undo: Vec<Edit>,
40    /// Applied in order on redo.
41    redo: Vec<Edit>,
42}
43
44#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
45pub struct History {
46    /// Depth cap (0001 §3: full trees per project bloat fast).
47    revisions: Vec<Revision>,
48    current: usize,
49    /// Open transaction: (undo ops, redo ops), recorded in apply order.
50    pending: Option<(Vec<Edit>, Vec<Edit>)>,
51}
52
53impl Default for History {
54    /// Revisions[0] is the root sentinel — `current == 0` means "nothing
55    /// to undo" and undo never lands above it.
56    fn default() -> Self {
57        Self {
58            revisions: vec![Revision {
59                parent: 0,
60                last_child: None,
61                undo: vec![],
62                redo: vec![],
63            }],
64            current: 0,
65            pending: None,
66        }
67    }
68}
69
70/// One row of the undo-tree browser (`Space u`).
71#[derive(Debug, Clone)]
72pub struct RevisionRow {
73    pub index: usize,
74    pub parent: usize,
75    /// Depth in the tree (root = 0) — the browser indents by it.
76    pub depth: usize,
77    /// Short description of the change, e.g. `+ "foo"` / `- "bar"`.
78    pub summary: String,
79    pub is_current: bool,
80    /// True when the revision's parent isn't depth-1 above it in display
81    /// order — the browser draws a branch marker.
82    pub branches: bool,
83}
84
85#[derive(Clone, Copy)]
86pub(crate) enum HistoryAction {
87    Undo,
88    Redo,
89    Jump(usize),
90}
91
92impl History {
93    /// Count a move without changing the tree, so revision capacity is checked
94    /// before publishing history navigation or committing an open undo group.
95    pub(crate) fn movement_cost(&self, action: HistoryAction) -> Option<usize> {
96        let pending = self.pending.as_ref().map_or(0, |(undo, _)| undo.len());
97        match action {
98            HistoryAction::Undo if pending > 0 => Some(pending),
99            HistoryAction::Undo => {
100                (self.current != 0).then(|| self.revisions[self.current].undo.len())
101            }
102            HistoryAction::Redo if pending > 0 => None,
103            HistoryAction::Redo => self.revisions[self.current]
104                .last_child
105                .map(|child| self.revisions[child].redo.len()),
106            HistoryAction::Jump(target) => {
107                if target >= self.revisions.len() {
108                    return None;
109                }
110                let depth = |mut at: usize| {
111                    let mut depth = 0;
112                    while at != 0 {
113                        depth += 1;
114                        at = self.revisions[at].parent;
115                    }
116                    depth
117                };
118                let (mut from, mut to) = (self.current, target);
119                let (mut left, mut right) = (depth(from), depth(to));
120                let mut count = pending;
121                while from != to {
122                    if left >= right {
123                        count += self.revisions[from].undo.len();
124                        from = self.revisions[from].parent;
125                        left -= 1;
126                    } else {
127                        count += self.revisions[to].redo.len();
128                        to = self.revisions[to].parent;
129                        right -= 1;
130                    }
131                }
132                Some(count)
133            }
134        }
135    }
136
137    pub(crate) fn navigate(&mut self, action: HistoryAction) -> Option<Vec<Edit>> {
138        self.commit();
139        match action {
140            HistoryAction::Undo => self.undo_ops(),
141            HistoryAction::Redo => self.redo_ops(),
142            HistoryAction::Jump(target) => self.ops_to(target),
143        }
144    }
145}
146
147impl History {
148    /// The revision tree, newest-first, for the undo-tree browser.
149    pub fn tree_rows(&self) -> Vec<RevisionRow> {
150        let mut depths = vec![0usize; self.revisions.len()];
151        for i in 1..self.revisions.len() {
152            depths[i] = depths[self.revisions[i].parent] + 1;
153        }
154        let mut out: Vec<RevisionRow> = (1..self.revisions.len())
155            .rev()
156            .map(|i| {
157                let rev = &self.revisions[i];
158                let first = rev.redo.first();
159                let summary = match first {
160                    Some(e) => {
161                        let sign = match e.kind {
162                            EditKind::Insert => "+",
163                            EditKind::Delete => "-",
164                        };
165                        let text: String = e
166                            .text
167                            .chars()
168                            .take(24)
169                            .map(|c| if c == '\n' { '↵' } else { c })
170                            .collect();
171                        let more = if e.text.chars().count() > 24 {
172                            "…"
173                        } else {
174                            ""
175                        };
176                        format!("{sign} \"{text}{more}\"")
177                    }
178                    None => "(empty)".into(),
179                };
180                RevisionRow {
181                    index: i,
182                    parent: rev.parent,
183                    depth: depths[i],
184                    summary,
185                    is_current: i == self.current,
186                    // a sibling with the same parent already exists →
187                    // this revision forked off a branch
188                    branches: self.revisions[..i].iter().any(|r| r.parent == rev.parent),
189                }
190            })
191            .collect();
192        out.sort_by_key(|r| std::cmp::Reverse(r.index));
193        out
194    }
195
196    /// Edits that move the buffer from `current` to `target`: undo up to
197    /// the fork, redo down the target's branch. None when target is
198    /// unknown. `current` is updated; the caller applies the ops.
199    pub fn ops_to(&mut self, target: usize) -> Option<Vec<Edit>> {
200        if target >= self.revisions.len() {
201            return None;
202        }
203        // ancestors of current (inclusive), root-last
204        let mut anc_cur = Vec::new();
205        let mut at = self.current;
206        loop {
207            anc_cur.push(at);
208            if at == 0 {
209                break;
210            }
211            at = self.revisions[at].parent;
212        }
213        // walk target up to the fork
214        let mut up_path = Vec::new(); // target..fork, target-first
215        let mut t = target;
216        while !anc_cur.contains(&t) {
217            up_path.push(t);
218            t = self.revisions[t].parent;
219        }
220        let fork = t;
221        let mut ops = Vec::new();
222        // undo: current up to (not incl.) the fork
223        let mut c = self.current;
224        while c != fork {
225            let mut rev_undo = self.revisions[c].undo.clone();
226            rev_undo.reverse();
227            ops.extend(rev_undo);
228            c = self.revisions[c].parent;
229        }
230        // redo: fork down to target (reverse of the up-walk)
231        for &r in up_path.iter().rev() {
232            ops.extend(self.revisions[r].redo.clone());
233        }
234        // keep last_child pointers honest along both legs
235        let mut c = self.current;
236        while c != fork {
237            let p = self.revisions[c].parent;
238            self.revisions[p].last_child = Some(c);
239            c = p;
240        }
241        let mut p = fork;
242        for &r in up_path.iter().rev() {
243            self.revisions[p].last_child = Some(r);
244            p = r;
245        }
246        self.current = target;
247        Some(ops)
248    }
249}
250
251impl History {
252    pub fn begin(&mut self) {
253        if self.pending.is_none() {
254            self.pending = Some((Vec::new(), Vec::new()));
255        }
256    }
257
258    pub fn commit(&mut self) {
259        let Some((undo, redo)) = self.pending.take() else {
260            return;
261        };
262        if undo.is_empty() {
263            return;
264        }
265        let rev = Revision {
266            parent: self.current,
267            last_child: None,
268            undo,
269            redo,
270        };
271        self.revisions.push(rev);
272        let idx = self.revisions.len() - 1;
273        self.revisions[self.current].last_child = Some(idx);
274        self.current = idx;
275    }
276
277    /// Record one buffer mutation's inverse+forward pair.
278    pub fn record(&mut self, undo: Edit, redo: Edit) -> EditRef {
279        let revision = self.revisions.len();
280        let (undo_edits, redo_edits) = self.pending.get_or_insert_with(Default::default);
281        let index = redo_edits.len();
282        undo_edits.push(undo);
283        redo_edits.push(redo);
284        EditRef { revision, index }
285    }
286
287    pub(crate) fn inserted_text(&self, reference: EditRef) -> Option<&str> {
288        let edits = if reference.revision == self.revisions.len() {
289            &self.pending.as_ref()?.1
290        } else {
291            &self.revisions.get(reference.revision)?.redo
292        };
293        let edit = edits.get(reference.index)?;
294        (edit.kind == EditKind::Insert).then_some(edit.text.as_str())
295    }
296
297    pub fn can_undo(&self) -> bool {
298        self.current > 0
299    }
300
301    pub fn can_redo(&self) -> bool {
302        self.revisions
303            .get(self.current)
304            .and_then(|r| r.last_child)
305            .is_some()
306    }
307
308    /// The edits to apply to the buffer (in order) for one undo step.
309    pub fn undo_ops(&mut self) -> Option<Vec<Edit>> {
310        if self.current == 0 {
311            return None;
312        }
313        let rev = &self.revisions[self.current];
314        let parent = rev.parent;
315        let mut ops = rev.undo.clone();
316        ops.reverse();
317        self.revisions[parent].last_child = Some(self.current);
318        self.current = parent;
319        Some(ops)
320    }
321
322    pub fn redo_ops(&mut self) -> Option<Vec<Edit>> {
323        let child = self.revisions.get(self.current)?.last_child?;
324        let ops = self.revisions[child].redo.clone();
325        self.current = child;
326        Some(ops)
327    }
328
329    /// The redo ops of the revision just committed (the transaction's
330    /// edits in apply order — the anchor-map input, 0020 §14).
331    pub fn last_committed_ops(&self) -> Option<Vec<Edit>> {
332        if self.current == 0 {
333            return None;
334        }
335        Some(self.revisions[self.current].redo.clone())
336    }
337
338    /// The change list (vim g;/g,): positions of the ancestor chain's
339    /// committed revisions, NEWEST first. Each revision contributes its
340    /// earliest edit position — where that change began.
341    pub fn change_positions(&self) -> Vec<usize> {
342        let mut out = Vec::new();
343        let mut i = self.current;
344        while i != 0 {
345            let rev = &self.revisions[i];
346            // redo ops are in apply order; the first one's position is
347            // where the change starts
348            if let Some(first) = rev.redo.first() {
349                out.push(first.at);
350            }
351            i = rev.parent;
352        }
353        out
354    }
355
356    pub fn depth(&self) -> usize {
357        self.revisions.len()
358    }
359
360    /// Stable node identity only when no uncommitted edits are pending.
361    /// Unlike depth or the monotonic buffer revision, this identifies a
362    /// restored branch position after undo without accepting another branch.
363    pub fn committed_position(&self) -> Option<usize> {
364        if self
365            .pending
366            .as_ref()
367            .is_some_and(|(undo, _)| !undo.is_empty())
368        {
369            None
370        } else {
371            Some(self.current)
372        }
373    }
374
375    /// Persist only the newest linear undo path, bounded before cloning text.
376    /// Branches remain in memory. If the newest transaction exceeds the byte
377    /// limit, no history is retained: skipping it would make older edits invalid.
378    pub fn snapshot(&self, revisions: usize, text_bytes: usize) -> Self {
379        let mut chain = Vec::new();
380        let mut bytes = 0usize;
381        let mut retain = |undo: &Vec<Edit>, redo: &Vec<Edit>| {
382            let size = undo
383                .iter()
384                .chain(redo)
385                .try_fold(0usize, |n, e| n.checked_add(e.text.len()));
386            let Some(next) = size.and_then(|size| bytes.checked_add(size)) else {
387                return false;
388            };
389            if chain.len() >= revisions || next > text_bytes {
390                return false;
391            }
392            bytes = next;
393            chain.push((undo.clone(), redo.clone()));
394            true
395        };
396        if let Some((undo, redo)) = &self.pending {
397            if !undo.is_empty() && !retain(undo, redo) {
398                return Self::default();
399            }
400        }
401        let mut at = self.current;
402        while at != 0 {
403            let revision = &self.revisions[at];
404            if !retain(&revision.undo, &revision.redo) {
405                break;
406            }
407            at = revision.parent;
408        }
409        let mut snapshot = Self::default();
410        for (undo, redo) in chain.into_iter().rev() {
411            let parent = snapshot.current;
412            let index = snapshot.revisions.len();
413            snapshot.revisions[parent].last_child = Some(index);
414            snapshot.revisions.push(Revision {
415                parent,
416                last_child: None,
417                undo,
418                redo,
419            });
420            snapshot.current = index;
421        }
422        snapshot
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    fn edit(at: usize, text: &str, kind: EditKind) -> Edit {
431        Edit {
432            at,
433            text: text.into(),
434            kind,
435        }
436    }
437
438    #[test]
439    fn linear_undo_redo() {
440        let mut h = History::default();
441        h.begin();
442        h.record(
443            edit(0, "", EditKind::Delete),
444            edit(0, "x", EditKind::Insert),
445        );
446        h.commit();
447        assert!(h.can_undo());
448        let ops = h.undo_ops().unwrap();
449        assert_eq!(ops, vec![edit(0, "", EditKind::Delete)]);
450        assert!(h.can_redo());
451        let ops = h.redo_ops().unwrap();
452        assert_eq!(ops, vec![edit(0, "x", EditKind::Insert)]);
453        // at the tip after redo: undo is available, redo is not
454        assert!(h.can_undo());
455        assert!(!h.can_redo());
456    }
457
458    #[test]
459    fn edit_after_undo_forks_a_branch() {
460        let mut h = History::default();
461        h.begin();
462        h.record(
463            edit(0, "", EditKind::Delete),
464            edit(0, "a", EditKind::Insert),
465        );
466        h.commit();
467        h.undo_ops();
468        h.begin();
469        h.record(
470            edit(0, "", EditKind::Delete),
471            edit(0, "b", EditKind::Insert),
472        );
473        h.commit();
474        // the branch through "a" is still reachable: redo from root picks
475        // the last-visited child
476        assert!(h.depth() >= 2);
477    }
478}