Fix #4918: delete nested picks safely, keep picks per document

symbol.delete {names} expands every instance of the named symbols,
round by round, before it removes any definition, so a symbol and the
one placed inside it delete alike in either order. The Symbols panel's
picks belong to the document they were made in, and a single picked
symbol is deleted only while symbol.unused still lists it.
This commit is contained in:
Don Omondi
2026-11-01 07:38:01 +07:00
committed by bflatastic
parent 5b639169cd
commit 961694cba6
4 changed files with 105 additions and 17 deletions
+28 -2
View File
@@ -448,6 +448,9 @@ pub(super) fn symbol_update(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "name": name, "id": id.0 }))
}
/// How many levels of nested instances Delete Symbol expands before it gives up.
const MAX_DELETE_ROUNDS: usize = 256;
pub(super) fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "symbol.delete";
// `names` deletes several symbols in one undo step (the Symbols panel's Select All Unused, #4918).
@@ -475,13 +478,36 @@ pub(super) fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
let expand = switch_or(p, "expandInstances", true, C)?;
let label = if names.len() == 1 { "Delete Symbol" } else { "Delete Symbols" };
s.edit(label, |d, _| {
for name in &names {
let inst = instances_of(d, Some(name));
// Every instance goes before any definition, whatever the order of `names`: expanding an
// instance of one symbol can bring out instances of another being deleted (nested in its
// art), which expand in turn while their definitions are still there. Each round expands
// one level of nesting, so the rounds are capped.
let of_names = |d: &Document| {
let mut v = vec![];
d.walk(|n| {
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind
&& names.contains(symbol)
{
v.push(n.id);
}
});
v
};
for _ in 0..=MAX_DELETE_ROUNDS {
let inst = of_names(d);
if inst.is_empty() {
break;
}
if expand {
break_links(d, &inst, true)?;
} else {
remove_each(d, &inst)?;
}
}
if !of_names(d).is_empty() {
return Err(EngineError::Other("the symbols are nested too deeply to expand their instances".into()));
}
for name in &names {
d.symbols.retain(|x| &x.name != name);
set_natural_size(d, name, None);
d.set_symbol_registration(name, SymbolRegistration::CENTER);
+12
View File
@@ -1483,6 +1483,18 @@ fn symbol_unused_lists_the_symbols_nothing_places_and_delete_takes_several() {
assert!(unused(&mut s).is_empty());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(names(&s), ["Placed", "Inner", "Outer", "Nested", "Lonely", "Brushed"]);
// Deleting a symbol and the one placed inside it, in either order, expands every instance
// while both definitions are still there: no instance is left without its symbol.
for order in [["Inner", "Outer"], ["Outer", "Inner"]] {
s.execute("symbol.delete", &json!({"names": order})).unwrap();
let mut dangling = 0;
s.doc()
.unwrap()
.doc
.walk(|n| dangling += usize::from(matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Inner" || symbol == "Outer")));
assert_eq!(dangling, 0, "{order:?}");
s.execute("edit.undo", &json!({})).unwrap();
}
// Bad `names` are refused and change nothing.
for bad in [json!({"names": []}), json!({"names": "Placed"}), json!({"names": [1]}), json!({"names": ["Placed", "Nope"]})] {
assert!(s.execute("symbol.delete", &bad).is_err(), "{bad}");