longcipher_leptos_components/components/editor/
history.rs1use std::time::Instant;
6
7use serde::{Deserialize, Serialize};
8
9use super::cursor::CursorSet;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct HistoryEntry {
14 pub content: String,
16 pub cursors: CursorSet,
18 #[serde(skip)]
20 pub timestamp: Option<Instant>,
21}
22
23impl HistoryEntry {
24 #[must_use]
26 pub fn new(content: String, cursors: CursorSet) -> Self {
27 Self {
28 content,
29 cursors,
30 timestamp: Some(Instant::now()),
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct HistoryConfig {
38 pub max_entries: usize,
40 pub coalesce_window_ms: u64,
42}
43
44impl Default for HistoryConfig {
45 fn default() -> Self {
46 Self {
47 max_entries: 1000,
48 coalesce_window_ms: 500,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Default)]
55pub struct History {
56 undo_stack: Vec<HistoryEntry>,
58 redo_stack: Vec<HistoryEntry>,
60 config: HistoryConfig,
62 is_undoing: bool,
64}
65
66impl History {
67 #[must_use]
69 pub fn new() -> Self {
70 Self::default()
71 }
72
73 #[must_use]
75 pub fn with_config(config: HistoryConfig) -> Self {
76 Self {
77 config,
78 ..Default::default()
79 }
80 }
81
82 pub fn push(&mut self, content: String, cursors: CursorSet) {
87 if self.is_undoing {
88 return;
89 }
90
91 let entry = HistoryEntry::new(content, cursors);
92
93 if let Some(last) = self.undo_stack.last()
95 && let (Some(last_ts), Some(entry_ts)) = (last.timestamp, entry.timestamp)
96 {
97 let elapsed =
98 u64::try_from(entry_ts.duration_since(last_ts).as_millis()).unwrap_or(u64::MAX);
99 if elapsed < self.config.coalesce_window_ms {
100 return;
103 }
104 }
105
106 self.undo_stack.push(entry);
107 self.redo_stack.clear();
108
109 if self.undo_stack.len() > self.config.max_entries {
111 self.undo_stack.remove(0);
112 }
113 }
114
115 pub fn push_checkpoint(&mut self, content: String, cursors: CursorSet) {
117 if self.is_undoing {
118 return;
119 }
120
121 let mut entry = HistoryEntry::new(content, cursors);
122 entry.timestamp = None;
124
125 self.undo_stack.push(entry);
126 self.redo_stack.clear();
127
128 if self.undo_stack.len() > self.config.max_entries {
129 self.undo_stack.remove(0);
130 }
131 }
132
133 pub fn undo(
137 &mut self,
138 current_content: &str,
139 current_cursors: &CursorSet,
140 ) -> Option<HistoryEntry> {
141 let entry = self.undo_stack.pop()?;
142
143 self.redo_stack.push(HistoryEntry::new(
145 current_content.to_string(),
146 current_cursors.clone(),
147 ));
148
149 Some(entry)
150 }
151
152 pub fn redo(
156 &mut self,
157 current_content: &str,
158 current_cursors: &CursorSet,
159 ) -> Option<HistoryEntry> {
160 let entry = self.redo_stack.pop()?;
161
162 self.undo_stack.push(HistoryEntry::new(
164 current_content.to_string(),
165 current_cursors.clone(),
166 ));
167
168 Some(entry)
169 }
170
171 #[must_use]
173 pub fn can_undo(&self) -> bool {
174 !self.undo_stack.is_empty()
175 }
176
177 #[must_use]
179 pub fn can_redo(&self) -> bool {
180 !self.redo_stack.is_empty()
181 }
182
183 pub fn clear(&mut self) {
185 self.undo_stack.clear();
186 self.redo_stack.clear();
187 }
188
189 #[must_use]
191 pub fn undo_count(&self) -> usize {
192 self.undo_stack.len()
193 }
194
195 #[must_use]
197 pub fn redo_count(&self) -> usize {
198 self.redo_stack.len()
199 }
200
201 pub fn begin_undo(&mut self) {
203 self.is_undoing = true;
204 }
205
206 pub fn end_undo(&mut self) {
208 self.is_undoing = false;
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::components::editor::cursor::{Cursor, CursorPosition};
216
217 fn test_cursors() -> CursorSet {
218 CursorSet::new(Cursor::new(CursorPosition::zero()))
219 }
220
221 #[test]
222 fn test_undo_redo() {
223 let mut history = History::new();
224
225 history.push("state1".to_string(), test_cursors());
226 std::thread::sleep(std::time::Duration::from_millis(600));
227 history.push("state2".to_string(), test_cursors());
228
229 let entry = history.undo("state3", &test_cursors());
230 assert!(entry.is_some());
231 assert_eq!(entry.unwrap().content, "state2");
232
233 let entry = history.redo("state2", &test_cursors());
234 assert!(entry.is_some());
235 assert_eq!(entry.unwrap().content, "state3");
236 }
237
238 #[test]
239 fn test_redo_cleared_on_new_edit() {
240 let mut history = History::new();
241
242 history.push("state1".to_string(), test_cursors());
243 std::thread::sleep(std::time::Duration::from_millis(600));
244 history.push("state2".to_string(), test_cursors());
245
246 history.undo("state3", &test_cursors());
247 assert!(history.can_redo());
248
249 std::thread::sleep(std::time::Duration::from_millis(600));
250 history.push("state4".to_string(), test_cursors());
251 assert!(!history.can_redo());
252 }
253}