1//! Deleting or typing over a selection that crosses a table's edge does what OneNote 2010 did
2//! with the same keys (`tools/native_cross_container.py`, `corpus/cross-container`): nothing
3//! joins across the edge, cells inside are emptied, and where the selection runs on past the
4//! table its rows inside go, the table with them when all do.
5
6use canvas::{document::TextPosition, editor::CanvasEditor, layout::TextEngine};
7use onestore::{
8 Arena, ExGuid, Section, Store,
9 op::{Edit, Op},
10 page::{Page, PageObject, PageParagraph, ParagraphContent},
11};
12
13const CROSS: &[u8] = include_bytes!("../../../corpus/cross-container/native/notebook/Cross.one");
14
15/// The body's paragraphs in order: a text, or a table's rows of cell texts.
16#[derive(Debug, PartialEq)]
17enum Block {
18 Text(String),
19 Table(Vec<Vec<String>>),
20}
21
22fn blocks(page: &Page) -> Vec<Block> {
23 let text = |node: &PageParagraph| node.text().unwrap().text.text().to_owned();
24 page.objects
25 .iter()
26 .find_map(|object| match object {
27 PageObject::Outline(outline) => Some(&outline.paragraphs),
28 _ => None,
29 })
30 .unwrap()
31 .iter()
32 .map(|node| match &node.content {
33 ParagraphContent::Table(table) => Block::Table(
34 table
35 .rows
36 .iter()
37 .map(|row| {
38 row.cells
39 .iter()
40 .map(|cell| cell.paragraphs.iter().map(text).collect())
41 .collect()
42 })
43 .collect(),
44 ),
45 _ => Block::Text(text(node)),
46 })
47 .collect()
48}
49
50fn at(paragraph: usize, offset: u32) -> TextPosition {
51 TextPosition { paragraph, offset }
52}
53
54#[test]
55fn deleting_across_a_tables_edge_keeps_each_side() {
56 let arena = Arena::default();
57 let mut section = Section::open(&arena, CROSS.to_vec()).unwrap();
58 let pages = section.pages().unwrap();
59 let mut engine = TextEngine::default();
60 let text = |value: &str| Block::Text(value.into());
61 let table = |rows: &[[&str; 2]]| {
62 Block::Table(
63 rows.iter()
64 .map(|row| row.iter().map(|cell| cell.to_string()).collect())
65 .collect(),
66 )
67 };
68 // Each page's selection, whether Delete is pressed, and what is typed then.
69 let cases = [
70 (
71 "Delete into a table",
72 [at(0, 6), at(1, 5)],
73 true,
74 "",
75 vec![
76 text("Before"),
77 table(&[[" one", "Alpha two"], ["Beta one", "Beta two"]]),
78 text("After text"),
79 ],
80 ),
81 (
82 "Delete out of a table",
83 [at(2, 5), at(5, 5)],
84 true,
85 "X",
86 vec![
87 text("Before text"),
88 table(&[["Alpha one", "AlphaX"]]),
89 text(" text"),
90 ],
91 ),
92 (
93 "Type over a row",
94 [at(0, 6), at(3, 4)],
95 false,
96 "Y",
97 vec![
98 text("BeforeY"),
99 table(&[["", ""], [" one", "Beta two"]]),
100 text("After text"),
101 ],
102 ),
103 (
104 "Delete a whole table",
105 [at(0, 6), at(5, 5)],
106 true,
107 "",
108 vec![text("Before"), text(" text")],
109 ),
110 ];
111 let mut expected = Vec::new();
112 for (index, (title, selection, delete, typed, blocks_after)) in cases.into_iter().enumerate() {
113 let space: ExGuid = pages.iter().find(|page| page.1 == title).unwrap().0;
114 let page = section.page(space).unwrap();
115 let body = page
116 .objects
117 .iter()
118 .find_map(|object| match object {
119 PageObject::Outline(outline) => Some(outline.id),
120 _ => None,
121 })
122 .unwrap();
123 let mut editor = CanvasEditor::from_page(page, &mut engine).unwrap();
124 editor.focus_outline(body).unwrap();
125 editor.select(selection.into()).unwrap();
126 if delete {
127 editor.delete(&mut engine, false).unwrap();
128 }
129 if !typed.is_empty() {
130 editor.insert(&mut engine, typed).unwrap();
131 }
132 assert_eq!(
133 editor.selection().positions[1].paragraph,
134 selection[0].paragraph
135 );
136 let ops = editor
137 .take_ops()
138 .unwrap()
139 .into_iter()
140 .map(|op| Op::Page { space, op })
141 .collect();
142 let at = 134_000_000_000_000_000 + index as u64 * 10_000_000;
143 section.apply("Author", &Edit { at, ops }).unwrap();
144 assert_eq!(
145 blocks(&section.page(space).unwrap()),
146 blocks_after,
147 "{title}"
148 );
149 expected.push((space, blocks_after));
150 }
151 section.seal().unwrap();
152 let written = section.image();
153 let reread = Section::open(&arena, written.clone()).unwrap();
154 for (space, blocks_after) in &expected {
155 assert_eq!(blocks(&reread.page(*space).unwrap()), *blocks_after);
156 }
157 if let Some(directory) = std::env::var_os("SNOWBOUND_CROSS_CONTAINER_EXPORT") {
158 let directory = std::path::PathBuf::from(directory);
159 std::fs::create_dir(&directory).unwrap();
160 std::fs::write(directory.join("Cross.one"), &written).unwrap();
161 let file_id = Store::parse(&written).unwrap().header.file_id;
162 std::fs::write(
163 directory.join("Open Notebook.onetoc2"),
164 onestore::create_table_of_contents("Open Notebook.onetoc2", &[("Cross.one", file_id)])
165 .unwrap(),
166 )
167 .unwrap();
168 }
169}