iced_code_editor/canvas_editor/history.rs
1//! Command history management for undo/redo functionality.
2//!
3//! This module provides thread-safe command history tracking with configurable
4//! size limits and save point tracking for modified state detection.
5//!
6//! # Examples
7//!
8//! ## Basic Usage
9//!
10//! ```
11//! use iced_code_editor::CommandHistory;
12//!
13//! // Create a history with a limit of 100 operations
14//! let history = CommandHistory::new(100);
15//!
16//! // Check state
17//! assert_eq!(history.undo_count(), 0);
18//! assert_eq!(history.redo_count(), 0);
19//! assert!(!history.can_undo());
20//! ```
21//!
22//! ## Dynamic Configuration
23//!
24//! ```
25//! use iced_code_editor::CommandHistory;
26//!
27//! let history = CommandHistory::new(100);
28//!
29//! // Adjust history size based on available memory
30//! history.set_max_size(500);
31//! assert_eq!(history.max_size(), 500);
32//!
33//! // Clear all history when starting a new document
34//! history.clear();
35//! ```
36//!
37//! ## Save Point Tracking
38//!
39//! ```
40//! use iced_code_editor::CommandHistory;
41//!
42//! let history = CommandHistory::new(100);
43//!
44//! // Mark the current state as saved
45//! history.mark_saved();
46//! assert!(!history.is_modified());
47//!
48//! // After user makes changes...
49//! // history.push(some_command);
50//! // assert!(history.is_modified());
51//! ```
52
53// Allow unwrap on Mutex since this is safe in the single-threaded GUI context
54// The mutex is only used for interior mutability, not actual multi-threading
55#![allow(clippy::unwrap_used)]
56// The Mutex cannot be poisoned in our single-threaded context, so panics documented
57// below would never actually occur in practice
58#![allow(clippy::missing_panics_doc)]
59
60use super::command::{Command, CompositeCommand};
61use crate::text_buffer::TextBuffer;
62use std::sync::{Arc, Mutex};
63
64/// Manages command history for undo/redo operations.
65///
66/// The history maintains two stacks:
67/// - Undo stack: Commands that can be undone
68/// - Redo stack: Commands that can be redone (cleared when new commands are added)
69///
70/// Thread-safe using Arc<Mutex<>> for interior mutability.
71#[derive(Debug, Clone)]
72pub struct CommandHistory {
73 inner: Arc<Mutex<HistoryInner>>,
74}
75
76#[derive(Debug)]
77struct HistoryInner {
78 /// Stack of commands that can be undone
79 undo_stack: Vec<Box<dyn Command>>,
80 /// Stack of commands that can be redone
81 redo_stack: Vec<Box<dyn Command>>,
82 /// Maximum number of commands to keep in history
83 max_size: usize,
84 /// Index in undo_stack where document was last saved (None if never saved)
85 save_point: Option<usize>,
86 /// Current composite command being built (for grouping)
87 current_group: Option<CompositeCommand>,
88}
89
90impl CommandHistory {
91 /// Creates a new command history with the specified size limit.
92 ///
93 /// # Arguments
94 ///
95 /// * `max_size` - Maximum number of commands to keep in history
96 ///
97 /// # Returns
98 ///
99 /// A new `CommandHistory` instance
100 ///
101 /// # Example
102 ///
103 /// ```
104 /// use iced_code_editor::CommandHistory;
105 ///
106 /// let history = CommandHistory::new(100);
107 /// ```
108 pub fn new(max_size: usize) -> Self {
109 Self {
110 inner: Arc::new(Mutex::new(HistoryInner {
111 undo_stack: Vec::with_capacity(max_size.min(100)),
112 redo_stack: Vec::with_capacity(max_size.min(100)),
113 max_size,
114 save_point: None,
115 current_group: None,
116 })),
117 }
118 }
119
120 /// Adds a command to the history.
121 ///
122 /// This clears the redo stack and adds the command to the undo stack.
123 /// If currently grouping commands, adds to the current group instead.
124 ///
125 /// # Arguments
126 ///
127 /// * `command` - The command to add
128 pub fn push(&self, command: Box<dyn Command>) {
129 let mut inner = self.inner.lock().unwrap();
130
131 // If we're building a composite, add to it
132 if let Some(ref mut group) = inner.current_group {
133 group.add(command);
134 return;
135 }
136
137 // Clear redo stack when new command is added
138 inner.redo_stack.clear();
139
140 // Add to undo stack
141 inner.undo_stack.push(command);
142
143 // Enforce size limit
144 if inner.undo_stack.len() > inner.max_size {
145 inner.undo_stack.remove(0);
146 // Adjust save point if it exists
147 if let Some(ref mut sp) = inner.save_point {
148 if *sp > 0 {
149 *sp -= 1;
150 } else {
151 inner.save_point = None;
152 }
153 }
154 }
155
156 // Update save point - we've made changes
157 // The save point is now invalid unless it's still at the current position
158 }
159
160 /// Undoes the last command.
161 ///
162 /// # Arguments
163 ///
164 /// * `buffer` - The text buffer to modify
165 /// * `cursor` - The cursor position to update
166 ///
167 /// # Returns
168 ///
169 /// `true` if a command was undone, `false` if nothing to undo
170 pub fn undo(
171 &self,
172 buffer: &mut TextBuffer,
173 cursor: &mut (usize, usize),
174 ) -> bool {
175 let mut inner = self.inner.lock().unwrap();
176
177 // End any current grouping
178 if inner.current_group.is_some() {
179 Self::end_group_internal(&mut inner);
180 }
181
182 if let Some(mut command) = inner.undo_stack.pop() {
183 command.undo(buffer, cursor);
184 inner.redo_stack.push(command);
185 true
186 } else {
187 false
188 }
189 }
190
191 /// Redoes the last undone command.
192 ///
193 /// # Arguments
194 ///
195 /// * `buffer` - The text buffer to modify
196 /// * `cursor` - The cursor position to update
197 ///
198 /// # Returns
199 ///
200 /// `true` if a command was redone, `false` if nothing to redo
201 pub fn redo(
202 &self,
203 buffer: &mut TextBuffer,
204 cursor: &mut (usize, usize),
205 ) -> bool {
206 let mut inner = self.inner.lock().unwrap();
207
208 if let Some(mut command) = inner.redo_stack.pop() {
209 command.execute(buffer, cursor);
210 inner.undo_stack.push(command);
211 true
212 } else {
213 false
214 }
215 }
216
217 /// Returns whether there are commands that can be undone.
218 #[must_use]
219 pub fn can_undo(&self) -> bool {
220 let inner = self.inner.lock().unwrap();
221 !inner.undo_stack.is_empty() || inner.current_group.is_some()
222 }
223
224 /// Returns whether there are commands that can be redone.
225 #[must_use]
226 pub fn can_redo(&self) -> bool {
227 let inner = self.inner.lock().unwrap();
228 !inner.redo_stack.is_empty()
229 }
230
231 /// Marks the current position as the save point.
232 ///
233 /// This is used to track whether the document has been modified since
234 /// the last save. Call this after successfully saving the file.
235 pub fn mark_saved(&self) {
236 let mut inner = self.inner.lock().unwrap();
237 inner.save_point = Some(inner.undo_stack.len());
238 }
239
240 /// Returns whether the document has been modified since the last save.
241 ///
242 /// # Returns
243 ///
244 /// `true` if there are unsaved changes, `false` otherwise
245 #[must_use]
246 pub fn is_modified(&self) -> bool {
247 let inner = self.inner.lock().unwrap();
248
249 // If we're currently in a group, we're modified
250 if inner.current_group.is_some() {
251 return true;
252 }
253
254 match inner.save_point {
255 None => !inner.undo_stack.is_empty(),
256 Some(sp) => sp != inner.undo_stack.len(),
257 }
258 }
259
260 /// Clears all history.
261 ///
262 /// This removes all undo/redo commands and resets the save point.
263 /// Useful when starting a new document or resetting the editor state.
264 ///
265 /// # Example
266 ///
267 /// ```
268 /// use iced_code_editor::CommandHistory;
269 ///
270 /// let history = CommandHistory::new(100);
271 /// // ... perform some operations ...
272 ///
273 /// // Clear everything when opening a new document
274 /// history.clear();
275 /// assert_eq!(history.undo_count(), 0);
276 /// assert_eq!(history.redo_count(), 0);
277 /// assert!(!history.is_modified());
278 /// ```
279 pub fn clear(&self) {
280 let mut inner = self.inner.lock().unwrap();
281 inner.undo_stack.clear();
282 inner.redo_stack.clear();
283 inner.save_point = None;
284 inner.current_group = None;
285 }
286
287 /// Begins grouping subsequent commands into a composite.
288 ///
289 /// All commands added via `push()` will be grouped together until
290 /// `end_group()` is called. This is useful for grouping consecutive
291 /// typing operations.
292 ///
293 /// # Arguments
294 ///
295 /// * `description` - Description for the composite command
296 pub fn begin_group(&self, description: &str) {
297 let mut inner = self.inner.lock().unwrap();
298 if inner.current_group.is_none() {
299 inner.current_group =
300 Some(CompositeCommand::new(description.to_string()));
301 }
302 }
303
304 /// Ends the current command grouping.
305 ///
306 /// The grouped commands are added to the history as a single composite
307 /// command. If no commands were grouped, nothing is added.
308 pub fn end_group(&self) {
309 let mut inner = self.inner.lock().unwrap();
310 Self::end_group_internal(&mut inner);
311 }
312
313 /// Internal helper to end grouping (used when lock is already held).
314 fn end_group_internal(inner: &mut HistoryInner) {
315 if let Some(group) = inner.current_group.take()
316 && !group.is_empty()
317 {
318 // Clear redo stack
319 inner.redo_stack.clear();
320
321 // Add composite to undo stack
322 inner.undo_stack.push(Box::new(group));
323
324 // Enforce size limit
325 if inner.undo_stack.len() > inner.max_size {
326 inner.undo_stack.remove(0);
327 if let Some(ref mut sp) = inner.save_point {
328 if *sp > 0 {
329 *sp -= 1;
330 } else {
331 inner.save_point = None;
332 }
333 }
334 }
335 }
336 }
337
338 /// Returns the maximum history size.
339 ///
340 /// # Returns
341 ///
342 /// The maximum number of commands that can be stored in history.
343 ///
344 /// # Example
345 ///
346 /// ```
347 /// use iced_code_editor::CommandHistory;
348 ///
349 /// let history = CommandHistory::new(100);
350 /// assert_eq!(history.max_size(), 100);
351 /// ```
352 #[must_use]
353 pub fn max_size(&self) -> usize {
354 let inner = self.inner.lock().unwrap();
355 inner.max_size
356 }
357
358 /// Sets the maximum history size.
359 ///
360 /// If the current history exceeds the new size, older commands are removed.
361 /// This is useful for adjusting memory usage based on system resources.
362 ///
363 /// # Arguments
364 ///
365 /// * `max_size` - New maximum size (number of commands to keep)
366 ///
367 /// # Example
368 ///
369 /// ```
370 /// use iced_code_editor::CommandHistory;
371 ///
372 /// let history = CommandHistory::new(100);
373 ///
374 /// // Increase limit for memory-rich environments
375 /// history.set_max_size(500);
376 /// assert_eq!(history.max_size(), 500);
377 ///
378 /// // Decrease limit for constrained environments
379 /// history.set_max_size(50);
380 /// assert_eq!(history.max_size(), 50);
381 /// ```
382 pub fn set_max_size(&self, max_size: usize) {
383 let mut inner = self.inner.lock().unwrap();
384 inner.max_size = max_size;
385
386 // Trim if necessary
387 while inner.undo_stack.len() > max_size {
388 inner.undo_stack.remove(0);
389 if let Some(ref mut sp) = inner.save_point {
390 if *sp > 0 {
391 *sp -= 1;
392 } else {
393 inner.save_point = None;
394 }
395 }
396 }
397 }
398
399 /// Returns the current number of undo operations available.
400 ///
401 /// This can be useful for displaying history statistics or managing
402 /// UI state (e.g., enabling/disabling undo buttons).
403 ///
404 /// # Returns
405 ///
406 /// The number of commands that can be undone.
407 ///
408 /// # Example
409 ///
410 /// ```
411 /// use iced_code_editor::CommandHistory;
412 ///
413 /// let history = CommandHistory::new(100);
414 /// assert_eq!(history.undo_count(), 0);
415 ///
416 /// // After adding commands...
417 /// // assert!(history.undo_count() > 0);
418 /// ```
419 #[must_use]
420 pub fn undo_count(&self) -> usize {
421 let inner = self.inner.lock().unwrap();
422 inner.undo_stack.len()
423 }
424
425 /// Returns the current number of redo operations available.
426 ///
427 /// This can be useful for displaying history statistics or managing
428 /// UI state (e.g., enabling/disabling redo buttons).
429 ///
430 /// # Returns
431 ///
432 /// The number of commands that can be redone.
433 ///
434 /// # Example
435 ///
436 /// ```
437 /// use iced_code_editor::CommandHistory;
438 ///
439 /// let history = CommandHistory::new(100);
440 /// assert_eq!(history.redo_count(), 0);
441 ///
442 /// // After undoing some commands...
443 /// // assert!(history.redo_count() > 0);
444 /// ```
445 #[must_use]
446 pub fn redo_count(&self) -> usize {
447 let inner = self.inner.lock().unwrap();
448 inner.redo_stack.len()
449 }
450}
451
452// Implement Default for convenient usage
453impl Default for CommandHistory {
454 fn default() -> Self {
455 Self::new(100)
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::canvas_editor::command::InsertCharCommand;
463
464 #[test]
465 fn test_new_history() {
466 let history = CommandHistory::new(50);
467 assert_eq!(history.max_size(), 50);
468 assert!(!history.can_undo());
469 assert!(!history.can_redo());
470 }
471
472 #[test]
473 fn test_push_and_undo() {
474 let mut buffer = TextBuffer::new("hello");
475 let mut cursor = (0, 5);
476 let history = CommandHistory::new(10);
477
478 let mut cmd = InsertCharCommand::new(0, 5, '!', cursor);
479 cmd.execute(&mut buffer, &mut cursor);
480 history.push(Box::new(cmd));
481
482 assert!(history.can_undo());
483 assert_eq!(buffer.line(0), "hello!");
484
485 history.undo(&mut buffer, &mut cursor);
486 assert_eq!(buffer.line(0), "hello");
487 assert_eq!(cursor, (0, 5));
488 }
489
490 #[test]
491 fn test_redo() {
492 let mut buffer = TextBuffer::new("hello");
493 let mut cursor = (0, 5);
494 let history = CommandHistory::new(10);
495
496 let mut cmd = InsertCharCommand::new(0, 5, '!', cursor);
497 cmd.execute(&mut buffer, &mut cursor);
498 history.push(Box::new(cmd));
499
500 history.undo(&mut buffer, &mut cursor);
501 assert_eq!(buffer.line(0), "hello");
502
503 assert!(history.can_redo());
504 history.redo(&mut buffer, &mut cursor);
505 assert_eq!(buffer.line(0), "hello!");
506 assert_eq!(cursor, (0, 6));
507 }
508
509 #[test]
510 fn test_save_point() {
511 let mut buffer = TextBuffer::new("hello");
512 let mut cursor = (0, 5);
513 let history = CommandHistory::new(10);
514
515 assert!(!history.is_modified()); // New document is not modified
516
517 let mut cmd = InsertCharCommand::new(0, 5, '!', cursor);
518 cmd.execute(&mut buffer, &mut cursor);
519 history.push(Box::new(cmd));
520
521 assert!(history.is_modified()); // Now modified
522
523 history.mark_saved();
524 assert!(!history.is_modified()); // Saved
525
526 let mut cmd2 = InsertCharCommand::new(0, 6, '?', cursor);
527 cmd2.execute(&mut buffer, &mut cursor);
528 history.push(Box::new(cmd2));
529
530 assert!(history.is_modified()); // Modified again
531 }
532
533 #[test]
534 fn test_clear() {
535 let mut buffer = TextBuffer::new("hello");
536 let mut cursor = (0, 5);
537 let history = CommandHistory::new(10);
538
539 let mut cmd = InsertCharCommand::new(0, 5, '!', cursor);
540 cmd.execute(&mut buffer, &mut cursor);
541 history.push(Box::new(cmd));
542
543 assert!(history.can_undo());
544 history.clear();
545 assert!(!history.can_undo());
546 assert!(!history.is_modified());
547 }
548
549 #[test]
550 fn test_size_limit() {
551 let mut buffer = TextBuffer::new("a");
552 let mut cursor = (0, 1);
553 let history = CommandHistory::new(3);
554
555 // Add 5 commands (exceeds limit of 3)
556 for i in 0..5 {
557 let mut cmd = InsertCharCommand::new(0, 1 + i, 'x', cursor);
558 cmd.execute(&mut buffer, &mut cursor);
559 cursor.1 += 1;
560 history.push(Box::new(cmd));
561 }
562
563 // Should only have 3 in history
564 assert_eq!(history.undo_count(), 3);
565 }
566
567 #[test]
568 fn test_grouping() {
569 let mut buffer = TextBuffer::new("hello");
570 let mut cursor = (0, 5);
571 let history = CommandHistory::new(10);
572
573 history.begin_group("typing");
574
575 // Add multiple characters
576 for ch in "!!!".chars() {
577 let mut cmd = InsertCharCommand::new(0, cursor.1, ch, cursor);
578 cmd.execute(&mut buffer, &mut cursor);
579 // Don't manually increment cursor - execute() does it
580 history.push(Box::new(cmd));
581 }
582
583 history.end_group();
584
585 assert_eq!(buffer.line(0), "hello!!!");
586 assert_eq!(history.undo_count(), 1); // All grouped into one
587
588 // Single undo should remove all three characters
589 history.undo(&mut buffer, &mut cursor);
590 assert_eq!(buffer.line(0), "hello");
591 assert_eq!(cursor, (0, 5));
592 }
593
594 #[test]
595 fn test_push_clears_redo() {
596 let mut buffer = TextBuffer::new("hello");
597 let mut cursor = (0, 5);
598 let history = CommandHistory::new(10);
599
600 let mut cmd1 = InsertCharCommand::new(0, 5, '!', cursor);
601 cmd1.execute(&mut buffer, &mut cursor);
602 history.push(Box::new(cmd1));
603
604 history.undo(&mut buffer, &mut cursor);
605 assert!(history.can_redo());
606
607 // Push new command should clear redo stack
608 let mut cmd2 = InsertCharCommand::new(0, 5, '?', cursor);
609 cmd2.execute(&mut buffer, &mut cursor);
610 history.push(Box::new(cmd2));
611
612 assert!(!history.can_redo());
613 }
614}