Skip to main content

markdown_editor/
transaction.rs

1use std::ops::Range;
2
3use super::{
4    document::{BlockContent, EditorDocument},
5    id::BlockId,
6    inline::InlineText,
7    kind::BlockKindData,
8    selection::BlockSelection,
9    step::{DetachedBlock, Step, StepError, StepMap},
10};
11
12/// Why an edit happened; drives undo coalescing.
13#[non_exhaustive]
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
15pub enum EditOrigin {
16    Typing,
17    Backspace,
18    DeleteForward,
19    /// A word delete backwards (D21); coalesces only with word deletes.
20    WordBackspace,
21    /// A word delete forwards (D21); coalesces only with word deletes.
22    WordDelete,
23    Ime,
24    Paste,
25    Cut,
26    Command(&'static str),
27    InputRule(&'static str),
28    Api,
29    Undo,
30    Redo,
31}
32
33/// A committed group of steps. Opaque.
34#[derive(Clone, Debug)]
35pub struct Transaction {
36    pub(crate) steps: Vec<Step>,
37    pub(crate) inverse: Vec<Step>,
38    pub(crate) maps: Vec<StepMap>,
39    pub(crate) selection_before: BlockSelection,
40    pub(crate) selection_after: BlockSelection,
41    pub(crate) origin: EditOrigin,
42}
43
44impl Transaction {
45    /// Map a selection through every step; `None` if an endpoint's block
46    /// was removed.
47    pub(crate) fn map_selection(&self, selection: &BlockSelection) -> Option<BlockSelection> {
48        let map = |mut p| {
49            for step_map in &self.maps {
50                p = step_map.map(p)?;
51            }
52            Some(p)
53        };
54        match selection {
55            BlockSelection::Text { anchor, head } => Some(BlockSelection::Text {
56                anchor: map(*anchor)?,
57                head: map(*head)?,
58            }),
59            BlockSelection::Blocks { .. } | BlockSelection::Cells { .. } => Some(selection.clone()),
60        }
61    }
62
63    pub(crate) fn is_empty(&self) -> bool {
64        self.steps.is_empty()
65    }
66
67    /// Re-apply recorded steps (redo) or their inverses (undo).
68    pub(crate) fn replay(steps: &[Step], doc: &mut EditorDocument) -> Result<(), StepError> {
69        for step in steps {
70            step.apply(doc)?;
71        }
72        doc.refresh_order();
73        doc.revision += 1;
74        Ok(())
75    }
76}
77
78/// Builds a transaction against the live document. Every change goes
79/// through [`TransactionBuilder::step`], which records the step and its
80/// inverse.
81pub(crate) struct TransactionBuilder<'a> {
82    pub(crate) doc: &'a mut EditorDocument,
83    steps: Vec<Step>,
84    inverse: Vec<Step>,
85    maps: Vec<StepMap>,
86}
87
88impl<'a> TransactionBuilder<'a> {
89    pub(crate) fn new(doc: &'a mut EditorDocument) -> Self {
90        Self {
91            doc,
92            steps: Vec::new(),
93            inverse: Vec::new(),
94            maps: Vec::new(),
95        }
96    }
97
98    pub(crate) fn step(&mut self, step: Step) -> Result<StepMap, StepError> {
99        let (inverse, map) = step.apply(self.doc)?;
100        self.steps.push(step);
101        self.inverse.push(inverse);
102        self.maps.push(map.clone());
103        Ok(map)
104    }
105
106    pub(crate) fn replace_text(
107        &mut self,
108        block: BlockId,
109        range: Range<usize>,
110        insert: InlineText,
111    ) -> Result<(), StepError> {
112        if range.is_empty() && insert.is_empty() {
113            return Ok(());
114        }
115        self.step(Step::ReplaceText {
116            block,
117            range,
118            insert,
119        })
120        .map(|_| ())
121    }
122
123    pub(crate) fn replace_plain(
124        &mut self,
125        block: BlockId,
126        range: Range<usize>,
127        insert: &str,
128    ) -> Result<(), StepError> {
129        if range.is_empty() && insert.is_empty() {
130            return Ok(());
131        }
132        self.step(Step::ReplacePlain {
133            block,
134            range,
135            insert: insert.to_string(),
136        })
137        .map(|_| ())
138    }
139
140    pub(crate) fn insert(
141        &mut self,
142        parent: BlockId,
143        index: usize,
144        blocks: Vec<DetachedBlock>,
145    ) -> Result<(), StepError> {
146        if blocks.is_empty() {
147            return Ok(());
148        }
149        self.step(Step::InsertBlocks {
150            parent,
151            index,
152            blocks,
153        })
154        .map(|_| ())
155    }
156
157    /// Remove blocks and return copies of them (ids kept) so callers can
158    /// re-insert them elsewhere: a move is remove + insert.
159    pub(crate) fn remove(
160        &mut self,
161        parent: BlockId,
162        range: Range<usize>,
163    ) -> Result<Vec<DetachedBlock>, StepError> {
164        if range.is_empty() {
165            return Ok(Vec::new());
166        }
167        self.step(Step::RemoveBlocks { parent, range })?;
168        match self.inverse.last() {
169            Some(Step::InsertBlocks { blocks, .. }) => Ok(blocks.clone()),
170            _ => Err(StepError::OutOfRange),
171        }
172    }
173
174    pub(crate) fn remove_block(&mut self, id: BlockId) -> Result<DetachedBlock, StepError> {
175        let (parent, index) = self
176            .doc
177            .index_in_parent(id)
178            .ok_or(StepError::UnknownBlock(id))?;
179        self.remove(parent, index..index + 1)?
180            .pop()
181            .ok_or(StepError::OutOfRange)
182    }
183
184    /// Move `id` (keeping ids) to `index` of `parent`, where `index` counts
185    /// the target's children *after* the block has been taken out.
186    pub(crate) fn move_block(
187        &mut self,
188        id: BlockId,
189        parent: BlockId,
190        index: usize,
191    ) -> Result<(), StepError> {
192        let block = self.remove_block(id)?;
193        self.insert(parent, index, vec![block])
194    }
195
196    pub(crate) fn set_kind(
197        &mut self,
198        block: BlockId,
199        kind: BlockKindData,
200    ) -> Result<(), StepError> {
201        if self.doc.kind(block) == Some(&kind) {
202            return Ok(());
203        }
204        self.step(Step::SetKind { block, kind }).map(|_| ())
205    }
206
207    /// Structural normalization (design ยง5.1), appended as ordinary steps so
208    /// undo stays exact:
209    /// - a `List` holds only `ListItem`s,
210    /// - a `ListItem` starts with a leaf,
211    /// - no container other than `Root` is empty,
212    /// - adjacent sibling lists of the same type are merged (Markdown cannot
213    ///   write them apart),
214    /// - there is at least one caret leaf.
215    pub(crate) fn normalize(&mut self) -> Result<(), StepError> {
216        loop {
217            let Some(fix) = self.find_violation() else {
218                return Ok(());
219            };
220            match fix {
221                Fix::RemoveEmpty(id) => {
222                    self.remove_block(id)?;
223                }
224                Fix::WrapInItem(id) => {
225                    let (parent, index) = self
226                        .doc
227                        .index_in_parent(id)
228                        .ok_or(StepError::UnknownBlock(id))?;
229                    let block = self.remove_block(id)?;
230                    let item = DetachedBlock::container(
231                        BlockKindData::ListItem {
232                            checked: None,
233                            spread: false,
234                        },
235                        vec![block],
236                    );
237                    self.insert(parent, index, vec![item])?;
238                }
239                Fix::LeadingParagraph(item) => {
240                    self.insert(item, 0, vec![DetachedBlock::paragraph(InlineText::new())])?;
241                }
242                Fix::MergeLists(first, second) => {
243                    let count = self.doc.children(second).len();
244                    let items = self.remove(second, 0..count)?;
245                    let len = self.doc.children(first).len();
246                    self.insert(first, len, items)?;
247                    self.remove_block(second)?;
248                }
249                Fix::NoLeaf => {
250                    let root = self.doc.root;
251                    let len = self.doc.children(root).len();
252                    self.insert(root, len, vec![DetachedBlock::paragraph(InlineText::new())])?;
253                }
254            }
255        }
256    }
257
258    fn find_violation(&self) -> Option<Fix> {
259        let doc = &*self.doc;
260        let mut stack = vec![doc.root];
261        while let Some(id) = stack.pop() {
262            let entry = doc.get(id)?;
263            if let BlockContent::Children(children) = &entry.content {
264                if children.is_empty() && entry.kind != BlockKindData::Root {
265                    return Some(Fix::RemoveEmpty(id));
266                }
267                if let BlockKindData::List { .. } = entry.kind
268                    && let Some(stray) = children
269                        .iter()
270                        .find(|c| !matches!(doc.kind(**c), Some(BlockKindData::ListItem { .. })))
271                {
272                    return Some(Fix::WrapInItem(*stray));
273                }
274                if let BlockKindData::ListItem { .. } = entry.kind
275                    && !children.first().is_some_and(|c| doc.is_caret_leaf(*c))
276                {
277                    return Some(Fix::LeadingParagraph(id));
278                }
279                for pair in children.windows(2) {
280                    if let (
281                        Some(BlockKindData::List { ordered: a, .. }),
282                        Some(BlockKindData::List { ordered: b, .. }),
283                    ) = (doc.kind(pair[0]), doc.kind(pair[1]))
284                        && a == b
285                    {
286                        return Some(Fix::MergeLists(pair[0], pair[1]));
287                    }
288                }
289                stack.extend(children.iter().copied());
290            }
291        }
292        if doc.walk_leaves().is_empty() {
293            return Some(Fix::NoLeaf);
294        }
295        None
296    }
297
298    pub(crate) fn finish(
299        mut self,
300        origin: EditOrigin,
301        selection_before: BlockSelection,
302        selection_after: BlockSelection,
303    ) -> Result<Transaction, StepError> {
304        self.normalize()?;
305        self.doc.refresh_order();
306        if !self.steps.is_empty() {
307            self.doc.revision += 1;
308        }
309        debug_assert_eq!(self.doc.check_invariants(), Ok(()));
310        let mut inverse = self.inverse;
311        inverse.reverse();
312        Ok(Transaction {
313            steps: self.steps,
314            inverse,
315            maps: self.maps,
316            selection_before,
317            selection_after,
318            origin,
319        })
320    }
321
322    /// Undo whatever was applied so far (used when a command fails halfway).
323    pub(crate) fn rollback(self) {
324        for step in self.inverse.iter().rev() {
325            let _ = step.apply(self.doc);
326        }
327        self.doc.refresh_order();
328    }
329}
330
331enum Fix {
332    RemoveEmpty(BlockId),
333    WrapInItem(BlockId),
334    LeadingParagraph(BlockId),
335    MergeLists(BlockId, BlockId),
336    NoLeaf,
337}