From 4b18b307beae406ff96ffe4e761f32403ce943a9 Mon Sep 17 00:00:00 2001 From: dfgodvan Date: Sun, 18 Oct 2026 23:53:39 +0300 Subject: [PATCH] Selection rectangles walk only the paragraphs the range reaches (#2967) Document::paths_between listed every paragraph of the story (every cell paragraph of every table) and then kept those between the two paths, so each selection_rects call cost the whole document. The canvas calls it every frame for each Find reading highlight (up to 5,000), each content control frame, Read Aloud and the hyperlink under the pointer. It now walks only the blocks, rows and cells between the two paths: for a one-paragraph range in a document of 26,000 paragraphs (four 1,500-row tables), 1.0 ms -> 2.6 us. Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> --- crates/doc/src/edit.rs | 28 ++++++++++++++++++++++++++- crates/doc/src/lib.rs | 43 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) 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 {