diff --git a/crates/doc/src/edit.rs b/crates/doc/src/edit.rs index 800a527df..db0098c3b 100644 --- a/crates/doc/src/edit.rs +++ b/crates/doc/src/edit.rs @@ -392,7 +392,12 @@ impl Document { if a.story != b.story { return Vec::new(); } - self.para_paths(a.story).into_iter().filter(|p| *p >= a.path && *p <= b.path).collect() + // Only the blocks the range reaches are walked (not every paragraph of the story). + let mut out = Vec::new(); + if let Some(bl) = self.story(a.story) { + crate::walk_between(bl, &mut Vec::new(), &mut out, 0, (&a.path.0, &b.path.0)); + } + out } /// Delete `a..b`. Returns the position where the deletion collapsed to. On error the document @@ -823,6 +828,27 @@ mod tests { Document::from_text(&lines.join("\n")) } + /// Paths between two positions are those of all the story's paragraphs between them. + #[test] + fn paths_between_matches_every_path_filtered() { + let mut d = doc(&["a", "b", "c", "d", "e"]); + let mut inner = Table::new(2, 2, 100.0); + inner.rows[1].cells[0].blocks.push(crate::para_block(crate::Paragraph::new())); + let mut t = Table::new(3, 2, 200.0); + t.rows[1].cells[1].blocks.insert(0, Arc::new(Block::Table(inner))); + d.insert_block(StoryRef::Body, &Path::top(2), Block::Table(t)).unwrap(); + d.insert_block(StoryRef::Body, &Path::top(4), Block::Table(Table::new(1, 3, 200.0))).unwrap(); + let all = d.para_paths(StoryRef::Body); + for a in &all { + for b in &all { + let (pa, pb) = (Pos { story: StoryRef::Body, path: a.clone(), off: 0 }, Pos { story: StoryRef::Body, path: b.clone(), off: 0 }); + let (lo, hi) = if a <= b { (a, b) } else { (b, a) }; + let want: Vec = all.iter().filter(|p| *p >= lo && *p <= hi).cloned().collect(); + assert_eq!(d.paths_between(&pa, &pb), want, "{a:?} {b:?}"); + } + } + } + #[test] fn split_and_join() { let mut d = doc(&["Hello world"]); diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index c41a14b2b..d313f6be1 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -1476,6 +1476,49 @@ fn walk(bl: &Blocks, prefix: &mut Vec, out: &mut Vec, depth: usize) { } } +/// [`walk`] for the paragraphs from path `a` to `b` inclusive, skipping the blocks outside. +pub(crate) fn walk_between(bl: &Blocks, prefix: &mut Vec, out: &mut Vec, depth: usize, (a, b): (&[u32], &[u32])) { + use std::cmp::Ordering::{Greater, Less}; + if depth > PARA_WALK_DEPTH { + return; + } + // Where the paths starting with `q` lie against `x`: all before, all after, or either. + let side = |q: &[u32], x: &[u32]| { + let m = q.len().min(x.len()); + q[..m].cmp(&x[..m]) + }; + for (i, blk) in bl.iter().enumerate() { + prefix.push(i as u32); + if side(prefix, b) == Greater { + prefix.pop(); + break; + } + if side(prefix, a) != Less { + match &**blk { + Block::Para(_) => { + if prefix.as_slice() >= a && prefix.as_slice() <= b { + out.push(Path(prefix.clone())); + } + } + Block::Table(t) => { + for (r, row) in t.rows.iter().enumerate() { + for (c, cell) in row.cells.iter().enumerate() { + prefix.push(r as u32); + prefix.push(c as u32); + if side(prefix, a) != Less && side(prefix, b) != Greater { + walk_between(&cell.blocks, prefix, out, depth + 1, (a, b)); + } + prefix.pop(); + prefix.pop(); + } + } + } + } + } + prefix.pop(); + } +} + /// Which text of a paragraph [`text_of`] collects. #[derive(Clone, Copy)] enum TextMode {