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
8/// One buffer mutation as seen by history. Both directions are stored so
9/// redo replays exactly what undo undid — no re-derivation.
10#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11pub struct Edit {
12    pub at: usize,
13    /// Text inserted (for Insert) or removed (for Delete) by this edit.
14    pub text: String,
15    pub kind: EditKind,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub enum EditKind {
20    Insert,
21    Delete,
22}
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
25struct Revision {
26    parent: usize,
27    last_child: Option<usize>,
28    /// Applied in reverse order on undo.
29    undo: Vec<Edit>,
30    /// Applied in order on redo.
31    redo: Vec<Edit>,
32}
33
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct History {
36    /// Depth cap (0001 §3: full trees per project bloat fast).
37    revisions: Vec<Revision>,
38    current: usize,
39    /// Open transaction: (undo ops, redo ops), recorded in apply order.
40    pending: Option<(Vec<Edit>, Vec<Edit>)>,
41}
42
43impl Default for History {
44    /// Revisions[0] is the root sentinel — `current == 0` means "nothing
45    /// to undo" and undo never lands above it.
46    fn default() -> Self {
47        Self {
48            revisions: vec![Revision {
49                parent: 0,
50                last_child: None,
51                undo: vec![],
52                redo: vec![],
53            }],
54            current: 0,
55            pending: None,
56        }
57    }
58}
59
60/// One row of the undo-tree browser (`Space u`).
61#[derive(Debug, Clone)]
62pub struct RevisionRow {
63    pub index: usize,
64    pub parent: usize,
65    /// Depth in the tree (root = 0) — the browser indents by it.
66    pub depth: usize,
67    /// Short description of the change, e.g. `+ "foo"` / `- "bar"`.
68    pub summary: String,
69    pub is_current: bool,
70    /// True when the revision's parent isn't depth-1 above it in display
71    /// order — the browser draws a branch marker.
72    pub branches: bool,
73}
74
75impl History {
76    /// The revision tree, newest-first, for the undo-tree browser.
77    pub fn tree_rows(&self) -> Vec<RevisionRow> {
78        let mut depths = vec![0usize; self.revisions.len()];
79        for i in 1..self.revisions.len() {
80            depths[i] = depths[self.revisions[i].parent] + 1;
81        }
82        let mut out: Vec<RevisionRow> = (1..self.revisions.len())
83            .rev()
84            .map(|i| {
85                let rev = &self.revisions[i];
86                let first = rev.redo.first();
87                let summary = match first {
88                    Some(e) => {
89                        let sign = match e.kind {
90                            EditKind::Insert => "+",
91                            EditKind::Delete => "-",
92                        };
93                        let text: String = e
94                            .text
95                            .chars()
96                            .take(24)
97                            .map(|c| if c == '\n' { '↵' } else { c })
98                            .collect();
99                        let more = if e.text.chars().count() > 24 {
100                            "…"
101                        } else {
102                            ""
103                        };
104                        format!("{sign} \"{text}{more}\"")
105                    }
106                    None => "(empty)".into(),
107                };
108                RevisionRow {
109                    index: i,
110                    parent: rev.parent,
111                    depth: depths[i],
112                    summary,
113                    is_current: i == self.current,
114                    // a sibling with the same parent already exists →
115                    // this revision forked off a branch
116                    branches: self.revisions[..i].iter().any(|r| r.parent == rev.parent),
117                }
118            })
119            .collect();
120        out.sort_by_key(|r| std::cmp::Reverse(r.index));
121        out
122    }
123
124    /// Edits that move the buffer from `current` to `target`: undo up to
125    /// the fork, redo down the target's branch. None when target is
126    /// unknown. `current` is updated; the caller applies the ops.
127    pub fn ops_to(&mut self, target: usize) -> Option<Vec<Edit>> {
128        if target >= self.revisions.len() {
129            return None;
130        }
131        // ancestors of current (inclusive), root-last
132        let mut anc_cur = Vec::new();
133        let mut at = self.current;
134        loop {
135            anc_cur.push(at);
136            if at == 0 {
137                break;
138            }
139            at = self.revisions[at].parent;
140        }
141        // walk target up to the fork
142        let mut up_path = Vec::new(); // target..fork, target-first
143        let mut t = target;
144        while !anc_cur.contains(&t) {
145            up_path.push(t);
146            t = self.revisions[t].parent;
147        }
148        let fork = t;
149        let mut ops = Vec::new();
150        // undo: current up to (not incl.) the fork
151        let mut c = self.current;
152        while c != fork {
153            let mut rev_undo = self.revisions[c].undo.clone();
154            rev_undo.reverse();
155            ops.extend(rev_undo);
156            c = self.revisions[c].parent;
157        }
158        // redo: fork down to target (reverse of the up-walk)
159        for &r in up_path.iter().rev() {
160            ops.extend(self.revisions[r].redo.clone());
161        }
162        // keep last_child pointers honest along both legs
163        let mut c = self.current;
164        while c != fork {
165            let p = self.revisions[c].parent;
166            self.revisions[p].last_child = Some(c);
167            c = p;
168        }
169        let mut p = fork;
170        for &r in up_path.iter().rev() {
171            self.revisions[p].last_child = Some(r);
172            p = r;
173        }
174        self.current = target;
175        Some(ops)
176    }
177}
178
179impl History {
180    pub fn begin(&mut self) {
181        if self.pending.is_none() {
182            self.pending = Some((Vec::new(), Vec::new()));
183        }
184    }
185
186    pub fn commit(&mut self) {
187        let Some((undo, redo)) = self.pending.take() else {
188            return;
189        };
190        if undo.is_empty() {
191            return;
192        }
193        let rev = Revision {
194            parent: self.current,
195            last_child: None,
196            undo,
197            redo,
198        };
199        self.revisions.push(rev);
200        let idx = self.revisions.len() - 1;
201        self.revisions[self.current].last_child = Some(idx);
202        self.current = idx;
203    }
204
205    /// Record one buffer mutation's inverse+forward pair.
206    pub fn record(&mut self, undo: Edit, redo: Edit) {
207        if self.pending.is_none() {
208            // a lone edit outside a transaction is its own revision
209            self.begin();
210        }
211        if let Some((u, r)) = &mut self.pending {
212            u.push(undo);
213            r.push(redo);
214        }
215    }
216
217    pub fn can_undo(&self) -> bool {
218        self.current > 0
219    }
220
221    pub fn can_redo(&self) -> bool {
222        self.revisions
223            .get(self.current)
224            .and_then(|r| r.last_child)
225            .is_some()
226    }
227
228    /// The edits to apply to the buffer (in order) for one undo step.
229    pub fn undo_ops(&mut self) -> Option<Vec<Edit>> {
230        if self.current == 0 {
231            return None;
232        }
233        let rev = &self.revisions[self.current];
234        let parent = rev.parent;
235        let mut ops = rev.undo.clone();
236        ops.reverse();
237        self.revisions[parent].last_child = Some(self.current);
238        self.current = parent;
239        Some(ops)
240    }
241
242    pub fn redo_ops(&mut self) -> Option<Vec<Edit>> {
243        let child = self.revisions.get(self.current)?.last_child?;
244        let ops = self.revisions[child].redo.clone();
245        self.current = child;
246        Some(ops)
247    }
248
249    pub fn depth(&self) -> usize {
250        self.revisions.len()
251    }
252
253    /// Cap the tree at `cap` revisions: keep the ancestor chain of
254    /// `current` (branches past it fall off — in-memory trees keep
255    /// branches; the cap is about bounded state).
256    pub fn cap(&mut self, cap: usize) {
257        if self.revisions.len() <= cap {
258            return;
259        }
260        // collect the ancestor chain from current to root
261        let mut chain = Vec::new();
262        let mut at = self.current;
263        loop {
264            chain.push(at);
265            if at == 0 {
266                break;
267            }
268            at = self.revisions[at].parent;
269        }
270        chain.reverse();
271        if chain.len() > cap {
272            chain = chain[chain.len() - cap..].to_vec();
273        }
274        let mut remap = std::collections::HashMap::new();
275        let mut new_revisions = Vec::with_capacity(chain.len());
276        for (new_idx, &old_idx) in chain.iter().enumerate() {
277            remap.insert(old_idx, new_idx);
278            let mut rev = self.revisions[old_idx].clone();
279            rev.parent = if new_idx == 0 { 0 } else { new_idx - 1 };
280            rev.last_child = rev.last_child.and_then(|c| remap.get(&c).copied());
281            new_revisions.push(rev);
282        }
283        self.revisions = new_revisions;
284        self.current = *remap.get(&self.current).unwrap_or(&0);
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    fn edit(at: usize, text: &str, kind: EditKind) -> Edit {
293        Edit {
294            at,
295            text: text.into(),
296            kind,
297        }
298    }
299
300    #[test]
301    fn linear_undo_redo() {
302        let mut h = History::default();
303        h.begin();
304        h.record(
305            edit(0, "", EditKind::Delete),
306            edit(0, "x", EditKind::Insert),
307        );
308        h.commit();
309        assert!(h.can_undo());
310        let ops = h.undo_ops().unwrap();
311        assert_eq!(ops, vec![edit(0, "", EditKind::Delete)]);
312        assert!(h.can_redo());
313        let ops = h.redo_ops().unwrap();
314        assert_eq!(ops, vec![edit(0, "x", EditKind::Insert)]);
315        // at the tip after redo: undo is available, redo is not
316        assert!(h.can_undo());
317        assert!(!h.can_redo());
318    }
319
320    #[test]
321    fn edit_after_undo_forks_a_branch() {
322        let mut h = History::default();
323        h.begin();
324        h.record(
325            edit(0, "", EditKind::Delete),
326            edit(0, "a", EditKind::Insert),
327        );
328        h.commit();
329        h.undo_ops();
330        h.begin();
331        h.record(
332            edit(0, "", EditKind::Delete),
333            edit(0, "b", EditKind::Insert),
334        );
335        h.commit();
336        // the branch through "a" is still reachable: redo from root picks
337        // the last-visited child
338        assert!(h.depth() >= 2);
339    }
340}