tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 614Markdown 274TOML 149RON 310606 Definiciones

⎇ main widgets/text-editor-core/src/undo.rs Rust · 197 líneas · 6.6 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Pila de undo/redo basada en [`EditDelta`].
2//!
3//! API simple: `push(delta)` añade al historial y limpia el stack de
4//! redo; `undo`/`redo` aplican o reaplican deltas existentes. No
5//! coalesce inserciones consecutivas — cada keystroke es un delta;
6//! para una UX más fina, el llamador puede agrupar deltas relacionados
7//! (ej. cada secuencia de chars imprimibles hasta whitespace).
8
9use alloc::vec::Vec;
10
11use crate::buffer::Buffer;
12use crate::cursor::Cursor;
13use crate::ops::EditDelta;
14
15const DEFAULT_CAPACITY: usize = 256;
16
17#[derive(Debug, Clone, Default)]
18pub struct UndoStack {
19    /// Deltas aplicados, en orden cronológico. El `Vec::last` es el
20    /// próximo candidato a deshacer.
21    done: Vec<EditDelta>,
22    /// Deltas deshechos disponibles para redo (en orden inverso del
23    /// `undo`: el último deshecho es el primero a rehacer).
24    undone: Vec<EditDelta>,
25    capacity: usize,
26}
27
28impl UndoStack {
29    pub fn new() -> Self {
30        Self::with_capacity(DEFAULT_CAPACITY)
31    }
32    pub fn with_capacity(capacity: usize) -> Self {
33        Self {
34            done: Vec::with_capacity(capacity.min(64)),
35            undone: Vec::new(),
36            capacity,
37        }
38    }
39
40    /// Registra un delta. Limpia el stack de redo (la rama alternativa
41    /// se pierde, como en todo editor estándar).
42    pub fn push(&mut self, delta: EditDelta) {
43        self.done.push(delta);
44        self.undone.clear();
45        if self.done.len() > self.capacity {
46            // Truncamos por el extremo viejo.
47            let drop = self.done.len() - self.capacity;
48            self.done.drain(0..drop);
49        }
50    }
51
52    /// Registra un delta intentando **fusionarlo** con el anterior cuando
53    /// `coalesce` es true. Sirve para que una ráfaga de tecleo cuente como
54    /// un solo undo en vez de uno por carácter. Sólo fusiona inserciones
55    /// puras (sin borrado) y contiguas (el nuevo arranca justo donde
56    /// terminó el previo), y nunca a través de un salto de línea. En
57    /// cualquier otro caso cae a [`Self::push`] normal. El caller pasa
58    /// `coalesce = false` para forzar un corte de grupo (movimiento de
59    /// caret, pegado, borrado, etc.).
60    pub fn push_coalesce(&mut self, delta: EditDelta, coalesce: bool) {
61        if coalesce {
62            if let Some(prev) = self.done.last_mut() {
63                let prev_insert = prev.removed.is_empty() && !prev.inserted.is_empty();
64                let new_insert = delta.removed.is_empty() && !delta.inserted.is_empty();
65                let contiguous = delta.start == prev.start + prev.inserted.chars().count();
66                let no_newline =
67                    !prev.inserted.ends_with('\n') && !delta.inserted.contains('\n');
68                if prev_insert && new_insert && contiguous && no_newline {
69                    prev.inserted.push_str(&delta.inserted);
70                    prev.cursor_after = delta.cursor_after;
71                    self.undone.clear();
72                    return;
73                }
74            }
75        }
76        self.push(delta);
77    }
78
79    pub fn can_undo(&self) -> bool {
80        !self.done.is_empty()
81    }
82    pub fn can_redo(&self) -> bool {
83        !self.undone.is_empty()
84    }
85
86    pub fn undo(&mut self, buf: &mut Buffer, cursor: &mut Cursor) -> bool {
87        let Some(delta) = self.done.pop() else {
88            return false;
89        };
90        delta.undo(buf, cursor);
91        self.undone.push(delta);
92        true
93    }
94
95    pub fn redo(&mut self, buf: &mut Buffer, cursor: &mut Cursor) -> bool {
96        let Some(delta) = self.undone.pop() else {
97            return false;
98        };
99        delta.apply(buf, cursor);
100        self.done.push(delta);
101        true
102    }
103
104    pub fn clear(&mut self) {
105        self.done.clear();
106        self.undone.clear();
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::ops::replace_selection;
114
115    #[test]
116    fn undo_y_redo_son_simetricos() {
117        let mut b = Buffer::from_str("a");
118        let mut c = Cursor::at(0, 1);
119        let mut st = UndoStack::new();
120
121        st.push(replace_selection(&mut b, &mut c, "b"));
122        st.push(replace_selection(&mut b, &mut c, "c"));
123        assert_eq!(b.text(), "abc");
124
125        assert!(st.undo(&mut b, &mut c));
126        assert_eq!(b.text(), "ab");
127        assert!(st.undo(&mut b, &mut c));
128        assert_eq!(b.text(), "a");
129
130        assert!(st.redo(&mut b, &mut c));
131        assert_eq!(b.text(), "ab");
132        assert!(st.redo(&mut b, &mut c));
133        assert_eq!(b.text(), "abc");
134    }
135
136    #[test]
137    fn push_limpia_redo() {
138        let mut b = Buffer::from_str("a");
139        let mut c = Cursor::at(0, 1);
140        let mut st = UndoStack::new();
141        st.push(replace_selection(&mut b, &mut c, "b"));
142        st.undo(&mut b, &mut c);
143        assert!(st.can_redo());
144        st.push(replace_selection(&mut b, &mut c, "X"));
145        assert!(!st.can_redo());
146    }
147
148    #[test]
149    fn push_coalesce_fusiona_tecleo_contiguo() {
150        let mut b = Buffer::from_str("");
151        let mut c = Cursor::at(0, 0);
152        let mut st = UndoStack::new();
153        // Tipear "abc" carácter por carácter, coalescing.
154        for ch in ["a", "b", "c"] {
155            let d = replace_selection(&mut b, &mut c, ch);
156            st.push_coalesce(d, true);
157        }
158        assert_eq!(b.text(), "abc");
159        // Un solo undo borra los tres.
160        assert!(st.undo(&mut b, &mut c));
161        assert_eq!(b.text(), "");
162        assert!(!st.can_undo());
163    }
164
165    #[test]
166    fn push_coalesce_corta_en_newline_y_sin_coalesce() {
167        let mut b = Buffer::from_str("");
168        let mut c = Cursor::at(0, 0);
169        let mut st = UndoStack::new();
170        st.push_coalesce(replace_selection(&mut b, &mut c, "ab"), true);
171        // Enter / salto de línea = grupo aparte (coalesce false).
172        st.push_coalesce(replace_selection(&mut b, &mut c, "\n"), false);
173        st.push_coalesce(replace_selection(&mut b, &mut c, "cd"), true);
174        assert_eq!(b.text(), "ab\ncd");
175        st.undo(&mut b, &mut c);
176        assert_eq!(b.text(), "ab\n"); // sólo "cd"
177        st.undo(&mut b, &mut c);
178        assert_eq!(b.text(), "ab"); // sólo el "\n"
179        st.undo(&mut b, &mut c);
180        assert_eq!(b.text(), ""); // "ab"
181    }
182
183    #[test]
184    fn capacity_descartan_viejos() {
185        let mut b = Buffer::from_str("");
186        let mut c = Cursor::at(0, 0);
187        let mut st = UndoStack::with_capacity(2);
188        for ch in ["a", "b", "c"] {
189            st.push(replace_selection(&mut b, &mut c, ch));
190        }
191        // Sólo deberían quedar los últimos 2 deltas; el undo del primero
192        // (cuando ya no está) no debería hacer nada.
193        st.undo(&mut b, &mut c);
194        st.undo(&mut b, &mut c);
195        assert!(!st.undo(&mut b, &mut c));
196    }
197}