Skip to main content

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}