From a4aa1960cf7ea9ee0a91f5a6455fb877967b821e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Mon, 2 Nov 2026 20:34:19 -0500 Subject: [PATCH] Threaded text: a frame listed twice flows the story once Reflow copied each listed frame's text into its thread's story, so a document that listed one frame several times in a thread, or in several threads, repeated that text in the story and wrote the grown story back for the next thread to copy again, growing the text with every repetition while the document opened. Reflow now keeps each frame at its first place in the thread list only. --- crates/engine/src/cmd/threads.rs | 7 ++++++- crates/engine/src/cmd/threads/tests.rs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/threads.rs b/crates/engine/src/cmd/threads.rs index 3bb35b979..db7082f05 100644 --- a/crates/engine/src/cmd/threads.rs +++ b/crates/engine/src/cmd/threads.rs @@ -93,7 +93,12 @@ thread_local! { /// makes "changed" a pointer comparison). Deleted frames' text flows into the remaining ones; a /// frame that is now point type leaves its thread with the text it holds. pub(crate) fn reflow(before: &Document, doc: &mut Document) { - let threads = std::mem::take(&mut doc.text_threads); + // A frame is in one thread, once: a file that lists it again (in its thread or another) would + // copy its text into the story each time, and write the grown story back for the next thread + // to copy again. Only its first place counts. + let mut seen = std::collections::HashSet::new(); + let threads: Vec> = + std::mem::take(&mut doc.text_threads).into_iter().map(|t| t.into_iter().filter(|id| seen.insert(*id)).collect()).collect(); let mut kept = vec![]; for thread in threads { let changed = thread.iter().any(|id| match (before.node(*id), doc.node(*id)) { diff --git a/crates/engine/src/cmd/threads/tests.rs b/crates/engine/src/cmd/threads/tests.rs index 07531d4ed..2dfb119fe 100644 --- a/crates/engine/src/cmd/threads/tests.rs +++ b/crates/engine/src/cmd/threads/tests.rs @@ -40,6 +40,24 @@ fn thread_flows_reflows_on_edit_and_survives_delete() { assert_eq!(s.doc().unwrap().doc.text_threads.len(), 1); } +/// A file that lists a frame more than once, in its own thread or in another, flows the story +/// through each frame once: repeated places would copy its text again and again. +#[test] +fn frames_listed_twice_flow_the_story_once() { + let (mut s, a, b) = setup(); + s.execute("text.thread.create", &json!({})).unwrap(); + let mut d = (*s.doc().unwrap().doc).clone(); + let (fa, fb) = (NodeId(a), NodeId(b)); + d.text_threads = vec![vec![fa, fb, fa, fb, fa], vec![fb, fa], vec![fa, fb]]; + reflow(&Document::new(1.0, 1.0), &mut d); + assert_eq!(d.text_threads, vec![vec![fa, fb]]); + let plain = |id: NodeId| match &d.node(id).unwrap().kind { + NodeKind::Text(t) => t.plain_text(), + _ => panic!("not text"), + }; + assert_eq!(format!("{}{}", plain(fa), plain(fb)), TEXT.repeat(4)); +} + #[test] fn edits_copy_only_the_stories_of_threads_whose_frames_changed() { let (mut s, a, b) = setup();