text_document_common/undo_redo.rs
1// Generated by Qleany v1.4.8 from undo_redo.tera
2use crate::event::{Event, EventHub, Origin, UndoRedoEvent};
3use crate::types::EntityId;
4use anyhow::{Result, anyhow};
5use std::any::Any;
6use std::collections::HashMap;
7use std::sync::Arc;
8
9/// Trait for commands that can be undone and redone.
10///
11/// Implementors can optionally support command merging by overriding the
12/// `can_merge` and `merge` methods. This allows the UndoRedoManager to combine
13/// multiple commands of the same type into a single command, which is useful for
14/// operations like continuous typing or dragging.
15pub trait UndoRedoCommand: Send {
16 /// Undoes the command, reverting its effects
17 fn undo(&mut self) -> Result<()>;
18
19 /// Redoes the command, reapplying its effects
20 fn redo(&mut self) -> Result<()>;
21
22 /// Returns true if this command can be merged with the other command.
23 ///
24 /// By default, commands cannot be merged. Override this method to enable
25 /// merging for specific command types.
26 ///
27 /// # Example
28 /// ```test
29 /// fn can_merge(&self, other: &dyn UndoRedoCommand) -> bool {
30 /// // Check if the other command is of the same type
31 /// if let Some(_) = other.as_any().downcast_ref::<Self>() {
32 /// return true;
33 /// }
34 /// false
35 /// }
36 /// ```
37 fn can_merge(&self, _other: &dyn UndoRedoCommand) -> bool {
38 false
39 }
40
41 /// Merges this command with the other command.
42 /// Returns true if the merge was successful.
43 ///
44 /// This method is called only if `can_merge` returns true.
45 ///
46 /// # Example
47 /// ```test
48 /// use common::undo_redo::UndoRedoCommand;
49 ///
50 /// fn merge(&mut self, other: &dyn UndoRedoCommand) -> bool {
51 /// if let Some(other_cmd) = other.as_any().downcast_ref::<Self>() {
52 /// // Merge the commands
53 /// self.value += other_cmd.value;
54 /// return true;
55 /// }
56 /// false
57 /// }
58 /// ```
59 fn merge(&mut self, _other: &dyn UndoRedoCommand) -> bool {
60 false
61 }
62
63 /// Returns the type ID of this command for type checking.
64 ///
65 /// This is used for downcasting in the `can_merge` and `merge` methods.
66 ///
67 /// # Example
68 /// ```test
69 /// fn as_any(&self) -> &dyn Any {
70 /// self
71 /// }
72 /// ```
73 fn as_any(&self) -> &dyn Any;
74}
75
76/// A composite command that groups multiple commands as one.
77///
78/// This allows treating a sequence of commands as a single unit for undo/redo operations.
79/// When a composite command is undone or redone, all its contained commands are undone
80/// or redone in the appropriate order.
81///
82/// # Example
83/// ```test
84/// use common::undo_redo::CompositeCommand;
85/// let mut composite = CompositeCommand::new();
86/// composite.add_command(Box::new(Command1::new()));
87/// composite.add_command(Box::new(Command2::new()));
88/// // Now composite can be treated as a single command
89/// ```
90pub struct CompositeCommand {
91 commands: Vec<Box<dyn UndoRedoCommand>>,
92 pub stack_id: u64,
93}
94
95impl CompositeCommand {
96 /// Creates a new empty composite command.
97 pub fn new(stack_id: Option<u64>) -> Self {
98 CompositeCommand {
99 commands: Vec::new(),
100 stack_id: stack_id.unwrap_or(0),
101 }
102 }
103
104 /// Adds a command to this composite.
105 ///
106 /// Commands are executed, undone, and redone in the order they are added.
107 pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
108 self.commands.push(command);
109 }
110
111 /// Returns true if this composite contains no commands.
112 pub fn is_empty(&self) -> bool {
113 self.commands.is_empty()
114 }
115}
116
117impl UndoRedoCommand for CompositeCommand {
118 fn undo(&mut self) -> Result<()> {
119 // Undo commands in reverse order
120 for command in self.commands.iter_mut().rev() {
121 command.undo()?;
122 }
123 Ok(())
124 }
125
126 fn redo(&mut self) -> Result<()> {
127 // Redo commands in original order
128 for command in self.commands.iter_mut() {
129 command.redo()?;
130 }
131 Ok(())
132 }
133
134 fn as_any(&self) -> &dyn Any {
135 self
136 }
137}
138/// Trait for commands that can be executed asynchronously with progress tracking and cancellation.
139///
140/// This trait extends the basic UndoRedoCommand trait with asynchronous capabilities.
141/// Implementors must also implement the UndoRedoCommand trait to ensure compatibility
142/// with the existing undo/redo system.
143pub trait AsyncUndoRedoCommand: UndoRedoCommand {
144 /// Starts the undo operation asynchronously and returns immediately.
145 /// Returns Ok(()) if the operation was successfully started.
146 fn start_undo(&mut self) -> Result<()>;
147
148 /// Starts the redo operation asynchronously and returns immediately.
149 /// Returns Ok(()) if the operation was successfully started.
150 fn start_redo(&mut self) -> Result<()>;
151
152 /// Checks the progress of the current operation.
153 /// Returns a value between 0.0 (not started) and 1.0 (completed).
154 fn check_progress(&self) -> f32;
155
156 /// Attempts to cancel the in-progress operation.
157 /// Returns Ok(()) if cancellation was successful or if no operation is in progress.
158 fn cancel(&mut self) -> Result<()>;
159
160 /// Checks if the current operation is complete.
161 /// Returns true if the operation has finished successfully.
162 fn is_complete(&self) -> bool;
163}
164
165#[derive(Default)]
166struct StackData {
167 undo_stack: Vec<Box<dyn UndoRedoCommand>>,
168 redo_stack: Vec<Box<dyn UndoRedoCommand>>,
169}
170
171/// Manager for undo and redo operations.
172///
173/// The UndoRedoManager maintains multiple stacks of commands:
174/// - Each stack has an undo stack for commands that can be undone
175/// - Each stack has a redo stack for commands that have been undone and can be redone
176///
177/// It also supports:
178/// - Grouping multiple commands as a single unit using begin_composite/end_composite
179/// - Merging commands of the same type when appropriate
180/// - Switching between different stacks
181pub struct UndoRedoManager {
182 stacks: HashMap<u64, StackData>,
183 next_stack_id: u64,
184 in_progress_composite: Option<CompositeCommand>,
185 composite_nesting_level: usize,
186 composite_stack_id: Option<u64>,
187 event_hub: Option<Arc<EventHub>>,
188}
189
190impl Default for UndoRedoManager {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196impl UndoRedoManager {
197 /// Creates a new empty UndoRedoManager with one default stack (ID 0).
198 pub fn new() -> Self {
199 let mut stacks = HashMap::new();
200 stacks.insert(0, StackData::default());
201 UndoRedoManager {
202 stacks,
203 next_stack_id: 1,
204 in_progress_composite: None,
205 composite_nesting_level: 0,
206 composite_stack_id: None,
207 event_hub: None,
208 }
209 }
210
211 /// Inject the event hub to allow sending undo/redo related events
212 pub fn set_event_hub(&mut self, event_hub: &Arc<EventHub>) {
213 self.event_hub = Some(Arc::clone(event_hub));
214 }
215
216 /// Undoes the most recent command on the specified stack.
217 /// If `stack_id` is None, the global stack (ID 0) is used.
218 ///
219 /// The undone command is moved to the redo stack.
220 /// Returns Ok(()) if successful or if there are no commands to undo.
221 pub fn undo(&mut self, stack_id: Option<u64>) -> Result<()> {
222 let target_stack_id = stack_id.unwrap_or(0);
223 let stack = self
224 .stacks
225 .get_mut(&target_stack_id)
226 .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
227
228 if let Some(mut command) = stack.undo_stack.pop() {
229 if let Err(e) = command.undo() {
230 log::error!("Undo failed, dropping command: {e}");
231 // command dropped intentionally
232 return Err(e);
233 }
234 stack.redo_stack.push(command);
235 if let Some(event_hub) = &self.event_hub {
236 event_hub.send_event(Event {
237 origin: Origin::UndoRedo(UndoRedoEvent::Undone),
238 ids: Vec::<EntityId>::new(),
239 data: None,
240 });
241 }
242 }
243 Ok(())
244 }
245
246 /// Redoes the most recently undone command on the specified stack.
247 /// If `stack_id` is None, the global stack (ID 0) is used.
248 ///
249 /// The redone command is moved back to the undo stack.
250 /// Returns Ok(()) if successful or if there are no commands to redo.
251 pub fn redo(&mut self, stack_id: Option<u64>) -> Result<()> {
252 let target_stack_id = stack_id.unwrap_or(0);
253 let stack = self
254 .stacks
255 .get_mut(&target_stack_id)
256 .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
257
258 if let Some(mut command) = stack.redo_stack.pop() {
259 if let Err(e) = command.redo() {
260 log::error!("Redo failed, dropping command: {e}");
261 // command dropped intentionally
262 return Err(e);
263 }
264 stack.undo_stack.push(command);
265 if let Some(event_hub) = &self.event_hub {
266 event_hub.send_event(Event {
267 origin: Origin::UndoRedo(UndoRedoEvent::Redone),
268 ids: Vec::<EntityId>::new(),
269 data: None,
270 });
271 }
272 }
273 Ok(())
274 }
275
276 /// Begins a composite command group.
277 ///
278 /// All commands added between begin_composite and end_composite will be treated as a single command.
279 /// This is useful for operations that logically represent a single action but require multiple
280 /// commands to implement.
281 ///
282 /// # Example
283 /// ```test
284 /// let mut manager = UndoRedoManager::new();
285 /// manager.begin_composite();
286 /// manager.add_command(Box::new(Command1::new()));
287 /// manager.add_command(Box::new(Command2::new()));
288 /// manager.end_composite();
289 /// // Now undo() will undo both commands as a single unit
290 /// ```
291 pub fn begin_composite(&mut self, stack_id: Option<u64>) {
292 if self.composite_stack_id.is_some() && self.composite_stack_id != stack_id {
293 panic!(
294 "Cannot begin a composite on a different stack while another composite is in progress"
295 );
296 }
297
298 // Set the target stack ID for this composite
299 self.composite_stack_id = stack_id;
300
301 // Increment the nesting level
302 self.composite_nesting_level += 1;
303
304 // If there's no composite in progress, create one
305 if self.in_progress_composite.is_none() {
306 self.in_progress_composite = Some(CompositeCommand::new(stack_id));
307 }
308
309 // not sure if we want to send events for composites
310 if let Some(event_hub) = &self.event_hub {
311 event_hub.send_event(Event {
312 origin: Origin::UndoRedo(UndoRedoEvent::BeginComposite),
313 ids: Vec::<EntityId>::new(),
314 data: None,
315 });
316 }
317 }
318
319 /// Ends the current composite command group and adds it to the specified undo stack.
320 ///
321 /// If no commands were added to the composite, nothing is added to the undo stack.
322 /// If this is a nested composite, only the outermost composite is added to the undo stack.
323 pub fn end_composite(&mut self) {
324 // Decrement the nesting level
325 if self.composite_nesting_level > 0 {
326 self.composite_nesting_level -= 1;
327 }
328
329 // Only end the composite if we're at the outermost level
330 if self.composite_nesting_level == 0 {
331 if let Some(composite) = self.in_progress_composite.take()
332 && !composite.is_empty()
333 {
334 let target_stack_id = self.composite_stack_id.unwrap_or(0);
335 let stack = self
336 .stacks
337 .get_mut(&target_stack_id)
338 .expect("Stack must exist");
339 stack.undo_stack.push(Box::new(composite));
340 stack.redo_stack.clear();
341 }
342 // not sure if we want to send events for composites
343 if let Some(event_hub) = &self.event_hub {
344 event_hub.send_event(Event {
345 origin: Origin::UndoRedo(UndoRedoEvent::EndComposite),
346 ids: Vec::<EntityId>::new(),
347 data: None,
348 });
349 }
350 }
351 }
352
353 pub fn cancel_composite(&mut self) {
354 // Decrement the nesting level
355 if self.composite_nesting_level > 0 {
356 self.composite_nesting_level -= 1;
357 }
358
359 // Undo any sub-commands that were already executed in this composite
360 if let Some(ref mut composite) = self.in_progress_composite {
361 let _ = composite.undo();
362 }
363
364 self.in_progress_composite = None;
365 self.composite_stack_id = None;
366
367 // not sure if we want to send events for composites
368 if let Some(event_hub) = &self.event_hub {
369 event_hub.send_event(Event {
370 origin: Origin::UndoRedo(UndoRedoEvent::CancelComposite),
371 ids: Vec::<EntityId>::new(),
372 data: None,
373 });
374 }
375 }
376
377 /// Adds a command to the global undo stack (ID 0).
378 pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
379 let _ = self.add_command_to_stack(command, None);
380 }
381
382 /// Adds a command to the specified undo stack.
383 /// If `stack_id` is None, the global stack (ID 0) is used.
384 ///
385 /// This method handles several cases:
386 /// 1. If a composite command is in progress, the command is added to the composite
387 /// 2. If the command can be merged with the last command on the specified undo stack, they are merged
388 /// 3. Otherwise, the command is added to the specified undo stack as a new entry
389 ///
390 /// In all cases, the redo stack of the stack is cleared when a new command is added.
391 pub fn add_command_to_stack(
392 &mut self,
393 command: Box<dyn UndoRedoCommand>,
394 stack_id: Option<u64>,
395 ) -> Result<()> {
396 // If we have a composite in progress, add the command to it
397 if let Some(composite) = &mut self.in_progress_composite {
398 // ensure that the stack_id is the same as the composite's stack
399 if composite.stack_id != stack_id.unwrap_or(0) {
400 return Err(anyhow!(
401 "Cannot add command to composite with different stack ID"
402 ));
403 }
404 composite.add_command(command);
405 return Ok(());
406 }
407
408 let target_stack_id = stack_id.unwrap_or(0);
409 let stack = self
410 .stacks
411 .get_mut(&target_stack_id)
412 .ok_or_else(|| anyhow!("Stack with ID {} does not exist", target_stack_id))?;
413
414 // Try to merge with the last command if possible
415 if let Some(last_command) = stack.undo_stack.last_mut()
416 && last_command.can_merge(&*command)
417 && last_command.merge(&*command)
418 {
419 // Successfully merged, no need to add the new command
420 stack.redo_stack.clear();
421 return Ok(());
422 }
423
424 // If we couldn't merge, just add the command normally
425 stack.undo_stack.push(command);
426 stack.redo_stack.clear();
427 Ok(())
428 }
429
430 /// Returns true if there are commands that can be undone on the specified stack.
431 /// If `stack_id` is None, the global stack (ID 0) is used.
432 pub fn can_undo(&self, stack_id: Option<u64>) -> bool {
433 let target_stack_id = stack_id.unwrap_or(0);
434 self.stacks
435 .get(&target_stack_id)
436 .map(|s| !s.undo_stack.is_empty())
437 .unwrap_or(false)
438 }
439
440 /// Returns true if there are commands that can be redone on the specified stack.
441 /// If `stack_id` is None, the global stack (ID 0) is used.
442 pub fn can_redo(&self, stack_id: Option<u64>) -> bool {
443 let target_stack_id = stack_id.unwrap_or(0);
444 self.stacks
445 .get(&target_stack_id)
446 .map(|s| !s.redo_stack.is_empty())
447 .unwrap_or(false)
448 }
449
450 /// Clears the undo and redo history for a specific stack.
451 ///
452 /// This method removes all commands from both the undo and redo stacks of the specified stack.
453 pub fn clear_stack(&mut self, stack_id: u64) {
454 if let Some(stack) = self.stacks.get_mut(&stack_id) {
455 stack.undo_stack.clear();
456 stack.redo_stack.clear();
457 }
458 }
459
460 /// Clears all undo and redo history from all stacks.
461 pub fn clear_all_stacks(&mut self) {
462 for stack in self.stacks.values_mut() {
463 stack.undo_stack.clear();
464 stack.redo_stack.clear();
465 }
466 self.in_progress_composite = None;
467 self.composite_nesting_level = 0;
468 }
469
470 /// Creates a new undo/redo stack and returns its ID.
471 pub fn create_new_stack(&mut self) -> u64 {
472 let id = self.next_stack_id;
473 self.stacks.insert(id, StackData::default());
474 self.next_stack_id += 1;
475 id
476 }
477
478 /// Deletes an undo/redo stack by its ID.
479 ///
480 /// The default stack (ID 0) cannot be deleted.
481 pub fn delete_stack(&mut self, stack_id: u64) -> Result<()> {
482 if stack_id == 0 {
483 return Err(anyhow!("Cannot delete the default stack"));
484 }
485 if self.stacks.remove(&stack_id).is_some() {
486 Ok(())
487 } else {
488 Err(anyhow!("Stack with ID {} does not exist", stack_id))
489 }
490 }
491
492 /// Gets the size of the undo stack for a specific stack.
493 pub fn get_stack_size(&self, stack_id: u64) -> usize {
494 self.stacks
495 .get(&stack_id)
496 .map(|s| s.undo_stack.len())
497 .unwrap_or(0)
498 }
499}