mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 12:27:11 -06:00
Merge PR #4162: Tables: Split Table fixes the vertical merges and row content controls it splits
This commit is contained in:
@@ -165,6 +165,54 @@ impl Table {
|
||||
self.rows.remove(r);
|
||||
}
|
||||
|
||||
/// Split the table before row `at`: the rows from `at` on move to the returned table, which
|
||||
/// has this table's properties and grid. A vertical merge that the split cuts ends in this
|
||||
/// table's last row and starts again in the new table's first row. A row content control
|
||||
/// around rows on both sides closes after this table's last row and opens again (without its
|
||||
/// `w:id`) before the new table's first row. Content controls that close after this table
|
||||
/// close after the new one.
|
||||
pub fn split_off(&mut self, at: usize) -> Table {
|
||||
let at = at.min(self.rows.len());
|
||||
let mut open = Vec::new();
|
||||
for row in self.rows.iter().take(at) {
|
||||
open.extend(row.controls.open.iter().cloned());
|
||||
let close = usize::try_from(row.controls.close).unwrap_or(usize::MAX).min(open.len());
|
||||
open.truncate(open.len() - close);
|
||||
}
|
||||
let mut tail = Table {
|
||||
props: self.props.clone(),
|
||||
grid: self.grid.clone(),
|
||||
rows: self.rows.split_off(at),
|
||||
controls: ControlWrap { open: Vec::new(), close: std::mem::take(&mut self.controls.close) },
|
||||
};
|
||||
if let Some(last) = self.rows.last_mut() {
|
||||
last.controls.close = last.controls.close.saturating_add(u32::try_from(open.len()).unwrap_or(u32::MAX));
|
||||
for c in &mut last.cells {
|
||||
if c.props.vmerge == VMerge::Restart {
|
||||
c.props.vmerge = VMerge::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in &mut open {
|
||||
c.id = None;
|
||||
}
|
||||
if let Some(first) = tail.rows.first_mut() {
|
||||
let own = std::mem::replace(&mut first.controls.open, open);
|
||||
first.controls.open.extend(own);
|
||||
}
|
||||
let starts: Vec<(usize, usize)> = (0..tail.rows.first().map_or(0, |row| row.cells.len()))
|
||||
.filter(|&c| tail.rows.first().and_then(|row| row.cells.get(c)).is_some_and(|x| x.props.vmerge == VMerge::Continue))
|
||||
.map(|c| (c, tail.grid_col(0, c)))
|
||||
.collect();
|
||||
for (c, g) in starts {
|
||||
let more = tail.cell_at_grid(1, g).and_then(|i| tail.rows.get(1)?.cells.get(i)).is_some_and(|x| x.props.vmerge == VMerge::Continue);
|
||||
if let Some(x) = tail.rows.first_mut().and_then(|r| r.cells.get_mut(c)) {
|
||||
x.props.vmerge = if more { VMerge::Restart } else { VMerge::None };
|
||||
}
|
||||
}
|
||||
tail
|
||||
}
|
||||
|
||||
/// Insert a grid column at grid index `g` (cells spanning it widen instead).
|
||||
pub fn insert_col(&mut self, g: usize) {
|
||||
if self.grid.len() >= MAX_COLS {
|
||||
|
||||
@@ -844,11 +844,7 @@ fn split_table(s: &mut Session, _: &Value) -> CmdResult {
|
||||
}
|
||||
let story = s.sel.focus.story;
|
||||
let t = s.doc.table_mut(story, &tp)?;
|
||||
let tail_rows = t.rows.split_off(r);
|
||||
// Content controls closing after the table now close after its second part.
|
||||
let close = std::mem::take(&mut t.controls.close);
|
||||
let controls = wordcraft_doc::ControlWrap { open: Vec::new(), close };
|
||||
let tail = Table { props: t.props.clone(), grid: t.grid.clone(), rows: tail_rows, controls };
|
||||
let tail = t.split_off(r);
|
||||
let after = tp.with_last(tp.last() + 1);
|
||||
s.doc.insert_block(story, &after, Block::Para(Paragraph::new()))?;
|
||||
s.doc.insert_block(story, &after.with_last(after.last() + 1), Block::Table(tail))?;
|
||||
|
||||
@@ -1969,6 +1969,65 @@ fn delete_row_on_a_merge_start_promotes_the_cell_below() {
|
||||
assert_eq!(vm(&s, 1), VMerge::Continue);
|
||||
}
|
||||
|
||||
/// Split Table under a vertically merged cell: the merge ends in the first table and starts
|
||||
/// again in the second.
|
||||
#[test]
|
||||
fn split_table_ends_and_restarts_a_merge_it_cuts() {
|
||||
use wordcraft_doc::props::VMerge::{self, Continue, None as No, Restart};
|
||||
let cell = |r: u32, c: u32| json!({"story": "body", "path": [0, r, c, 0], "off": 0});
|
||||
let tables = |s: &Session| -> Vec<Vec<Vec<VMerge>>> {
|
||||
let rows = |t: &wordcraft_doc::Table| t.rows.iter().map(|r| r.cells.iter().map(|c| c.props.vmerge).collect()).collect();
|
||||
s.doc.body.iter().filter_map(|b| b.as_table()).map(rows).collect()
|
||||
};
|
||||
let merged = |last: u32| {
|
||||
let mut s = table_from("A\tB\nC\tD\nE\tF\nG\tH");
|
||||
run(&mut s, "select.range", json!({"anchor": cell(0, 0), "focus": cell(last, 0)}));
|
||||
run(&mut s, "table.merge", json!({}));
|
||||
s
|
||||
};
|
||||
// Rows 0 to 2 merged, split at row 1: the second table's merge covers rows 1 and 2.
|
||||
let mut s = merged(2);
|
||||
let before = tables(&s);
|
||||
run(&mut s, "caret.set", json!({"pos": cell(1, 1)}));
|
||||
run(&mut s, "table.splitTable", json!({}));
|
||||
assert_eq!(tables(&s), [vec![vec![No, No]], vec![vec![Restart, No], vec![Continue, No], vec![No, No]]]);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(tables(&s), before);
|
||||
// Rows 0 and 1 merged, split at row 1: neither part keeps a merge.
|
||||
let mut s = merged(1);
|
||||
run(&mut s, "caret.set", json!({"pos": cell(1, 1)}));
|
||||
run(&mut s, "table.splitTable", json!({}));
|
||||
assert_eq!(tables(&s), [vec![vec![No, No]], vec![vec![No, No], vec![No, No], vec![No, No]]]);
|
||||
}
|
||||
|
||||
/// Split Table inside a row content control (a repeating section) around several rows: the
|
||||
/// control closes after the first table and opens again before the second.
|
||||
#[test]
|
||||
fn split_table_keeps_a_row_control_on_both_parts() {
|
||||
let cell = |r: u32, c: u32| json!({"story": "body", "path": [0, r, c, 0], "off": 0});
|
||||
let control = |tag: &str| wordcraft_doc::ContentControl { id: Some(7), tag: tag.into(), ..Default::default() };
|
||||
let mut s = table_from("A\tB\nC\tD\nE\tF\nG\tH");
|
||||
if let Some(b) = s.doc.body.iter_mut().find(|b| b.as_table().is_some())
|
||||
&& let wordcraft_doc::Block::Table(t) = std::sync::Arc::make_mut(b)
|
||||
{
|
||||
// "outer" goes around every row, and "inner" goes around row 1 alone.
|
||||
t.rows[0].controls.open = vec![control("outer")];
|
||||
t.rows[1].controls.open = vec![control("inner")];
|
||||
t.rows[1].controls.close = 1;
|
||||
t.rows[3].controls.close = 1;
|
||||
}
|
||||
run(&mut s, "caret.set", json!({"pos": cell(2, 0)}));
|
||||
run(&mut s, "table.splitTable", json!({}));
|
||||
let wraps = |t: &wordcraft_doc::Table| -> Vec<(Vec<String>, u32)> {
|
||||
t.rows.iter().map(|r| (r.controls.open.iter().map(|c| c.tag.clone()).collect(), r.controls.close)).collect()
|
||||
};
|
||||
let parts: Vec<&wordcraft_doc::Table> = s.doc.body.iter().filter_map(|b| b.as_table()).collect();
|
||||
assert_eq!(parts.len(), 2);
|
||||
assert_eq!(wraps(parts[0]), [(vec!["outer".to_string()], 0), (vec!["inner".to_string()], 2)], "the first table closes the control");
|
||||
assert_eq!(wraps(parts[1]), [(vec!["outer".to_string()], 0), (vec![], 1)], "the second table opens it again");
|
||||
assert_eq!(parts[1].rows[0].controls.open[0].id, None, "the copy doesn't repeat the control's id");
|
||||
}
|
||||
|
||||
fn cells(s: &Session) -> Option<((usize, usize), (usize, usize))> {
|
||||
s.cell_range().map(|x| (x.rows, x.cols))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user