Engine: an empty ids names nothing; states.create and conveyor.collect honour id

named_targets reads ids/id as targets() does, so {ids: [], id} can't
pass the gate and then run on nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-06 09:04:22 -04:00
co-authored by Claude Opus 5.5
parent 7e69c82b89
commit f1767a84fd
4 changed files with 12 additions and 5 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ pub fn specs() -> Vec<CommandSpec> {
ok()
}),
cmd!(noundo "conveyor.collect", "Collect", [], None, "{ids? (default: the selection)} — each object onto the Content Collector conveyor → {count}", has_selection, |s, p| {
let ids = super::ids_param(p, "ids").unwrap_or_else(|| s.active().map(|d| d.selection.items.clone()).unwrap_or_default());
let ids = super::targets(s, p)?;
let keep = s.active().map(|d| d.selection.clone());
for id in ids {
let name = s.doc()?.doc.item(id).map(|it| it.default_label().trim_matches(|c| c == '<' || c == '>').to_string()).unwrap_or_default();
+3 -2
View File
@@ -269,8 +269,9 @@ pub(crate) fn named_targets(spec: &CommandSpec, p: &Value) -> Option<Vec<ItemId>
if !spec.params.split(|c: char| !c.is_ascii_alphanumeric()).any(|w| w == "ids") {
return None;
}
if let Some(ids) = ids_param(p, "ids").filter(|v| !v.is_empty()) {
return Some(ids);
// As `targets` reads them: `ids`, when given, wins over `id` (an empty list names nothing).
if let Some(ids) = ids_param(p, "ids") {
return (!ids.is_empty()).then_some(ids);
}
id_param(p, "id").map(|i| vec![i])
}
+6
View File
@@ -2413,5 +2413,11 @@ mod named_target_tests {
let e = s.execute("object.textFrameOptions", &json!({"ids": [99999], "columns": 1})).unwrap_err().to_string();
assert!(e.contains("no object with id 99999"), "{e}");
assert_eq!(columns(&s), Some(2));
// An empty `ids` names nothing (as `targets` reads it), even beside an `id`.
let e = s.execute("object.textFrameOptions", &json!({"ids": [], "id": r["id"], "columns": 3})).unwrap_err().to_string();
assert!(e.contains("nothing selected"), "{e}");
assert_eq!(columns(&s), Some(2));
// Commands documented with `ids` honour `id` too.
assert_eq!(s.execute("conveyor.collect", &json!({"id": r["id"]})).unwrap()["count"], 1);
}
}
+2 -2
View File
@@ -3,7 +3,7 @@
use designcraft_doc::{ItemId, Shape};
use serde_json::{Value, json};
use super::{CommandSpec, bad, cmd, has_selection, ids_param};
use super::{CommandSpec, bad, cmd, has_selection};
use crate::{Result, Session};
fn target(s: &Session, p: &Value) -> Result<ItemId> {
@@ -24,7 +24,7 @@ pub fn specs() -> Vec<CommandSpec> {
"{ids? (default: the selection; one group's objects or several objects become states)} → {id, states}",
has_selection,
|s, p| {
let ids = ids_param(p, "ids").unwrap_or_else(|| s.active().map(|d| d.selection.items.clone()).unwrap_or_default());
let ids = super::targets(s, p)?;
let single_group = ids.len() == 1 && s.doc()?.doc.item(ids[0]).is_some_and(|it| it.shape == Shape::Group && it.children().len() > 1);
let gid = if single_group {
ids[0]