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}