Skip to main content

editor_core/
undo.rs

1//! Undo/redo history model and tree management.
2
3use super::{CommandError, Selection, SelectionSetSnapshot};
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6use std::time::{Duration, Instant};
7
8const DEFAULT_UNDO_COALESCING_TIMEOUT: Duration = Duration::from_secs(1);
9
10#[derive(Debug, Clone)]
11pub(super) struct TextEdit {
12    pub(super) start_before: usize,
13    pub(super) start_after: usize,
14    pub(super) deleted_text: String,
15    pub(super) inserted_text: String,
16}
17
18impl TextEdit {
19    pub(super) fn deleted_len(&self) -> usize {
20        self.deleted_text.chars().count()
21    }
22
23    pub(super) fn inserted_len(&self) -> usize {
24        self.inserted_text.chars().count()
25    }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum UndoEditKind {
30    Insert,
31    Explicit,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35struct CoalescingEdit {
36    start_before: usize,
37    start_after: usize,
38    deleted_len: usize,
39    inserted_len: usize,
40}
41
42#[derive(Debug, Clone)]
43struct UndoCoalescingState {
44    group_id: usize,
45    last_at: Instant,
46    edit_kind: UndoEditKind,
47    after_selection: SelectionSetSnapshot,
48    edits: Vec<CoalescingEdit>,
49}
50
51impl UndoCoalescingState {
52    fn from_step(
53        group_id: usize,
54        edit_kind: UndoEditKind,
55        step: &UndoStep,
56        last_at: Instant,
57    ) -> Option<Self> {
58        Some(Self {
59            group_id,
60            last_at,
61            edit_kind,
62            after_selection: step.after_selection.clone(),
63            edits: coalescing_edits_for_kind(edit_kind, step)?,
64        })
65    }
66
67    fn extend_with(
68        &self,
69        edit_kind: UndoEditKind,
70        step: &UndoStep,
71        now: Instant,
72        timeout: Duration,
73    ) -> Option<Vec<CoalescingEdit>> {
74        if self.edit_kind != edit_kind {
75            return None;
76        }
77        if edit_kind == UndoEditKind::Insert
78            && now.saturating_duration_since(self.last_at) >= timeout
79        {
80            return None;
81        }
82        if self.after_selection != step.before_selection {
83            return None;
84        }
85
86        let next_edits = coalescing_edits_for_kind(edit_kind, step)?;
87        if self.edits.len() != next_edits.len() {
88            return None;
89        }
90
91        for (previous, next) in self.edits.iter().zip(&next_edits) {
92            match edit_kind {
93                UndoEditKind::Insert => {
94                    let previous_end = previous.start_after.saturating_add(previous.inserted_len);
95                    if previous_end != next.start_before {
96                        return None;
97                    }
98                }
99                UndoEditKind::Explicit => {
100                    if previous.start_after != next.start_before
101                        || previous.inserted_len != next.deleted_len
102                    {
103                        return None;
104                    }
105                }
106            }
107        }
108
109        Some(next_edits)
110    }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub(super) enum UndoCoalescingMode {
115    None,
116    Insert,
117    Explicit,
118}
119
120fn coalescing_edits_for_kind(
121    edit_kind: UndoEditKind,
122    step: &UndoStep,
123) -> Option<Vec<CoalescingEdit>> {
124    if step.edits.is_empty() {
125        return None;
126    }
127
128    let mut edits = Vec::with_capacity(step.edits.len());
129    for edit in &step.edits {
130        let deleted_len = edit.deleted_len();
131        let inserted_len = edit.inserted_len();
132        match edit_kind {
133            UndoEditKind::Insert => {
134                if deleted_len != 0 || inserted_len == 0 || edit.inserted_text.contains('\n') {
135                    return None;
136                }
137            }
138            UndoEditKind::Explicit => {
139                if deleted_len == 0 && inserted_len == 0 {
140                    return None;
141                }
142            }
143        }
144
145        edits.push(CoalescingEdit {
146            start_before: edit.start_before,
147            start_after: edit.start_after,
148            deleted_len,
149            inserted_len,
150        });
151    }
152
153    edits.sort_by_key(|edit| (edit.start_before, edit.start_after));
154    Some(edits)
155}
156
157#[derive(Debug, Clone)]
158pub(super) struct UndoStep {
159    pub(super) group_id: usize,
160    pub(super) edits: Vec<TextEdit>,
161    pub(super) before_selection: SelectionSetSnapshot,
162    pub(super) after_selection: SelectionSetSnapshot,
163}
164
165#[derive(Debug)]
166pub(super) struct UndoRedoManager {
167    /// All nodes in the undo tree (stable indices).
168    ///
169    /// - Node `0` is the root ("base" state) and has no step.
170    /// - Other nodes contain one [`UndoStep`] representing an edit from `parent → node`.
171    nodes: Vec<UndoNode>,
172    /// Current node id in the undo tree.
173    current: UndoNodeId,
174    /// Count of nodes that currently hold a step (excludes root and pruned tombstones).
175    step_count: usize,
176    max_undo: usize,
177    /// Clean point tracking (node id in the undo tree).
178    clean_node: Option<UndoNodeId>,
179    next_group_id: usize,
180    open_group: Option<UndoCoalescingState>,
181    coalescing_timeout: Duration,
182}
183
184type UndoNodeId = usize;
185
186#[derive(Debug)]
187struct UndoNode {
188    parent: Option<UndoNodeId>,
189    children: Vec<UndoNodeId>,
190    /// The selected child branch to use for `Redo` when multiple exist.
191    preferred_child: Option<UndoNodeId>,
192    /// The step that produced this node (none for root and pruned tombstones).
193    step: Option<UndoStep>,
194}
195
196impl UndoRedoManager {
197    pub(super) fn new(max_undo: usize) -> Self {
198        let max_undo = max_undo.max(1);
199        Self {
200            nodes: vec![UndoNode {
201                parent: None,
202                children: Vec::new(),
203                preferred_child: None,
204                step: None,
205            }],
206            current: 0,
207            step_count: 0,
208            max_undo,
209            clean_node: Some(0),
210            next_group_id: 0,
211            open_group: None,
212            coalescing_timeout: DEFAULT_UNDO_COALESCING_TIMEOUT,
213        }
214    }
215
216    fn invalid_history_error(context: &str, node: UndoNodeId) -> CommandError {
217        CommandError::Other(format!("Invalid undo history: {context} (node={node})"))
218    }
219
220    fn live_current_node(&self) -> bool {
221        self.current == 0
222            || self
223                .nodes
224                .get(self.current)
225                .is_some_and(|node| node.step.is_some())
226    }
227
228    pub(super) fn can_undo(&self) -> bool {
229        self.current != 0
230            && self
231                .nodes
232                .get(self.current)
233                .is_some_and(|node| node.step.is_some())
234    }
235
236    pub(super) fn can_redo(&self) -> bool {
237        self.selected_child(self.current).is_some()
238    }
239
240    pub(super) fn undo_depth(&self) -> usize {
241        let mut depth = 0usize;
242        let mut node = self.current;
243        let mut remaining = self.nodes.len();
244        while node != 0 && remaining > 0 {
245            let Some(current) = self.nodes.get(node) else {
246                break;
247            };
248            let has_step = current.step.is_some();
249            if !has_step {
250                break;
251            }
252            depth = depth.saturating_add(1);
253            let Some(parent) = current.parent else {
254                break;
255            };
256            node = parent;
257            remaining -= 1;
258        }
259        depth
260    }
261
262    pub(super) fn redo_depth(&self) -> usize {
263        let mut depth = 0usize;
264        let mut node = self.current;
265        let mut remaining = self.nodes.len();
266        while remaining > 0 {
267            let Some(child) = self.selected_child(node) else {
268                break;
269            };
270            depth = depth.saturating_add(1);
271            node = child;
272            remaining -= 1;
273        }
274        depth
275    }
276
277    pub(super) fn current_group_id(&self) -> Option<usize> {
278        self.open_group.as_ref().map(|group| group.group_id)
279    }
280
281    pub(super) fn coalescing_timeout(&self) -> Duration {
282        self.coalescing_timeout
283    }
284
285    pub(super) fn set_coalescing_timeout(&mut self, timeout: Duration) {
286        self.coalescing_timeout = timeout;
287    }
288
289    pub(super) fn is_clean(&self) -> bool {
290        self.clean_node == Some(self.current)
291    }
292
293    pub(super) fn mark_clean(&mut self) {
294        self.clean_node = Some(self.current);
295        self.end_group();
296    }
297
298    pub(super) fn end_group(&mut self) {
299        self.open_group = None;
300    }
301
302    pub(super) fn push_step(&mut self, step: UndoStep, coalescible_insert: bool) -> usize {
303        let mode = if coalescible_insert {
304            UndoCoalescingMode::Insert
305        } else {
306            UndoCoalescingMode::None
307        };
308        self.push_step_with_mode(step, mode)
309    }
310
311    pub(super) fn push_explicit_coalescing_step(&mut self, step: UndoStep) -> usize {
312        self.push_step_with_mode(step, UndoCoalescingMode::Explicit)
313    }
314
315    fn push_step_with_mode(&mut self, mut step: UndoStep, mode: UndoCoalescingMode) -> usize {
316        let now = Instant::now();
317        let edit_kind = match mode {
318            UndoCoalescingMode::None => None,
319            UndoCoalescingMode::Insert => Some(UndoEditKind::Insert),
320            UndoCoalescingMode::Explicit => Some(UndoEditKind::Explicit),
321        };
322        let next_edits = edit_kind.and_then(|kind| coalescing_edits_for_kind(kind, &step));
323        let reuse_open_group = next_edits.is_some()
324            && self.clean_node != Some(self.current)
325            && edit_kind.is_some_and(|kind| {
326                self.open_group
327                    .as_ref()
328                    .and_then(|group| group.extend_with(kind, &step, now, self.coalescing_timeout))
329                    .is_some()
330            });
331
332        if reuse_open_group {
333            if let Some(group) = &self.open_group {
334                step.group_id = group.group_id;
335            }
336        } else {
337            step.group_id = self.next_group_id;
338            self.next_group_id = self.next_group_id.wrapping_add(1);
339        }
340
341        if let Some(kind) = edit_kind.filter(|_| next_edits.is_some()) {
342            self.open_group = UndoCoalescingState::from_step(step.group_id, kind, &step, now);
343        } else {
344            self.open_group = None;
345        }
346
347        let group_id = step.group_id;
348        let parent = if self.live_current_node() {
349            self.current
350        } else {
351            debug_assert!(
352                self.live_current_node(),
353                "undo history current node is stale"
354            );
355            self.current = 0;
356            self.open_group = None;
357            0
358        };
359        let new_id = self.nodes.len();
360        self.nodes.push(UndoNode {
361            parent: Some(parent),
362            children: Vec::new(),
363            preferred_child: None,
364            step: Some(step),
365        });
366
367        if let Some(parent_node) = self.nodes.get_mut(parent) {
368            parent_node.children.push(new_id);
369            parent_node.preferred_child = Some(new_id);
370        } else {
371            debug_assert!(
372                parent < self.nodes.len(),
373                "undo history parent node is invalid"
374            );
375            self.current = new_id;
376            return group_id;
377        }
378        self.current = new_id;
379        self.step_count = self.step_count.saturating_add(1);
380
381        self.prune_if_needed();
382        group_id
383    }
384
385    pub(super) fn pop_undo_group(&mut self) -> Result<Option<Vec<UndoStep>>, CommandError> {
386        let mut node = self.current;
387        if node == 0 {
388            return Ok(None);
389        }
390        let group_id = self
391            .nodes
392            .get(node)
393            .ok_or_else(|| Self::invalid_history_error("current node is out of bounds", node))?
394            .step
395            .as_ref()
396            .ok_or_else(|| Self::invalid_history_error("current node is stale", node))?
397            .group_id;
398
399        let mut steps: Vec<UndoStep> = Vec::new();
400        while node != 0 {
401            let (step, parent) = {
402                let current = self.nodes.get(node).ok_or_else(|| {
403                    Self::invalid_history_error("undo path node is out of bounds", node)
404                })?;
405                let step = current
406                    .step
407                    .as_ref()
408                    .ok_or_else(|| Self::invalid_history_error("undo path node is stale", node))?;
409                if step.group_id != group_id {
410                    break;
411                }
412                let parent = current.parent.ok_or_else(|| {
413                    Self::invalid_history_error("undo path node has no parent", node)
414                })?;
415                (step.clone(), parent)
416            };
417
418            if self.nodes.get(parent).is_none() {
419                return Err(Self::invalid_history_error(
420                    "undo path parent is out of bounds",
421                    parent,
422                ));
423            }
424
425            steps.push(step);
426
427            // Remember which branch we came from so redo follows it.
428            if let Some(parent_node) = self.nodes.get_mut(parent) {
429                parent_node.preferred_child = Some(node);
430            }
431
432            node = parent;
433        }
434
435        if steps.is_empty() {
436            return Ok(None);
437        }
438
439        self.current = node;
440        Ok(Some(steps))
441    }
442
443    pub(super) fn pop_redo_group(&mut self) -> Result<Option<Vec<UndoStep>>, CommandError> {
444        let Some(first) = self.selected_child_checked(self.current)? else {
445            return Ok(None);
446        };
447        let group_id = self
448            .nodes
449            .get(first)
450            .and_then(|node| node.step.as_ref())
451            .ok_or_else(|| Self::invalid_history_error("redo child node is stale", first))?
452            .group_id;
453
454        let mut node = self.current;
455        let mut steps: Vec<UndoStep> = Vec::new();
456        while let Some(child) = self.selected_child_checked(node)? {
457            let step = {
458                let child_node = self.nodes.get(child).ok_or_else(|| {
459                    Self::invalid_history_error("redo child node is out of bounds", child)
460                })?;
461                let step = child_node.step.as_ref().ok_or_else(|| {
462                    Self::invalid_history_error("redo child node is stale", child)
463                })?;
464                if step.group_id != group_id {
465                    break;
466                }
467                step.clone()
468            };
469
470            // Ensure this branch remains the selected redo path.
471            let parent = self.nodes.get_mut(node).ok_or_else(|| {
472                Self::invalid_history_error("redo parent node is out of bounds", node)
473            })?;
474            parent.preferred_child = Some(child);
475
476            steps.push(step);
477            node = child;
478        }
479
480        if steps.is_empty() {
481            return Ok(None);
482        }
483
484        self.current = node;
485        Ok(Some(steps))
486    }
487
488    pub(super) fn redo_branch_count(&self) -> usize {
489        self.nodes
490            .get(self.current)
491            .map_or(0, |node| node.children.len())
492    }
493
494    pub(super) fn selected_redo_branch_index(&self) -> Option<usize> {
495        let node = self.nodes.get(self.current)?;
496        let selected = self.selected_child(self.current)?;
497        node.children.iter().position(|c| *c == selected)
498    }
499
500    pub(super) fn select_redo_branch(&mut self, index: usize) -> Result<(), CommandError> {
501        let children = self
502            .nodes
503            .get(self.current)
504            .ok_or_else(|| {
505                Self::invalid_history_error("current redo node is out of bounds", self.current)
506            })?
507            .children
508            .clone();
509        if index >= children.len() {
510            return Err(CommandError::Other(format!(
511                "Invalid redo branch index {} (count={})",
512                index,
513                children.len()
514            )));
515        }
516        let child = children[index];
517        self.ensure_live_redo_child(child)?;
518        self.nodes
519            .get_mut(self.current)
520            .ok_or_else(|| {
521                Self::invalid_history_error("current redo node is out of bounds", self.current)
522            })?
523            .preferred_child = Some(child);
524        Ok(())
525    }
526
527    pub(super) fn selected_child(&self, node: UndoNodeId) -> Option<UndoNodeId> {
528        self.selected_child_checked(node).ok().flatten()
529    }
530
531    fn selected_child_checked(&self, node: UndoNodeId) -> Result<Option<UndoNodeId>, CommandError> {
532        let n = self.nodes.get(node).ok_or_else(|| {
533            Self::invalid_history_error("redo parent node is out of bounds", node)
534        })?;
535        if n.children.is_empty() {
536            return Ok(None);
537        }
538        let selected = if let Some(pref) = n.preferred_child
539            && n.children.contains(&pref)
540        {
541            pref
542        } else {
543            match n.children.last().copied() {
544                Some(child) => child,
545                None => return Ok(None),
546            }
547        };
548        self.ensure_live_redo_child(selected)?;
549        Ok(Some(selected))
550    }
551
552    fn ensure_live_redo_child(&self, child: UndoNodeId) -> Result<(), CommandError> {
553        let child_node = self.nodes.get(child).ok_or_else(|| {
554            Self::invalid_history_error("redo child node is out of bounds", child)
555        })?;
556        if child_node.step.is_none() {
557            return Err(Self::invalid_history_error(
558                "redo child node is stale",
559                child,
560            ));
561        }
562        Ok(())
563    }
564
565    pub(super) fn prune_if_needed(&mut self) {
566        while self.step_count > self.max_undo {
567            if let Some(leaf) = self.find_prunable_leaf() {
568                self.remove_leaf_node(leaf);
569                continue;
570            }
571
572            if self.prune_root_child() {
573                continue;
574            }
575
576            // No safe pruning candidate found; give up.
577            break;
578        }
579    }
580
581    pub(super) fn find_prunable_leaf(&self) -> Option<UndoNodeId> {
582        (1..self.nodes.len())
583            .filter(|id| *id != self.current)
584            .filter(|id| self.nodes.get(*id).is_some_and(|node| node.step.is_some()))
585            .filter(|id| {
586                self.nodes
587                    .get(*id)
588                    .is_some_and(|node| node.children.is_empty())
589            })
590            .min()
591    }
592
593    pub(super) fn remove_leaf_node(&mut self, id: UndoNodeId) {
594        let parent = match self.nodes.get(id).and_then(|node| node.parent) {
595            Some(parent) => parent,
596            None => {
597                debug_assert!(self.nodes.get(id).and_then(|node| node.parent).is_some());
598                return;
599            }
600        };
601        let Some(parent_node) = self.nodes.get_mut(parent) else {
602            debug_assert!(
603                parent < self.nodes.len(),
604                "undo history leaf parent is invalid"
605            );
606            return;
607        };
608        parent_node.children.retain(|c| *c != id);
609        if parent_node.preferred_child == Some(id) {
610            parent_node.preferred_child = parent_node.children.last().copied();
611        }
612
613        if self.clean_node == Some(id) {
614            self.clean_node = None;
615        }
616
617        let Some(node) = self.nodes.get_mut(id) else {
618            debug_assert!(id < self.nodes.len(), "undo history leaf node is invalid");
619            return;
620        };
621        node.parent = None;
622        node.children.clear();
623        node.preferred_child = None;
624        node.step = None;
625
626        self.step_count = self.step_count.saturating_sub(1);
627    }
628
629    pub(super) fn prune_root_child(&mut self) -> bool {
630        let Some(root) = self.nodes.first() else {
631            debug_assert!(!self.nodes.is_empty(), "undo history is missing root node");
632            return false;
633        };
634        let children = root.children.clone();
635        if children.len() != 1 {
636            return false;
637        }
638        let doomed = children[0];
639        if doomed == self.current {
640            return false;
641        }
642        if self
643            .nodes
644            .get(doomed)
645            .is_none_or(|node| node.step.is_none())
646        {
647            return false;
648        }
649
650        // Re-parent all of `doomed`'s children directly under root (root now represents the state
651        // *after* `doomed`).
652        let adopted = match self.nodes.get_mut(doomed) {
653            Some(doomed_node) => std::mem::take(&mut doomed_node.children),
654            None => {
655                debug_assert!(
656                    doomed < self.nodes.len(),
657                    "undo history pruned node is invalid"
658                );
659                return false;
660            }
661        };
662        for child in &adopted {
663            if let Some(child_node) = self.nodes.get_mut(*child) {
664                child_node.parent = Some(0);
665            } else {
666                debug_assert!(
667                    *child < self.nodes.len(),
668                    "undo history child node is invalid"
669                );
670                return false;
671            }
672        }
673
674        let Some(root) = self.nodes.get_mut(0) else {
675            debug_assert!(!self.nodes.is_empty(), "undo history is missing root node");
676            return false;
677        };
678        root.children = adopted;
679        root.preferred_child = root.children.last().copied();
680
681        if self.clean_node == Some(doomed) {
682            self.clean_node = Some(0);
683        }
684
685        let Some(doomed_node) = self.nodes.get_mut(doomed) else {
686            debug_assert!(
687                doomed < self.nodes.len(),
688                "undo history pruned node is invalid"
689            );
690            return false;
691        };
692        doomed_node.parent = None;
693        doomed_node.children.clear();
694        doomed_node.preferred_child = None;
695        doomed_node.step = None;
696
697        self.step_count = self.step_count.saturating_sub(1);
698        true
699    }
700}
701
702/// A persistable snapshot of the undo/redo history for a single document.
703///
704/// This is intended for "hot exit" workflows: persist the editor text plus this snapshot, then
705/// restore both on startup so undo/redo keeps working across restarts.
706///
707/// Notes:
708/// - This snapshot does **not** include the current document text; callers are responsible for
709///   persisting/restoring the text separately.
710/// - Restoring a snapshot into a different document text will produce undefined undo behavior.
711#[derive(Debug, Clone, PartialEq, Eq)]
712#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
713pub struct UndoHistorySnapshot {
714    /// Snapshot format version for forward compatibility.
715    pub format_version: u32,
716    /// Undo steps (oldest → newest).
717    pub undo_stack: Vec<UndoHistoryStep>,
718    /// Redo steps (oldest → newest).
719    pub redo_stack: Vec<UndoHistoryStep>,
720    /// Configured maximum undo history depth.
721    pub max_undo: usize,
722    /// Clean point tracking in the linearized history (index into `undo_stack + redo_stack`).
723    pub clean_index: Option<usize>,
724    /// The next group id to allocate.
725    pub next_group_id: usize,
726    /// Currently open coalescing group id, if any.
727    pub open_group_id: Option<usize>,
728}
729
730#[derive(Debug, Clone, PartialEq, Eq)]
731#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
732/// A single undo/redo step in a persisted history snapshot.
733pub struct UndoHistoryStep {
734    /// Undo group id. Grouped undo pops all adjacent steps with the same id.
735    pub group_id: usize,
736    /// Text edits captured by this step.
737    pub edits: Vec<UndoHistoryTextEdit>,
738    /// Selection set snapshot before applying this step.
739    pub before_selection: UndoHistorySelectionSet,
740    /// Selection set snapshot after applying this step.
741    pub after_selection: UndoHistorySelectionSet,
742}
743
744#[derive(Debug, Clone, PartialEq, Eq)]
745#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
746/// A single persisted text edit in an undo/redo step.
747pub struct UndoHistoryTextEdit {
748    /// Start offset in the document *before* applying the edit.
749    pub start_before: usize,
750    /// Start offset in the document *after* applying the edit.
751    pub start_after: usize,
752    /// Deleted text (from the pre-edit document).
753    pub deleted_text: String,
754    /// Inserted text (from the post-edit document).
755    pub inserted_text: String,
756}
757
758#[derive(Debug, Clone, PartialEq, Eq)]
759#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
760/// Persisted selection set state captured alongside undo/redo steps.
761pub struct UndoHistorySelectionSet {
762    /// All selections (primary + secondary); may include empty selections (carets).
763    pub selections: Vec<Selection>,
764    /// Index of the primary selection in `selections`.
765    pub primary_index: usize,
766}
767
768/// Errors produced when restoring undo history snapshots.
769#[derive(Debug, Clone, PartialEq, Eq)]
770pub enum UndoHistoryRestoreError {
771    /// The snapshot uses a newer/unknown format version.
772    UnsupportedFormatVersion(u32),
773    /// The snapshot contains an invalid clean point index.
774    InvalidCleanIndex {
775        /// The invalid clean index value found in the snapshot.
776        clean_index: usize,
777        /// The maximum valid clean index (`undo_stack.len() + redo_stack.len()`).
778        max_index: usize,
779    },
780}
781
782impl std::fmt::Display for UndoHistoryRestoreError {
783    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
784        match self {
785            Self::UnsupportedFormatVersion(v) => {
786                write!(f, "Unsupported undo history snapshot format_version={}", v)
787            }
788            Self::InvalidCleanIndex {
789                clean_index,
790                max_index,
791            } => write!(
792                f,
793                "Invalid undo history snapshot clean_index={} (max={})",
794                clean_index, max_index
795            ),
796        }
797    }
798}
799
800impl std::error::Error for UndoHistoryRestoreError {}
801
802const UNDO_HISTORY_SNAPSHOT_FORMAT_VERSION: u32 = 1;
803
804impl UndoRedoManager {
805    pub(super) fn snapshot(&self) -> UndoHistorySnapshot {
806        let undo_nodes = self.undo_path_nodes_oldest_first();
807        let redo_nodes_nearest_first = self.redo_path_nodes_nearest_first();
808
809        let clean_index = self.clean_node.and_then(|clean| {
810            if clean == 0 {
811                return Some(0);
812            }
813            if let Some(pos) = undo_nodes.iter().position(|id| *id == clean) {
814                return Some(pos + 1);
815            }
816            if let Some(pos) = redo_nodes_nearest_first.iter().position(|id| *id == clean) {
817                return Some(undo_nodes.len() + pos + 1);
818            }
819            None
820        });
821
822        UndoHistorySnapshot {
823            format_version: UNDO_HISTORY_SNAPSHOT_FORMAT_VERSION,
824            undo_stack: self
825                .undo_path_steps_oldest_first()
826                .iter()
827                .map(|step| UndoHistoryStep {
828                    group_id: step.group_id,
829                    edits: step
830                        .edits
831                        .iter()
832                        .map(|edit| UndoHistoryTextEdit {
833                            start_before: edit.start_before,
834                            start_after: edit.start_after,
835                            deleted_text: edit.deleted_text.clone(),
836                            inserted_text: edit.inserted_text.clone(),
837                        })
838                        .collect(),
839                    before_selection: UndoHistorySelectionSet {
840                        selections: step.before_selection.selections.clone(),
841                        primary_index: step.before_selection.primary_index,
842                    },
843                    after_selection: UndoHistorySelectionSet {
844                        selections: step.after_selection.selections.clone(),
845                        primary_index: step.after_selection.primary_index,
846                    },
847                })
848                .collect(),
849            redo_stack: self
850                .redo_path_steps_newest_first()
851                .iter()
852                .map(|step| UndoHistoryStep {
853                    group_id: step.group_id,
854                    edits: step
855                        .edits
856                        .iter()
857                        .map(|edit| UndoHistoryTextEdit {
858                            start_before: edit.start_before,
859                            start_after: edit.start_after,
860                            deleted_text: edit.deleted_text.clone(),
861                            inserted_text: edit.inserted_text.clone(),
862                        })
863                        .collect(),
864                    before_selection: UndoHistorySelectionSet {
865                        selections: step.before_selection.selections.clone(),
866                        primary_index: step.before_selection.primary_index,
867                    },
868                    after_selection: UndoHistorySelectionSet {
869                        selections: step.after_selection.selections.clone(),
870                        primary_index: step.after_selection.primary_index,
871                    },
872                })
873                .collect(),
874            max_undo: self.max_undo,
875            clean_index,
876            next_group_id: self.next_group_id,
877            open_group_id: self.current_group_id(),
878        }
879    }
880
881    pub(super) fn restore_from_snapshot(
882        &mut self,
883        snapshot: UndoHistorySnapshot,
884    ) -> Result<(), UndoHistoryRestoreError> {
885        if snapshot.format_version != UNDO_HISTORY_SNAPSHOT_FORMAT_VERSION {
886            return Err(UndoHistoryRestoreError::UnsupportedFormatVersion(
887                snapshot.format_version,
888            ));
889        }
890
891        let undo_steps = snapshot
892            .undo_stack
893            .into_iter()
894            .map(|step| UndoStep {
895                group_id: step.group_id,
896                edits: step
897                    .edits
898                    .into_iter()
899                    .map(|edit| TextEdit {
900                        start_before: edit.start_before,
901                        start_after: edit.start_after,
902                        deleted_text: edit.deleted_text,
903                        inserted_text: edit.inserted_text,
904                    })
905                    .collect(),
906                before_selection: SelectionSetSnapshot {
907                    selections: step.before_selection.selections,
908                    primary_index: step.before_selection.primary_index,
909                },
910                after_selection: SelectionSetSnapshot {
911                    selections: step.after_selection.selections,
912                    primary_index: step.after_selection.primary_index,
913                },
914            })
915            .collect::<Vec<_>>();
916
917        let redo_steps = snapshot
918            .redo_stack
919            .into_iter()
920            .map(|step| UndoStep {
921                group_id: step.group_id,
922                edits: step
923                    .edits
924                    .into_iter()
925                    .map(|edit| TextEdit {
926                        start_before: edit.start_before,
927                        start_after: edit.start_after,
928                        deleted_text: edit.deleted_text,
929                        inserted_text: edit.inserted_text,
930                    })
931                    .collect(),
932                before_selection: SelectionSetSnapshot {
933                    selections: step.before_selection.selections,
934                    primary_index: step.before_selection.primary_index,
935                },
936                after_selection: SelectionSetSnapshot {
937                    selections: step.after_selection.selections,
938                    primary_index: step.after_selection.primary_index,
939                },
940            })
941            .collect::<Vec<_>>();
942
943        let total_steps = undo_steps.len() + redo_steps.len();
944        let max_undo = snapshot.max_undo.max(total_steps).max(1);
945        if let Some(clean_index) = snapshot.clean_index
946            && clean_index > total_steps
947        {
948            return Err(UndoHistoryRestoreError::InvalidCleanIndex {
949                clean_index,
950                max_index: total_steps,
951            });
952        }
953
954        // Rebuild as a linear chain (snapshots do not persist alternative branches).
955        self.nodes = vec![UndoNode {
956            parent: None,
957            children: Vec::new(),
958            preferred_child: None,
959            step: None,
960        }];
961        self.current = 0;
962        self.step_count = 0;
963
964        let mut node = 0usize;
965        let mut undo_node_ids: Vec<UndoNodeId> = Vec::new();
966        for step in undo_steps {
967            let new_id = self.nodes.len();
968            self.nodes.push(UndoNode {
969                parent: Some(node),
970                children: Vec::new(),
971                preferred_child: None,
972                step: Some(step),
973            });
974            if let Some(parent_node) = self.nodes.get_mut(node) {
975                parent_node.children.push(new_id);
976                parent_node.preferred_child = Some(new_id);
977            } else {
978                debug_assert!(
979                    node < self.nodes.len(),
980                    "undo history restore parent is invalid"
981                );
982            }
983            node = new_id;
984            undo_node_ids.push(new_id);
985            self.step_count = self.step_count.saturating_add(1);
986        }
987
988        let current_node = node;
989
990        // `redo_steps` in the v1 snapshot format is stored in "stack order" (newest → oldest).
991        // We recreate the redo path in redo order (oldest → newest) as a simple child chain.
992        let mut redo_node_ids_nearest_first: Vec<UndoNodeId> = Vec::new();
993        let mut redo_parent = current_node;
994        for step in redo_steps.into_iter().rev() {
995            let new_id = self.nodes.len();
996            self.nodes.push(UndoNode {
997                parent: Some(redo_parent),
998                children: Vec::new(),
999                preferred_child: None,
1000                step: Some(step),
1001            });
1002            if let Some(parent_node) = self.nodes.get_mut(redo_parent) {
1003                parent_node.children.push(new_id);
1004                parent_node.preferred_child = Some(new_id);
1005            } else {
1006                debug_assert!(
1007                    redo_parent < self.nodes.len(),
1008                    "undo history restore redo parent is invalid"
1009                );
1010            }
1011            redo_parent = new_id;
1012            redo_node_ids_nearest_first.push(new_id);
1013            self.step_count = self.step_count.saturating_add(1);
1014        }
1015
1016        self.current = current_node;
1017        self.max_undo = max_undo;
1018        self.next_group_id = snapshot.next_group_id;
1019        self.open_group = snapshot.open_group_id.and_then(|group_id| {
1020            let step = self.nodes.get(self.current)?.step.as_ref()?;
1021            if step.group_id != group_id {
1022                return None;
1023            }
1024            let kind = if coalescing_edits_for_kind(UndoEditKind::Insert, step).is_some() {
1025                UndoEditKind::Insert
1026            } else {
1027                UndoEditKind::Explicit
1028            };
1029            UndoCoalescingState::from_step(group_id, kind, step, Instant::now())
1030        });
1031
1032        self.clean_node = snapshot.clean_index.and_then(|idx| {
1033            if idx == 0 {
1034                return Some(0);
1035            }
1036            let undo_len = undo_node_ids.len();
1037            if idx <= undo_len {
1038                return undo_node_ids.get(idx - 1).copied();
1039            }
1040            let redo_pos = idx.saturating_sub(undo_len + 1);
1041            redo_node_ids_nearest_first.get(redo_pos).copied()
1042        });
1043
1044        Ok(())
1045    }
1046
1047    pub(super) fn undo_path_nodes_oldest_first(&self) -> Vec<UndoNodeId> {
1048        let mut nodes: Vec<UndoNodeId> = Vec::new();
1049        let mut node = self.current;
1050        let mut remaining = self.nodes.len();
1051        while node != 0 && remaining > 0 {
1052            let Some(current) = self.nodes.get(node) else {
1053                break;
1054            };
1055            if current.step.is_none() {
1056                break;
1057            }
1058            nodes.push(node);
1059            let Some(parent) = current.parent else {
1060                break;
1061            };
1062            node = parent;
1063            remaining -= 1;
1064        }
1065        nodes.reverse();
1066        nodes
1067    }
1068
1069    pub(super) fn redo_path_nodes_nearest_first(&self) -> Vec<UndoNodeId> {
1070        let mut out: Vec<UndoNodeId> = Vec::new();
1071        let mut node = self.current;
1072        while let Some(child) = self.selected_child(node) {
1073            out.push(child);
1074            node = child;
1075        }
1076        out
1077    }
1078
1079    pub(super) fn undo_path_steps_oldest_first(&self) -> Vec<UndoStep> {
1080        self.undo_path_nodes_oldest_first()
1081            .into_iter()
1082            .filter_map(|id| {
1083                self.nodes
1084                    .get(id)
1085                    .and_then(|node| node.step.as_ref())
1086                    .cloned()
1087            })
1088            .collect()
1089    }
1090
1091    /// Redo path steps in the legacy "redo stack order" (newest → oldest), matching the v1
1092    /// `UndoHistorySnapshot` semantics.
1093    pub(super) fn redo_path_steps_newest_first(&self) -> Vec<UndoStep> {
1094        let mut steps: Vec<UndoStep> = self
1095            .redo_path_nodes_nearest_first()
1096            .into_iter()
1097            .filter_map(|id| {
1098                self.nodes
1099                    .get(id)
1100                    .and_then(|node| node.step.as_ref())
1101                    .cloned()
1102            })
1103            .collect();
1104        steps.reverse();
1105        steps
1106    }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111    use super::super::{Position, SelectionDirection};
1112    use super::*;
1113
1114    fn selection_snapshot() -> SelectionSetSnapshot {
1115        SelectionSetSnapshot {
1116            selections: vec![Selection {
1117                start: Position::new(0, 0),
1118                end: Position::new(0, 0),
1119                direction: SelectionDirection::Forward,
1120            }],
1121            primary_index: 0,
1122        }
1123    }
1124
1125    fn undo_step(group_id: usize) -> UndoStep {
1126        UndoStep {
1127            group_id,
1128            edits: vec![TextEdit {
1129                start_before: 0,
1130                start_after: 0,
1131                deleted_text: String::new(),
1132                inserted_text: "x".to_string(),
1133            }],
1134            before_selection: selection_snapshot(),
1135            after_selection: selection_snapshot(),
1136        }
1137    }
1138
1139    fn tombstone_node(parent: Option<UndoNodeId>) -> UndoNode {
1140        UndoNode {
1141            parent,
1142            children: Vec::new(),
1143            preferred_child: None,
1144            step: None,
1145        }
1146    }
1147
1148    #[test]
1149    fn pop_undo_group_reports_stale_current_node() {
1150        let mut manager = UndoRedoManager::new(10);
1151        manager.nodes.push(tombstone_node(Some(0)));
1152        manager.current = 1;
1153
1154        let err = manager
1155            .pop_undo_group()
1156            .expect_err("stale current node should be an explicit error");
1157        assert!(err.to_string().contains("current node is stale"));
1158    }
1159
1160    #[test]
1161    fn pop_redo_group_reports_stale_child_node() {
1162        let mut manager = UndoRedoManager::new(10);
1163        manager.nodes.push(tombstone_node(Some(0)));
1164        manager.nodes[0].children.push(1);
1165        manager.nodes[0].preferred_child = Some(1);
1166
1167        let err = manager
1168            .pop_redo_group()
1169            .expect_err("stale redo child should be an explicit error");
1170        assert!(err.to_string().contains("redo child node is stale"));
1171    }
1172
1173    #[test]
1174    fn checked_history_accessors_do_not_panic_on_invalid_current_node() {
1175        let mut manager = UndoRedoManager::new(10);
1176        manager.current = 999;
1177
1178        assert!(!manager.can_undo());
1179        assert!(!manager.can_redo());
1180        assert_eq!(manager.undo_depth(), 0);
1181        assert_eq!(manager.redo_depth(), 0);
1182        assert_eq!(manager.redo_branch_count(), 0);
1183        assert_eq!(manager.selected_redo_branch_index(), None);
1184    }
1185
1186    #[test]
1187    fn valid_undo_redo_group_paths_still_work() {
1188        let mut manager = UndoRedoManager::new(10);
1189        let group = manager.push_step(undo_step(0), false);
1190        assert_eq!(group, 0);
1191
1192        let undo_steps = manager.pop_undo_group().unwrap().unwrap();
1193        assert_eq!(undo_steps.len(), 1);
1194        assert_eq!(manager.current, 0);
1195
1196        let redo_steps = manager.pop_redo_group().unwrap().unwrap();
1197        assert_eq!(redo_steps.len(), 1);
1198        assert_eq!(manager.current, 1);
1199    }
1200}