From c02e864873e5a0783947b7512a83f92e30bf1d61 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:22:29 +0000 Subject: [PATCH] Replace every occurrence in edit.findReplace when replacement contains find Found while testing crates/engine/src/cmd/edit.rs:975-981 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/edit.rs | 15 +++++++++++---- crates/engine/src/tests.rs | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 57ba9fb0..696f8dfe 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -970,13 +970,20 @@ fn find_replace(s: &mut Session, p: &Value) -> Result { } if let NodeKind::Text(t) = &mut n.kind { // Replace occurrence by occurrence so styles shift correctly. + // The search resumes after each inserted replacement, so a replacement that + // contains `find` neither loops forever nor stops the other occurrences. let mut guard = 0; - while let Some(pos) = t.characters.find(find.as_str()) { - let start = t.characters.get(..pos).map(|x| x.chars().count()).unwrap_or(0); - let len = find.chars().count(); + let mut cursor = 0; + let len = find.chars().count(); + let r_len = r.chars().count(); + loop { + let from = t.characters.char_indices().nth(cursor).map_or(t.characters.len(), |(b, _)| b); + let Some(pos) = t.characters.get(from..).and_then(|tail| tail.find(find.as_str())) else { break }; + let start = cursor + t.characters.get(from..from + pos).map_or(0, |x| x.chars().count()); t.replace_range(start, start + len, r); + cursor = start + r_len; guard += 1; - if guard > 10_000 || r.contains(find.as_str()) { + if guard > 10_000 { break; } } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 40da2f1d..04e97a73 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -161,6 +161,28 @@ fn text_create_autosize_and_edit() { assert_eq!(s.doc().unwrap().node(t).unwrap().kind.text().unwrap().characters, "Hello"); } +#[test] +fn find_replace_replaces_every_occurrence_when_replacement_contains_find() { + for (text, find, replace, want) in [ + ("cat and cat", "cat", "cats", "cats and cats"), + ("cat and cat", "cat", "category", "category and category"), + ("banana", "a", "aa", "baanaanaa"), + ] { + let mut s = sess(); + let t = id(&s.execute("text.create", &json!({"text": text, "x": 0, "y": 0})).unwrap()); + s.execute("edit.findReplace", &json!({"find": find, "replace": replace})).unwrap(); + assert_eq!(s.doc().unwrap().node(t).unwrap().kind.text().unwrap().characters, want, "{text}: {find} -> {replace}"); + } +} + +#[test] +fn find_replace_rejects_empty_find() { + let mut s = sess(); + let t = id(&s.execute("text.create", &json!({"text": "cat", "x": 0, "y": 0})).unwrap()); + assert!(s.execute("edit.findReplace", &json!({"find": "", "replace": "x"})).is_err()); + assert_eq!(s.doc().unwrap().node(t).unwrap().kind.text().unwrap().characters, "cat"); +} + #[test] fn components_and_instances_sync() { let mut s = sess();