Skip to main content

taino_edit_core/
state.rs

1//! Editor state: [`EditorState`] (document + selection + schema + history),
2//! [`Transaction`] (a [`Transform`] that also tracks selection and history
3//! intent), and a bounded undo/redo [`History`].
4//!
5//! v0.1 ships history as the one built-in stateful component rather than a
6//! general typed-plugin registry; the plugin-registry generalisation is a
7//! v0.2 item (see ROADMAP). Undo/redo is exact for linear single-user
8//! editing, which is the v0.1 target.
9
10use crate::node::Node;
11use crate::plugin::{Plugin, PluginKey, PluginSet, PluginStates};
12use crate::schema::Schema;
13use crate::selection::Selection;
14use crate::step::{Step, StepError};
15use crate::transform::Transform;
16
17const DEFAULT_HISTORY_DEPTH: usize = 100;
18
19fn apply_steps(doc: &Node, steps: &[Box<dyn Step>], schema: &Schema) -> Result<Node, StepError> {
20    let mut cur = doc.clone();
21    for s in steps {
22        cur = s.apply(&cur, schema)?;
23    }
24    Ok(cur)
25}
26
27#[derive(Debug, Clone)]
28struct HistEntry {
29    /// Applied to the *new* doc, reproduce the *old* doc.
30    undo: Vec<Box<dyn Step>>,
31    /// Applied to the *old* doc, reproduce the *new* doc.
32    redo: Vec<Box<dyn Step>>,
33    selection_before: Selection,
34    selection_after: Selection,
35}
36
37/// A bounded, linear undo/redo stack.
38#[derive(Debug, Clone)]
39pub struct History {
40    done: Vec<HistEntry>,
41    undone: Vec<HistEntry>,
42    depth: usize,
43}
44
45impl Default for History {
46    fn default() -> Self {
47        History {
48            done: Vec::new(),
49            undone: Vec::new(),
50            depth: DEFAULT_HISTORY_DEPTH,
51        }
52    }
53}
54
55impl History {
56    /// A history bounded to `depth` undoable groups.
57    pub fn with_depth(depth: usize) -> Self {
58        History {
59            depth,
60            ..Default::default()
61        }
62    }
63
64    /// Number of undoable groups.
65    pub fn undo_depth(&self) -> usize {
66        self.done.len()
67    }
68
69    /// Number of redoable groups.
70    pub fn redo_depth(&self) -> usize {
71        self.undone.len()
72    }
73
74    fn record(&mut self, mut entry: HistEntry, join: bool) {
75        self.undone.clear();
76        if join {
77            if let Some(prev) = self.done.last_mut() {
78                // Combined undo: newest→mid (entry.undo) then mid→old
79                // (prev.undo); combined redo: old→mid then mid→new.
80                let mut undo = std::mem::take(&mut entry.undo);
81                undo.extend(std::mem::take(&mut prev.undo));
82                prev.undo = undo;
83                prev.redo.extend(std::mem::take(&mut entry.redo));
84                prev.selection_after = entry.selection_after;
85                return;
86            }
87        }
88        self.done.push(entry);
89        if self.done.len() > self.depth {
90            self.done.remove(0);
91        }
92    }
93}
94
95/// The complete editor state.
96#[derive(Debug, Clone)]
97pub struct EditorState {
98    doc: Node,
99    selection: Selection,
100    schema: Schema,
101    history: History,
102    plugins: PluginStates,
103}
104
105impl EditorState {
106    /// A fresh state for `doc`, caret at the document start.
107    pub fn new(doc: Node, schema: Schema) -> Self {
108        Self::with_plugins(doc, schema, PluginSet::new())
109    }
110
111    /// A fresh state pre-loaded with a [`PluginSet`]. Each plugin's
112    /// [`Plugin::init`] is called with the (partially-initialised) state,
113    /// so plugins can derive their initial value from the doc.
114    pub fn with_plugins(doc: Node, schema: Schema, plugins: PluginSet) -> Self {
115        let seed = EditorState {
116            doc,
117            selection: Selection::caret(0),
118            schema,
119            history: History::default(),
120            plugins: PluginStates::default(),
121        };
122        let plugin_states = PluginStates::from_set(plugins, &seed);
123        EditorState {
124            plugins: plugin_states,
125            ..seed
126        }
127    }
128
129    /// Use a custom history depth.
130    pub fn with_history(mut self, history: History) -> Self {
131        self.history = history;
132        self
133    }
134
135    /// Borrow this state's plugin value (returns `None` if no plugin
136    /// of type `P` was registered when the state was built).
137    pub fn plugin<P: Plugin>(&self, _key: PluginKey<P>) -> Option<&P::State> {
138        self.plugins.get::<P>()
139    }
140
141    /// The current document.
142    pub fn doc(&self) -> &Node {
143        &self.doc
144    }
145    /// The current selection.
146    pub fn selection(&self) -> Selection {
147        self.selection
148    }
149    /// The schema.
150    pub fn schema(&self) -> &Schema {
151        &self.schema
152    }
153    /// Undoable / redoable depth.
154    pub fn history(&self) -> &History {
155        &self.history
156    }
157
158    /// Begin a transaction from the current state.
159    pub fn tr(&self) -> Transaction {
160        Transaction {
161            tr: Transform::new(self.doc.clone()),
162            selection: self.selection,
163            selection_set: false,
164            add_to_history: true,
165            join: false,
166            history_intent: None,
167        }
168    }
169
170    /// Apply `tx`, returning the next state. Selection is mapped through the
171    /// transaction unless the transaction set one explicitly; a changing,
172    /// history-tracked transaction records an undo group. Transactions
173    /// carrying a [`HistoryIntent`] resolve to [`undo`](Self::undo) /
174    /// [`redo`](Self::redo) on this state (and never push another history
175    /// entry); if the stack is empty the current state is returned unchanged.
176    pub fn apply(&self, tx: Transaction) -> EditorState {
177        if let Some(intent) = tx.history_intent {
178            return match intent {
179                HistoryIntent::Undo => self.undo().unwrap_or_else(|| self.clone()),
180                HistoryIntent::Redo => self.redo().unwrap_or_else(|| self.clone()),
181            };
182        }
183        let new_doc = tx.tr.doc().clone();
184        let selection = if tx.selection_set {
185            tx.selection
186        } else {
187            self.selection.map(&new_doc, tx.tr.mapping())
188        };
189
190        let mut history = self.history.clone();
191        if tx.add_to_history && tx.tr.doc_changed() {
192            if let Ok(undo) = tx.tr.invert_steps() {
193                let redo: Vec<Box<dyn Step>> = tx.tr.steps().to_vec();
194                history.record(
195                    HistEntry {
196                        undo,
197                        redo,
198                        selection_before: self.selection,
199                        selection_after: selection,
200                    },
201                    tx.join,
202                );
203            }
204        }
205
206        // Fold plugin states forward against the just-applied tx.
207        let plugins = self.plugins.apply(&tx, self);
208
209        EditorState {
210            doc: new_doc,
211            selection,
212            schema: self.schema.clone(),
213            history,
214            plugins,
215        }
216    }
217
218    /// Undo the most recent group, or `None` if nothing to undo.
219    pub fn undo(&self) -> Option<EditorState> {
220        let entry = self.history.done.last()?.clone();
221        let doc = apply_steps(&self.doc, &entry.undo, &self.schema).ok()?;
222        let mut history = self.history.clone();
223        history.done.pop();
224        history.undone.push(entry.clone());
225        Some(EditorState {
226            doc,
227            selection: entry.selection_before,
228            schema: self.schema.clone(),
229            history,
230            plugins: self.plugins.clone(),
231        })
232    }
233
234    /// Redo the most recently undone group, or `None`.
235    pub fn redo(&self) -> Option<EditorState> {
236        let entry = self.history.undone.last()?.clone();
237        let doc = apply_steps(&self.doc, &entry.redo, &self.schema).ok()?;
238        let mut history = self.history.clone();
239        history.undone.pop();
240        history.done.push(entry.clone());
241        Some(EditorState {
242            doc,
243            selection: entry.selection_after,
244            schema: self.schema.clone(),
245            history,
246            plugins: self.plugins.clone(),
247        })
248    }
249}
250
251/// Whether a transaction is asking the state to walk its undo/redo stack
252/// instead of applying steps.
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum HistoryIntent {
255    /// Undo the most recent done group.
256    Undo,
257    /// Redo the most recently undone group.
258    Redo,
259}
260
261/// A pending change: a [`Transform`] plus selection and history intent.
262#[derive(Debug, Clone)]
263pub struct Transaction {
264    tr: Transform,
265    selection: Selection,
266    selection_set: bool,
267    add_to_history: bool,
268    join: bool,
269    history_intent: Option<HistoryIntent>,
270}
271
272impl Transaction {
273    /// The (in-progress) transformed document.
274    pub fn doc(&self) -> &Node {
275        self.tr.doc()
276    }
277
278    /// Mutable access to the underlying transform (apply steps via its
279    /// helpers, e.g. `tx.transform().replace(..)`).
280    pub fn transform(&mut self) -> &mut Transform {
281        &mut self.tr
282    }
283
284    /// Explicitly set the selection for the resulting state.
285    pub fn set_selection(&mut self, selection: Selection) -> &mut Self {
286        self.selection = selection;
287        self.selection_set = true;
288        self
289    }
290
291    /// Exclude this transaction from undo history.
292    pub fn no_history(&mut self) -> &mut Self {
293        self.add_to_history = false;
294        self
295    }
296
297    /// Merge this transaction into the previous undo group (e.g. continued
298    /// typing). Grouping is caller-driven in v0.1.
299    pub fn join_history(&mut self) -> &mut Self {
300        self.join = true;
301        self
302    }
303
304    /// Whether the document was changed.
305    pub fn doc_changed(&self) -> bool {
306        self.tr.doc_changed()
307    }
308
309    /// Tag this transaction so [`EditorState::apply`] walks the undo/redo
310    /// stack instead of applying steps. Used by the History extension's
311    /// commands to dispatch through the normal `Command`/`Dispatch` pipeline.
312    pub fn set_history_intent(&mut self, intent: HistoryIntent) -> &mut Self {
313        self.history_intent = Some(intent);
314        self
315    }
316
317    /// The history intent on this transaction, if any.
318    pub fn history_intent(&self) -> Option<HistoryIntent> {
319        self.history_intent
320    }
321}