Skip to main content

ftui_runtime/undo/
history.rs

1#![forbid(unsafe_code)]
2
3//! History stack for undo/redo operations.
4//!
5//! This module provides the [`HistoryManager`] which maintains dual stacks
6//! for undo and redo operations with support for:
7//!
8//! - **Memory limits**: Oldest commands evicted when budget exceeded
9//! - **Depth limits**: Maximum number of commands in history
10//! - **Branch handling**: New actions clear the redo stack
11//! - **Command merging**: Consecutive similar commands batched together
12//!
13//! # Invariants
14//!
15//! 1. `total_bytes` always equals sum of `size_bytes()` for all commands
16//! 2. `undo_stack.len() <= config.max_depth` (after any operation)
17//! 3. `total_bytes <= config.max_bytes` (after any operation, if enforced)
18//! 4. Redo stack is cleared whenever a new command is pushed
19//!
20//! # Memory Model
21//!
22//! Commands are stored in `VecDeque` for O(1) eviction from the front.
23//! Memory accounting uses each command's `size_bytes()` method.
24//!
25//! ```text
26//! push(cmd5)
27//! ┌───────────────────────────────────────────────┐
28//! │ Undo Stack: [cmd1, cmd2, cmd3, cmd4, cmd5]    │
29//! │ Redo Stack: []                                 │
30//! └───────────────────────────────────────────────┘
31//!
32//! undo() x2
33//! ┌───────────────────────────────────────────────┐
34//! │ Undo Stack: [cmd1, cmd2, cmd3]                │
35//! │ Redo Stack: [cmd4, cmd5]                       │
36//! └───────────────────────────────────────────────┘
37//!
38//! push(cmd6)  <-- new branch, clears redo
39//! ┌───────────────────────────────────────────────┐
40//! │ Undo Stack: [cmd1, cmd2, cmd3, cmd6]          │
41//! │ Redo Stack: []                                 │
42//! └───────────────────────────────────────────────┘
43//! ```
44
45use std::collections::VecDeque;
46use std::fmt;
47
48use super::command::{MergeConfig, UndoableCmd};
49
50/// Configuration for the history manager.
51#[derive(Debug, Clone)]
52pub struct HistoryConfig {
53    /// Maximum number of commands to keep in undo history.
54    pub max_depth: usize,
55    /// Maximum total bytes for all commands (0 = unlimited).
56    pub max_bytes: usize,
57    /// Configuration for command merging.
58    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, // 10 MB
66            merge_config: MergeConfig::default(),
67        }
68    }
69}
70
71impl HistoryConfig {
72    /// Create a new configuration with custom limits.
73    #[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    /// Set the merge configuration.
83    #[must_use]
84    pub fn with_merge_config(mut self, config: MergeConfig) -> Self {
85        self.merge_config = config;
86        self
87    }
88
89    /// Create unlimited configuration (for testing).
90    #[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
100/// Manager for undo/redo history.
101///
102/// Maintains dual stacks for undo and redo operations with
103/// configurable memory and depth limits.
104pub struct HistoryManager {
105    /// Commands available for undo (newest at back).
106    undo_stack: VecDeque<Box<dyn UndoableCmd>>,
107    /// Commands available for redo (newest at back).
108    redo_stack: VecDeque<Box<dyn UndoableCmd>>,
109    /// Configuration for limits and merging.
110    config: HistoryConfig,
111    /// Total bytes used by all commands.
112    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    /// Create a new history manager with the given configuration.
134    #[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    // ========================================================================
145    // Core Operations
146    // ========================================================================
147
148    /// Push a command onto the undo stack.
149    ///
150    /// This clears the redo stack (new branch) and enforces limits.
151    /// The command is NOT executed - it's assumed to have already been executed.
152    pub fn push(&mut self, cmd: Box<dyn UndoableCmd>) {
153        // Clear redo stack (new branch)
154        self.clear_redo();
155
156        // Try to merge with the last command
157        let cmd = match self.try_merge(cmd) {
158            Ok(()) => {
159                // Merged successfully, just enforce limits
160                self.enforce_limits();
161                return;
162            }
163            Err(cmd) => cmd,
164        };
165
166        // Add to undo stack
167        self.total_bytes += cmd.size_bytes();
168        self.undo_stack.push_back(cmd);
169
170        // Enforce limits
171        self.enforce_limits();
172    }
173
174    /// Undo the last command.
175    ///
176    /// Moves the command from undo stack to redo stack and calls undo().
177    ///
178    /// # Returns
179    ///
180    /// - `Ok(description)` if undo succeeded
181    /// - `Err(error)` if undo failed (command remains on undo stack)
182    /// - `None` if no commands to undo
183    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                // Move to redo stack
190                self.redo_stack.push_back(cmd);
191                Some(Ok(description))
192            }
193            Err(e) => {
194                // Put back on undo stack
195                self.undo_stack.push_back(cmd);
196                Some(Err(e))
197            }
198        }
199    }
200
201    /// Redo the last undone command.
202    ///
203    /// Moves the command from redo stack to undo stack and calls redo().
204    ///
205    /// # Returns
206    ///
207    /// - `Ok(description)` if redo succeeded
208    /// - `Err(error)` if redo failed (command remains on redo stack)
209    /// - `None` if no commands to redo
210    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                // Move to undo stack
217                self.undo_stack.push_back(cmd);
218                Some(Ok(description))
219            }
220            Err(e) => {
221                // Put back on redo stack
222                self.redo_stack.push_back(cmd);
223                Some(Err(e))
224            }
225        }
226    }
227
228    /// Check if undo is available.
229    #[must_use]
230    pub fn can_undo(&self) -> bool {
231        !self.undo_stack.is_empty()
232    }
233
234    /// Check if redo is available.
235    #[must_use]
236    pub fn can_redo(&self) -> bool {
237        !self.redo_stack.is_empty()
238    }
239
240    // ========================================================================
241    // Info
242    // ========================================================================
243
244    /// Get the undo stack depth.
245    #[must_use]
246    pub fn undo_depth(&self) -> usize {
247        self.undo_stack.len()
248    }
249
250    /// Get the redo stack depth.
251    #[must_use]
252    pub fn redo_depth(&self) -> usize {
253        self.redo_stack.len()
254    }
255
256    /// Get descriptions for undo commands (most recent first).
257    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    /// Get descriptions for redo commands (most recent first).
267    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    /// Get the description of the next undo command.
277    #[must_use]
278    pub fn next_undo_description(&self) -> Option<&str> {
279        self.undo_stack.back().map(|c| c.description())
280    }
281
282    /// Get the description of the next redo command.
283    #[must_use]
284    pub fn next_redo_description(&self) -> Option<&str> {
285        self.redo_stack.back().map(|c| c.description())
286    }
287
288    /// Get total memory usage in bytes.
289    #[must_use]
290    pub fn memory_usage(&self) -> usize {
291        self.total_bytes
292    }
293
294    /// Get the current configuration.
295    #[must_use]
296    pub fn config(&self) -> &HistoryConfig {
297        &self.config
298    }
299
300    // ========================================================================
301    // Maintenance
302    // ========================================================================
303
304    /// Clear all history (both undo and redo).
305    pub fn clear(&mut self) {
306        self.undo_stack.clear();
307        self.redo_stack.clear();
308        self.total_bytes = 0;
309    }
310
311    /// Clear only the redo stack.
312    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    /// Enforce depth and memory limits by evicting oldest commands.
319    fn enforce_limits(&mut self) {
320        // Enforce depth limit (only applies to undo stack)
321        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        // Enforce memory limit (if set) - applies to TOTAL history
328        if self.config.max_bytes > 0 {
329            while self.total_bytes > self.config.max_bytes {
330                // First try to drop from redo stack (future/speculative history)
331                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                // Then drop from undo stack (oldest history)
337                if let Some(cmd) = self.undo_stack.pop_front() {
338                    self.total_bytes = self.total_bytes.saturating_sub(cmd.size_bytes());
339                } else {
340                    // Both stacks empty, nothing to drop
341                    break;
342                }
343            }
344        }
345    }
346
347    /// Try to merge a command with the last one on the undo stack.
348    ///
349    /// Returns `Ok(())` if merged, `Err(cmd)` if not merged.
350    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        // Check if merge is possible
356        if !last.can_merge(cmd.as_ref(), &self.config.merge_config) {
357            return Err(cmd);
358        }
359
360        // Verify the command has merge text available
361        if cmd.merge_text().is_none() {
362            return Err(cmd);
363        }
364
365        // Adjust memory accounting (old size will change)
366        let old_size = last.size_bytes();
367
368        // Perform the merge (pass full command for position context)
369        if !last.accept_merge(cmd.as_ref()) {
370            return Err(cmd);
371        }
372
373        // Update memory accounting
374        let new_size = last.size_bytes();
375        self.total_bytes = self.total_bytes.saturating_sub(old_size) + new_size;
376
377        // The incoming command is consumed (merged into last)
378        Ok(())
379    }
380}
381
382// ============================================================================
383// Tests
384// ============================================================================
385
386#[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    /// Helper to create a simple insert command for testing.
394    /// The command is pre-executed so undo() will work correctly.
395    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        // Execute the command first so undo() will work
413        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        // Push new command
474        mgr.push(make_insert_cmd("world"));
475
476        // Redo should be cleared
477        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        // Should only keep 3 commands
491        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        // All should be "Insert text"
504        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        // Push 3 commands
571        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        // Undo all
579        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        // Redo all
587        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
617    #[test]
618    fn test_config_new_custom_limits() {
619        let config = HistoryConfig::new(50, 4096);
620        assert_eq!(config.max_depth, 50);
621        assert_eq!(config.max_bytes, 4096);
622    }
623
624    #[test]
625    fn test_config_with_merge_config() {
626        let mc = MergeConfig::default();
627        let config = HistoryConfig::new(10, 0).with_merge_config(mc);
628        // Builder pattern should work; verify merge_config is set
629        assert_eq!(config.max_depth, 10);
630    }
631
632    #[test]
633    fn test_config_accessor() {
634        let config = HistoryConfig::new(42, 1024);
635        let mgr = HistoryManager::new(config);
636        assert_eq!(mgr.config().max_depth, 42);
637        assert_eq!(mgr.config().max_bytes, 1024);
638    }
639
640    #[test]
641    fn test_undo_descriptions_limited() {
642        let mut mgr = HistoryManager::default();
643        mgr.push(make_insert_cmd("a"));
644        mgr.push(make_insert_cmd("b"));
645        mgr.push(make_insert_cmd("c"));
646
647        let descs = mgr.undo_descriptions(2);
648        assert_eq!(descs.len(), 2, "should limit to 2 descriptions");
649    }
650
651    #[test]
652    fn test_redo_descriptions() {
653        let mut mgr = HistoryManager::default();
654        mgr.push(make_insert_cmd("a"));
655        mgr.push(make_insert_cmd("b"));
656        mgr.undo();
657        mgr.undo();
658
659        let descs = mgr.redo_descriptions(5);
660        assert_eq!(descs.len(), 2);
661
662        let descs_limited = mgr.redo_descriptions(1);
663        assert_eq!(descs_limited.len(), 1);
664    }
665
666    #[test]
667    fn test_memory_byte_limit_evicts_old_commands() {
668        // Each insert cmd is at least a few bytes. Use a very low byte limit.
669        let config = HistoryConfig::new(100, 1); // 1 byte limit
670        let mut mgr = HistoryManager::new(config);
671
672        // Push several commands - enforce_limits should evict old ones
673        for i in 0..5 {
674            mgr.push(make_insert_cmd(&format!("cmd{i}")));
675        }
676
677        // With 1-byte limit, commands should get evicted
678        assert!(
679            mgr.undo_depth() < 5,
680            "byte limit should evict old commands, depth={}",
681            mgr.undo_depth()
682        );
683    }
684
685    #[test]
686    fn test_memory_tracking_after_undo_redo() {
687        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
688        mgr.push(make_insert_cmd("a"));
689        let after_push = mgr.memory_usage();
690
691        mgr.undo();
692        let after_undo = mgr.memory_usage();
693        // Memory should be same (moved to redo stack)
694        assert_eq!(after_push, after_undo);
695
696        mgr.redo();
697        let after_redo = mgr.memory_usage();
698        assert_eq!(after_push, after_redo);
699    }
700
701    // ====================================================================
702    // Undo/redo failure paths
703    // ====================================================================
704
705    /// Helper to create a command whose undo always fails.
706    fn make_failing_undo_cmd() -> Box<dyn UndoableCmd> {
707        use crate::undo::command::CommandError;
708        let buffer = Arc::new(Mutex::new(String::new()));
709        let b1 = buffer.clone();
710
711        Box::new(
712            TextInsertCmd::new(WidgetId::new(1), 0, "x")
713                .with_apply(move |_, _, txt| {
714                    let mut buf = b1.lock().unwrap();
715                    buf.push_str(txt);
716                    Ok(())
717                })
718                .with_remove(move |_, _, _| Err(CommandError::Other("undo fail".to_string()))),
719        )
720    }
721
722    /// Helper to create a command whose redo (execute) always fails
723    /// but whose undo succeeds.
724    fn make_failing_redo_cmd() -> (Box<dyn UndoableCmd>, Arc<Mutex<String>>) {
725        use crate::undo::command::CommandError;
726        let buffer = Arc::new(Mutex::new(String::new()));
727        let b1 = buffer.clone();
728        let b2 = buffer.clone();
729        let call_count = Arc::new(Mutex::new(0u32));
730        let cc = call_count.clone();
731
732        let cmd = TextInsertCmd::new(WidgetId::new(1), 0, "y")
733            .with_apply(move |_, _, txt| {
734                let mut count = cc.lock().unwrap();
735                *count += 1;
736                if *count > 1 {
737                    return Err(CommandError::Other("redo fail".to_string()));
738                }
739                let mut buf = b1.lock().unwrap();
740                buf.push_str(txt);
741                Ok(())
742            })
743            .with_remove(move |_, _, len| {
744                let mut buf = b2.lock().unwrap();
745                buf.drain(..len);
746                Ok(())
747            });
748
749        (Box::new(cmd), buffer)
750    }
751
752    #[test]
753    fn test_undo_failure_keeps_command_on_stack() {
754        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
755
756        // Execute the command manually first, then push
757        let mut cmd = make_failing_undo_cmd();
758        cmd.execute().unwrap();
759        mgr.push(cmd);
760
761        assert_eq!(mgr.undo_depth(), 1);
762
763        // Undo should fail
764        let result = mgr.undo();
765        assert!(result.is_some());
766        assert!(result.unwrap().is_err());
767
768        // Command should still be on undo stack
769        assert_eq!(mgr.undo_depth(), 1);
770        assert_eq!(mgr.redo_depth(), 0);
771    }
772
773    #[test]
774    fn test_redo_failure_keeps_command_on_redo_stack() {
775        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
776
777        let (mut cmd, _buffer) = make_failing_redo_cmd();
778        cmd.execute().unwrap(); // First execute succeeds
779        mgr.push(cmd);
780
781        // Undo should succeed
782        let result = mgr.undo();
783        assert!(result.unwrap().is_ok());
784        assert_eq!(mgr.redo_depth(), 1);
785
786        // Redo should fail (second execute fails)
787        let result = mgr.redo();
788        assert!(result.is_some());
789        assert!(result.unwrap().is_err());
790
791        // Command should remain on redo stack
792        assert_eq!(mgr.redo_depth(), 1);
793        assert_eq!(mgr.undo_depth(), 0);
794    }
795
796    // ====================================================================
797    // Merge path
798    // ====================================================================
799
800    #[test]
801    fn test_push_merges_consecutive_inserts() {
802        let buffer = Arc::new(Mutex::new(String::new()));
803        let b1 = buffer.clone();
804        let b2 = buffer.clone();
805        let b3 = buffer.clone();
806        let b4 = buffer.clone();
807
808        let mut mgr = HistoryManager::new(HistoryConfig::default());
809
810        // First insert "a" at position 0
811        let mut cmd1 = TextInsertCmd::new(WidgetId::new(1), 0, "a")
812            .with_apply(move |_, pos, txt| {
813                let mut buf = b1.lock().unwrap();
814                buf.insert_str(pos, txt);
815                Ok(())
816            })
817            .with_remove(move |_, pos, len| {
818                let mut buf = b2.lock().unwrap();
819                buf.drain(pos..pos + len);
820                Ok(())
821            });
822        cmd1.execute().unwrap();
823        mgr.push(Box::new(cmd1));
824
825        // Second insert "b" at position 1 (consecutive)
826        let mut cmd2 = TextInsertCmd::new(WidgetId::new(1), 1, "b")
827            .with_apply(move |_, pos, txt| {
828                let mut buf = b3.lock().unwrap();
829                buf.insert_str(pos, txt);
830                Ok(())
831            })
832            .with_remove(move |_, pos, len| {
833                let mut buf = b4.lock().unwrap();
834                buf.drain(pos..pos + len);
835                Ok(())
836            });
837        cmd2.metadata.timestamp = mgr.undo_stack.back().unwrap().metadata().timestamp;
838        cmd2.execute().unwrap();
839        mgr.push(Box::new(cmd2));
840
841        // Should have merged into 1 command
842        assert_eq!(
843            mgr.undo_depth(),
844            1,
845            "consecutive inserts should merge into 1 command"
846        );
847    }
848
849    // ====================================================================
850    // Memory eviction: redo evicted before undo
851    // ====================================================================
852
853    #[test]
854    fn test_push_clears_redo_memory_accounting() {
855        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
856
857        mgr.push(make_insert_cmd("redo_me"));
858        let mem_after_push = mgr.memory_usage();
859
860        mgr.undo();
861        // Memory unchanged (moved to redo)
862        assert_eq!(mgr.memory_usage(), mem_after_push);
863
864        // New push clears redo; total memory should only be for the new cmd
865        mgr.push(make_insert_cmd("new"));
866        assert_eq!(mgr.redo_depth(), 0);
867        // Memory should be for "new" only (redo was cleared)
868        assert!(mgr.memory_usage() > 0);
869    }
870
871    // ====================================================================
872    // Descriptions on empty stacks
873    // ====================================================================
874
875    #[test]
876    fn test_descriptions_empty_stacks() {
877        let mgr = HistoryManager::default();
878        assert!(mgr.undo_descriptions(10).is_empty());
879        assert!(mgr.redo_descriptions(10).is_empty());
880        assert_eq!(mgr.next_undo_description(), None);
881        assert_eq!(mgr.next_redo_description(), None);
882    }
883
884    // ====================================================================
885    // Memory accounting after push clears redo
886    // ====================================================================
887
888    #[test]
889    fn test_memory_decreases_when_push_clears_redo() {
890        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
891
892        mgr.push(make_insert_cmd("aaa"));
893        mgr.push(make_insert_cmd("bbb"));
894        mgr.undo(); // "bbb" moves to redo
895        let mem_with_redo = mgr.memory_usage();
896        assert!(mem_with_redo > 0);
897
898        // Push clears redo
899        mgr.push(make_insert_cmd("ccc"));
900
901        // Memory should account for "aaa" + "ccc" but not "bbb"
902        assert_eq!(mgr.redo_depth(), 0);
903        // Memory after clearing redo and adding new should be different
904        let mem_after = mgr.memory_usage();
905        assert!(mem_after > 0);
906    }
907
908    // ====================================================================
909    // Depth limit with eviction
910    // ====================================================================
911
912    #[test]
913    fn test_depth_limit_evicts_oldest() {
914        let config = HistoryConfig::new(2, 0);
915        let mut mgr = HistoryManager::new(config);
916
917        mgr.push(make_insert_cmd("first"));
918        mgr.push(make_insert_cmd("second"));
919        mgr.push(make_insert_cmd("third"));
920
921        assert_eq!(mgr.undo_depth(), 2);
922        // Most recent two should remain
923        let descs = mgr.undo_descriptions(2);
924        assert_eq!(descs.len(), 2);
925    }
926
927    // ====================================================================
928    // Default impl
929    // ====================================================================
930
931    #[test]
932    fn test_default_impl() {
933        let mgr = HistoryManager::default();
934        assert_eq!(mgr.config().max_depth, 100);
935        assert_eq!(mgr.config().max_bytes, 10 * 1024 * 1024);
936    }
937
938    // ====================================================================
939    // try_merge: merge_text() returns None
940    // ====================================================================
941
942    /// A command that reports can_merge = true but merge_text = None.
943    /// This exercises the early-exit at line 361-363.
944    struct MergeableNoText {
945        metadata: crate::undo::command::CommandMetadata,
946    }
947
948    impl MergeableNoText {
949        fn new() -> Self {
950            Self {
951                metadata: crate::undo::command::CommandMetadata::new("MergeableNoText"),
952            }
953        }
954    }
955
956    impl crate::undo::command::UndoableCmd for MergeableNoText {
957        fn execute(&mut self) -> crate::undo::command::CommandResult {
958            Ok(())
959        }
960        fn undo(&mut self) -> crate::undo::command::CommandResult {
961            Ok(())
962        }
963        fn description(&self) -> &str {
964            &self.metadata.description
965        }
966        fn size_bytes(&self) -> usize {
967            std::mem::size_of::<Self>()
968        }
969        fn can_merge(
970            &self,
971            _other: &dyn crate::undo::command::UndoableCmd,
972            _config: &MergeConfig,
973        ) -> bool {
974            true // Says it can merge...
975        }
976        fn merge_text(&self) -> Option<&str> {
977            None // ...but has no text to merge
978        }
979        fn accept_merge(&mut self, _other: &dyn crate::undo::command::UndoableCmd) -> bool {
980            false
981        }
982        fn metadata(&self) -> &crate::undo::command::CommandMetadata {
983            &self.metadata
984        }
985        fn as_any(&self) -> &dyn std::any::Any {
986            self
987        }
988        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
989            self
990        }
991    }
992
993    #[test]
994    fn test_try_merge_exits_early_when_merge_text_none() {
995        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
996
997        mgr.push(Box::new(MergeableNoText::new()));
998        assert_eq!(mgr.undo_depth(), 1);
999
1000        // Second push: can_merge=true but merge_text=None → not merged
1001        mgr.push(Box::new(MergeableNoText::new()));
1002        assert_eq!(mgr.undo_depth(), 2);
1003    }
1004
1005    // ====================================================================
1006    // try_merge: accept_merge returns false
1007    // ====================================================================
1008
1009    /// A command where can_merge=true, merge_text=Some, but accept_merge=false.
1010    struct MergeableRejectsAccept {
1011        metadata: crate::undo::command::CommandMetadata,
1012    }
1013
1014    impl MergeableRejectsAccept {
1015        fn new() -> Self {
1016            Self {
1017                metadata: crate::undo::command::CommandMetadata::new("MergeableRejectsAccept"),
1018            }
1019        }
1020    }
1021
1022    impl crate::undo::command::UndoableCmd for MergeableRejectsAccept {
1023        fn execute(&mut self) -> crate::undo::command::CommandResult {
1024            Ok(())
1025        }
1026        fn undo(&mut self) -> crate::undo::command::CommandResult {
1027            Ok(())
1028        }
1029        fn description(&self) -> &str {
1030            &self.metadata.description
1031        }
1032        fn size_bytes(&self) -> usize {
1033            std::mem::size_of::<Self>()
1034        }
1035        fn can_merge(
1036            &self,
1037            _other: &dyn crate::undo::command::UndoableCmd,
1038            _config: &MergeConfig,
1039        ) -> bool {
1040            true
1041        }
1042        fn merge_text(&self) -> Option<&str> {
1043            Some("text")
1044        }
1045        fn accept_merge(&mut self, _other: &dyn crate::undo::command::UndoableCmd) -> bool {
1046            false // Rejects the merge
1047        }
1048        fn metadata(&self) -> &crate::undo::command::CommandMetadata {
1049            &self.metadata
1050        }
1051        fn as_any(&self) -> &dyn std::any::Any {
1052            self
1053        }
1054        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1055            self
1056        }
1057    }
1058
1059    #[test]
1060    fn test_try_merge_not_merged_when_accept_merge_false() {
1061        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
1062
1063        mgr.push(Box::new(MergeableRejectsAccept::new()));
1064        assert_eq!(mgr.undo_depth(), 1);
1065
1066        mgr.push(Box::new(MergeableRejectsAccept::new()));
1067        assert_eq!(mgr.undo_depth(), 2);
1068    }
1069
1070    // ====================================================================
1071    // enforce_limits: redo evicted before undo under memory pressure
1072    // ====================================================================
1073
1074    #[test]
1075    fn test_push_always_clears_redo_before_enforce() {
1076        // Note: enforce_limits' redo eviction path is unreachable
1077        // because push() always calls clear_redo() first. Test the
1078        // observable behavior: push after undo clears redo.
1079        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
1080
1081        mgr.push(make_insert_cmd("redo_item"));
1082        mgr.undo();
1083        assert_eq!(mgr.redo_depth(), 1);
1084
1085        // New push always clears redo (new branch)
1086        mgr.push(make_insert_cmd("new_item"));
1087        assert_eq!(mgr.redo_depth(), 0);
1088    }
1089
1090    // ====================================================================
1091    // max_bytes = 0 means unlimited
1092    // ====================================================================
1093
1094    #[test]
1095    fn test_max_bytes_zero_means_unlimited() {
1096        let config = HistoryConfig::new(100, 0); // 0 = unlimited bytes
1097        let mut mgr = HistoryManager::new(config);
1098
1099        for i in 0..50 {
1100            mgr.push(make_insert_cmd(&format!("cmd{i}")));
1101        }
1102
1103        // All 50 should be retained (depth limit is 100)
1104        assert_eq!(mgr.undo_depth(), 50);
1105        assert!(mgr.memory_usage() > 0);
1106    }
1107
1108    // ====================================================================
1109    // Memory accounting after successful merge
1110    // ====================================================================
1111
1112    #[test]
1113    fn test_memory_accounting_after_merge() {
1114        let buffer = Arc::new(Mutex::new(String::new()));
1115        let b1 = buffer.clone();
1116        let b2 = buffer.clone();
1117        let b3 = buffer.clone();
1118        let b4 = buffer.clone();
1119
1120        let mut mgr = HistoryManager::new(HistoryConfig::default());
1121
1122        // First insert
1123        let mut cmd1 = TextInsertCmd::new(WidgetId::new(1), 0, "a")
1124            .with_apply(move |_, pos, txt| {
1125                let mut buf = b1.lock().unwrap();
1126                buf.insert_str(pos, txt);
1127                Ok(())
1128            })
1129            .with_remove(move |_, pos, len| {
1130                let mut buf = b2.lock().unwrap();
1131                buf.drain(pos..pos + len);
1132                Ok(())
1133            });
1134        cmd1.execute().unwrap();
1135        mgr.push(Box::new(cmd1));
1136        let mem_after_first = mgr.memory_usage();
1137
1138        // Second insert (consecutive, same timestamp → merges)
1139        let mut cmd2 = TextInsertCmd::new(WidgetId::new(1), 1, "b")
1140            .with_apply(move |_, pos, txt| {
1141                let mut buf = b3.lock().unwrap();
1142                buf.insert_str(pos, txt);
1143                Ok(())
1144            })
1145            .with_remove(move |_, pos, len| {
1146                let mut buf = b4.lock().unwrap();
1147                buf.drain(pos..pos + len);
1148                Ok(())
1149            });
1150        cmd2.metadata.timestamp = mgr.undo_stack.back().unwrap().metadata().timestamp;
1151        cmd2.execute().unwrap();
1152        mgr.push(Box::new(cmd2));
1153
1154        // Still 1 command (merged)
1155        assert_eq!(mgr.undo_depth(), 1);
1156
1157        // Memory should increase by the merged text size
1158        let mem_after_merge = mgr.memory_usage();
1159        assert!(
1160            mem_after_merge > mem_after_first,
1161            "memory should increase after merge: {} vs {}",
1162            mem_after_merge,
1163            mem_after_first
1164        );
1165    }
1166
1167    // ====================================================================
1168    // HistoryConfig Debug and Clone
1169    // ====================================================================
1170
1171    #[test]
1172    fn test_history_config_debug() {
1173        let config = HistoryConfig::new(50, 4096);
1174        let debug_str = format!("{:?}", config);
1175        assert!(debug_str.contains("HistoryConfig"));
1176        assert!(debug_str.contains("50"));
1177        assert!(debug_str.contains("4096"));
1178    }
1179
1180    #[test]
1181    fn test_history_config_clone() {
1182        let config = HistoryConfig::new(50, 4096);
1183        let cloned = config.clone();
1184        assert_eq!(cloned.max_depth, 50);
1185        assert_eq!(cloned.max_bytes, 4096);
1186    }
1187
1188    // ====================================================================
1189    // Depth limit + memory limit interaction
1190    // ====================================================================
1191
1192    #[test]
1193    fn test_depth_and_byte_limits_both_enforced() {
1194        // Depth limit 3, byte limit very small
1195        let config = HistoryConfig::new(3, 1);
1196        let mut mgr = HistoryManager::new(config);
1197
1198        for i in 0..10 {
1199            mgr.push(make_insert_cmd(&format!("cmd{i}")));
1200        }
1201
1202        // Both limits apply; depth ≤ 3 AND bytes may further reduce
1203        assert!(mgr.undo_depth() <= 3);
1204    }
1205
1206    // ====================================================================
1207    // Push onto empty undo stack (try_merge early exit on empty)
1208    // ====================================================================
1209
1210    #[test]
1211    fn test_try_merge_returns_err_on_empty_stack() {
1212        let mut mgr = HistoryManager::new(HistoryConfig::default());
1213
1214        // First push should not attempt merge (empty stack)
1215        mgr.push(make_insert_cmd("first"));
1216        assert_eq!(mgr.undo_depth(), 1);
1217    }
1218
1219    // ====================================================================
1220    // Undo returns description on success
1221    // ====================================================================
1222
1223    #[test]
1224    fn test_undo_returns_description_string() {
1225        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
1226        mgr.push(make_insert_cmd("hello"));
1227
1228        let result = mgr.undo().unwrap();
1229        assert_eq!(result.unwrap(), "Insert text");
1230    }
1231
1232    #[test]
1233    fn test_redo_returns_description_string() {
1234        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
1235        mgr.push(make_insert_cmd("hello"));
1236        mgr.undo();
1237
1238        let result = mgr.redo().unwrap();
1239        assert_eq!(result.unwrap(), "Insert text");
1240    }
1241
1242    // ====================================================================
1243    // Clear with items on both stacks
1244    // ====================================================================
1245
1246    #[test]
1247    fn test_clear_resets_memory_with_both_stacks() {
1248        let mut mgr = HistoryManager::new(HistoryConfig::unlimited());
1249        mgr.push(make_insert_cmd("a"));
1250        mgr.push(make_insert_cmd("b"));
1251        mgr.push(make_insert_cmd("c"));
1252        mgr.undo(); // "c" to redo
1253        mgr.undo(); // "b" to redo
1254
1255        assert_eq!(mgr.undo_depth(), 1);
1256        assert_eq!(mgr.redo_depth(), 2);
1257        assert!(mgr.memory_usage() > 0);
1258
1259        mgr.clear();
1260
1261        assert_eq!(mgr.undo_depth(), 0);
1262        assert_eq!(mgr.redo_depth(), 0);
1263        assert_eq!(mgr.memory_usage(), 0);
1264    }
1265
1266    // ====================================================================
1267    // Depth limit of 1
1268    // ====================================================================
1269
1270    #[test]
1271    fn test_depth_limit_one() {
1272        let config = HistoryConfig::new(1, 0);
1273        let mut mgr = HistoryManager::new(config);
1274
1275        mgr.push(make_insert_cmd("first"));
1276        mgr.push(make_insert_cmd("second"));
1277        mgr.push(make_insert_cmd("third"));
1278
1279        assert_eq!(mgr.undo_depth(), 1);
1280    }
1281
1282    // ====================================================================
1283    // Depth limit of 0 (edge case)
1284    // ====================================================================
1285
1286    #[test]
1287    fn test_depth_limit_zero_evicts_everything() {
1288        let config = HistoryConfig::new(0, 0);
1289        let mut mgr = HistoryManager::new(config);
1290
1291        mgr.push(make_insert_cmd("will_be_evicted"));
1292
1293        // Depth limit 0 means everything gets evicted
1294        assert_eq!(mgr.undo_depth(), 0);
1295    }
1296}