Skip to main content

incular_runtime/
undo.rs

1//! Retained undo/redo state for editable text.
2//!
3//! The controller owns history independently from any widget element. It
4//! observes the authoritative `TextEditingController`, stores compact
5//! text deltas with the selection of each prior value, and exposes availability through the
6//! runtime's `Signal` type so only consumers that read history state rebuild.
7//!
8//! History policy — what enters the stacks:
9//! Undoable: observed committed text transitions, including typing,
10//! deletion, cut/paste, programmatic sets and committed IME composition.
11//! Historical notification arguments are not authoritative current state;
12//! reentrant edits before this observer runs can coalesce intermediate values.
13//! Transient, never a step: preedit set/clear, selection and caret moves,
14//! and the composing marker. A cancelled composition leaves no trace, and
15//! any committed value change clears the transient overlay so preedit never
16//! describes a stale value. Material installs Widgets formatters through
17//! retained subscriptions; Text applies them before committing or notifying
18//! history, so rejected intermediate values are not recorded.
19//! Redo clears on the next committed text change, including after an undo.
20//! Controller replacement starts fresh: the old history drops with its
21//! listener and the new controller tracks from its current value.
22//! Restoration is a baseline, not an edit: binding rebaselines current and
23//! clears both stacks instead of recording an undo step.
24//! Reentrant edits made by listeners during notification commit normally —
25//! newer wins and snapshots are never restored over them; edits made while
26//! an undo/redo application is dispatching are absorbed into current without
27//! creating steps, so current always tracks the editor.
28
29use crate::Signal;
30use incular_text::{ComposingRange, TextEditingController, TextEditingValue, TextSelection};
31use std::{
32    cell::{Cell, RefCell},
33    rc::{Rc, Weak},
34};
35
36/// Observable availability state for an [`UndoHistoryController`].
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct UndoHistoryState {
39    pub can_undo: bool,
40    pub can_redo: bool,
41}
42
43/// A compact delta that turns the value *after* it back into the value before
44/// it: `before.text = after.text[..start] + replaced + after.text[start +
45/// inserted_len..]`, with the selection and composing range of `before`.
46struct Step {
47    start: usize,
48    replaced: Box<str>,
49    inserted_len: usize,
50    selection: TextSelection,
51    composing: Option<ComposingRange>,
52}
53
54impl Step {
55    /// Builds the step that turns `after` back into `before`.
56    fn between(before: &TextEditingValue, after: &TextEditingValue) -> Self {
57        let (a, b) = (before.text.as_str(), after.text.as_str());
58        let mut prefix = a
59            .bytes()
60            .zip(b.bytes())
61            .take_while(|(left, right)| left == right)
62            .count();
63        while !(a.is_char_boundary(prefix) && b.is_char_boundary(prefix)) {
64            prefix -= 1;
65        }
66        let limit = a.len().min(b.len()) - prefix;
67        let mut suffix = a
68            .bytes()
69            .rev()
70            .zip(b.bytes().rev())
71            .take(limit)
72            .take_while(|(left, right)| left == right)
73            .count();
74        while !(a.is_char_boundary(a.len() - suffix) && b.is_char_boundary(b.len() - suffix)) {
75            suffix -= 1;
76        }
77        Self {
78            start: prefix,
79            replaced: a[prefix..a.len() - suffix].into(),
80            inserted_len: b.len() - suffix - prefix,
81            selection: before.selection,
82            composing: before.composing,
83        }
84    }
85
86    /// Reconstructs the value before this step from the value after it.
87    fn revert(&self, after: &TextEditingValue) -> TextEditingValue {
88        let text = &after.text;
89        let tail = &text[self.start + self.inserted_len..];
90        let mut out = String::with_capacity(self.start + self.replaced.len() + tail.len());
91        out.push_str(&text[..self.start]);
92        out.push_str(&self.replaced);
93        out.push_str(tail);
94        TextEditingValue {
95            text: out,
96            selection: self.selection,
97            composing: self.composing,
98        }
99    }
100}
101
102struct History {
103    current: TextEditingValue,
104    undo: Vec<Step>,
105    redo: Vec<Step>,
106    max_entries: usize,
107}
108
109impl History {
110    /// Adopts `value` as current without creating a step. Only the top undo
111    /// and redo steps are expressed relative to current, so when the text
112    /// changes they are rebased onto the new text.
113    fn absorb(&mut self, value: TextEditingValue) {
114        if self.current.text != value.text {
115            for stack in [&mut self.undo, &mut self.redo] {
116                if let Some(top) = stack.last_mut() {
117                    let target = top.revert(&self.current);
118                    *top = Step::between(&target, &value);
119                }
120            }
121        }
122        self.current = value;
123    }
124}
125
126const DEFAULT_MAX_ENTRIES: usize = 100;
127
128fn trim_history(entries: &mut Vec<Step>, max_entries: usize) {
129    if entries.len() > max_entries {
130        let remove = entries.len() - max_entries;
131        entries.drain(..remove);
132    }
133}
134
135struct Inner {
136    editor: TextEditingController,
137    history: RefCell<History>,
138    applying: Cell<bool>,
139    restoration_generation: Cell<u64>,
140    state: Signal<UndoHistoryState>,
141    listener: usize,
142}
143
144impl Inner {
145    fn availability(&self) -> UndoHistoryState {
146        let history = self.history.borrow();
147        UndoHistoryState {
148            can_undo: !history.undo.is_empty(),
149            can_redo: !history.redo.is_empty(),
150        }
151    }
152
153    fn sync_state(&self) {
154        let _ = self.state.set(self.availability());
155    }
156
157    fn record(&self, value: TextEditingValue) {
158        // Restoration is a baseline, not an edit: rebaseline instead of
159        // recording an undo step when the generation changed.
160        let generation = self.editor.restoration_generation();
161        if generation != self.restoration_generation.get() {
162            self.restoration_generation.set(generation);
163            let mut history = self.history.borrow_mut();
164            history.current = value;
165            history.undo.clear();
166            history.redo.clear();
167            drop(history);
168            self.sync_state();
169            return;
170        }
171        if self.applying.get() {
172            self.history.borrow_mut().absorb(value);
173            return;
174        }
175
176        let changed = {
177            let mut history = self.history.borrow_mut();
178            if history.current == value {
179                false
180            } else if history.current.text == value.text {
181                // Selection/caret changes are part of the current editing
182                // state, but should not create an undo step by themselves.
183                history.current = value;
184                false
185            } else {
186                let previous = std::mem::replace(&mut history.current, value);
187                if history.max_entries > 0 {
188                    let step = Step::between(&previous, &history.current);
189                    history.undo.push(step);
190                    let max_entries = history.max_entries;
191                    trim_history(&mut history.undo, max_entries);
192                }
193                history.redo.clear();
194                true
195            }
196        };
197        if changed {
198            self.sync_state();
199        }
200    }
201
202    fn apply_history_value(&self, value: TextEditingValue) {
203        struct RestorePrevious<'a> {
204            inner: &'a Inner,
205            previous: bool,
206        }
207
208        impl Drop for RestorePrevious<'_> {
209            fn drop(&mut self) {
210                let value = self.inner.editor.value();
211                self.inner.history.borrow_mut().absorb(value);
212                self.inner.applying.set(self.previous);
213                if !std::thread::panicking() {
214                    self.inner.sync_state();
215                }
216            }
217        }
218
219        let previous = self.applying.replace(true);
220        let _guard = RestorePrevious {
221            inner: self,
222            previous,
223        };
224        self.editor.set_value(value);
225    }
226}
227
228impl Drop for Inner {
229    fn drop(&mut self) {
230        self.editor.remove_listener(self.listener);
231    }
232}
233
234/// A cloneable, widget-independent undo/redo controller for text editing.
235#[derive(Clone)]
236pub struct UndoHistoryController {
237    inner: Rc<Inner>,
238}
239
240impl std::fmt::Debug for UndoHistoryController {
241    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        formatter
243            .debug_struct("UndoHistoryController")
244            .field("editor", &self.inner.editor)
245            .field("state", &self.status())
246            .finish()
247    }
248}
249
250impl UndoHistoryController {
251    /// Starts tracking an existing editor without adding its initial value to
252    /// history.
253    #[must_use]
254    pub fn new(editor: TextEditingController) -> Self {
255        let initial = editor.value();
256        let inner = Rc::new_cyclic(|weak: &Weak<Inner>| {
257            let listener = {
258                let weak = weak.clone();
259                editor.add_listener(move |_| {
260                    if let Some(inner) = weak.upgrade() {
261                        inner.record(inner.editor.value());
262                    }
263                })
264            };
265            Inner {
266                editor: editor.clone(),
267                history: RefCell::new(History {
268                    current: initial.clone(),
269                    undo: Vec::new(),
270                    redo: Vec::new(),
271                    max_entries: DEFAULT_MAX_ENTRIES,
272                }),
273                applying: Cell::new(false),
274                restoration_generation: Cell::new(editor.restoration_generation()),
275                state: Signal::new(UndoHistoryState::default()),
276                listener,
277            }
278        });
279        Self { inner }
280    }
281
282    #[must_use]
283    pub fn editor(&self) -> TextEditingController {
284        self.inner.editor.clone()
285    }
286
287    /// Returns the signal used by reactive consumers to observe availability.
288    #[must_use]
289    pub fn state_signal(&self) -> Signal<UndoHistoryState> {
290        self.inner.state.clone()
291    }
292
293    /// Short alias for [`Self::state_signal`].
294    #[must_use]
295    pub fn signal(&self) -> Signal<UndoHistoryState> {
296        self.state_signal()
297    }
298
299    #[must_use]
300    pub fn status(&self) -> UndoHistoryState {
301        self.inner.state.get()
302    }
303
304    #[must_use]
305    pub fn can_undo(&self) -> bool {
306        self.status().can_undo
307    }
308
309    #[must_use]
310    pub fn can_redo(&self) -> bool {
311        self.status().can_redo
312    }
313
314    /// Limits the number of committed text states retained by this history.
315    /// A value of zero disables recording while keeping the current editor
316    /// value usable. Existing entries are trimmed immediately.
317    pub fn set_max_entries(&self, max_entries: usize) {
318        let mut history = self.inner.history.borrow_mut();
319        history.max_entries = max_entries;
320        trim_history(&mut history.undo, max_entries);
321        trim_history(&mut history.redo, max_entries);
322        drop(history);
323        self.inner.sync_state();
324    }
325
326    #[must_use]
327    pub fn max_entries(&self) -> usize {
328        self.inner.history.borrow().max_entries
329    }
330
331    /// Moves the editor to the previous value, if one exists.
332    #[must_use]
333    pub fn undo(&self) -> bool {
334        let target = {
335            let mut history = self.inner.history.borrow_mut();
336            let Some(step) = history.undo.pop() else {
337                return false;
338            };
339            let target = step.revert(&history.current);
340            let current = std::mem::replace(&mut history.current, target.clone());
341            if history.max_entries > 0 {
342                history.redo.push(Step::between(&current, &target));
343                let max_entries = history.max_entries;
344                trim_history(&mut history.redo, max_entries);
345            }
346            target
347        };
348        self.inner.apply_history_value(target);
349        true
350    }
351
352    /// Moves the editor to the next value, if one exists.
353    #[must_use]
354    pub fn redo(&self) -> bool {
355        let target = {
356            let mut history = self.inner.history.borrow_mut();
357            let Some(step) = history.redo.pop() else {
358                return false;
359            };
360            let target = step.revert(&history.current);
361            let current = std::mem::replace(&mut history.current, target.clone());
362            if history.max_entries > 0 {
363                history.undo.push(Step::between(&current, &target));
364                let max_entries = history.max_entries;
365                trim_history(&mut history.undo, max_entries);
366            }
367            target
368        };
369        self.inner.apply_history_value(target);
370        true
371    }
372
373    /// Discards undo and redo entries while retaining the current editor text.
374    pub fn clear(&self) {
375        let mut history = self.inner.history.borrow_mut();
376        history.current = self.inner.editor.value();
377        history.undo.clear();
378        history.redo.clear();
379        drop(history);
380        self.inner.sync_state();
381    }
382}