common/undo_redo.rs
1// Generated by Qleany v1.10.0 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::fmt;
8use std::sync::Arc;
9
10/// The stack id that means **do not record this**.
11///
12/// Some writes must happen and must not appear in the user's history: a buffer
13/// mirrored to the store on a timer, a cache line, an index rebuilt on open.
14/// The generated commands always push — `stack_id: None` resolves to the global
15/// stack `0` rather than opting out — so before this const the only way to keep
16/// such a write out of the history was to push it somewhere and clear that
17/// somewhere afterwards, which nobody remembers to do. Passing this id instead
18/// drops the command at the door: nothing is stored, so there is nothing to
19/// clear, and stack `0` stays empty.
20///
21/// `create_new_stack` counts up from 1, so this value can never collide with a
22/// real stack.
23pub const UNTRACKED_STACK_ID: u64 = u64::MAX;
24
25/// A stable, machine-readable name for what a command did.
26///
27/// Deliberately **not** a localized string. This crate is generated and knows
28/// nothing about locales or about the application's translation catalogue, so
29/// it hands back a key — `("binder_item", "remove")` — and the application
30/// turns that into *"Undo deleting a scene"* in the reader's language.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct UndoLabel {
33 /// What was acted on: an entity name, or a feature use case's own name.
34 pub subject: &'static str,
35 /// What was done to it — `"create"`, `"update"`, `"remove"`, … Empty when
36 /// `subject` already names the whole act (a feature use case such as
37 /// `"trash_binder_items"` is not an *action on* something else).
38 pub action: &'static str,
39}
40
41impl UndoLabel {
42 pub const fn new(subject: &'static str, action: &'static str) -> Self {
43 Self { subject, action }
44 }
45
46 /// A whole-act label, for a command whose `subject` is the act itself.
47 pub const fn act(subject: &'static str) -> Self {
48 Self {
49 subject,
50 action: "",
51 }
52 }
53}
54
55/// What [`UndoRedoManager::undo_if_head`] actually did.
56///
57/// The reason this exists: a "take that back" affordance offered *for one
58/// operation* — the Undo button on a toast — cannot use plain `undo()`, which
59/// pops whatever is on top. Anything pushed in the meantime (an autosave, a
60/// second window, a background job) silently becomes the thing that gets undone
61/// instead. Naming the operation and being told it is no longer the head is the
62/// difference between a correct affordance and a data-loss bug.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum UndoStatus {
65 /// The named command was on top, and has been undone.
66 Undone,
67 /// Something else has been pushed since; nothing was undone.
68 Superseded,
69 /// The stack is empty; nothing was undone.
70 Empty,
71}
72
73/// Trait for commands that can be undone and redone.
74///
75/// Implementors can optionally support command merging by overriding the
76/// `can_merge` and `merge` methods. This allows the UndoRedoManager to combine
77/// multiple commands of the same type into a single command, which is useful for
78/// operations like continuous typing or dragging.
79pub trait UndoRedoCommand: Send {
80 /// Undoes the command, reverting its effects
81 fn undo(&mut self) -> Result<()>;
82
83 /// Redoes the command, reapplying its effects
84 fn redo(&mut self) -> Result<()>;
85
86 /// Returns true if this command can be merged with the other command.
87 ///
88 /// By default, commands cannot be merged. Override this method to enable
89 /// merging for specific command types.
90 ///
91 /// # Example
92 /// ```test
93 /// fn can_merge(&self, other: &dyn UndoRedoCommand) -> bool {
94 /// // Check if the other command is of the same type
95 /// if let Some(_) = other.as_any().downcast_ref::<Self>() {
96 /// return true;
97 /// }
98 /// false
99 /// }
100 /// ```
101 fn can_merge(&self, _other: &dyn UndoRedoCommand) -> bool {
102 false
103 }
104
105 /// Merges this command with the other command.
106 /// Returns true if the merge was successful.
107 ///
108 /// This method is called only if `can_merge` returns true.
109 ///
110 /// # Example
111 /// ```test
112 /// use common::undo_redo::UndoRedoCommand;
113 ///
114 /// fn merge(&mut self, other: &dyn UndoRedoCommand) -> bool {
115 /// if let Some(other_cmd) = other.as_any().downcast_ref::<Self>() {
116 /// // Merge the commands
117 /// self.value += other_cmd.value;
118 /// return true;
119 /// }
120 /// false
121 /// }
122 /// ```
123 fn merge(&mut self, _other: &dyn UndoRedoCommand) -> bool {
124 false
125 }
126
127 /// Returns the type ID of this command for type checking.
128 ///
129 /// This is used for downcasting in the `can_merge` and `merge` methods.
130 ///
131 /// # Example
132 /// ```test
133 /// fn as_any(&self) -> &dyn Any {
134 /// self
135 /// }
136 /// ```
137 fn as_any(&self) -> &dyn Any;
138
139 /// A stable, machine-readable name for what this command did, for a menu
140 /// that says *"Undo rename"* rather than a bare *"Undo"*.
141 ///
142 /// Defaulted to `None` so an existing implementor compiles unchanged; a
143 /// command that returns `None` simply leaves the row generic.
144 fn label(&self) -> Option<UndoLabel> {
145 None
146 }
147}
148
149/// A composite command that groups multiple commands as one.
150///
151/// This allows treating a sequence of commands as a single unit for undo/redo operations.
152/// When a composite command is undone or redone, all its contained commands are undone
153/// or redone in the appropriate order.
154///
155/// # Example
156/// ```test
157/// use common::undo_redo::CompositeCommand;
158/// let mut composite = CompositeCommand::new();
159/// composite.add_command(Box::new(Command1::new()));
160/// composite.add_command(Box::new(Command2::new()));
161/// // Now composite can be treated as a single command
162/// ```
163pub struct CompositeCommand {
164 label: Option<UndoLabel>,
165 commands: Vec<Box<dyn UndoRedoCommand>>,
166 pub stack_id: u64,
167}
168
169impl fmt::Debug for CompositeCommand {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.debug_struct("CompositeCommand")
172 .field("commands_len", &self.commands.len())
173 .field("stack_id", &self.stack_id)
174 .finish()
175 }
176}
177
178impl CompositeCommand {
179 /// Creates a new empty composite command.
180 pub fn new(stack_id: Option<u64>) -> Self {
181 Self::labeled(stack_id, None)
182 }
183
184 /// A composite that names itself.
185 ///
186 /// A group's constituent labels are the wrong answer for a menu — a
187 /// three-entity capture would read *"Undo update"* after the least
188 /// interesting of its parts. The caller who opened the group is the only
189 /// one who knows what the group *was*, so it says so here.
190 pub fn labeled(stack_id: Option<u64>, label: Option<UndoLabel>) -> Self {
191 CompositeCommand {
192 label,
193 commands: Vec::new(),
194 stack_id: stack_id.unwrap_or(0),
195 }
196 }
197
198 /// Adds a command to this composite.
199 ///
200 /// Commands are executed, undone, and redone in the order they are added.
201 pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
202 self.commands.push(command);
203 }
204
205 /// Returns true if this composite contains no commands.
206 pub fn is_empty(&self) -> bool {
207 self.commands.is_empty()
208 }
209
210 /// Name this group after the fact — for a caller that only learns what the
211 /// group was once its parts have run.
212 pub fn set_label(&mut self, label: Option<UndoLabel>) {
213 self.label = label;
214 }
215}
216
217impl UndoRedoCommand for CompositeCommand {
218 fn undo(&mut self) -> Result<()> {
219 // Undo commands in reverse order
220 for command in self.commands.iter_mut().rev() {
221 command.undo()?;
222 }
223 Ok(())
224 }
225
226 fn redo(&mut self) -> Result<()> {
227 // Redo commands in original order
228 for command in self.commands.iter_mut() {
229 command.redo()?;
230 }
231 Ok(())
232 }
233
234 fn as_any(&self) -> &dyn Any {
235 self
236 }
237
238 /// The group's own name, never a constituent's — see [`CompositeCommand::labeled`].
239 fn label(&self) -> Option<UndoLabel> {
240 self.label
241 }
242}
243/// Trait for commands that can be executed asynchronously with progress tracking and cancellation.
244///
245/// This trait extends the basic UndoRedoCommand trait with asynchronous capabilities.
246/// Implementors must also implement the UndoRedoCommand trait to ensure compatibility
247/// with the existing undo/redo system.
248pub trait AsyncUndoRedoCommand: UndoRedoCommand {
249 /// Starts the undo operation asynchronously and returns immediately.
250 /// Returns Ok(()) if the operation was successfully started.
251 fn start_undo(&mut self) -> Result<()>;
252
253 /// Starts the redo operation asynchronously and returns immediately.
254 /// Returns Ok(()) if the operation was successfully started.
255 fn start_redo(&mut self) -> Result<()>;
256
257 /// Checks the progress of the current operation.
258 /// Returns a value between 0.0 (not started) and 1.0 (completed).
259 fn check_progress(&self) -> f32;
260
261 /// Attempts to cancel the in-progress operation.
262 /// Returns Ok(()) if cancellation was successful or if no operation is in progress.
263 fn cancel(&mut self) -> Result<()>;
264
265 /// Checks if the current operation is complete.
266 /// Returns true if the operation has finished successfully.
267 fn is_complete(&self) -> bool;
268}
269
270/// One step of history, and the number that names it.
271///
272/// The sequence is what lets a caller say *"undo the thing I just did"* rather
273/// than *"undo whatever is on top"* — see [`UndoStatus`]. It is minted once, at
274/// the push, and travels with the command across the undo/redo boundary, so an
275/// entry keeps its identity however many times it is stepped over.
276struct UndoEntry {
277 command: Box<dyn UndoRedoCommand>,
278 seq: u64,
279 /// Closed to merging — see [`UndoRedoManager::seal_head`].
280 sealed: bool,
281}
282
283#[derive(Default)]
284struct StackData {
285 undo_stack: Vec<UndoEntry>,
286 redo_stack: Vec<UndoEntry>,
287}
288
289impl fmt::Debug for StackData {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 f.debug_struct("StackData")
292 .field("undo_len", &self.undo_stack.len())
293 .field("redo_len", &self.redo_stack.len())
294 .finish()
295 }
296}
297
298/// Manager for undo and redo operations.
299///
300/// The UndoRedoManager maintains multiple stacks of commands:
301/// - Each stack has an undo stack for commands that can be undone
302/// - Each stack has a redo stack for commands that have been undone and can be redone
303///
304/// It also supports:
305/// - Grouping multiple commands as a single unit using begin_composite/end_composite
306/// - Merging commands of the same type when appropriate
307/// - Switching between different stacks
308#[derive(Debug)]
309pub struct UndoRedoManager {
310 stacks: HashMap<u64, StackData>,
311 next_stack_id: u64,
312 in_progress_composite: Option<CompositeCommand>,
313 composite_nesting_level: usize,
314 composite_stack_id: Option<u64>,
315 composite_label: Option<UndoLabel>,
316 event_hub: Option<Arc<EventHub>>,
317 /// Monotonic across every stack, so a sequence number is unique in the
318 /// process and a caller never has to say *which* stack it means.
319 next_seq: u64,
320 /// The sequence of the entry the most recent push landed in — a fresh one,
321 /// or the one it merged into. Read it straight after the call that pushed;
322 /// it is how a caller learns the number to hand back to
323 /// [`undo_if_head`](UndoRedoManager::undo_if_head) later.
324 last_pushed_seq: Option<u64>,
325 /// How many entries a stack may hold before the oldest are dropped.
326 /// `None` — the default — is unbounded, which is what a short-lived
327 /// document wants and what a day-long editing session does not.
328 undo_limit: Option<usize>,
329}
330
331impl Default for UndoRedoManager {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337impl UndoRedoManager {
338 /// Creates a new empty UndoRedoManager with one default stack (ID 0).
339 pub fn new() -> Self {
340 let mut stacks = HashMap::new();
341 stacks.insert(0, StackData::default());
342 UndoRedoManager {
343 stacks,
344 next_stack_id: 1,
345 in_progress_composite: None,
346 composite_nesting_level: 0,
347 composite_stack_id: None,
348 composite_label: None,
349 event_hub: None,
350 next_seq: 1,
351 last_pushed_seq: None,
352 undo_limit: None,
353 }
354 }
355
356 /// Emit one undo/redo event, naming the stack it happened on.
357 ///
358 /// The stack id travels in `data` because `ids` means *entity* ids, and a
359 /// process holding one stack per open document needs to tell whose history
360 /// moved — without it every subscriber has to re-poll every stack it knows.
361 fn emit(&self, event: UndoRedoEvent, stack_id: u64) {
362 if let Some(event_hub) = &self.event_hub {
363 event_hub.send_event(Event {
364 origin: Origin::UndoRedo(event),
365 ids: Vec::<EntityId>::new(),
366 data: Some(stack_id.to_string()),
367 });
368 }
369 }
370
371 /// Inject the event hub to allow sending undo/redo related events
372 pub fn set_event_hub(&mut self, event_hub: &Arc<EventHub>) {
373 self.event_hub = Some(Arc::clone(event_hub));
374 }
375
376 /// Undoes the most recent command on the specified stack.
377 /// If `stack_id` is None, the global stack (ID 0) is used.
378 ///
379 /// The undone command is moved to the redo stack.
380 /// Returns Ok(()) if successful or if there are no commands to undo.
381 pub fn undo(&mut self, stack_id: Option<u64>) -> Result<()> {
382 let target_stack_id = stack_id.unwrap_or(0);
383 let stack = self
384 .stacks
385 .get_mut(&target_stack_id)
386 .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
387
388 let stepped = if let Some(mut entry) = stack.undo_stack.pop() {
389 if let Err(e) = entry.command.undo() {
390 log::error!("Undo failed, re-pushing command to undo stack: {e}");
391 stack.undo_stack.push(entry);
392 return Err(e);
393 }
394 stack.redo_stack.push(entry);
395 true
396 } else {
397 false
398 };
399 if stepped {
400 self.emit(UndoRedoEvent::Undone, target_stack_id);
401 }
402 Ok(())
403 }
404
405 /// Redoes the most recently undone command on the specified stack.
406 /// If `stack_id` is None, the global stack (ID 0) is used.
407 ///
408 /// The redone command is moved back to the undo stack.
409 /// Returns Ok(()) if successful or if there are no commands to redo.
410 pub fn redo(&mut self, stack_id: Option<u64>) -> Result<()> {
411 let target_stack_id = stack_id.unwrap_or(0);
412 let stack = self
413 .stacks
414 .get_mut(&target_stack_id)
415 .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
416
417 let stepped = if let Some(mut entry) = stack.redo_stack.pop() {
418 if let Err(e) = entry.command.redo() {
419 log::error!("Redo failed, re-pushing command to redo stack: {e}");
420 stack.redo_stack.push(entry);
421 return Err(e);
422 }
423 stack.undo_stack.push(entry);
424 true
425 } else {
426 false
427 };
428 if stepped {
429 self.emit(UndoRedoEvent::Redone, target_stack_id);
430 }
431 Ok(())
432 }
433
434 /// Undo **only if** `seq` still names the top of the stack.
435 ///
436 /// The affordance this exists for is a toast's Undo button: it is offered
437 /// for one specific operation, and between the offer and the click anything
438 /// may have pushed — an autosave, a second window, a background job. Plain
439 /// `undo()` would take back that later thing instead, silently and with the
440 /// user believing they undid what the toast named. Naming the operation
441 /// turns a wrong action into an honest [`UndoStatus::Superseded`].
442 pub fn undo_if_head(&mut self, stack_id: Option<u64>, seq: u64) -> Result<UndoStatus> {
443 let target_stack_id = stack_id.unwrap_or(0);
444 let stack = self
445 .stacks
446 .get(&target_stack_id)
447 .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
448
449 match stack.undo_stack.last() {
450 None => Ok(UndoStatus::Empty),
451 Some(entry) if entry.seq != seq => Ok(UndoStatus::Superseded),
452 Some(_) => {
453 self.undo(stack_id)?;
454 Ok(UndoStatus::Undone)
455 }
456 }
457 }
458
459 /// Begins a composite command group.
460 ///
461 /// All commands added between begin_composite and end_composite will be treated as a single command.
462 /// This is useful for operations that logically represent a single action but require multiple
463 /// commands to implement.
464 ///
465 /// # Example
466 /// ```test
467 /// let mut manager = UndoRedoManager::new();
468 /// manager.begin_composite();
469 /// manager.add_command(Box::new(Command1::new()));
470 /// manager.add_command(Box::new(Command2::new()));
471 /// manager.end_composite();
472 /// // Now undo() will undo both commands as a single unit
473 /// ```
474 pub fn begin_composite(&mut self, stack_id: Option<u64>) -> Result<()> {
475 self.begin_composite_labeled(stack_id, None)
476 }
477
478 /// [`begin_composite`](Self::begin_composite), naming what the group is.
479 ///
480 /// Worth the extra call: a group's constituent labels describe its parts,
481 /// so a menu built from them reads *"Undo update"* for what the writer
482 /// experienced as *"add a character to the story bible"*. Only the caller
483 /// that opened the group knows the answer.
484 pub fn begin_composite_labeled(
485 &mut self,
486 stack_id: Option<u64>,
487 label: Option<UndoLabel>,
488 ) -> Result<()> {
489 if stack_id == Some(UNTRACKED_STACK_ID) {
490 return Err(anyhow!(
491 "Cannot open a composite on the untracked stack: its commands are \
492 dropped, so the group could never be undone"
493 ));
494 }
495 if self.composite_stack_id.is_some() && self.composite_stack_id != stack_id {
496 return Err(anyhow!(
497 "Cannot begin a composite on a different stack while another composite is in progress"
498 ));
499 }
500
501 // Set the target stack ID for this composite
502 self.composite_stack_id = stack_id;
503
504 // Increment the nesting level
505 self.composite_nesting_level += 1;
506
507 // If there's no composite in progress, create one. A nested
508 // `begin_composite_labeled` may name a group its opener left anonymous,
509 // but never rename one that already has a name: the outermost caller
510 // owns the description.
511 if self.in_progress_composite.is_none() {
512 self.in_progress_composite = Some(CompositeCommand::labeled(stack_id, label));
513 self.composite_label = label;
514 } else if self.composite_label.is_none() {
515 self.composite_label = label;
516 }
517
518 self.emit(UndoRedoEvent::BeginComposite, stack_id.unwrap_or(0));
519 Ok(())
520 }
521
522 /// Ends the current composite command group and adds it to the specified undo stack.
523 ///
524 /// If no commands were added to the composite, nothing is added to the undo stack.
525 /// If this is a nested composite, only the outermost composite is added to the undo stack.
526 pub fn end_composite(&mut self) {
527 // Decrement the nesting level
528 if self.composite_nesting_level > 0 {
529 self.composite_nesting_level -= 1;
530 }
531
532 // Only end the composite if we're at the outermost level
533 if self.composite_nesting_level == 0 {
534 if let Some(composite) = self.in_progress_composite.take()
535 && !composite.is_empty()
536 {
537 let target_stack_id = self.composite_stack_id.unwrap_or(0);
538 // A missing stack is a caller bug (the stack was dropped
539 // while a composite was open on it), but `end_composite`
540 // returns `()` and is called from a dozen fire-and-forget
541 // UI sites, so it cannot report one. It used to
542 // `.expect("Stack must exist")` — which killed the whole
543 // process, mid-edit, over a lost undo entry.
544 //
545 // Same shape as `database::write_guard`'s `acquire`: loud in
546 // a debug build so the bug is caught in development, and a
547 // degradation in a shipped one — the grouped commands stay
548 // applied, they just aren't undoable as a unit.
549 if self.stacks.contains_key(&target_stack_id) {
550 let mut composite = composite;
551 composite.set_label(self.composite_label);
552 self.push_entry(target_stack_id, Box::new(composite));
553 } else {
554 // Nothing was recorded, so nothing may be named: leaving
555 // the register alone would hand the next
556 // `last_pushed_seq` reader an earlier entry that is
557 // still the head.
558 self.last_pushed_seq = None;
559 debug_assert!(
560 false,
561 "end_composite: undo stack {} does not exist — a composite was \
562 opened on a stack that has since been removed",
563 target_stack_id
564 );
565 }
566 } else {
567 // An empty group records nothing either — same reasoning.
568 self.last_pushed_seq = None;
569 }
570 let ended_on = self.composite_stack_id.unwrap_or(0);
571 self.composite_label = None;
572 // Clear the target too, not just the label. `begin_composite`
573 // refuses a stack that differs from the one already open, so a
574 // target left set after the group closed locks every *other* stack
575 // out of compositing for the life of the manager — in a process
576 // holding one stack per open document, the first document to group
577 // anything would be the only one that ever could again.
578 // `cancel_composite` has always cleared it; this path forgot.
579 self.composite_stack_id = None;
580 self.emit(UndoRedoEvent::EndComposite, ended_on);
581 }
582 }
583
584 pub fn cancel_composite(&mut self) {
585 // Decrement the nesting level
586 if self.composite_nesting_level > 0 {
587 self.composite_nesting_level -= 1;
588 }
589
590 // Undo any sub-commands that were already executed in this composite
591 if let Some(ref mut composite) = self.in_progress_composite {
592 let _ = composite.undo();
593 }
594
595 let cancelled_on = self.composite_stack_id.unwrap_or(0);
596 self.in_progress_composite = None;
597 self.composite_stack_id = None;
598 self.composite_label = None;
599 // A cancelled group is not history: same reasoning as the record-nothing
600 // paths of `end_composite`.
601 self.last_pushed_seq = None;
602
603 self.emit(UndoRedoEvent::CancelComposite, cancelled_on);
604 }
605
606 /// Adds a command to the global undo stack (ID 0).
607 pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
608 let _ = self.add_command_to_stack(command, None);
609 }
610
611 /// Adds a command to the specified undo stack.
612 /// If `stack_id` is None, the global stack (ID 0) is used.
613 ///
614 /// This method handles several cases:
615 /// 1. If a composite command is in progress, the command is added to the composite
616 /// 2. If the command can be merged with the last command on the specified undo stack, they are merged
617 /// 3. Otherwise, the command is added to the specified undo stack as a new entry
618 ///
619 /// In all cases, the redo stack of the stack is cleared when a new command is added.
620 pub fn add_command_to_stack(
621 &mut self,
622 command: Box<dyn UndoRedoCommand>,
623 stack_id: Option<u64>,
624 ) -> Result<()> {
625 // Checked before the composite branch, not after: an untracked write is
626 // untracked whatever else is open. Folding one into a group would make
627 // the group undo something the caller explicitly said was not history.
628 if stack_id == Some(UNTRACKED_STACK_ID) {
629 self.last_pushed_seq = None;
630 return Ok(());
631 }
632
633 // If we have a composite in progress, add the command to it
634 if let Some(composite) = &mut self.in_progress_composite {
635 // ensure that the stack_id is the same as the composite's stack
636 if composite.stack_id != stack_id.unwrap_or(0) {
637 return Err(anyhow!(
638 "Cannot add command to composite with different stack ID"
639 ));
640 }
641 composite.add_command(command);
642 // The history did not grow: the group is still open, and it is the
643 // group — not this command — that will get a sequence. Clearing the
644 // register is what stops a caller reading `last_pushed_seq` here and
645 // being handed the number of some *earlier*, unrelated entry, which
646 // is still the head and which `undo_if_head` would therefore
647 // cheerfully undo. See `last_pushed_seq`'s own contract.
648 self.last_pushed_seq = None;
649 return Ok(());
650 }
651
652 let target_stack_id = stack_id.unwrap_or(0);
653 let stack = self
654 .stacks
655 .get_mut(&target_stack_id)
656 .ok_or_else(|| anyhow!("Stack with ID {} does not exist", target_stack_id))?;
657
658 // Try to merge with the last command if possible
659 if let Some(last) = stack.undo_stack.last_mut()
660 && !last.sealed
661 && last.command.can_merge(&*command)
662 && last.command.merge(&*command)
663 {
664 // Merged: the history did not grow, so the entry keeps the sequence
665 // it already had. A caller that pushed and then read
666 // `last_pushed_seq` gets the entry its command actually landed in,
667 // which for a burst of coalesced typing is the burst.
668 let seq = last.seq;
669 stack.redo_stack.clear();
670 self.last_pushed_seq = Some(seq);
671 self.emit(UndoRedoEvent::StackChanged, target_stack_id);
672 return Ok(());
673 }
674
675 // If we couldn't merge, just add the command normally.
676 self.push_entry(target_stack_id, command);
677 Ok(())
678 }
679
680 /// Push one command as a fresh entry: mint its sequence, clear the redo
681 /// branch, trim the stack to [`undo_limit`](Self::set_undo_limit), and
682 /// announce it. The single place history grows.
683 fn push_entry(&mut self, target_stack_id: u64, command: Box<dyn UndoRedoCommand>) {
684 let seq = self.next_seq;
685 self.next_seq = self.next_seq.wrapping_add(1);
686 let limit = self.undo_limit;
687
688 let Some(stack) = self.stacks.get_mut(&target_stack_id) else {
689 // No stack, no entry — so no sequence to hand out and no change to
690 // announce. Reporting one anyway would name an entry that was never
691 // stored.
692 self.last_pushed_seq = None;
693 return;
694 };
695 stack.undo_stack.push(UndoEntry {
696 command,
697 seq,
698 sealed: false,
699 });
700 stack.redo_stack.clear();
701 if let Some(limit) = limit
702 && stack.undo_stack.len() > limit
703 {
704 // Oldest first: the far end of a long session is the part
705 // nobody reaches for, and every entry may be pinning a
706 // snapshot of the store.
707 let excess = stack.undo_stack.len() - limit;
708 stack.undo_stack.drain(0..excess);
709 }
710
711 self.last_pushed_seq = Some(seq);
712 self.emit(UndoRedoEvent::StackChanged, target_stack_id);
713 }
714
715 /// Returns true if there are commands that can be undone on the specified stack.
716 /// If `stack_id` is None, the global stack (ID 0) is used.
717 pub fn can_undo(&self, stack_id: Option<u64>) -> bool {
718 let target_stack_id = stack_id.unwrap_or(0);
719 self.stacks
720 .get(&target_stack_id)
721 .map(|s| !s.undo_stack.is_empty())
722 .unwrap_or(false)
723 }
724
725 /// Returns true if there are commands that can be redone on the specified stack.
726 /// If `stack_id` is None, the global stack (ID 0) is used.
727 pub fn can_redo(&self, stack_id: Option<u64>) -> bool {
728 let target_stack_id = stack_id.unwrap_or(0);
729 self.stacks
730 .get(&target_stack_id)
731 .map(|s| !s.redo_stack.is_empty())
732 .unwrap_or(false)
733 }
734
735 /// Clears the undo and redo history for a specific stack.
736 ///
737 /// This method removes all commands from both the undo and redo stacks of the specified stack.
738 pub fn clear_stack(&mut self, stack_id: u64) {
739 let cleared = if let Some(stack) = self.stacks.get_mut(&stack_id) {
740 stack.undo_stack.clear();
741 stack.redo_stack.clear();
742 true
743 } else {
744 false
745 };
746 if cleared {
747 self.emit(UndoRedoEvent::StackChanged, stack_id);
748 }
749 }
750
751 /// Clears all undo and redo history from all stacks.
752 pub fn clear_all_stacks(&mut self) {
753 for stack in self.stacks.values_mut() {
754 stack.undo_stack.clear();
755 stack.redo_stack.clear();
756 }
757 self.in_progress_composite = None;
758 self.composite_nesting_level = 0;
759 self.composite_label = None;
760 // The target too — same reason `end_composite` clears it: a target left
761 // behind refuses every later group on a different stack.
762 self.composite_stack_id = None;
763 self.last_pushed_seq = None;
764 let ids: Vec<u64> = self.stacks.keys().copied().collect();
765 for id in ids {
766 self.emit(UndoRedoEvent::StackChanged, id);
767 }
768 }
769
770 /// Creates a new undo/redo stack and returns its ID.
771 pub fn create_new_stack(&mut self) -> u64 {
772 let id = self.next_stack_id;
773 self.stacks.insert(id, StackData::default());
774 self.next_stack_id += 1;
775 id
776 }
777
778 /// Deletes an undo/redo stack by its ID.
779 ///
780 /// The default stack (ID 0) cannot be deleted.
781 pub fn delete_stack(&mut self, stack_id: u64) -> Result<()> {
782 if stack_id == 0 {
783 return Err(anyhow!("Cannot delete the default stack"));
784 }
785 if self.stacks.remove(&stack_id).is_some() {
786 Ok(())
787 } else {
788 Err(anyhow!("Stack with ID {} does not exist", stack_id))
789 }
790 }
791
792 /// Gets the size of the undo stack for a specific stack.
793 pub fn get_stack_size(&self, stack_id: u64) -> usize {
794 self.stacks
795 .get(&stack_id)
796 .map(|s| s.undo_stack.len())
797 .unwrap_or(0)
798 }
799
800 /// Gets the size of the redo stack for a specific stack.
801 pub fn get_redo_stack_size(&self, stack_id: u64) -> usize {
802 self.stacks
803 .get(&stack_id)
804 .map(|s| s.redo_stack.len())
805 .unwrap_or(0)
806 }
807
808 /// The sequence number of the entry the most recent push landed in.
809 ///
810 /// Read it immediately after the call that pushed — it is how a caller
811 /// learns the number to hand to [`undo_if_head`](Self::undo_if_head) later,
812 /// and every later push overwrites it. `None` after a push that recorded
813 /// nothing (an untracked stack, or a command folded into an open
814 /// composite).
815 pub fn last_pushed_seq(&self) -> Option<u64> {
816 self.last_pushed_seq
817 }
818
819 /// The sequence number now on top of a stack, if any.
820 pub fn head_seq(&self, stack_id: Option<u64>) -> Option<u64> {
821 self.stacks
822 .get(&stack_id.unwrap_or(0))?
823 .undo_stack
824 .last()
825 .map(|e| e.seq)
826 }
827
828 /// What the next undo on this stack would take back, as a machine key for
829 /// the application to translate. `None` when the stack is empty or the
830 /// command declined to name itself.
831 pub fn undo_label(&self, stack_id: Option<u64>) -> Option<UndoLabel> {
832 self.stacks
833 .get(&stack_id.unwrap_or(0))?
834 .undo_stack
835 .last()?
836 .command
837 .label()
838 }
839
840 /// What the next redo on this stack would re-apply. See [`undo_label`](Self::undo_label).
841 pub fn redo_label(&self, stack_id: Option<u64>) -> Option<UndoLabel> {
842 self.stacks
843 .get(&stack_id.unwrap_or(0))?
844 .redo_stack
845 .last()?
846 .command
847 .label()
848 }
849
850 /// Close the top entry to merging, so the next command starts a new one.
851 ///
852 /// Merging exists so a burst of typing is one undo step. It decides purely
853 /// on the *shape* of two commands — adjacent, close in time, same kind —
854 /// and cannot see that something unrelated happened in between. So type,
855 /// rename a chapter from another panel, type again, and the two bursts merge
856 /// **across** the rename: one undo then takes back text entered before an
857 /// event the writer remembers as a dividing line.
858 ///
859 /// A caller that knows such a line was crossed says so here. Idempotent, and
860 /// a no-op on an empty or missing stack.
861 pub fn seal_head(&mut self, stack_id: Option<u64>) {
862 if let Some(stack) = self.stacks.get_mut(&stack_id.unwrap_or(0))
863 && let Some(last) = stack.undo_stack.last_mut()
864 {
865 last.sealed = true;
866 }
867 }
868
869 /// Bound how many entries a stack keeps, dropping the oldest past the
870 /// limit. `None` (the default) is unbounded.
871 ///
872 /// Unbounded is right for a document that lives as long as its window and
873 /// wrong for a session measured in hours: every entry may pin a snapshot of
874 /// the store, so an uncapped stack has no ceiling at all. Applied on the
875 /// next push, not retroactively.
876 pub fn set_undo_limit(&mut self, limit: Option<usize>) {
877 self.undo_limit = limit;
878 }
879
880 /// The current entry limit, if one is set.
881 pub fn undo_limit(&self) -> Option<usize> {
882 self.undo_limit
883 }
884}