From dc64aaa41b09e0aa308441c5b9f88794b10a0743 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 13 Oct 2026 09:29:00 +0700 Subject: [PATCH] Commands: ids stand in for the selection where the params description names the key, checked over every command --- crates/engine/src/cmd/params.rs | 6 ++++ crates/engine/src/lib.rs | 14 +++----- .../engine/src/tests_ids_without_selection.rs | 34 +++++++++++++++++-- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/cmd/params.rs b/crates/engine/src/cmd/params.rs index a5433a68..13142f25 100644 --- a/crates/engine/src/cmd/params.rs +++ b/crates/engine/src/cmd/params.rs @@ -150,6 +150,12 @@ fn accepted(doc: &'static str, depth: usize) -> Option> { Some(keys) } +/// Does the description `doc` name `key` as a parameter (its own keys, and those of the commands +/// its `…` entries name)? +pub(crate) fn takes_key(doc: &'static str, key: &str) -> bool { + doc_keys(doc).is_ok_and(|parsed| parsed.keys.contains(&key)) || accepted(doc, 0).is_some_and(|keys| keys.contains(&key)) +} + /// The edits (insert, delete, replace, swap of neighbours) that turn `a` into `b`, ignoring case. fn distance(a: &str, b: &str) -> usize { let (a, b): (Vec, Vec) = (a.chars().map(|c| c.to_ascii_lowercase()).collect(), b.chars().map(|c| c.to_ascii_lowercase()).collect()); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 717e9f67..f63c7be4 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1977,13 +1977,9 @@ mod tests_widthtool; #[cfg(test)] mod tests_xform; -/// The non-empty `ids` given to a command whose params description takes them (an `ids` word in it). -fn given_ids(description: &str, params: &Value) -> Option> { - let takes = description.match_indices("ids").any(|(i, _)| { - let before = description.get(..i).and_then(|s| s.chars().next_back()); - let after = description.get(i + 3..).and_then(|s| s.chars().next()); - !before.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') && !after.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') - }); - let ids: Vec = params.get("ids")?.as_array()?.iter().filter_map(Value::as_u64).map(NodeId).collect(); - (takes && !ids.is_empty()).then_some(ids) +/// The non-empty `ids` given to a command whose params description names `ids` as a key (the +/// key callers outside the app may pass it, [`cmd::params::takes_key`]). +fn given_ids(description: &'static str, params: &Value) -> Option> { + let ids = cmd::ids_param(params, "ids").filter(|ids| !ids.is_empty())?; + cmd::params::takes_key(description, "ids").then_some(ids) } diff --git a/crates/engine/src/tests_ids_without_selection.rs b/crates/engine/src/tests_ids_without_selection.rs index 752fa630..710fe7d2 100644 --- a/crates/engine/src/tests_ids_without_selection.rs +++ b/crates/engine/src/tests_ids_without_selection.rs @@ -44,10 +44,40 @@ fn the_selection_is_put_back_and_the_check_still_applies() { } #[test] -fn only_an_ids_word_counts() { +fn only_an_ids_key_counts() { let p = json!({"ids": [3]}); assert_eq!(given_ids("{ids?: [id]}", &p), Some(vec![NodeId(3)])); - assert_eq!(given_ids(r#"{"ids":[id]}"#, &p), Some(vec![NodeId(3)])); + assert_eq!(given_ids("{id?|ids?, name}", &p), Some(vec![NodeId(3)])); assert_eq!(given_ids("{grids: n, guides: bool}", &p), None); + // A word in what the description says about another key isn't the key. + assert_eq!(given_ids("{name: of the swatch the ids of the selection take}", &p), None); assert_eq!(given_ids("{ids?: [id]}", &json!({"ids": []})), None); } + +/// Every command that names `ids` and is refused with nothing selected: given the object as `ids` +/// it does what it does with the object selected (the same result or the same error, and the same +/// document), so no command is let through to run on an empty selection. +#[test] +fn ids_do_what_the_selection_does_in_every_command_that_takes_them() { + let mut checked = vec![]; + for spec in command_specs() { + let (mut given, id) = doc_with_rect(); + if !cmd::params::takes_key(spec.params, "ids") || (spec.enabled)(&given).is_ok() { + continue; + } + let (mut selected, _) = doc_with_rect(); + selected.execute("select.set", &json!({"ids": [id]})).unwrap(); + let (a, b) = (given.execute(spec.id, &json!({"ids": [id]})), selected.execute(spec.id, &json!({}))); + match (&a, &b) { + (Ok(_), Ok(_)) => {} + (Err(x), Err(y)) => assert_eq!(x.to_string(), y.to_string(), "{}", spec.id), + _ => panic!("{}: with ids {a:?}, with the selection {b:?}", spec.id), + } + match (given.doc(), selected.doc()) { + (Ok(g), Ok(s)) => assert!(g.doc == s.doc, "{}: the documents differ", spec.id), + (g, s) => assert_eq!(g.is_ok(), s.is_ok(), "{}", spec.id), + } + checked.push((spec.id, a.is_ok())); + } + assert!(checked.iter().filter(|c| c.1).count() >= 20, "{checked:?}"); +}