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>
This commit is contained in:
dfgodvan
2026-10-18 16:53:39 -04:00
committed by GitHub
co-authored by dfgodvan
parent 972252d0b4
commit 4b18b307be
2 changed files with 70 additions and 1 deletions
+27 -1
View File
@@ -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<Path> = 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"]);
+43
View File
@@ -1476,6 +1476,49 @@ fn walk(bl: &Blocks, prefix: &mut Vec<u32>, out: &mut Vec<Path>, 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<u32>, out: &mut Vec<Path>, 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 {