tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}