From 30a0f08698b45047498da78670baccfe6a7b155b Mon Sep 17 00:00:00 2001 From: VoiceLessQ Date: Thu, 29 Oct 2026 00:08:22 -0200 Subject: [PATCH] Story: copy a paragraph's format for all new returns at once (#1588) insert_with_raw gave each return in the inserted text its own Vec::insert of the paragraph's format, and each insert moves every format after it. A ParaFormat is 2,888 bytes, so the time grew with the square of the returns: Story::with_text took 2.2 s for 10,000 returns, 15.8 s for 20,000 and 90 s for 40,000. Placing a plain-text file goes through it. The copies now go in with one splice. --- ROADMAP.md | 1 + crates/doc/src/story.rs | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c02176bab..3887ecda8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -380,6 +380,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-29** · Inserting text with many returns no longer takes time that grows with their square (part of #1588): each return copied its paragraph's format with its own insert, moving every format after it. Splitting 20,000 returns into paragraphs took 15.8 s; placing a text file of them now takes 0.2 s, and one of a million returns 5.5 s. No readiness percentage change. - **2026-10-27** · An iconic panel's flyout stays open while you work in the canvas or a text field: Auto-Collapse Iconic Panels is off by default, and preferences saved with it on start with it off. Preferences › Interface › Panels holds the setting next to the column bar's menu item. No readiness percentage change. - **2026-10-24** · Column rules run from the top line's ascent to the bottom line's descent, as InDesign draws them, instead of from the top to the bottom of the columns, on screen, in PDF and in PowerPoint export. The top and bottom insets shorten the rule from those lines. Existing documents draw their column rules at the new length. No readiness percentage change. - **2026-10-20** · Edit › Keyboard Shortcuts is craft-keymap's shared dialog: commands grouped by menu with Show and Filter, recording that asks before taking a key from another command, undo and redo, named shortcut sets (edits of Default become Custom; an imported set becomes a set of its own), an on-screen US keyboard, InDesign Show Set files reviewed before they apply, JSON export and an HTML summary. ui.json keeps the `shortcuts` map earlier versions read, for the active set. No readiness percentage change. diff --git a/crates/doc/src/story.rs b/crates/doc/src/story.rs index 05adcb35f..bb56505f7 100644 --- a/crates/doc/src/story.rs +++ b/crates/doc/src/story.rs @@ -385,11 +385,11 @@ impl Story { // Paragraph formats: each new '\n' splits the current paragraph; the new paragraphs copy it. let pi = self.para_at(pos); let newlines = text.bytes().filter(|b| *b == b'\n').count(); - if newlines > 0 { - let f = self.paras[pi].clone(); - for _ in 0..newlines { - self.paras.insert(pi + 1, f.clone()); - } + if newlines > 0 + && let Some(f) = self.paras.get(pi).cloned() + { + // All at once: an insert per return made many returns quadratic (#1588). + self.paras.splice(pi + 1..pi + 1, std::iter::repeat_n(f, newlines)); } self.text.insert_str(pos, text); // Character runs. @@ -669,6 +669,19 @@ mod tests { assert_eq!(s.para_ranges(), vec![0..6]); } + /// Each return inserted its paragraph format on its own, quadratic in the returns (#1588). + #[test] + fn many_returns_split_a_paragraph_at_once() { + let para = ParaFormat { style: "Body".into(), ..ParaFormat::default() }; + let started = std::time::Instant::now(); + let s = Story::with_text(StoryId(1), &"\n".repeat(20_000), para.clone()); + let took = started.elapsed(); + assert!(took < std::time::Duration::from_secs(1), "took {took:?}"); + assert_eq!(s.paras.len(), 20_001); + assert!(s.paras.iter().all(|p| *p == para)); + s.check().unwrap(); + } + #[test] fn char_formats_split_and_merge() { let mut s = st("hello world");