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();