Skip to main content

odox_ui/
flow_model.rs

1//! The paragraphs under a flow's root, as the page editor sees them.
2//!
3//! A paragraph is named by its path of child indices from the root the flow
4//! draws, which is the path [`crate::Flow`] reports it under. The editor's
5//! edits become `odox-core` edits on the tree, and an edit that begins an undo
6//! step is recorded in [`Editing`] once it has succeeded. A format writes
7//! automatic styles, which the document's [`Styles`] is told of as they are
8//! written. DESIGN.md ยง11.
9//
10// Author: David M. Anderson
11// Built with AI assistance (Claude, Anthropic)
12
13use std::cell::OnceCell;
14use std::ops::Range;
15
16use eframe::egui::Id;
17use egui_richedit::{Edit, Mark, Model, Position, RichEdit, Selection};
18use odox_core::edit::{self, is_paragraph};
19use odox_core::{Element, ListKind, Ns, Refused, Styles};
20
21use crate::Editing;
22use crate::flow::is_block_container;
23
24/// The editor over a flow's paragraphs.
25pub type PageEditor = RichEdit<Vec<usize>>;
26
27/// The id the page editor takes the keyboard under. The shell reads it to
28/// know that typing on the page still leaves its shortcuts to it.
29pub(crate) fn page_editor_id() -> Id {
30    Id::new("odox-page-editor")
31}
32
33/// A page editor with nothing selected.
34pub fn page_editor() -> PageEditor {
35    RichEdit::new(page_editor_id())
36}
37
38/// What a button for a kind of paragraph asks for.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Block {
41    /// A heading of a level, or body text again where the paragraphs already
42    /// are one.
43    Heading(u8),
44    /// Body text.
45    Body,
46    /// A bulleted list, or no list where the paragraphs already are one.
47    Bullets,
48    /// A numbered list, or no list where the paragraphs already are one.
49    Numbers,
50}
51
52/// The paragraphs under a root in a content tree, for one frame's editing.
53pub struct FlowModel<'a> {
54    content: &'a mut Element,
55    styles: &'a mut Styles,
56    /// Where the flow's root is under the content root.
57    root: Vec<usize>,
58    editing: &'a mut Editing,
59    /// The part of the root the paragraphs are kept to, as a path under it:
60    /// on a slide, the one shape being typed into.
61    scope: Vec<usize>,
62    /// Put in front of every path an undo step remembers, for a view whose
63    /// paths mean something only with it: on a deck, which slide.
64    tag: Vec<usize>,
65    /// Every editable paragraph in drawing order, found when first asked for
66    /// and forgotten when an edit changes it.
67    order: OnceCell<Vec<Vec<usize>>>,
68}
69
70impl<'a> FlowModel<'a> {
71    /// The paragraphs under the element at a path in a content tree, whose
72    /// document's styles are `styles`.
73    pub fn new(
74        content: &'a mut Element,
75        styles: &'a mut Styles,
76        root: Vec<usize>,
77        editing: &'a mut Editing,
78    ) -> Self {
79        Self {
80            content,
81            styles,
82            root,
83            editing,
84            scope: Vec::new(),
85            tag: Vec::new(),
86            order: OnceCell::new(),
87        }
88    }
89
90    /// Have the caret an undo step remembers begin with these indices, which
91    /// the view takes off again when it is asked to put the caret back.
92    #[must_use]
93    pub fn tagged(mut self, tag: Vec<usize>) -> Self {
94        self.tag = tag;
95        self
96    }
97
98    /// Keep to the paragraphs under one element beneath the root, so that
99    /// moving and joining never leave it: a slide's label does not run on
100    /// into the next shape.
101    #[must_use]
102    pub fn within(mut self, scope: Vec<usize>) -> Self {
103        self.scope = scope;
104        self
105    }
106
107    fn root(&self) -> Option<&Element> {
108        self.content.at(&self.root)
109    }
110
111    fn paragraph_mut(&mut self, path: &[usize]) -> Option<&mut Element> {
112        self.content
113            .at_mut(&self.root)?
114            .at_mut(path)
115            .filter(|element| is_paragraph(element))
116    }
117
118    fn order(&self) -> &[Vec<usize>] {
119        self.order.get_or_init(|| {
120            let mut out = self.root().map(paragraph_paths).unwrap_or_default();
121            out.retain(|path| path.starts_with(&self.scope));
122            out
123        })
124    }
125
126    fn neighbour(&self, paragraph: &[usize], step: isize) -> Option<Vec<usize>> {
127        let order = self.order();
128        let index = order.iter().position(|p| p == paragraph)?;
129        order.get(index.checked_add_signed(step)?).cloned()
130    }
131
132    fn replace(
133        &mut self,
134        from: &Position<Vec<usize>>,
135        to: &Position<Vec<usize>>,
136        text: &str,
137    ) -> Option<()> {
138        let root = self.content.at_mut(&self.root)?;
139        match edit::replace_range(
140            root,
141            (&from.paragraph, from.offset),
142            (&to.paragraph, to.offset),
143            text,
144        ) {
145            Ok(()) => Some(()),
146            Err(Refused::Structure) => self.clear_between(from, to, text),
147            Err(_) => None,
148        }
149    }
150
151    /// A range that crosses a table or a frame, which no join takes apart:
152    /// the selected text goes from every paragraph it covers, each paragraph
153    /// stays where it is, and the new text goes in at the start. Nothing to
154    /// remove and nothing to put in, as Backspace at the start of a cell, is
155    /// refused rather than made an edit of nothing.
156    fn clear_between(
157        &mut self,
158        from: &Position<Vec<usize>>,
159        to: &Position<Vec<usize>>,
160        text: &str,
161    ) -> Option<()> {
162        let ranges = self.covered(from, to)?;
163        if text.is_empty() && ranges.iter().all(|(_, range)| range.is_empty()) {
164            return None;
165        }
166        for (path, range) in ranges.into_iter().rev() {
167            let with = if path == from.paragraph { text } else { "" };
168            let paragraph = self.paragraph_mut(&path)?;
169            edit::replace(paragraph, range, with);
170        }
171        Some(())
172    }
173
174    /// Each paragraph from one position to another in drawing order, with
175    /// the range of its text between them.
176    fn covered(
177        &self,
178        from: &Position<Vec<usize>>,
179        to: &Position<Vec<usize>>,
180    ) -> Option<Vec<(Vec<usize>, Range<usize>)>> {
181        let order = self.order();
182        let first = order.iter().position(|p| *p == from.paragraph)?;
183        let last = order.iter().position(|p| *p == to.paragraph)?;
184        let covered: Vec<Vec<usize>> = order.get(first..=last)?.to_vec();
185        Some(
186            covered
187                .into_iter()
188                .map(|path| {
189                    let len = self.text(&path).map_or(0, |t| t.chars().count());
190                    let start = if path == from.paragraph {
191                        from.offset
192                    } else {
193                        0
194                    };
195                    let end = if path == to.paragraph { to.offset } else { len };
196                    (path, start..end.max(start))
197                })
198                .collect(),
199        )
200    }
201
202    /// The paragraphs a selection runs over, in the order they are drawn.
203    fn selected_paragraphs(&self, selection: &Selection<Vec<usize>>) -> Option<Vec<Vec<usize>>> {
204        let order = self.order();
205        let anchor = order
206            .iter()
207            .position(|p| *p == selection.anchor.paragraph)?;
208        let focus = order.iter().position(|p| *p == selection.focus.paragraph)?;
209        Some(order[anchor.min(focus)..=anchor.max(focus)].to_vec())
210    }
211
212    /// Whether a block is what every paragraph a selection runs over already
213    /// is, which is when its button is lit and pressing it takes it off.
214    pub fn is_block(&self, selection: &Selection<Vec<usize>>, block: Block) -> bool {
215        let Some(paragraphs) = self.selected_paragraphs(selection) else {
216            return false;
217        };
218        paragraphs.iter().all(|paragraph| {
219            let path = [self.root.as_slice(), paragraph].concat();
220            let (heading, list) = edit::block_state(self.content, &path, self.styles);
221            match block {
222                Block::Heading(level) => heading == Some(level),
223                Block::Body => heading.is_none(),
224                Block::Bullets => list == Some(ListKind::Bullet),
225                Block::Numbers => list == Some(ListKind::Number),
226            }
227        })
228    }
229
230    /// Make the paragraphs a selection runs over a kind of paragraph, or take
231    /// them out of it where they already are one, as one step to undo, and put
232    /// the selection back over the same paragraphs. Answers whether the
233    /// document changed.
234    pub fn apply_block(&mut self, editor: &mut PageEditor, block: Block) -> bool {
235        let Some(selection) = editor.selection().cloned() else {
236            return false;
237        };
238        let Some(paragraphs) = self.selected_paragraphs(&selection) else {
239            return false;
240        };
241        let place = |path: &Vec<usize>, order: &[Vec<usize>]| order.iter().position(|p| p == path);
242        let (Some(anchor), Some(focus)) = (
243            place(&selection.anchor.paragraph, self.order()),
244            place(&selection.focus.paragraph, self.order()),
245        ) else {
246            return false;
247        };
248        let lit = self.is_block(&selection, block);
249        let before = self.content.clone();
250        let done = match block {
251            Block::Heading(_) | Block::Body => {
252                let level = match block {
253                    Block::Heading(level) if !lit => Some(level),
254                    _ => None,
255                };
256                self.set_headings(&paragraphs, level)
257            }
258            Block::Bullets | Block::Numbers => {
259                let kind = match block {
260                    Block::Bullets => ListKind::Bullet,
261                    _ => ListKind::Number,
262                };
263                self.set_lists(&paragraphs, (!lit).then_some(kind))
264            }
265        };
266        if done.is_none() || *self.content == before {
267            *self.content = before;
268            return false;
269        }
270        self.order = OnceCell::new();
271        let begins = Position::new(
272            [self.tag.as_slice(), &selection.focus.paragraph].concat(),
273            selection.focus.offset,
274        );
275        self.editing.touch();
276        self.editing.record_snapshot(before, Some(begins));
277        let order = self.order().to_vec();
278        if let (Some(a), Some(f)) = (order.get(anchor), order.get(focus)) {
279            editor.select(Selection {
280                anchor: Position::new(a.clone(), selection.anchor.offset),
281                focus: Position::new(f.clone(), selection.focus.offset),
282            });
283        }
284        true
285    }
286
287    fn set_headings(&mut self, paragraphs: &[Vec<usize>], level: Option<u8>) -> Option<()> {
288        let automatic =
289            |content: &Element| content.child(&Ns::Office, "automatic-styles").is_some();
290        for paragraph in paragraphs {
291            let had = automatic(self.content);
292            let path = [self.root.as_slice(), paragraph].concat();
293            edit::set_heading(self.content, &path, level, self.styles).ok()?;
294            self.follow_styles(had);
295        }
296        Some(())
297    }
298
299    fn set_lists(&mut self, paragraphs: &[Vec<usize>], kind: Option<ListKind>) -> Option<()> {
300        let had = self
301            .content
302            .child(&Ns::Office, "automatic-styles")
303            .is_some();
304        let paths: Vec<Vec<usize>> = paragraphs
305            .iter()
306            .map(|paragraph| [self.root.as_slice(), paragraph].concat())
307            .collect();
308        edit::set_list(self.content, &paths, kind, self.styles).ok()?;
309        self.follow_styles(had);
310        Some(())
311    }
312
313    /// Styles written into a document that had nowhere to keep them go before
314    /// its body, and move the body, and the root, one along.
315    fn follow_styles(&mut self, had: bool) {
316        if !had
317            && self
318                .content
319                .child(&Ns::Office, "automatic-styles")
320                .is_some()
321            && let Some(first) = self.root.first_mut()
322        {
323            *first += 1;
324        }
325    }
326
327    /// Give the text from one position to another a mark, or take it off,
328    /// paragraph by paragraph.
329    fn format(
330        &mut self,
331        from: &Position<Vec<usize>>,
332        to: &Position<Vec<usize>>,
333        mark: edit::Mark,
334        on: bool,
335    ) -> Option<()> {
336        let automatic =
337            |content: &Element| content.child(&Ns::Office, "automatic-styles").is_some();
338        for (path, range) in self.covered(from, to)? {
339            if range.is_empty() {
340                continue;
341            }
342            let had = automatic(self.content);
343            let mut at = self.root.clone();
344            at.extend(&path);
345            edit::format(self.content, &at, range, mark, on, self.styles).ok()?;
346            // Styles written into a document that had nowhere to keep them go
347            // before its body, and move the body, and the root, one along.
348            if !had
349                && automatic(self.content)
350                && let Some(first) = self.root.first_mut()
351            {
352                *first += 1;
353            }
354        }
355        Some(())
356    }
357}
358
359/// The library's name for a mark.
360fn core_mark(mark: Mark) -> edit::Mark {
361    match mark {
362        Mark::Bold => edit::Mark::Bold,
363        Mark::Italic => edit::Mark::Italic,
364        Mark::Underline => edit::Mark::Underline,
365        Mark::Strike => edit::Mark::Strike,
366    }
367}
368
369impl Model for FlowModel<'_> {
370    type Paragraph = Vec<usize>;
371
372    fn text(&self, paragraph: &Vec<usize>) -> Option<String> {
373        self.root()?
374            .at(paragraph)
375            .filter(|element| is_paragraph(element))
376            .map(edit::text)
377    }
378
379    fn next(&self, paragraph: &Vec<usize>) -> Option<Vec<usize>> {
380        self.neighbour(paragraph, 1)
381    }
382
383    fn previous(&self, paragraph: &Vec<usize>) -> Option<Vec<usize>> {
384        self.neighbour(paragraph, -1)
385    }
386
387    fn first(&self) -> Option<Vec<usize>> {
388        self.order().first().cloned()
389    }
390
391    fn last(&self) -> Option<Vec<usize>> {
392        self.order().last().cloned()
393    }
394
395    fn marked(
396        &self,
397        from: &Position<Vec<usize>>,
398        to: &Position<Vec<usize>>,
399        mark: Mark,
400    ) -> Option<bool> {
401        let root = self.root()?;
402        let mark = core_mark(mark);
403        let ranges: Vec<_> = self
404            .covered(from, to)?
405            .into_iter()
406            .filter(|(_, range)| !range.is_empty())
407            .collect();
408        if ranges.is_empty() {
409            let at = from.offset..from.offset;
410            return edit::marked(root.at(&from.paragraph)?, at, mark, self.styles);
411        }
412        let mut answer = None;
413        for (path, range) in ranges {
414            let value = edit::marked(root.at(&path)?, range, mark, self.styles)?;
415            if answer.is_some_and(|a| a != value) {
416                return None;
417            }
418            answer = Some(value);
419        }
420        answer
421    }
422
423    fn apply(
424        &mut self,
425        edit: Edit<'_, Vec<usize>>,
426        new_step: bool,
427    ) -> Option<Position<Vec<usize>>> {
428        // An edit to a document that matches its file always begins a step,
429        // whatever the editor thinks: typing that runs on past a save is a
430        // change the save does not hold, and has to be one a close asks about.
431        let new_step = new_step || !self.editing.modified();
432        // Taken before trying, because only trying says whether the edit is
433        // made. A refusal is decided before anything in the tree changes.
434        // Where the edit begins is where an undo of it puts the caret.
435        let before = new_step.then(|| self.content.clone());
436        let begins = match &edit {
437            Edit::Replace { from, .. } | Edit::Format { from, .. } => from,
438            Edit::Split { at } => at,
439        };
440        let begins = Position::new(
441            [self.tag.as_slice(), &begins.paragraph].concat(),
442            begins.offset,
443        );
444        let at = match edit {
445            Edit::Replace { from, to, text } => self
446                .replace(&from, &to, text)
447                .map(|()| Position::new(from.paragraph, from.offset + text.chars().count())),
448            Edit::Split { at } => {
449                let root = self.content.at_mut(&self.root)?;
450                edit::split_at(root, &at.paragraph, at.offset)
451                    .ok()
452                    .map(|second| Position::new(second, 0))
453            }
454            Edit::Format { from, to, mark, on } => {
455                self.format(&from, &to, core_mark(mark), on).map(|()| to)
456            }
457        };
458        if at.is_some() {
459            self.editing.touch();
460            if let Some(before) = before {
461                self.editing.record_snapshot(before, Some(begins));
462            }
463            self.order = OnceCell::new();
464        }
465        at
466    }
467}
468
469/// Every paragraph under a root, in the order [`crate::Flow`] draws them, by
470/// the paths it reports them under.
471pub(crate) fn paragraph_paths(root: &Element) -> Vec<Vec<usize>> {
472    let mut out = Vec::new();
473    blocks(root, &mut Vec::new(), &mut out);
474    out
475}
476
477/// The paragraphs among a run of blocks, in the order [`crate::Flow`] draws
478/// them, and under the same paths.
479fn blocks(parent: &Element, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
480    for (index, element) in parent.elements_indexed() {
481        path.push(index);
482        block(element, path, out);
483        path.pop();
484    }
485}
486
487fn block(element: &Element, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
488    if is_paragraph(element) {
489        out.push(path.clone());
490    } else if element.is(&Ns::Text, "list") {
491        for (item_index, item) in element.elements_indexed() {
492            if item.is(&Ns::Text, "list-item") || item.is(&Ns::Text, "list-header") {
493                path.push(item_index);
494                blocks(item, path, out);
495                path.pop();
496            }
497        }
498    } else if element.is(&Ns::Table, "table")
499        || element.is(&Ns::Table, "table-header-rows")
500        || element.is(&Ns::Table, "table-rows")
501        || element.is(&Ns::Table, "table-row-group")
502    {
503        blocks(element, path, out);
504    } else if element.is(&Ns::Table, "table-row") {
505        for (cell_index, cell) in element.elements_indexed() {
506            if cell.is(&Ns::Table, "table-cell") {
507                path.push(cell_index);
508                blocks(cell, path, out);
509                path.pop();
510            }
511        }
512    } else if element.is(&Ns::Draw, "frame") {
513        if let Some((box_index, text_box)) = element
514            .elements_indexed()
515            .find(|(_, e)| e.is(&Ns::Draw, "text-box"))
516        {
517            path.push(box_index);
518            blocks(text_box, path, out);
519            path.pop();
520        }
521    } else if element.name.ns == Ns::Draw {
522        // A drawing shape keeps its label as paragraphs of its own, where a
523        // frame keeps them in a text box.
524        blocks(element, path, out);
525    } else if is_block_container(element) {
526        blocks(element, path, out);
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::FlowModel;
533    use crate::Editing;
534    use egui_richedit::{Edit, Model, Position};
535    use odox_core::{Element, Node, Ns, Styles};
536
537    fn element(prefix: &str, local: &str, ns: Ns, children: Vec<Node>) -> Node {
538        let mut e = Element::new(prefix, local, ns);
539        e.children = children;
540        Node::Element(e)
541    }
542
543    fn p(text: &str) -> Node {
544        element("text", "p", Ns::Text, vec![Node::Text(text.to_owned())])
545    }
546
547    /// `office:text` holding a paragraph, a list of one item, a table of one
548    /// cell, and a paragraph, under a content root.
549    fn content() -> Element {
550        let list = element(
551            "text",
552            "list",
553            Ns::Text,
554            vec![element("text", "list-item", Ns::Text, vec![p("item")])],
555        );
556        let table = element(
557            "table",
558            "table",
559            Ns::Table,
560            vec![element(
561                "table",
562                "table-row",
563                Ns::Table,
564                vec![element("table", "table-cell", Ns::Table, vec![p("cell")])],
565            )],
566        );
567        let body = element(
568            "office",
569            "text",
570            Ns::Office,
571            vec![p("one"), list, table, p("two")],
572        );
573        let mut root = Element::new("office", "document-content", Ns::Office);
574        root.children = vec![element("office", "body", Ns::Office, vec![body])];
575        root
576    }
577
578    #[test]
579    fn paragraphs_follow_one_another_through_lists_and_tables() {
580        let mut content = content();
581        let mut styles = Styles::collect(Some(&content), None);
582        let mut editing = Editing::default();
583        let model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
584        let mut at = vec![0];
585        let mut seen = vec![model.text(&at).expect("the first")];
586        while let Some(next) = model.next(&at) {
587            seen.push(model.text(&next).expect("a paragraph"));
588            assert_eq!(model.previous(&next), Some(at));
589            at = next;
590        }
591        assert_eq!(seen, ["one", "item", "cell", "two"]);
592    }
593
594    #[test]
595    fn a_selection_over_a_whole_table_takes_it_and_joins_the_ends() {
596        let mut content = content();
597        let mut styles = Styles::collect(Some(&content), None);
598        let mut editing = Editing::default();
599        editing.reset();
600        let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
601        // From inside "item" to inside "two", over the table's one cell.
602        let at = model.apply(
603            Edit::Replace {
604                from: Position::new(vec![1, 0, 0], 2),
605                to: Position::new(vec![3], 1),
606                text: "X",
607            },
608            true,
609        );
610        assert_eq!(at, Some(Position::new(vec![1, 0, 0], 3)));
611        assert_eq!(model.text(&vec![1, 0, 0]).as_deref(), Some("itXwo"));
612        assert_eq!(
613            model.next(&vec![1, 0, 0]),
614            None,
615            "the table and \"two\" are gone"
616        );
617    }
618
619    #[test]
620    fn a_selection_out_of_a_cell_takes_the_text_and_leaves_the_cell() {
621        let mut content = content();
622        let mut styles = Styles::collect(Some(&content), None);
623        let mut editing = Editing::default();
624        editing.reset();
625        let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
626        // From inside "cell" to inside "two".
627        let at = model.apply(
628            Edit::Replace {
629                from: Position::new(vec![2, 0, 0, 0], 2),
630                to: Position::new(vec![3], 1),
631                text: "",
632            },
633            true,
634        );
635        assert_eq!(at, Some(Position::new(vec![2, 0, 0, 0], 2)));
636        assert_eq!(model.text(&vec![2, 0, 0, 0]).as_deref(), Some("ce"));
637        assert_eq!(model.text(&vec![3]).as_deref(), Some("wo"));
638    }
639
640    #[test]
641    fn backspace_at_a_list_item_joins_it_to_the_paragraph_before() {
642        let mut content = content();
643        let mut styles = Styles::collect(Some(&content), None);
644        let mut editing = Editing::default();
645        editing.reset();
646        let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
647        let at = model.apply(
648            Edit::Replace {
649                from: Position::new(vec![0], 3),
650                to: Position::new(vec![1, 0, 0], 0),
651                text: "",
652            },
653            true,
654        );
655        assert_eq!(at, Some(Position::new(vec![0], 3)));
656        assert_eq!(model.text(&vec![0]).as_deref(), Some("oneitem"));
657        // The list had one item, and went with it.
658        assert_eq!(model.text(&vec![1, 0, 0, 0]).as_deref(), Some("cell"));
659    }
660
661    #[test]
662    fn a_tagged_model_remembers_the_caret_under_its_tag() {
663        let mut content = content();
664        let mut styles = Styles::collect(Some(&content), None);
665        let mut editing = Editing::default();
666        editing.reset();
667        let mut model =
668            FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing).tagged(vec![7]);
669        model.apply(
670            Edit::Split {
671                at: Position::new(vec![0], 1),
672            },
673            true,
674        );
675        let current = content.clone();
676        let (_, caret) = editing.undo(&current, None).expect("a step to undo");
677        assert_eq!(caret, Some(Position::new(vec![7, 0], 1)));
678    }
679
680    #[test]
681    fn typing_that_runs_on_past_a_save_is_a_change() {
682        let mut content = content();
683        let mut styles = Styles::collect(Some(&content), None);
684        let mut editing = Editing::default();
685        editing.reset();
686        let at = |offset| Position::new(vec![0], offset);
687        let typed = |offset| Edit::Replace {
688            from: at(offset),
689            to: at(offset),
690            text: "x",
691        };
692        let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
693        model.apply(typed(0), true);
694        model.editing.mark_saved();
695        // The same run of typing, as far as the editor knows.
696        model.apply(typed(1), false);
697        assert!(
698            editing.modified(),
699            "what was typed after the save is unsaved"
700        );
701    }
702
703    #[test]
704    fn an_edit_that_begins_a_step_is_recorded_once_it_succeeds() {
705        let mut content = content();
706        let mut styles = Styles::collect(Some(&content), None);
707        let mut editing = Editing::default();
708        editing.reset();
709        let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
710        let at = model.apply(
711            Edit::Split {
712                at: Position::new(vec![0], 1),
713            },
714            true,
715        );
716        assert_eq!(at, Some(Position::new(vec![1], 0)));
717        assert_eq!(model.text(&vec![0]).as_deref(), Some("o"));
718        assert_eq!(model.text(&vec![1]).as_deref(), Some("ne"));
719
720        // Backspace at the start of a table's cell: nothing to join and
721        // nothing to remove, which is refused and is not a step.
722        let refused = model.apply(
723            Edit::Replace {
724                from: Position::new(vec![2, 0, 0], 4),
725                to: Position::new(vec![3, 0, 0, 0], 0),
726                text: "",
727            },
728            true,
729        );
730        assert_eq!(refused, None);
731        assert!(editing.can_undo());
732        let mut undone = 0;
733        let mut current = content.clone();
734        while let Some(previous) = editing.undo(&current, None).map(|(tree, _)| tree) {
735            current = previous;
736            undone += 1;
737        }
738        assert_eq!(undone, 1);
739    }
740}