From be51941628f2f3069eac709cf67fa69411cf36a5 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 19 Oct 2026 04:23:26 +0000 Subject: [PATCH] keymap: add Keymap::conflicts_with and document the edit calls conflicts_with lists the commands that share one command's chords, with that command as a, without filtering every conflict. The README shows set_bindings with one binding for an editor with one shortcut per command; assign adds a binding. --- crates/keymap/README.md | 15 ++++++++++++++- crates/keymap/src/keymap.rs | 26 ++++++++++++++++++++++++++ crates/keymap/tests/keymap.rs | 22 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/keymap/README.md b/crates/keymap/README.md index 74c72833..4f4e062e 100644 --- a/crates/keymap/README.md +++ b/crates/keymap/README.md @@ -113,10 +113,23 @@ let mut keymap = Keymap::new(catalogue, Scopes::new(), Platform::current()).with let _problems = keymap.problems(); // After each change in the shortcut editor: -keymap.assign(&"file.save".into(), Binding::parse("Cmd+Alt+S").unwrap(), Policy::Steal).ok(); +keymap.set_bindings(&"file.save".into(), vec![Binding::parse("Cmd+Alt+S").unwrap()], Policy::Steal).ok(); store.update(|p| p.shortcuts = keymap.overrides().clone()); ``` +## Editing + +- `set_bindings(&command, bindings, policy)` replaces a command's bindings. An editor with one + shortcut per command calls it with the one binding (`vec![binding]`); `assign` adds a binding + to those the command has. +- `remove` takes one binding away, `clear` leaves the command without shortcuts, `reset` gives it + its defaults back and `reset_all` does so for every command. +- `Policy` says what happens to other commands holding the chord: `Steal` takes it from them, + `Keep` lets both keep it and reports the clash, `Refuse` changes nothing and returns + `AssignError::Taken`. +- `conflicts()` lists every pair of commands on one chord; `conflicts_with(&command)` lists the + commands that share one command's chords, with that command as `a`. + ## Importing a shortcut set The app's reader turns a file (an InDesign Show Set, an Illustrator `.kys`) into an diff --git a/crates/keymap/src/keymap.rs b/crates/keymap/src/keymap.rs index a55f50ac..e57d7bf1 100644 --- a/crates/keymap/src/keymap.rs +++ b/crates/keymap/src/keymap.rs @@ -594,6 +594,32 @@ impl Keymap { out } + /// The conflicts [`Keymap::conflicts`] reports for one command's bindings, with that command + /// as `a`, in the order of its bindings, then catalogue order. Empty for an unknown command. + pub fn conflicts_with(&self, command: &CommandRef) -> Vec { + let Some(i) = self.find(command) else { return Vec::new() }; + let mut out = Vec::new(); + for a in &self.current[i] { + let chord = a.0.effective(self.platform); + let Some(holders) = self.by_trigger.get(&chord) else { continue }; + for &(j, k) in holders { + if j == i || self.same_group(i, j) { + continue; + } + let Some(b) = self.at(j, k) else { continue }; + let Some(clash) = self.conflict_relation(&a.1, &b.1) else { continue }; + out.push(Conflict { + chord, + a: (self.catalogue[i].command.clone(), self.public(i, a)), + b: (self.catalogue[j].command.clone(), self.public(j, b)), + kind: if clash { ConflictKind::Clash } else { ConflictKind::Shadow }, + in_defaults: self.defaults[i].contains(a) && self.defaults[j].contains(b), + }); + } + } + out + } + /// What is wrong with the stored overrides: load notes, unknown commands, texts that do not /// parse, reserved chords, unknown scopes, duplicate entries. pub fn problems(&self) -> Vec { diff --git a/crates/keymap/tests/keymap.rs b/crates/keymap/tests/keymap.rs index 24d2ade8..558f37ff 100644 --- a/crates/keymap/tests/keymap.rs +++ b/crates/keymap/tests/keymap.rs @@ -246,6 +246,28 @@ fn conflicts_tell_clashes_from_shadows() { assert!(c("edit.copy", "color.copy") < c("edit.copy", "timeline.copy"), "{order:?}"); assert!(c("edit.copy", "timeline.copy") < c("color.copy", "timeline.copy"), "{order:?}"); + // One command's conflicts, with that command as `a`. + let with = |id: &str| -> Vec<(String, String, ConflictKind)> { + km.conflicts_with(&cmd(id)).into_iter().map(|c| (c.chord.to_string(), c.b.0.id, c.kind)).collect() + }; + let w = |chord: &str, other: &str, kind| (chord.to_string(), other.to_string(), kind); + assert_eq!(with("edit.copy"), [w("Cmd+C", "color.copy", ConflictKind::Clash), w("Cmd+C", "timeline.copy", ConflictKind::Shadow)]); + assert_eq!(with("edit.cut"), [w("Cmd+X", "text.cut", ConflictKind::Shadow), w("Cmd+D", "edit.find", ConflictKind::Clash)]); + assert!(with("nope").is_empty()); + for entry in km.catalogue() { + let id = entry.command.id.as_str(); + let mut direct: Vec<_> = km.conflicts_with(&entry.command).into_iter().map(|c| (c.chord, c.a.0.id, c.b.0.id, c.kind, c.in_defaults)).collect(); + let mut filtered: Vec<_> = km + .conflicts() + .into_iter() + .filter(|c| c.a.0.id == id || c.b.0.id == id) + .map(|c| if c.a.0.id == id { (c.chord, c.a.0.id, c.b.0.id, c.kind, c.in_defaults) } else { (c.chord, c.b.0.id, c.a.0.id, c.kind, c.in_defaults) }) + .collect(); + direct.sort(); + filtered.sort(); + assert_eq!(direct, filtered, "{id}"); + } + let strict = Keymap::new(km.catalogue().to_vec(), scopes().with_panel_fallthrough(false), Platform::Mac); assert!(!strict.conflicts().iter().any(|c| c.chord.to_string() == "Cmd+R")); }