From 838d0e86f221116dfff6b7ec845d0edb0e567d38 Mon Sep 17 00:00:00 2001 From: Mateusz Sych <151755914+SychPL@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:26:18 +0200 Subject: [PATCH] Component rename also renames its placements (#1665) (#1685) Thanks @SychPL. --- crates/engine/src/cmd/component.rs | 14 +++++++++++--- crates/engine/src/cmd/occurrence_tests.rs | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/component.rs b/crates/engine/src/cmd/component.rs index ffe5510f..80a4c5ff 100644 --- a/crates/engine/src/cmd/component.rs +++ b/crates/engine/src/cmd/component.rs @@ -214,10 +214,18 @@ fn rename(s: &mut Session, p: &Value) -> Result { let id = component(s, p, "component", cmd)?; let name = str_(p, "name").map(str::trim).filter(|n| !n.is_empty() && n.len() <= 128).ok_or_else(|| bad(cmd, "`name` must be 1…128 characters"))?; + let d = s.doc_mut(); if id == 0 { - s.doc_mut().name = name.to_string(); - } else if let Some(c) = s.doc_mut().components.iter_mut().find(|c| c.id == id) { - c.name = name.to_string(); + d.name = name.to_string(); + } else if let Some(c) = d.components.iter_mut().find(|c| c.id == id) { + let old = std::mem::replace(&mut c.name, name.to_string()); + // Placements still named after the component ("Old:2") follow it (#1665); others keep theirs. + for o in d.occurrences.iter_mut().filter(|o| o.component == id) { + let k = o.name.strip_prefix(old.as_str()).and_then(|r| r.strip_prefix(':')); + if let Some(k) = k.filter(|k| !k.is_empty() && k.bytes().all(|b| b.is_ascii_digit())) { + o.name = format!("{name}:{k}"); + } + } } Ok(json!({"component": id, "name": name})) } diff --git a/crates/engine/src/cmd/occurrence_tests.rs b/crates/engine/src/cmd/occurrence_tests.rs index 31a1bed8..c88c7f8c 100644 --- a/crates/engine/src/cmd/occurrence_tests.rs +++ b/crates/engine/src/cmd/occurrence_tests.rs @@ -66,6 +66,22 @@ fn instance_selection_survives_rename_move_capture_and_serde() { assert_eq!(roundtrip, s.selection); } +#[test] +fn component_rename_carries_into_placement_names() { + // #1665: the browser row shows the placement's name, so a rename has to reach it. + let mut s = Session::default(); + let (component, first) = created(&mut s, "Block"); + let second = run(&mut s, "occurrence.copy", json!({"component": component, "translate": [20,0,0]}))["occurrence"].as_u64().unwrap(); + let third = run(&mut s, "occurrence.copy", json!({"component": component, "translate": [40,0,0]}))["occurrence"].as_u64().unwrap(); + let name = |s: &Session, id: u64| s.doc.occurrences.iter().find(|o| o.id == id).unwrap().name.clone(); + assert_eq!(name(&s, second), "Block:2"); + // A placement named otherwise (an import's own name) keeps it. + s.doc_mut().occurrences.iter_mut().find(|o| o.id == third).unwrap().name = "Blockade:3".into(); + run(&mut s, "component.rename", json!({"component": component, "name": "box"})); + assert_eq!([name(&s, first), name(&s, second), name(&s, third)], ["box:1", "box:2", "Blockade:3"]); + assert_eq!(run(&mut s, "component.list", json!({}))["components"][0]["children"][0]["name"], "box:1"); +} + #[test] fn instance_selection_delete_removes_only_selected_instance() { let mut s = Session::default();