diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 2640eadd5a..eb80c02c39 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -5929,6 +5929,20 @@ fn replacing_a_line_keeps_a_distinct_nearby_line() { } } +#[test] +fn text_blocks_split_at_headings_and_blank_lines() { + let page = |size: u32, gap: u32, words: &[&str], top: u32| -> String { + words.iter().enumerate().map(|(i, w)| format!("BT /F1 {size} Tf 40 {} Td ({w}) Tj ET\n", top - i as u32 * gap)).collect() + }; + let count = |content: String| text::text_blocks(&text_page(&content), 0).unwrap().len(); + let heading = page(12, 14, &["First line of text", "second line here", "third line ends"], 340) + + &page(12, 14, &["HEADING"], 340 - 28 - 24) + + &page(12, 14, &["Next paragraph one", "next two", "next three"], 340 - 28 - 24 - 24); + assert_eq!(count(heading), 3); + assert_eq!(count(page(12, 14, &["Para one a", "para one b"], 340) + &page(12, 14, &["Para two a", "para two b"], 340 - 42)), 2); + assert_eq!(count(page(24, 29, &["VISIBLE", "HIDDEN"], 340)), 1); +} + #[test] fn editing_text_keeps_an_overlapping_watermark_and_covered_paragraphs() { let listed = |doc: &Document| text::text_lines(doc, 0).unwrap().into_iter().map(|l| l.text).collect::>(); diff --git a/crates/edit/src/text_regions/grouping.rs b/crates/edit/src/text_regions/grouping.rs index 1dcb9a9373..72a516292d 100644 --- a/crates/edit/src/text_regions/grouping.rs +++ b/crates/edit/src/text_regions/grouping.rs @@ -331,7 +331,19 @@ fn group_layout(lines: &[TextLine], mixed: bool) -> Vec { let scale = gap.abs().max(ngap.abs()); if same && ngap > tol * 0.8 && scale < tol * 4.0 && (ngap - gap).abs() <= scale * 0.08 + 0.5 { Some(gap) } else { None } }); - let max_gap = own_leading.filter(|lead| *lead > tol * 0.4).or(stated).or(repeated).map_or(tol * 2.05, |lead| lead * 1.5); + // A lone row with none of those: the step to the row after it is the leading of the + // paragraph it opens, and the gap above it is a heading's or a blank line's when it + // is much wider (#3577). + let following = rows.get(row_at.saturating_add(1)).and_then(|next| { + let nfirst = next.first().and_then(|i| lines.get(*i))?; + let ngap = first.frame_origin().1 - nfirst.frame_origin().1; + (same_plane(first, nfirst) && ngap > tol * 0.8 && ngap < tol * 2.05).then_some(ngap) + }); + let max_gap = own_leading + .filter(|lead| *lead > tol * 0.4) + .or(stated) + .or(repeated) + .map_or_else(|| following.map_or(tol * 2.05, |lead| (lead * 1.35).min(tol * 2.05)), |lead| lead * 1.5); // OCR lines stay apart: the scan fixes where each one is, so they never rewrap together. (!mixed || !(invisible(prev) || invisible(first))) && (mixed || same_style(prev, first))