1use std::cell::OnceCell;
14use std::ops::Range;
15
16use eframe::egui::Id;
17use egui_richedit::{Edit, Fragment, 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::find::{Match, Replaced};
23use odox_core::edit::is_block_container;
24
25pub type PageEditor = RichEdit<Vec<usize>>;
27
28pub(crate) fn page_editor_id() -> Id {
31 Id::new("odox-page-editor")
32}
33
34pub fn page_editor() -> PageEditor {
36 RichEdit::new(page_editor_id())
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum Block {
42 Heading(u8),
45 Body,
47 Bullets,
49 Numbers,
51}
52
53pub struct FlowModel<'a> {
55 content: &'a mut Element,
56 styles: &'a mut Styles,
57 root: Vec<usize>,
59 editing: &'a mut Editing,
60 scope: Vec<usize>,
63 tag: Vec<usize>,
66 order: OnceCell<Vec<Vec<usize>>>,
69}
70
71impl<'a> FlowModel<'a> {
72 pub fn new(
75 content: &'a mut Element,
76 styles: &'a mut Styles,
77 root: Vec<usize>,
78 editing: &'a mut Editing,
79 ) -> Self {
80 Self {
81 content,
82 styles,
83 root,
84 editing,
85 scope: Vec::new(),
86 tag: Vec::new(),
87 order: OnceCell::new(),
88 }
89 }
90
91 #[must_use]
94 pub fn tagged(mut self, tag: Vec<usize>) -> Self {
95 self.tag = tag;
96 self
97 }
98
99 #[must_use]
103 pub fn within(mut self, scope: Vec<usize>) -> Self {
104 self.scope = scope;
105 self
106 }
107
108 fn root(&self) -> Option<&Element> {
109 self.content.at(&self.root)
110 }
111
112 fn paragraph_mut(&mut self, path: &[usize]) -> Option<&mut Element> {
113 self.content
114 .at_mut(&self.root)?
115 .at_mut(path)
116 .filter(|element| is_paragraph(element))
117 }
118
119 fn order(&self) -> &[Vec<usize>] {
120 self.order.get_or_init(|| {
121 let mut out = self.root().map(paragraph_paths).unwrap_or_default();
122 out.retain(|path| path.starts_with(&self.scope));
123 out
124 })
125 }
126
127 fn neighbour(&self, paragraph: &[usize], step: isize) -> Option<Vec<usize>> {
128 let order = self.order();
129 let index = order.iter().position(|p| p == paragraph)?;
130 order.get(index.checked_add_signed(step)?).cloned()
131 }
132
133 fn replace(
134 &mut self,
135 from: &Position<Vec<usize>>,
136 to: &Position<Vec<usize>>,
137 text: &str,
138 ) -> Option<()> {
139 let root = self.content.at_mut(&self.root)?;
140 match edit::replace_range(
141 root,
142 (&from.paragraph, from.offset),
143 (&to.paragraph, to.offset),
144 text,
145 ) {
146 Ok(()) => Some(()),
147 Err(Refused::Structure) => self.clear_between(from, to, text),
148 Err(_) => None,
149 }
150 }
151
152 fn clear_between(
158 &mut self,
159 from: &Position<Vec<usize>>,
160 to: &Position<Vec<usize>>,
161 text: &str,
162 ) -> Option<()> {
163 let ranges = self.covered(from, to)?;
164 if text.is_empty() && ranges.iter().all(|(_, range)| range.is_empty()) {
165 return None;
166 }
167 for (path, range) in ranges.into_iter().rev() {
168 let with = if path == from.paragraph { text } else { "" };
169 let paragraph = self.paragraph_mut(&path)?;
170 edit::replace(paragraph, range, with);
171 }
172 Some(())
173 }
174
175 fn covered(
178 &self,
179 from: &Position<Vec<usize>>,
180 to: &Position<Vec<usize>>,
181 ) -> Option<Vec<(Vec<usize>, Range<usize>)>> {
182 let order = self.order();
183 let first = order.iter().position(|p| *p == from.paragraph)?;
184 let last = order.iter().position(|p| *p == to.paragraph)?;
185 let covered: Vec<Vec<usize>> = order.get(first..=last)?.to_vec();
186 Some(
187 covered
188 .into_iter()
189 .map(|path| {
190 let len = self.text(&path).map_or(0, |t| t.chars().count());
191 let start = if path == from.paragraph {
192 from.offset
193 } else {
194 0
195 };
196 let end = if path == to.paragraph { to.offset } else { len };
197 (path, start..end.max(start))
198 })
199 .collect(),
200 )
201 }
202
203 fn selected_paragraphs(&self, selection: &Selection<Vec<usize>>) -> Option<Vec<Vec<usize>>> {
205 let order = self.order();
206 let anchor = order
207 .iter()
208 .position(|p| *p == selection.anchor.paragraph)?;
209 let focus = order.iter().position(|p| *p == selection.focus.paragraph)?;
210 Some(order[anchor.min(focus)..=anchor.max(focus)].to_vec())
211 }
212
213 pub fn is_block(&self, selection: &Selection<Vec<usize>>, block: Block) -> bool {
216 let Some(paragraphs) = self.selected_paragraphs(selection) else {
217 return false;
218 };
219 paragraphs.iter().all(|paragraph| {
220 let path = [self.root.as_slice(), paragraph].concat();
221 let (heading, list) = edit::block_state(self.content, &path, self.styles);
222 match block {
223 Block::Heading(level) => heading == Some(level),
224 Block::Body => heading.is_none(),
225 Block::Bullets => list == Some(ListKind::Bullet),
226 Block::Numbers => list == Some(ListKind::Number),
227 }
228 })
229 }
230
231 pub fn replace_matches(&mut self, matches: &[&Match], with: &str, new_step: bool) -> Replaced {
236 let mut done = Replaced::default();
237 for found in matches.iter().rev() {
238 let edit = Edit::Replace {
239 from: Position::new(found.paragraph.clone(), found.range.start),
240 to: Position::new(found.paragraph.clone(), found.range.end),
241 text: with,
242 };
243 if self.apply(edit, new_step && done.replaced == 0).is_some() {
244 done.replaced += 1;
245 } else {
246 done.skipped += 1;
247 }
248 }
249 done
250 }
251
252 pub fn apply_block(&mut self, editor: &mut PageEditor, block: Block) -> bool {
257 let Some(selection) = editor.selection().cloned() else {
258 return false;
259 };
260 let Some(paragraphs) = self.selected_paragraphs(&selection) else {
261 return false;
262 };
263 let place = |path: &Vec<usize>, order: &[Vec<usize>]| order.iter().position(|p| p == path);
264 let (Some(anchor), Some(focus)) = (
265 place(&selection.anchor.paragraph, self.order()),
266 place(&selection.focus.paragraph, self.order()),
267 ) else {
268 return false;
269 };
270 let lit = self.is_block(&selection, block);
271 let before = self.content.clone();
272 let done = match block {
273 Block::Heading(_) | Block::Body => {
274 let level = match block {
275 Block::Heading(level) if !lit => Some(level),
276 _ => None,
277 };
278 self.set_headings(¶graphs, level)
279 }
280 Block::Bullets | Block::Numbers => {
281 let kind = match block {
282 Block::Bullets => ListKind::Bullet,
283 _ => ListKind::Number,
284 };
285 self.set_lists(¶graphs, (!lit).then_some(kind))
286 }
287 };
288 if done.is_none() || *self.content == before {
289 *self.content = before;
290 return false;
291 }
292 self.order = OnceCell::new();
293 let begins = Position::new(
294 [self.tag.as_slice(), &selection.focus.paragraph].concat(),
295 selection.focus.offset,
296 );
297 self.editing.touch();
298 self.editing.record_snapshot(before, Some(begins));
299 let order = self.order().to_vec();
300 if let (Some(a), Some(f)) = (order.get(anchor), order.get(focus)) {
301 editor.select(Selection {
302 anchor: Position::new(a.clone(), selection.anchor.offset),
303 focus: Position::new(f.clone(), selection.focus.offset),
304 });
305 }
306 true
307 }
308
309 fn set_headings(&mut self, paragraphs: &[Vec<usize>], level: Option<u8>) -> Option<()> {
310 let automatic =
311 |content: &Element| content.child(&Ns::Office, "automatic-styles").is_some();
312 for paragraph in paragraphs {
313 let had = automatic(self.content);
314 let path = [self.root.as_slice(), paragraph].concat();
315 edit::set_heading(self.content, &path, level, self.styles).ok()?;
316 self.follow_styles(had);
317 }
318 Some(())
319 }
320
321 fn set_lists(&mut self, paragraphs: &[Vec<usize>], kind: Option<ListKind>) -> Option<()> {
322 let had = self
323 .content
324 .child(&Ns::Office, "automatic-styles")
325 .is_some();
326 let paths: Vec<Vec<usize>> = paragraphs
327 .iter()
328 .map(|paragraph| [self.root.as_slice(), paragraph].concat())
329 .collect();
330 edit::set_list(self.content, &paths, kind, self.styles).ok()?;
331 self.follow_styles(had);
332 Some(())
333 }
334
335 fn follow_styles(&mut self, had: bool) {
338 if !had
339 && self
340 .content
341 .child(&Ns::Office, "automatic-styles")
342 .is_some()
343 && let Some(first) = self.root.first_mut()
344 {
345 *first += 1;
346 }
347 }
348
349 fn split(&mut self, at: &Position<Vec<usize>>) -> Option<Vec<usize>> {
352 let at_end_of_heading = self
353 .root()
354 .and_then(|root| root.at(&at.paragraph))
355 .is_some_and(|paragraph| {
356 edit::heading_level(paragraph).is_some()
357 && at.offset >= edit::text(paragraph).chars().count()
358 });
359 let root = self.content.at_mut(&self.root)?;
360 let second = edit::split_at(root, &at.paragraph, at.offset).ok()?;
361 if at_end_of_heading {
362 let path = [self.root.as_slice(), &second].concat();
363 let _ = edit::set_heading(self.content, &path, None, self.styles);
366 }
367 Some(second)
368 }
369
370 fn format(
373 &mut self,
374 from: &Position<Vec<usize>>,
375 to: &Position<Vec<usize>>,
376 mark: edit::Mark,
377 on: bool,
378 ) -> Option<()> {
379 let automatic =
380 |content: &Element| content.child(&Ns::Office, "automatic-styles").is_some();
381 for (path, range) in self.covered(from, to)? {
382 if range.is_empty() {
383 continue;
384 }
385 let had = automatic(self.content);
386 let mut at = self.root.clone();
387 at.extend(&path);
388 edit::format(self.content, &at, range, mark, on, self.styles).ok()?;
389 if !had
392 && automatic(self.content)
393 && let Some(first) = self.root.first_mut()
394 {
395 *first += 1;
396 }
397 }
398 Some(())
399 }
400}
401
402fn core_mark(mark: Mark) -> edit::Mark {
404 match mark {
405 Mark::Bold => edit::Mark::Bold,
406 Mark::Italic => edit::Mark::Italic,
407 Mark::Underline => edit::Mark::Underline,
408 Mark::Strike => edit::Mark::Strike,
409 }
410}
411
412impl Model for FlowModel<'_> {
413 type Paragraph = Vec<usize>;
414
415 fn text(&self, paragraph: &Vec<usize>) -> Option<String> {
416 self.root()?
417 .at(paragraph)
418 .filter(|element| is_paragraph(element))
419 .map(edit::text)
420 }
421
422 fn next(&self, paragraph: &Vec<usize>) -> Option<Vec<usize>> {
423 self.neighbour(paragraph, 1)
424 }
425
426 fn previous(&self, paragraph: &Vec<usize>) -> Option<Vec<usize>> {
427 self.neighbour(paragraph, -1)
428 }
429
430 fn first(&self) -> Option<Vec<usize>> {
431 self.order().first().cloned()
432 }
433
434 fn last(&self) -> Option<Vec<usize>> {
435 self.order().last().cloned()
436 }
437
438 fn marked(
439 &self,
440 from: &Position<Vec<usize>>,
441 to: &Position<Vec<usize>>,
442 mark: Mark,
443 ) -> Option<bool> {
444 let root = self.root()?;
445 let mark = core_mark(mark);
446 let ranges: Vec<_> = self
447 .covered(from, to)?
448 .into_iter()
449 .filter(|(_, range)| !range.is_empty())
450 .collect();
451 if ranges.is_empty() {
452 let at = from.offset..from.offset;
453 return edit::marked(root.at(&from.paragraph)?, at, mark, self.styles);
454 }
455 let mut answer = None;
456 for (path, range) in ranges {
457 let value = edit::marked(root.at(&path)?, range, mark, self.styles)?;
458 if answer.is_some_and(|a| a != value) {
459 return None;
460 }
461 answer = Some(value);
462 }
463 answer
464 }
465
466 fn fragment(&self, from: &Position<Vec<usize>>, to: &Position<Vec<usize>>) -> Option<Fragment> {
467 let root = self.root()?;
468 let items: Vec<Element> = self
469 .covered(from, to)?
470 .into_iter()
471 .filter_map(|(path, range)| Some(edit::slice(root.at(&path)?, range)))
472 .collect();
473 (!items.is_empty()).then(|| Fragment::new(items))
474 }
475
476 fn paste_fragment(
477 &mut self,
478 at: &Position<Vec<usize>>,
479 fragment: &Fragment,
480 new_step: bool,
481 ) -> Option<Position<Vec<usize>>> {
482 let items = fragment.get::<Vec<Element>>()?;
483 let new_step = new_step || !self.editing.modified();
484 let before = new_step.then(|| self.content.clone());
485 let root = self.content.at_mut(&self.root)?;
486 let (path, offset) = edit::paste_fragment(root, &at.paragraph, at.offset, items).ok()?;
487 self.editing.touch();
488 if let Some(before) = before {
489 let begins = Position::new([self.tag.as_slice(), &at.paragraph].concat(), at.offset);
490 self.editing.record_snapshot(before, Some(begins));
491 }
492 self.order = OnceCell::new();
493 Some(Position::new(path, offset))
494 }
495
496 fn apply(
497 &mut self,
498 edit: Edit<'_, Vec<usize>>,
499 new_step: bool,
500 ) -> Option<Position<Vec<usize>>> {
501 let new_step = new_step || !self.editing.modified();
505 let before = new_step.then(|| self.content.clone());
509 let begins = match &edit {
510 Edit::Replace { from, .. } | Edit::Format { from, .. } => from,
511 Edit::Split { at } => at,
512 };
513 let begins = Position::new(
514 [self.tag.as_slice(), &begins.paragraph].concat(),
515 begins.offset,
516 );
517 let at = match edit {
518 Edit::Replace { from, to, text } => self
519 .replace(&from, &to, text)
520 .map(|()| Position::new(from.paragraph, from.offset + text.chars().count())),
521 Edit::Split { at } => self.split(&at).map(|second| Position::new(second, 0)),
522 Edit::Format { from, to, mark, on } => {
523 self.format(&from, &to, core_mark(mark), on).map(|()| to)
524 }
525 };
526 if at.is_some() {
527 self.editing.touch();
528 if let Some(before) = before {
529 self.editing.record_snapshot(before, Some(begins));
530 }
531 self.order = OnceCell::new();
532 }
533 at
534 }
535}
536
537pub(crate) fn paragraph_paths(root: &Element) -> Vec<Vec<usize>> {
540 let mut out = Vec::new();
541 blocks(root, &mut Vec::new(), &mut out);
542 out
543}
544
545fn blocks(parent: &Element, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
548 for (index, element) in parent.elements_indexed() {
549 path.push(index);
550 block(element, path, out);
551 path.pop();
552 }
553}
554
555fn block(element: &Element, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
556 if is_paragraph(element) {
557 out.push(path.clone());
558 } else if element.is(&Ns::Text, "list") {
559 for (item_index, item) in element.elements_indexed() {
560 if item.is(&Ns::Text, "list-item") || item.is(&Ns::Text, "list-header") {
561 path.push(item_index);
562 blocks(item, path, out);
563 path.pop();
564 }
565 }
566 } else if element.is(&Ns::Table, "table")
567 || element.is(&Ns::Table, "table-header-rows")
568 || element.is(&Ns::Table, "table-rows")
569 || element.is(&Ns::Table, "table-row-group")
570 {
571 blocks(element, path, out);
572 } else if element.is(&Ns::Table, "table-row") {
573 for (cell_index, cell) in element.elements_indexed() {
574 if cell.is(&Ns::Table, "table-cell") {
575 path.push(cell_index);
576 blocks(cell, path, out);
577 path.pop();
578 }
579 }
580 } else if element.is(&Ns::Draw, "frame") {
581 if let Some((box_index, text_box)) = element
582 .elements_indexed()
583 .find(|(_, e)| e.is(&Ns::Draw, "text-box"))
584 {
585 path.push(box_index);
586 blocks(text_box, path, out);
587 path.pop();
588 }
589 } else if element.name.ns == Ns::Draw {
590 blocks(element, path, out);
593 } else if is_block_container(element) {
594 blocks(element, path, out);
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::FlowModel;
601 use crate::Editing;
602 use egui_richedit::{Edit, Model, Position};
603 use odox_core::{Element, Node, Ns, Styles};
604
605 fn element(prefix: &str, local: &str, ns: Ns, children: Vec<Node>) -> Node {
606 let mut e = Element::new(prefix, local, ns);
607 e.children = children;
608 Node::Element(e)
609 }
610
611 fn p(text: &str) -> Node {
612 element("text", "p", Ns::Text, vec![Node::Text(text.to_owned())])
613 }
614
615 fn content() -> Element {
618 let list = element(
619 "text",
620 "list",
621 Ns::Text,
622 vec![element("text", "list-item", Ns::Text, vec![p("item")])],
623 );
624 let table = element(
625 "table",
626 "table",
627 Ns::Table,
628 vec![element(
629 "table",
630 "table-row",
631 Ns::Table,
632 vec![element("table", "table-cell", Ns::Table, vec![p("cell")])],
633 )],
634 );
635 let body = element(
636 "office",
637 "text",
638 Ns::Office,
639 vec![p("one"), list, table, p("two")],
640 );
641 let mut root = Element::new("office", "document-content", Ns::Office);
642 root.children = vec![element("office", "body", Ns::Office, vec![body])];
643 root
644 }
645
646 #[test]
647 fn paragraphs_follow_one_another_through_lists_and_tables() {
648 let mut content = content();
649 let mut styles = Styles::collect(Some(&content), None);
650 let mut editing = Editing::default();
651 let model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
652 let mut at = vec![0];
653 let mut seen = vec![model.text(&at).expect("the first")];
654 while let Some(next) = model.next(&at) {
655 seen.push(model.text(&next).expect("a paragraph"));
656 assert_eq!(model.previous(&next), Some(at));
657 at = next;
658 }
659 assert_eq!(seen, ["one", "item", "cell", "two"]);
660 }
661
662 #[test]
663 fn a_selection_over_a_whole_table_takes_it_and_joins_the_ends() {
664 let mut content = content();
665 let mut styles = Styles::collect(Some(&content), None);
666 let mut editing = Editing::default();
667 editing.reset();
668 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
669 let at = model.apply(
671 Edit::Replace {
672 from: Position::new(vec![1, 0, 0], 2),
673 to: Position::new(vec![3], 1),
674 text: "X",
675 },
676 true,
677 );
678 assert_eq!(at, Some(Position::new(vec![1, 0, 0], 3)));
679 assert_eq!(model.text(&vec![1, 0, 0]).as_deref(), Some("itXwo"));
680 assert_eq!(
681 model.next(&vec![1, 0, 0]),
682 None,
683 "the table and \"two\" are gone"
684 );
685 }
686
687 #[test]
688 fn a_selection_out_of_a_cell_takes_the_text_and_leaves_the_cell() {
689 let mut content = content();
690 let mut styles = Styles::collect(Some(&content), None);
691 let mut editing = Editing::default();
692 editing.reset();
693 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
694 let at = model.apply(
696 Edit::Replace {
697 from: Position::new(vec![2, 0, 0, 0], 2),
698 to: Position::new(vec![3], 1),
699 text: "",
700 },
701 true,
702 );
703 assert_eq!(at, Some(Position::new(vec![2, 0, 0, 0], 2)));
704 assert_eq!(model.text(&vec![2, 0, 0, 0]).as_deref(), Some("ce"));
705 assert_eq!(model.text(&vec![3]).as_deref(), Some("wo"));
706 }
707
708 #[test]
709 fn backspace_at_a_list_item_joins_it_to_the_paragraph_before() {
710 let mut content = content();
711 let mut styles = Styles::collect(Some(&content), None);
712 let mut editing = Editing::default();
713 editing.reset();
714 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
715 let at = model.apply(
716 Edit::Replace {
717 from: Position::new(vec![0], 3),
718 to: Position::new(vec![1, 0, 0], 0),
719 text: "",
720 },
721 true,
722 );
723 assert_eq!(at, Some(Position::new(vec![0], 3)));
724 assert_eq!(model.text(&vec![0]).as_deref(), Some("oneitem"));
725 assert_eq!(model.text(&vec![1, 0, 0, 0]).as_deref(), Some("cell"));
727 }
728
729 #[test]
730 fn a_tagged_model_remembers_the_caret_under_its_tag() {
731 let mut content = content();
732 let mut styles = Styles::collect(Some(&content), None);
733 let mut editing = Editing::default();
734 editing.reset();
735 let mut model =
736 FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing).tagged(vec![7]);
737 model.apply(
738 Edit::Split {
739 at: Position::new(vec![0], 1),
740 },
741 true,
742 );
743 let current = content.clone();
744 let (_, caret) = editing.undo(¤t, None).expect("a step to undo");
745 assert_eq!(caret, Some(Position::new(vec![7, 0], 1)));
746 }
747
748 fn heading_document() -> Element {
749 odox_core::xml::parse(
750 br#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"><office:body><office:text><text:h text:outline-level="1" text:style-name="H">Title</text:h><text:p text:style-name="Body">a</text:p><text:p text:style-name="Body">b</text:p></office:text></office:body></office:document-content>"#,
751 "test",
752 )
753 .expect("a document")
754 }
755
756 #[test]
757 fn enter_at_the_end_of_a_heading_starts_body_text() {
758 let mut content = heading_document();
759 let mut styles = Styles::collect(Some(&content), None);
760 let mut editing = Editing::default();
761 editing.reset();
762 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
763 let at = model.apply(
764 Edit::Split {
765 at: Position::new(vec![0], 5),
766 },
767 true,
768 );
769 assert_eq!(at, Some(Position::new(vec![1], 0)));
770 let second = content.at(&[0, 0, 1]).expect("the new paragraph");
771 assert!(second.is(&Ns::Text, "p"));
772 assert_eq!(second.attr(&Ns::Text, "style-name"), Some("Body"));
773 assert!(
774 content
775 .at(&[0, 0, 0])
776 .expect("the heading")
777 .is(&Ns::Text, "h")
778 );
779 }
780
781 #[test]
782 fn replacing_every_match_is_one_step_and_the_offsets_hold() {
783 let mut content = heading_document();
784 let mut styles = Styles::collect(Some(&content), None);
785 let mut editing = Editing::default();
786 editing.reset();
787 let matches = crate::find::in_paragraphs(content.at(&[0, 0]).expect("the body"), 0, "t");
788 assert_eq!(matches.len(), 2, "the two t of Title");
789 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
790 let done = model.replace_matches(&matches.iter().collect::<Vec<_>>(), "TT", true);
791 assert_eq!(done.replaced, 2);
792 assert_eq!(model.text(&vec![0]).as_deref(), Some("TTiTTle"));
793 let mut undone = 0;
794 let mut current = content.clone();
795 while let Some((previous, _)) = editing.undo(¤t, None) {
796 current = previous;
797 undone += 1;
798 }
799 assert_eq!(undone, 1);
800 }
801
802 #[test]
803 fn a_copy_keeps_spans_and_a_paste_puts_them_back_in_one_step() {
804 let mut content = odox_core::xml::parse(
805 br#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"><office:body><office:text><text:p>one <text:span text:style-name="T1">two</text:span> three</text:p><text:p>x</text:p></office:text></office:body></office:document-content>"#,
806 "test",
807 )
808 .expect("a document");
809 let mut styles = Styles::collect(Some(&content), None);
810 let mut editing = Editing::default();
811 editing.reset();
812 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
813 let fragment = model
814 .fragment(&Position::new(vec![0], 3), &Position::new(vec![0], 7))
815 .expect("a copy");
816 let after = model
817 .paste_fragment(&Position::new(vec![1], 1), &fragment, true)
818 .expect("it goes in");
819 assert_eq!(after, Position::new(vec![1], 5));
820 let written = String::from_utf8(odox_core::xml::serialize(&content)).expect("UTF-8");
821 assert!(
822 written.contains(
823 r#"<text:p>x<text:s/><text:span text:style-name="T1">two</text:span></text:p>"#
824 ),
825 "{written}"
826 );
827 assert_eq!(editing.undo(&content.clone(), None).map(|_| ()), Some(()));
828 assert!(!editing.can_undo(), "one step");
829 }
830
831 #[test]
832 fn enter_in_the_middle_of_a_heading_leaves_two_headings() {
833 let mut content = heading_document();
834 let mut styles = Styles::collect(Some(&content), None);
835 let mut editing = Editing::default();
836 editing.reset();
837 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
838 model.apply(
839 Edit::Split {
840 at: Position::new(vec![0], 2),
841 },
842 true,
843 );
844 assert!(
845 content
846 .at(&[0, 0, 1])
847 .expect("the second half")
848 .is(&Ns::Text, "h")
849 );
850 }
851
852 #[test]
853 fn typing_that_runs_on_past_a_save_is_a_change() {
854 let mut content = content();
855 let mut styles = Styles::collect(Some(&content), None);
856 let mut editing = Editing::default();
857 editing.reset();
858 let at = |offset| Position::new(vec![0], offset);
859 let typed = |offset| Edit::Replace {
860 from: at(offset),
861 to: at(offset),
862 text: "x",
863 };
864 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
865 model.apply(typed(0), true);
866 model.editing.mark_saved();
867 model.apply(typed(1), false);
869 assert!(
870 editing.modified(),
871 "what was typed after the save is unsaved"
872 );
873 }
874
875 #[test]
876 fn an_edit_that_begins_a_step_is_recorded_once_it_succeeds() {
877 let mut content = content();
878 let mut styles = Styles::collect(Some(&content), None);
879 let mut editing = Editing::default();
880 editing.reset();
881 let mut model = FlowModel::new(&mut content, &mut styles, vec![0, 0], &mut editing);
882 let at = model.apply(
883 Edit::Split {
884 at: Position::new(vec![0], 1),
885 },
886 true,
887 );
888 assert_eq!(at, Some(Position::new(vec![1], 0)));
889 assert_eq!(model.text(&vec![0]).as_deref(), Some("o"));
890 assert_eq!(model.text(&vec![1]).as_deref(), Some("ne"));
891
892 let refused = model.apply(
895 Edit::Replace {
896 from: Position::new(vec![2, 0, 0], 4),
897 to: Position::new(vec![3, 0, 0, 0], 0),
898 text: "",
899 },
900 true,
901 );
902 assert_eq!(refused, None);
903 assert!(editing.can_undo());
904 let mut undone = 0;
905 let mut current = content.clone();
906 while let Some(previous) = editing.undo(¤t, None).map(|(tree, _)| tree) {
907 current = previous;
908 undone += 1;
909 }
910 assert_eq!(undone, 1);
911 }
912}