Skip to main content

fission_text_engine/
edit.rs

1//! Edit primitives, transactions, and undo/redo history.
2
3use crate::buffer::TextBuffer;
4use std::ops::Range;
5
6// ── Single edit ─────────────────────────────────────────────────────────────
7
8/// A single atomic text edit expressed as a byte-range replacement.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct TextEdit {
11    /// Byte range in the document **before** this edit is applied.
12    pub range: Range<usize>,
13    /// The text that replaces `range`.
14    pub new_text: String,
15    /// The text that was present in `range` before the edit (stored so we can
16    /// invert the operation for undo).
17    pub old_text: String,
18}
19
20impl TextEdit {
21    /// Create a new edit.
22    pub fn new(
23        range: Range<usize>,
24        new_text: impl Into<String>,
25        old_text: impl Into<String>,
26    ) -> Self {
27        Self {
28            range,
29            new_text: new_text.into(),
30            old_text: old_text.into(),
31        }
32    }
33
34    /// Return the inverse edit that undoes this one.
35    ///
36    /// The inverse replaces the `new_text` (at the position it was inserted)
37    /// with the original `old_text`.
38    pub fn inverse(&self) -> TextEdit {
39        TextEdit {
40            range: self.range.start..(self.range.start + self.new_text.len()),
41            new_text: self.old_text.clone(),
42            old_text: self.new_text.clone(),
43        }
44    }
45}
46
47// ── Transaction ─────────────────────────────────────────────────────────────
48
49/// A group of [`TextEdit`]s that should be undone / redone as a unit.
50///
51/// Edits within a transaction are stored in **application order** (first edit
52/// first).  When inverting for undo, the list is reversed.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct EditTransaction {
55    pub edits: Vec<TextEdit>,
56}
57
58impl EditTransaction {
59    /// Create an empty transaction.
60    pub fn new() -> Self {
61        Self { edits: Vec::new() }
62    }
63
64    /// Push an edit onto the transaction.
65    pub fn push(&mut self, edit: TextEdit) {
66        self.edits.push(edit);
67    }
68
69    /// Return the inverse transaction (for undo).
70    pub fn inverse(&self) -> EditTransaction {
71        let edits: Vec<TextEdit> = self.edits.iter().rev().map(|e| e.inverse()).collect();
72        EditTransaction { edits }
73    }
74
75    /// `true` when the transaction contains no edits.
76    pub fn is_empty(&self) -> bool {
77        self.edits.is_empty()
78    }
79}
80
81impl Default for EditTransaction {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87// ── History ─────────────────────────────────────────────────────────────────
88
89/// Bounded undo / redo history.
90///
91/// The maximum number of entries on each stack is configurable via
92/// [`EditHistory::with_max`].  When the undo stack exceeds the limit the
93/// oldest entry is discarded (FIFO eviction).
94#[derive(Debug, Clone)]
95pub struct EditHistory {
96    undo_stack: Vec<EditTransaction>,
97    redo_stack: Vec<EditTransaction>,
98    max_entries: usize,
99}
100
101/// Default maximum undo depth.
102const DEFAULT_MAX_ENTRIES: usize = 1000;
103
104impl EditHistory {
105    /// Create a history with the default max depth (1 000 entries).
106    pub fn new() -> Self {
107        Self {
108            undo_stack: Vec::new(),
109            redo_stack: Vec::new(),
110            max_entries: DEFAULT_MAX_ENTRIES,
111        }
112    }
113
114    /// Create a history with a custom maximum depth.
115    pub fn with_max(max_entries: usize) -> Self {
116        assert!(max_entries > 0, "max_entries must be > 0");
117        Self {
118            undo_stack: Vec::new(),
119            redo_stack: Vec::new(),
120            max_entries,
121        }
122    }
123
124    /// Apply a pre-built [`EditTransaction`] to `buffer`, record it in the
125    /// undo stack, and clear the redo stack (any previously undone work is
126    /// forked away).
127    pub fn apply(&mut self, txn: &EditTransaction, buffer: &mut TextBuffer) {
128        for edit in &txn.edits {
129            buffer.replace(edit.range.clone(), &edit.new_text);
130        }
131        self.undo_stack.push(txn.clone());
132        if self.undo_stack.len() > self.max_entries {
133            self.undo_stack.remove(0);
134        }
135        self.redo_stack.clear();
136    }
137
138    /// Convenience: build a single-edit transaction, apply it, and record it.
139    ///
140    /// `old_text` is captured automatically from the buffer.
141    pub fn apply_edit(&mut self, buffer: &mut TextBuffer, range: Range<usize>, new_text: &str) {
142        let old_text = buffer.slice(range.clone()).to_string();
143        let edit = TextEdit::new(range, new_text, old_text);
144        let mut txn = EditTransaction::new();
145        txn.push(edit);
146        self.apply(&txn, buffer);
147    }
148
149    /// Undo the most recent transaction, returning `true` if an undo was
150    /// performed.
151    pub fn undo(&mut self, buffer: &mut TextBuffer) -> bool {
152        let txn = match self.undo_stack.pop() {
153            Some(t) => t,
154            None => return false,
155        };
156        let inv = txn.inverse();
157        for edit in &inv.edits {
158            buffer.replace(edit.range.clone(), &edit.new_text);
159        }
160        self.redo_stack.push(txn);
161        true
162    }
163
164    /// Redo the most recently undone transaction, returning `true` if a redo
165    /// was performed.
166    pub fn redo(&mut self, buffer: &mut TextBuffer) -> bool {
167        let txn = match self.redo_stack.pop() {
168            Some(t) => t,
169            None => return false,
170        };
171        for edit in &txn.edits {
172            buffer.replace(edit.range.clone(), &edit.new_text);
173        }
174        self.undo_stack.push(txn);
175        true
176    }
177
178    /// Record a pre-built [`EditTransaction`] in the undo stack **without**
179    /// applying it to the buffer.  This is useful when the caller has already
180    /// mutated the buffer (or is about to) and just needs the history entry.
181    ///
182    /// Clears the redo stack and evicts the oldest entry when the stack
183    /// exceeds `max_entries`.
184    pub fn record(&mut self, txn: EditTransaction) {
185        self.undo_stack.push(txn);
186        if self.undo_stack.len() > self.max_entries {
187            self.undo_stack.remove(0);
188        }
189        self.redo_stack.clear();
190    }
191
192    /// Pop the most recent transaction from the undo stack.
193    ///
194    /// Returns `None` if the undo stack is empty.  The caller is responsible
195    /// for applying the inverse and pushing a matching entry onto the redo
196    /// stack via [`push_redo`](Self::push_redo).
197    pub fn pop_undo(&mut self) -> Option<EditTransaction> {
198        self.undo_stack.pop()
199    }
200
201    /// Pop the most recent transaction from the redo stack.
202    ///
203    /// Returns `None` if the redo stack is empty.  The caller is responsible
204    /// for re-applying the edits and pushing a matching entry onto the undo
205    /// stack via [`push_undo`](Self::push_undo).
206    pub fn pop_redo(&mut self) -> Option<EditTransaction> {
207        self.redo_stack.pop()
208    }
209
210    /// Push a transaction onto the redo stack (used by external undo logic).
211    pub fn push_redo(&mut self, txn: EditTransaction) {
212        self.redo_stack.push(txn);
213    }
214
215    /// Push a transaction onto the undo stack without clearing the redo stack
216    /// (used by external redo logic that manually manages both stacks).
217    pub fn push_undo(&mut self, txn: EditTransaction) {
218        self.undo_stack.push(txn);
219        if self.undo_stack.len() > self.max_entries {
220            self.undo_stack.remove(0);
221        }
222    }
223
224    /// Number of entries currently on the undo stack.
225    pub fn undo_depth(&self) -> usize {
226        self.undo_stack.len()
227    }
228
229    /// Number of entries currently on the redo stack.
230    pub fn redo_depth(&self) -> usize {
231        self.redo_stack.len()
232    }
233
234    /// Maximum entries per stack.
235    pub fn max_entries(&self) -> usize {
236        self.max_entries
237    }
238
239    /// Discard all history.
240    pub fn clear(&mut self) {
241        self.undo_stack.clear();
242        self.redo_stack.clear();
243    }
244}
245
246impl Default for EditHistory {
247    fn default() -> Self {
248        Self::new()
249    }
250}