From f1a89a48423a896511e6709bcf4dad8a213ff36e Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 22 Oct 2026 15:34:28 +0000 Subject: [PATCH] fix(edit): wrap added text once per write, not once per line Writing an added text item re-wrapped the whole text inside the loop over its lines to learn the line count, so multiline text cost time and temporary memory quadratic in its length. The wrapped lines are computed once. --- crates/edit/src/added.rs | 12 ++++++++++-- crates/edit/src/tests.rs | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/edit/src/added.rs b/crates/edit/src/added.rs index d246df725..671a72a1b 100644 --- a/crates/edit/src/added.rs +++ b/crates/edit/src/added.rs @@ -235,9 +235,17 @@ pub fn lines(t: &AddedText) -> Vec { wrapped(t).into_iter().map(|(line, _)| line).collect() } +#[cfg(test)] +thread_local! { + /// How often [`wrapped`] laid a text out on this thread, for the test that keeps it out of per-line loops. + pub(crate) static WRAP_CALLS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + /// [`lines`], each with whether its paragraph runs right to left (its first strong character /// does). fn wrapped(t: &AddedText) -> Vec<(String, bool)> { + #[cfg(test)] + WRAP_CALLS.with(|c| c.set(c.get().saturating_add(1))); let width = (t.rect[2] - t.rect[0]).max(t.size); let mut out = Vec::new(); for para in t.text.split('\n') { @@ -780,7 +788,8 @@ fn draw(doc: &mut Document, c: &Content, view: [f64; 6], taken: &Dict, existing: let r = text_rect(t); let [cr, cg, cb] = t.color.map(|v| v.clamp(0.0, 1.0)); out.extend(format!("BT /{name} {} Tf {} {} {} rg\n", n(t.size), n(cr), n(cg), n(cb)).bytes()); - for (i, line) in lines(t).iter().enumerate() { + let all = lines(t); + for (i, line) in all.iter().enumerate() { let w = t.family.width(line, t.size, t.bold, t.italic); let x = match t.align { Align::Left | Align::Justify => r[0], @@ -790,7 +799,6 @@ fn draw(doc: &mut Document, c: &Content, view: [f64; 6], taken: &Dict, existing: // Baseline: 0.8 em below the line top. let y = r[3] - (i as f64 * 1.2 + 0.95) * t.size; // Justified: word spacing makes every line but the last fill the box. - let all = lines(t); let spaces = line.matches(' ').count(); let tw = if t.align == Align::Justify && i + 1 < all.len() && spaces > 0 { ((r[2] - r[0]) - w).max(0.0) / spaces as f64 } else { 0.0 }; diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index cf3860237..73b818be4 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -6355,3 +6355,14 @@ fn reading_keeps_dotted_leader_entries_whole_and_in_order() { ] ); } + +#[test] +fn added_text_is_wrapped_once_per_write() { + let mut doc = fixture(); + let lines: Vec = (0..40).map(|i| format!("line {i}")).collect(); + let text = AddedText { rect: [72.0, 100.0, 300.0, 700.0], text: lines.join("\n"), size: 10.0, align: Align::Justify, ..AddedText::default() }; + added::WRAP_CALLS.with(|c| c.set(0)); + add_content(&mut doc, 0, &Content::Text(text)).unwrap(); + let calls = added::WRAP_CALLS.with(std::cell::Cell::get); + assert!(calls <= 4, "the text was wrapped {calls} times for 40 lines"); +}