1use super::model::{Row, Table};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Cell {
23 pub row: usize,
25 pub column: usize,
28}
29
30pub fn column_at(line: &str, col: usize) -> usize {
38 let mut seen = 0usize;
39 let mut escaped = false;
40 for (i, ch) in line.char_indices() {
41 if i >= col {
42 break;
43 }
44 if escaped {
45 escaped = false;
46 continue;
47 }
48 match ch {
49 '\\' => escaped = true,
50 '|' => seen += 1,
51 _ => {}
52 }
53 }
54 seen.saturating_sub(1)
57}
58
59pub fn offset_of_column(line: &str, column: usize) -> usize {
66 let mut seen = 0usize;
67 let mut escaped = false;
68 for (i, ch) in line.char_indices() {
69 if escaped {
70 escaped = false;
71 continue;
72 }
73 match ch {
74 '\\' => escaped = true,
75 '|' => {
76 if seen == column + 1 {
77 return i;
78 }
79 seen += 1;
80 if seen == column + 1 {
81 let start = i + 1;
84 return if line[start..].starts_with(' ') {
85 start + 1
86 } else {
87 start
88 };
89 }
90 }
91 _ => {}
92 }
93 }
94 line.len()
95}
96
97impl Table {
98 fn cells_at(&self, row: usize) -> &[String] {
100 match self.rows.get(row) {
101 Some(Row::Cells(c)) => c,
102 _ => &[],
103 }
104 }
105
106 fn is_rule(&self, row: usize) -> bool {
108 matches!(self.rows.get(row), Some(Row::Separator { .. }))
109 }
110
111 pub fn next_cell(&self, at: Cell) -> Option<Cell> {
119 let width = self.cells_at(at.row).len();
120 if at.column + 1 < width {
121 return Some(Cell {
122 row: at.row,
123 column: at.column + 1,
124 });
125 }
126 let mut row = at.row + 1;
127 while row < self.rows.len() {
128 if !self.is_rule(row) && !self.cells_at(row).is_empty() {
129 return Some(Cell { row, column: 0 });
130 }
131 row += 1;
132 }
133 None
134 }
135
136 pub fn prev_cell(&self, at: Cell) -> Option<Cell> {
138 if at.column > 0 {
139 return Some(Cell {
140 row: at.row,
141 column: at.column - 1,
142 });
143 }
144 let mut row = at.row;
145 while row > 0 {
146 row -= 1;
147 if !self.is_rule(row) {
148 let width = self.cells_at(row).len();
149 if width > 0 {
150 return Some(Cell {
151 row,
152 column: width - 1,
153 });
154 }
155 }
156 }
157 None
158 }
159
160 pub fn move_row(&self, at: Cell, delta: isize) -> Option<(Table, Cell)> {
167 if self.is_rule(at.row) {
168 return None;
169 }
170 let mut target = at.row as isize;
171 loop {
172 target += delta;
173 if target < 0 || target as usize >= self.rows.len() {
174 return None;
175 }
176 if !self.is_rule(target as usize) {
177 break;
178 }
179 }
180 let target = target as usize;
181 let mut next = self.clone();
182 next.rows.swap(at.row, target);
183 Some((
184 next,
185 Cell {
186 row: target,
187 column: at.column,
188 },
189 ))
190 }
191
192 pub fn move_column(&self, at: Cell, delta: isize) -> Option<(Table, Cell)> {
194 let columns = self.columns();
195 let target = at.column as isize + delta;
196 if target < 0 || target as usize >= columns {
197 return None;
198 }
199 let target = target as usize;
200 let mut next = self.clone();
201 for row in &mut next.rows {
202 match row {
203 Row::Cells(cells) => {
204 if cells.len() < columns {
208 cells.resize(columns, String::new());
209 }
210 cells.swap(at.column, target);
211 }
212 Row::Separator { aligns, .. } => {
213 if aligns.len() >= columns.max(1) && target < aligns.len() {
214 aligns.swap(at.column, target);
215 }
216 }
217 }
218 }
219 Some((
220 next,
221 Cell {
222 row: at.row,
223 column: target,
224 },
225 ))
226 }
227
228 pub fn insert_row(&self, at: Cell) -> (Table, Cell) {
234 let columns = self.columns().max(1);
235 let mut next = self.clone();
236 let row = (at.row + 1).min(next.rows.len());
237 next.rows
238 .insert(row, Row::Cells(vec![String::new(); columns]));
239 (next, Cell { row, column: 0 })
240 }
241
242 pub fn insert_column(&self, at: Cell) -> (Table, Cell) {
244 let columns = self.columns();
245 let target = (at.column + 1).min(columns);
246 let mut next = self.clone();
247 for row in &mut next.rows {
248 match row {
249 Row::Cells(cells) => {
250 if cells.len() < columns {
251 cells.resize(columns, String::new());
252 }
253 cells.insert(target.min(cells.len()), String::new());
254 }
255 Row::Separator { aligns, .. } => {
256 if target <= aligns.len() {
257 aligns.insert(target, super::layout::Align::Left);
258 }
259 }
260 }
261 }
262 (
263 next,
264 Cell {
265 row: at.row,
266 column: target,
267 },
268 )
269 }
270
271 pub fn delete_row(&self, at: Cell) -> Option<(Table, Cell)> {
277 let content = self
278 .rows
279 .iter()
280 .filter(|r| matches!(r, Row::Cells(_)))
281 .count();
282 if content <= 1 && !self.is_rule(at.row) {
283 return None;
284 }
285 if at.row >= self.rows.len() {
286 return None;
287 }
288 let mut next = self.clone();
289 next.rows.remove(at.row);
290 let row = at.row.min(next.rows.len().saturating_sub(1));
291 Some((
292 next,
293 Cell {
294 row,
295 column: at.column,
296 },
297 ))
298 }
299
300 pub fn delete_column(&self, at: Cell) -> Option<(Table, Cell)> {
305 let columns = self.columns();
306 if columns <= 1 || at.column >= columns {
307 return None;
308 }
309 let mut next = self.clone();
310 for row in &mut next.rows {
311 match row {
312 Row::Cells(cells) => {
313 if at.column < cells.len() {
314 cells.remove(at.column);
315 }
316 }
317 Row::Separator { aligns, .. } => {
318 if at.column < aligns.len() {
319 aligns.remove(at.column);
320 }
321 }
322 }
323 }
324 Some((
325 next,
326 Cell {
327 row: at.row,
328 column: at.column.min(columns - 2),
329 },
330 ))
331 }
332
333 pub fn insert_rule(&self, at: Cell, fallback: char) -> (Table, Cell) {
341 let join = self.rule_join().unwrap_or(fallback);
342 let aligns = self.alignments_or_default();
343 let mut next = self.clone();
344 let row = (at.row + 1).min(next.rows.len());
345 next.rows.insert(row, Row::Separator { join, aligns });
346 let landing = if row + 1 < next.rows.len() {
350 row + 1
351 } else {
352 at.row
353 };
354 (
355 next,
356 Cell {
357 row: landing,
358 column: at.column,
359 },
360 )
361 }
362
363 fn rule_join(&self) -> Option<char> {
365 self.rows.iter().find_map(|r| match r {
366 Row::Separator { join, .. } => Some(*join),
367 Row::Cells(_) => None,
368 })
369 }
370
371 fn alignments_or_default(&self) -> Vec<super::layout::Align> {
372 self.rows
373 .iter()
374 .find_map(|r| match r {
375 Row::Separator { aligns, .. } => Some(aligns.clone()),
376 Row::Cells(_) => None,
377 })
378 .unwrap_or_else(|| vec![super::layout::Align::Left; self.columns()])
379 }
380
381 fn section(&self, at: usize) -> Option<(usize, usize)> {
390 if self.is_rule(at) {
391 return None;
392 }
393 let mut first = at;
394 while first > 0 && !self.is_rule(first - 1) {
395 first -= 1;
396 }
397 let mut last = at;
398 while last + 1 < self.rows.len() && !self.is_rule(last + 1) {
399 last += 1;
400 }
401 Some((first, last))
402 }
403
404 pub fn sort_section(&self, at: Cell, descending: bool) -> Option<(Table, Cell)> {
416 let (first, last) = self.section(at.row)?;
417 if last <= first {
418 return None;
419 }
420 let value = |row: usize| -> String {
421 self.cells_at(row)
422 .get(at.column)
423 .cloned()
424 .unwrap_or_default()
425 };
426 let numeric = (first..=last)
427 .map(value)
428 .filter(|v| !v.trim().is_empty())
429 .all(|v| v.trim().parse::<f64>().is_ok());
430
431 let mut order: Vec<usize> = (first..=last).collect();
432 order.sort_by(|&a, &b| {
433 let (va, vb) = (value(a), value(b));
434 let (ea, eb) = (va.trim().is_empty(), vb.trim().is_empty());
435 match (ea, eb) {
438 (true, true) => return std::cmp::Ordering::Equal,
439 (true, false) => return std::cmp::Ordering::Greater,
440 (false, true) => return std::cmp::Ordering::Less,
441 (false, false) => {}
442 }
443 let ord = if numeric {
444 let na = va.trim().parse::<f64>().unwrap_or(0.0);
445 let nb = vb.trim().parse::<f64>().unwrap_or(0.0);
446 na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal)
447 } else {
448 va.to_lowercase().cmp(&vb.to_lowercase())
449 };
450 if descending { ord.reverse() } else { ord }
451 });
452
453 let mut next = self.clone();
454 let sorted: Vec<Row> = order.iter().map(|&i| self.rows[i].clone()).collect();
455 next.rows.splice(first..=last, sorted);
456 let landed = order
459 .iter()
460 .position(|&i| i == at.row)
461 .map(|p| first + p)
462 .unwrap_or(at.row);
463 Some((
464 next,
465 Cell {
466 row: landed,
467 column: at.column,
468 },
469 ))
470 }
471
472 pub fn blank_cell(&self, at: Cell) -> Option<(Table, Cell)> {
474 let mut next = self.clone();
475 match next.rows.get_mut(at.row)? {
476 Row::Cells(cells) => {
477 let slot = cells.get_mut(at.column)?;
478 if slot.is_empty() {
479 return None;
480 }
481 slot.clear();
482 }
483 Row::Separator { .. } => return None,
484 }
485 Some((next, at))
486 }
487
488 pub fn copy_down(&self, at: Cell) -> Option<(Table, Cell)> {
500 let source = self.cells_at(at.row).get(at.column)?.clone();
501 let mut next = self.clone();
502 let mut target = at.row + 1;
503 while target < next.rows.len() && next.is_rule(target) {
506 target += 1;
507 }
508 if target >= next.rows.len() {
509 let columns = next.columns().max(at.column + 1);
510 next.rows.push(Row::Cells(vec![String::new(); columns]));
511 target = next.rows.len() - 1;
512 }
513 let columns = next.columns().max(at.column + 1);
514 match next.rows.get_mut(target)? {
515 Row::Cells(cells) => {
516 if cells.len() < columns {
517 cells.resize(columns, String::new());
518 }
519 cells[at.column] = increment_trailing_number(&source);
520 }
521 Row::Separator { .. } => return None,
522 }
523 Some((
524 next,
525 Cell {
526 row: target,
527 column: at.column,
528 },
529 ))
530 }
531
532 pub fn down_cell(&self, at: Cell) -> Option<Cell> {
539 let mut row = at.row + 1;
540 while row < self.rows.len() {
541 if !self.is_rule(row) {
542 return Some(Cell {
543 row,
544 column: at.column,
545 });
546 }
547 row += 1;
548 }
549 None
550 }
551
552 pub fn transpose(&self, at: Cell) -> Option<(Table, Cell)> {
560 let content: Vec<&[String]> = self
561 .rows
562 .iter()
563 .filter_map(|r| match r {
564 Row::Cells(c) => Some(c.as_slice()),
565 Row::Separator { .. } => None,
566 })
567 .collect();
568 if content.is_empty() {
569 return None;
570 }
571 let columns = content.iter().map(|r| r.len()).max().unwrap_or(0);
572 if columns == 0 {
573 return None;
574 }
575 let rows: Vec<Row> = (0..columns)
576 .map(|c| {
577 Row::Cells(
578 content
579 .iter()
580 .map(|r| r.get(c).cloned().unwrap_or_default())
581 .collect(),
582 )
583 })
584 .collect();
585 let content_index = self.rows[..at.row.min(self.rows.len())]
589 .iter()
590 .filter(|r| matches!(r, Row::Cells(_)))
591 .count();
592 Some((
593 Table {
594 first: self.first,
595 last: self.last,
596 rows,
597 indent: self.indent.clone(),
598 },
599 Cell {
600 row: at.column.min(columns.saturating_sub(1)),
601 column: content_index,
602 },
603 ))
604 }
605}
606
607fn increment_trailing_number(value: &str) -> String {
614 let digits_start = value
615 .char_indices()
616 .rev()
617 .take_while(|(_, c)| c.is_ascii_digit())
618 .map(|(i, _)| i)
619 .last();
620 let Some(start) = digits_start else {
621 return value.to_string();
622 };
623 let Ok(n) = value[start..].parse::<u64>() else {
624 return value.to_string();
625 };
626 let width = value.len() - start;
629 format!("{}{:0width$}", &value[..start], n + 1, width = width)
630}
631
632#[cfg(test)]
633mod tests {
634 #![allow(clippy::unwrap_used, clippy::panic)]
635 use super::*;
636
637 fn table(src: &str) -> Table {
638 let lines: Vec<String> = src.lines().map(str::to_string).collect();
639 let n = lines.len() as u32;
640 Table::at(|i| lines.get(i as usize).cloned(), 0, n).expect("a table")
641 }
642
643 fn ruled() -> Table {
645 table("| a | b |\n|---|---|\n| c | d |\n")
646 }
647
648 fn shape(t: &Table) -> Vec<String> {
649 t.render()
650 }
651
652 #[test]
655 fn the_caret_column_counts_pipes_to_its_left() {
656 let line = "| a | b | c |";
657 assert_eq!(column_at(line, 2), 0);
658 assert_eq!(column_at(line, 6), 1);
659 assert_eq!(column_at(line, 10), 2);
660 }
661
662 #[test]
665 fn an_escaped_pipe_is_not_a_cell_boundary() {
666 let line = r"| a \| b | c |";
667 assert_eq!(column_at(line, 8), 0, "still inside the first cell");
668 }
669
670 #[test]
673 fn a_caret_before_the_first_pipe_is_in_cell_zero() {
674 assert_eq!(column_at(" | a | b |", 0), 0);
675 }
676
677 #[test]
680 fn the_offset_of_a_column_lands_on_its_text() {
681 let line = "| aa | bb | cc |";
682 for c in 0..3 {
683 let at = offset_of_column(line, c);
684 assert_eq!(
685 &line[at..at + 2],
686 ["aa", "bb", "cc"][c],
687 "column {c} in {line:?}"
688 );
689 }
690 }
691
692 #[test]
695 fn tab_walks_the_row_then_wraps_to_the_next() {
696 let t = ruled();
697 let at = Cell { row: 0, column: 0 };
698 let next = t.next_cell(at).unwrap();
699 assert_eq!(next, Cell { row: 0, column: 1 });
700 assert_eq!(t.next_cell(next).unwrap(), Cell { row: 2, column: 0 });
703 }
704
705 #[test]
706 fn shift_tab_walks_backwards_and_wraps_to_the_end_of_the_row_above() {
707 let t = ruled();
708 let at = Cell { row: 2, column: 0 };
709 assert_eq!(t.prev_cell(at).unwrap(), Cell { row: 0, column: 1 });
710 }
711
712 #[test]
717 fn the_last_cell_has_no_next() {
718 let t = ruled();
719 assert!(t.next_cell(Cell { row: 2, column: 1 }).is_none());
720 assert!(t.prev_cell(Cell { row: 0, column: 0 }).is_none());
721 }
722
723 #[test]
726 fn a_row_moves_and_the_caret_follows_it() {
727 let t = table("| a |\n| b |\n");
728 let (moved, cell) = t.move_row(Cell { row: 0, column: 0 }, 1).unwrap();
729 assert_eq!(cell.row, 1, "the caret rides the row it moved");
730 assert!(shape(&moved)[0].contains('b'));
731 assert!(shape(&moved)[1].contains('a'));
732 }
733
734 #[test]
738 fn moving_a_row_skips_over_a_rule() {
739 let t = ruled();
740 let (moved, cell) = t.move_row(Cell { row: 0, column: 0 }, 1).unwrap();
741 assert_eq!(cell.row, 2);
742 assert!(
743 matches!(moved.rows[1], Row::Separator { .. }),
744 "the rule stayed put: {:?}",
745 moved.rows
746 );
747 }
748
749 #[test]
750 fn a_row_at_the_edge_does_not_move() {
751 let t = table("| a |\n| b |\n");
752 assert!(t.move_row(Cell { row: 0, column: 0 }, -1).is_none());
753 assert!(t.move_row(Cell { row: 1, column: 0 }, 1).is_none());
754 }
755
756 #[test]
757 fn inserting_a_row_puts_it_below_and_takes_the_caret_there() {
758 let t = ruled();
759 let (next, cell) = t.insert_row(Cell { row: 2, column: 1 });
760 assert_eq!(next.rows.len(), 4);
761 assert_eq!(cell, Cell { row: 3, column: 0 });
762 assert_eq!(next.columns(), 2, "the new row is full width");
763 }
764
765 #[test]
766 fn deleting_a_row_removes_it() {
767 let t = ruled();
768 let (next, _) = t.delete_row(Cell { row: 2, column: 0 }).unwrap();
769 assert_eq!(next.rows.len(), 2);
770 }
771
772 #[test]
775 fn the_last_content_row_is_kept() {
776 let t = table("| a |\n");
777 assert!(t.delete_row(Cell { row: 0, column: 0 }).is_none());
778 }
779
780 #[test]
783 fn a_column_moves_in_every_row_at_once() {
784 let t = ruled();
785 let (moved, cell) = t.move_column(Cell { row: 0, column: 0 }, 1).unwrap();
786 assert_eq!(cell.column, 1);
787 let out = shape(&moved);
788 assert!(out[0].starts_with("| b"), "{out:?}");
789 assert!(out[2].starts_with("| d"), "…and the row below too: {out:?}");
790 }
791
792 #[test]
796 fn moving_a_column_pads_a_short_row_instead_of_refusing() {
797 let t = table("| a | b |\n| c |\n");
798 let (moved, _) = t.move_column(Cell { row: 0, column: 0 }, 1).unwrap();
799 let out = shape(&moved);
800 assert_eq!(out.len(), 2);
801 assert!(
802 out[1].contains('c'),
803 "the short row kept its content: {out:?}"
804 );
805 }
806
807 #[test]
808 fn a_column_at_the_edge_does_not_move() {
809 let t = ruled();
810 assert!(t.move_column(Cell { row: 0, column: 0 }, -1).is_none());
811 assert!(t.move_column(Cell { row: 0, column: 1 }, 1).is_none());
812 }
813
814 #[test]
815 fn inserting_a_column_widens_every_row_including_the_rule() {
816 let t = ruled();
817 let (next, cell) = t.insert_column(Cell { row: 0, column: 0 });
818 assert_eq!(cell.column, 1);
819 assert_eq!(next.columns(), 3);
820 let rule = &shape(&next)[1];
821 assert_eq!(
822 rule.matches('|').count(),
823 4,
824 "the rule grew a column too: {rule}"
825 );
826 }
827
828 #[test]
829 fn deleting_a_column_narrows_every_row() {
830 let t = ruled();
831 let (next, _) = t.delete_column(Cell { row: 0, column: 0 }).unwrap();
832 assert_eq!(next.columns(), 1);
833 assert!(shape(&next)[0].contains('b'));
834 }
835
836 #[test]
838 fn the_last_column_is_kept() {
839 let t = table("| a |\n|---|\n");
840 assert!(t.delete_column(Cell { row: 0, column: 0 }).is_none());
841 }
842
843 #[test]
849 fn a_new_rule_copies_the_style_the_table_already_uses() {
850 let t = table("| a | b |\n|--+--|\n| c | d |\n");
851 let (next, _) = t.insert_rule(Cell { row: 2, column: 0 }, '|');
852 let out = shape(&next);
853 let rules: Vec<&String> = out.iter().filter(|l| l.contains('-')).collect();
854 assert_eq!(rules.len(), 2);
855 assert!(
856 rules.iter().all(|l| l.contains('+')),
857 "both rules are org's: {rules:?}"
858 );
859 }
860
861 #[test]
864 fn the_fallback_style_is_used_only_when_there_is_no_rule() {
865 let t = table("| a | b |\n| c | d |\n");
866 let (org, _) = t.insert_rule(Cell { row: 0, column: 0 }, '+');
867 assert!(shape(&org)[1].contains('+'), "{:?}", shape(&org));
868 let (md, _) = t.insert_rule(Cell { row: 0, column: 0 }, '|');
869 assert!(!shape(&md)[1].contains('+'), "{:?}", shape(&md));
870 }
871
872 #[test]
876 fn the_caret_moves_off_the_new_rule() {
877 let t = table("| a |\n| b |\n");
878 let (next, cell) = t.insert_rule(Cell { row: 0, column: 0 }, '|');
879 assert!(!next.is_rule(cell.row), "landed on a rule: {:?}", next.rows);
880 }
881
882 #[test]
888 fn a_numeric_column_sorts_numerically() {
889 let t = table("| 9 |\n| 10 |\n| 2 |\n");
890 let (next, _) = t.sort_section(Cell { row: 0, column: 0 }, false).unwrap();
891 let got: Vec<String> = shape(&next)
892 .iter()
893 .map(|l| l.trim_matches(['|', ' ']).to_string())
894 .collect();
895 assert_eq!(got, vec!["2", "9", "10"]);
896 }
897
898 #[test]
899 fn a_text_column_sorts_case_insensitively() {
900 let t = table("| beta |\n| Alpha |\n| gamma |\n");
901 let (next, _) = t.sort_section(Cell { row: 0, column: 0 }, false).unwrap();
902 let got: Vec<String> = shape(&next)
903 .iter()
904 .map(|l| l.trim_matches(['|', ' ']).to_string())
905 .collect();
906 assert_eq!(got, vec!["Alpha", "beta", "gamma"]);
907 }
908
909 #[test]
913 fn sorting_stays_inside_its_section() {
914 let t = table("| header |\n|---|\n| b |\n| a |\n");
915 let (next, _) = t.sort_section(Cell { row: 2, column: 0 }, false).unwrap();
916 let out = shape(&next);
917 assert!(out[0].contains("header"), "the header stayed put: {out:?}");
918 assert!(out[2].contains('a') && out[3].contains('b'), "{out:?}");
919 }
920
921 #[test]
925 fn empty_cells_sort_last_whichever_way_you_sort() {
926 let t = table("| b |\n| |\n| a |\n");
927 for descending in [false, true] {
928 let (next, _) = t
929 .sort_section(Cell { row: 0, column: 0 }, descending)
930 .unwrap();
931 let out = shape(&next);
932 assert!(
933 out[2].trim_matches(['|', ' ']).is_empty(),
934 "descending={descending}: {out:?}"
935 );
936 }
937 }
938
939 #[test]
941 fn the_caret_follows_the_row_it_was_on() {
942 let t = table("| 3 |\n| 1 |\n| 2 |\n");
943 let (_, cell) = t.sort_section(Cell { row: 0, column: 0 }, false).unwrap();
944 assert_eq!(cell.row, 2, "the `3` row is last now, and so is the caret");
945 }
946
947 #[test]
948 fn a_one_row_section_has_nothing_to_sort() {
949 let t = table("| a |\n");
950 assert!(t.sort_section(Cell { row: 0, column: 0 }, false).is_none());
951 }
952
953 #[test]
956 fn blanking_empties_the_cell_and_leaves_the_shape() {
957 let t = table("| a | b |\n");
958 let (next, _) = t.blank_cell(Cell { row: 0, column: 0 }).unwrap();
959 assert_eq!(next.columns(), 2, "still two columns");
960 assert!(shape(&next)[0].contains('b'));
961 assert!(!shape(&next)[0].contains('a'));
962 assert!(next.blank_cell(Cell { row: 0, column: 0 }).is_none());
965 }
966
967 #[test]
970 fn copy_down_increments_a_trailing_number() {
971 let t = table("| Q3 | x |\n| | |\n");
972 let (next, cell) = t.copy_down(Cell { row: 0, column: 0 }).unwrap();
973 assert_eq!(cell.row, 1, "the caret moved down with it");
974 assert!(shape(&next)[1].contains("Q4"), "{:?}", shape(&next));
975 }
976
977 #[test]
978 fn copy_down_carries_a_plain_label_unchanged() {
979 let t = table("| total |\n| |\n");
980 let (next, _) = t.copy_down(Cell { row: 0, column: 0 }).unwrap();
981 assert!(shape(&next)[1].contains("total"), "{:?}", shape(&next));
982 }
983
984 #[test]
987 fn copy_down_creates_the_row_it_needs() {
988 let t = table("| 1 |\n");
989 let (next, cell) = t.copy_down(Cell { row: 0, column: 0 }).unwrap();
990 assert_eq!(next.rows.len(), 2);
991 assert_eq!(cell.row, 1);
992 assert!(shape(&next)[1].contains('2'));
993 }
994
995 #[test]
998 fn an_incremented_number_keeps_its_width() {
999 assert_eq!(increment_trailing_number("09"), "10");
1000 assert_eq!(increment_trailing_number("item-9"), "item-10");
1001 assert_eq!(increment_trailing_number("total"), "total");
1002 assert_eq!(increment_trailing_number(""), "");
1003 }
1004
1005 #[test]
1008 fn down_cell_keeps_the_column_and_skips_a_rule() {
1009 let t = ruled();
1010 assert_eq!(
1011 t.down_cell(Cell { row: 0, column: 1 }).unwrap(),
1012 Cell { row: 2, column: 1 }
1013 );
1014 assert!(t.down_cell(Cell { row: 2, column: 1 }).is_none());
1015 }
1016
1017 #[test]
1018 fn transpose_swaps_rows_and_columns() {
1019 let t = table("| a | b | c |\n| 1 | 2 | 3 |\n");
1020 let (next, _) = t.transpose(Cell { row: 0, column: 0 }).unwrap();
1021 let out = shape(&next);
1022 assert_eq!(out.len(), 3, "three rows now: {out:?}");
1023 assert!(out[0].contains('a') && out[0].contains('1'), "{out:?}");
1024 assert!(out[2].contains('c') && out[2].contains('3'), "{out:?}");
1025 }
1026
1027 #[test]
1030 fn the_caret_transposes_too() {
1031 let t = table("| a | b | c |\n| 1 | 2 | 3 |\n");
1032 let (_, cell) = t.transpose(Cell { row: 1, column: 2 }).unwrap();
1033 assert_eq!(cell, Cell { row: 2, column: 1 }, "(1,2) becomes (2,1)");
1034 }
1035
1036 #[test]
1040 fn transpose_drops_the_rules() {
1041 let t = table("| a | b |\n|---|---|\n| 1 | 2 |\n");
1042 let (next, _) = t.transpose(Cell { row: 0, column: 0 }).unwrap();
1043 assert!(
1044 !next.rows.iter().any(|r| matches!(r, Row::Separator { .. })),
1045 "{:?}",
1046 next.rows
1047 );
1048 }
1049
1050 #[test]
1054 fn structure_operations_preserve_the_dialect() {
1055 let t = table("| a | b |\n|--+--|\n| c | d |\n");
1056 let (next, _) = t.insert_column(Cell { row: 0, column: 0 });
1057 assert!(
1058 shape(&next)[1].contains('+'),
1059 "org's join survives a column insert: {:?}",
1060 shape(&next)
1061 );
1062 }
1063}