ftui_runtime/undo/
history.rs1#![forbid(unsafe_code)]
2
3use std::collections::VecDeque;
46use std::fmt;
47
48use super::command::{MergeConfig, UndoableCmd};
49
50#[derive(Debug, Clone)]
52pub struct HistoryConfig {
53 pub max_depth: usize,
55 pub max_bytes: usize,
57 pub merge_config: MergeConfig,
59}
60
61impl Default for HistoryConfig {
62 fn default() -> Self {
63 Self {
64 max_depth: 100,
65 max_bytes: 10 * 1024 * 1024, merge_config: MergeConfig::default(),
67 }
68 }
69}
70
71impl HistoryConfig {
72 #[must_use]
74 pub fn new(max_depth: usize, max_bytes: usize) -> Self {
75 Self {
76 max_depth,
77 max_bytes,
78 merge_config: MergeConfig::default(),
79 }
80 }
81
82 #[must_use]
84 pub fn with_merge_config(mut self, config: MergeConfig) -> Self {
85 self.merge_config = config;
86 self
87 }
88
89 #[must_use]
91 pub fn unlimited() -> Self {
92 Self {
93 max_depth: usize::MAX,
94 max_bytes: 0,
95 merge_config: MergeConfig::default(),
96 }
97 }
98}
99
100pub struct HistoryManager {
105 undo_stack: VecDeque<Box<dyn UndoableCmd>>,
107 redo_stack: VecDeque<Box<dyn UndoableCmd>>,
109 config: HistoryConfig,
111 total_bytes: usize,
113}
114
115impl fmt::Debug for HistoryManager {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.debug_struct("HistoryManager")
118 .field("undo_depth", &self.undo_stack.len())
119 .field("redo_depth", &self.redo_stack.len())
120 .field("total_bytes", &self.total_bytes)
121 .field("config", &self.config)
122 .finish()
123 }
124}
125
126impl Default for HistoryManager {
127 fn default() -> Self {
128 Self::new(HistoryConfig::default())
129 }
130}
131
132impl HistoryManager {
133 #[must_use]
135 pub fn new(config: HistoryConfig) -> Self {
136 Self {
137 undo_stack: VecDeque::new(),
138 redo_stack: VecDeque::new(),
139 config,
140 total_bytes: 0,
141 }
142 }
143
144 pub fn push(&mut self, cmd: Box<dyn UndoableCmd>) {
153 self.clear_redo();
155
156 let cmd = match self.try_merge(cmd) {
158 Ok(()) => {
159 self.enforce_limits();
161 return;
162 }
163 Err(cmd) => cmd,
164 };
165
166 self.total_bytes += cmd.size_bytes();
168 self.undo_stack.push_back(cmd);
169
170 self.enforce_limits();
172 }
173
174 pub fn undo(&mut self) -> Option<Result<String, super::command::CommandError>> {
184 let mut cmd = self.undo_stack.pop_back()?;
185 let description = cmd.description().to_string();
186
187 match cmd.undo() {
188 Ok(()) => {
189 self.redo_stack.push_back(cmd);
191 Some(Ok(description))
192 }
193 Err(e) => {
194 self.undo_stack.push_back(cmd);
196 Some(Err(e))
197 }
198 }
199 }
200
201 pub fn redo(&mut self) -> Option<Result<String, super::command::CommandError>> {
211 let mut cmd = self.redo_stack.pop_back()?;
212 let description = cmd.description().to_string();
213
214 match cmd.redo() {
215 Ok(()) => {
216 self.undo_stack.push_back(cmd);
218 Some(Ok(description))
219 }
220 Err(e) => {
221 self.redo_stack.push_back(cmd);
223 Some(Err(e))
224 }
225 }
226 }
227
228 #[must_use]
230 pub fn can_undo(&self) -> bool {
231 !self.undo_stack.is_empty()
232 }
233
234 #[must_use]
236 pub fn can_redo(&self) -> bool {
237 !self.redo_stack.is_empty()
238 }
239
240 #[must_use]
246 pub fn undo_depth(&self) -> usize {
247 self.undo_stack.len()
248 }
249
250 #[must_use]
252 pub fn redo_depth(&self) -> usize {
253 self.redo_stack.len()
254 }
255
256 pub fn undo_descriptions(&self, limit: usize) -> Vec<&str> {
258 self.undo_stack
259 .iter()
260 .rev()
261 .take(limit)
262 .map(|c| c.description())
263 .collect()
264 }
265
266 pub fn redo_descriptions(&self, limit: usize) -> Vec<&str> {
268 self.redo_stack
269 .iter()
270 .rev()
271 .take(limit)
272 .map(|c| c.description())
273 .collect()
274 }
275
276 #[must_use]
278 pub fn next_undo_description(&self) -> Option<&str> {
279 self.undo_stack.back().map(|c| c.description())
280 }
281
282 #[must_use]
284 pub fn next_redo_description(&self) -> Option<&str> {
285 self.redo_stack.back().map(|c| c.description())
286 }
287
288 #[must_use]
290 pub fn memory_usage(&self) -> usize {
291 self.total_bytes
292 }
293
294 #[must_use]
296 pub fn config(&self) -> &HistoryConfig {
297 &self.config
298 }
299
300 pub fn clear(&mut self) {
306 self.undo_stack.clear();
307 self.redo_stack.clear();
308 self.total_bytes = 0;
309 }
310
311 fn clear_redo(&mut self) {
313 for cmd in self.redo_stack.drain(..) {
314 self.total_bytes = self.total_bytes.saturating_sub(cmd.size_bytes());
315 }
316 }
317
318 fn enforce_limits(&mut self) {
320 while self.undo_stack.len() > self.config.max_depth {
322 if let Some(cmd) = self.undo_stack.pop_front() {
323 self.total_bytes = self.total_bytes.saturating_sub(cmd.size_bytes());
324 }
325 }
326
327 if self.config.max_bytes > 0 {
329 while self.total_bytes > self.config.max_bytes {
330 if let Some(cmd) = self.redo_stack.pop_front() {
332 self.total_bytes = self.total_bytes.saturating_sub(cmd.size_bytes());
333 continue;
334 }
335
336 if let Some(cmd) = self.undo_stack.pop_front() {
338 self.total_bytes = self.total_bytes.saturating_sub(cmd.size_bytes());
339 } else {
340 break;
342 }
343 }
344 }
345 }
346
347 fn try_merge(&mut self, cmd: Box<dyn UndoableCmd>) -> Result<(), Box<dyn UndoableCmd>> {
351 let Some(last) = self.undo_stack.back_mut() else {
352 return Err(cmd);
353 };
354
355 if !last.can_merge(cmd.as_ref(), &self.config.merge_config) {
357 return Err(cmd);
358 }
359
360 if cmd.merge_text().is_none() {
362 return Err(cmd);
363 }
364
365 let old_size = last.size_bytes();
367
368 if !last.accept_merge(cmd.as_ref()) {
370 return Err(cmd);
371 }
372
373 let new_size = last.size_bytes();
375 self.total_bytes = self.total_bytes.saturating_sub(old_size) + new_size;
376
377 Ok(())
379 }
380}
381
382#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::undo::command::{TextInsertCmd, WidgetId};
390 use std::sync::Arc;
391 use std::sync::Mutex;
392
393 fn make_insert_cmd(text: &str) -> Box<dyn UndoableCmd> {
396 let buffer = Arc::new(Mutex::new(String::new()));
397 let b1 = buffer.clone();
398 let b2 = buffer.clone();
399
400 let mut cmd = TextInsertCmd::new(WidgetId::new(1), 0, text)
401 .with_apply(move |_, pos, txt| {
402 let mut buf = b1.lock().unwrap();
403 buf.insert_str(pos, txt);
404 Ok(())
405 })
406 .with_remove(move |_, pos, len| {
407 let mut buf = b2.lock().unwrap();
408 buf.drain(pos..pos + len);
409 Ok(())
410 });
411
412 cmd.execute().expect("test command should execute");
414
415 Box::new(cmd)
416 }
417
418 #[test]
419 fn test_new_manager() {
420 let mgr = HistoryManager::default();
421 assert!(!mgr.can_undo());
422 assert!(!mgr.can_redo());
423 assert_eq!(mgr.undo_depth(), 0);
424 assert_eq!(mgr.redo_depth(), 0);
425 }
426
427 #[test]
428 fn test_push_enables_undo() {
429 let mut mgr = HistoryManager::default();
430 mgr.push(make_insert_cmd("hello"));
431
432 assert!(mgr.can_undo());
433 assert!(!mgr.can_redo());
434 assert_eq!(mgr.undo_depth(), 1);
435 }
436
437 #[test]
438 fn test_undo_enables_redo() {
439 let mut mgr = HistoryManager::default();
440 mgr.push(make_insert_cmd("hello"));
441
442 let result = mgr.undo();
443 assert!(result.is_some());
444 assert!(result.unwrap().is_ok());
445
446 assert!(!mgr.can_undo());
447 assert!(mgr.can_redo());
448 assert_eq!(mgr.redo_depth(), 1);
449 }
450
451 #[test]
452 fn test_redo_moves_back_to_undo() {
453 let mut mgr = HistoryManager::default();
454 mgr.push(make_insert_cmd("hello"));
455 mgr.undo();
456
457 let result = mgr.redo();
458 assert!(result.is_some());
459 assert!(result.unwrap().is_ok());
460
461 assert!(mgr.can_undo());
462 assert!(!mgr.can_redo());
463 }
464
465 #[test]
466 fn test_push_clears_redo() {
467 let mut mgr = HistoryManager::default();
468 mgr.push(make_insert_cmd("hello"));
469 mgr.undo();
470
471 assert!(mgr.can_redo());
472
473 mgr.push(make_insert_cmd("world"));
475
476 assert!(!mgr.can_redo());
478 assert_eq!(mgr.redo_depth(), 0);
479 }
480
481 #[test]
482 fn test_max_depth_enforced() {
483 let config = HistoryConfig::new(3, 0);
484 let mut mgr = HistoryManager::new(config);
485
486 for i in 0..5 {
487 mgr.push(make_insert_cmd(&format!("cmd{}", i)));
488 }
489
490 assert_eq!(mgr.undo_depth(), 3);
492 }
493
494 #[test]
495 fn test_descriptions() {
496 let mut mgr = HistoryManager::default();
497 mgr.push(make_insert_cmd("a"));
498 mgr.push(make_insert_cmd("b"));
499 mgr.push(make_insert_cmd("c"));
500
501 let descs = mgr.undo_descriptions(5);
502 assert_eq!(descs.len(), 3);
503 assert!(descs.iter().all(|d| *d == "Insert text"));
505 }
506
507 #[test]
508 fn test_next_descriptions() {
509 let mut mgr = HistoryManager::default();
510 mgr.push(make_insert_cmd("hello"));
511
512 assert_eq!(mgr.next_undo_description(), Some("Insert text"));
513 assert_eq!(mgr.next_redo_description(), None);
514
515 mgr.undo();
516
517 assert_eq!(mgr.next_undo_description(), None);
518 assert_eq!(mgr.next_redo_description(), Some("Insert text"));
519 }
520
521 #[test]
522 fn test_clear() {
523 let mut mgr = HistoryManager::default();
524 mgr.push(make_insert_cmd("a"));
525 mgr.push(make_insert_cmd("b"));
526 mgr.undo();
527
528 assert!(mgr.can_undo());
529 assert!(mgr.can_redo());
530
531 mgr.clear();
532
533 assert!(!mgr.can_undo());
534 assert!(!mgr.can_redo());
535 assert_eq!(mgr.memory_usage(), 0);
536 }
537
538 #[test]
539 fn test_memory_tracking() {
540 let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
541
542 let initial = mgr.memory_usage();
543 assert_eq!(initial, 0);
544
545 mgr.push(make_insert_cmd("hello"));
546 let after_push = mgr.memory_usage();
547 assert!(after_push > 0);
548
549 mgr.push(make_insert_cmd("world"));
550 let after_second = mgr.memory_usage();
551 assert!(after_second > after_push);
552 }
553
554 #[test]
555 fn test_undo_without_commands() {
556 let mut mgr = HistoryManager::default();
557 assert!(mgr.undo().is_none());
558 }
559
560 #[test]
561 fn test_redo_without_commands() {
562 let mut mgr = HistoryManager::default();
563 assert!(mgr.redo().is_none());
564 }
565
566 #[test]
567 fn test_multiple_undo_redo_cycle() {
568 let mut mgr = HistoryManager::default();
569
570 mgr.push(make_insert_cmd("a"));
572 mgr.push(make_insert_cmd("b"));
573 mgr.push(make_insert_cmd("c"));
574
575 assert_eq!(mgr.undo_depth(), 3);
576 assert_eq!(mgr.redo_depth(), 0);
577
578 mgr.undo();
580 mgr.undo();
581 mgr.undo();
582
583 assert_eq!(mgr.undo_depth(), 0);
584 assert_eq!(mgr.redo_depth(), 3);
585
586 mgr.redo();
588 mgr.redo();
589 mgr.redo();
590
591 assert_eq!(mgr.undo_depth(), 3);
592 assert_eq!(mgr.redo_depth(), 0);
593 }
594
595 #[test]
596 fn test_config_default() {
597 let config = HistoryConfig::default();
598 assert_eq!(config.max_depth, 100);
599 assert_eq!(config.max_bytes, 10 * 1024 * 1024);
600 }
601
602 #[test]
603 fn test_config_unlimited() {
604 let config = HistoryConfig::unlimited();
605 assert_eq!(config.max_depth, usize::MAX);
606 assert_eq!(config.max_bytes, 0);
607 }
608
609 #[test]
610 fn test_debug_impl() {
611 let mgr = HistoryManager::default();
612 let debug_str = format!("{:?}", mgr);
613 assert!(debug_str.contains("HistoryManager"));
614 assert!(debug_str.contains("undo_depth"));
615 }
616}