Skip to main content

gpui/keymap/
collection.rs

1use crate::{Action, DummyKeyboardMapper, KeyBinding, KeyBindingContextPredicate, SharedString};
2use std::{any::TypeId, collections::HashMap};
3
4/// A generic collection of `Action` -> keystrokes which can be bound under a shared key-context.
5/// Use `as_keybindings` to create an iterator of `KeyBinding` to provide to `App::bind_keys`.
6///
7/// ```rust
8/// # use gpui::{ActionBindingCollection, Action};
9/// gpui::actions!([MyAction]);
10/// # #[gpui::test]
11/// # fn test(app: &mut gpui::TestAppContext) {
12/// app.bind_keys(ActionBindingCollection::default().with::<MyAction>("enter").as_keybindings(None));
13/// # }
14/// ```
15#[derive(Default)]
16pub struct ActionBindingCollection {
17    entries: HashMap<TypeId, ActionBindingEntry>,
18}
19
20#[derive(Debug)]
21struct ActionBindingEntry {
22    // The action used to build Keybinding
23    action: Box<dyn Action>,
24    // The keystrokes that trigger the action
25    // NOTE: could limit allocations by utilizing smallvec
26    keystrokes: Vec<SharedString>,
27}
28
29impl ActionBindingCollection {
30    /// Adds a keystroke to an action in the collection.
31    pub fn with<A: Action + Default>(mut self, keystrokes: impl Into<SharedString>) -> Self {
32        let action_id = TypeId::of::<A>();
33        let entry = self.entries.entry(action_id);
34        let entry = entry.or_insert_with(|| ActionBindingEntry {
35            action: Box::new(A::default()),
36            keystrokes: Vec::with_capacity(5),
37        });
38        entry.keystrokes.push(keystrokes.into());
39        self
40    }
41
42    /// Creates an iterator of `KeyBinding` which represents all action+keystrokes in this collection bound to the same key-context.
43    /// This sequence can be provided to `App::bind_keys`, and any elements which use the same `InteractiveElement::key_context` will receive the bound actions.
44    pub fn as_keybindings(&self, context: Option<&str>) -> impl Iterator<Item = KeyBinding> {
45        let context_predicate =
46            context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
47        let iter = self.entries.iter();
48        let iter = iter.map(move |(_, entry)| {
49            let action = &entry.action;
50            let context_predicate = context_predicate.clone();
51            entry.keystrokes.iter().map(move |keystrokes| {
52                let keybinding = KeyBinding::load(
53                    keystrokes.as_str(),
54                    action.boxed_clone(),
55                    context_predicate.clone(),
56                    false,
57                    None,
58                    &DummyKeyboardMapper,
59                );
60                keybinding.expect("failed to load keybinding")
61            })
62        });
63        iter.flatten()
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::TestAppContext;
71
72    actions!([TestAction]);
73
74    #[gpui::test]
75    fn test_default_empty(_cx: &mut TestAppContext) {
76        assert!(ActionBindingCollection::default().entries.is_empty());
77    }
78
79    #[gpui::test]
80    fn test_single_binding(_cx: &mut TestAppContext) {
81        let collection = ActionBindingCollection::default();
82        let collection = collection.with::<TestAction>("enter");
83
84        let entry = collection.entries.get(&TypeId::of::<TestAction>());
85        let entry = entry.map(|entry| &entry.keystrokes);
86        assert_eq!(entry, Some(&vec![SharedString::new_static("enter")]));
87
88        let out_bindings = collection.as_keybindings(None).collect::<Vec<_>>();
89        assert_eq!(out_bindings.len(), 1);
90    }
91
92    #[gpui::test]
93    fn test_single_action_many_keys(_cx: &mut TestAppContext) {
94        let collection = ActionBindingCollection::default();
95        let collection = collection.with::<TestAction>("enter");
96        let collection = collection.with::<TestAction>("escape");
97
98        let entry = collection.entries.get(&TypeId::of::<TestAction>());
99        let entry = entry.map(|entry| &entry.keystrokes);
100        assert_eq!(
101            entry,
102            Some(&vec![
103                SharedString::new_static("enter"),
104                SharedString::new_static("escape")
105            ])
106        );
107
108        let out_bindings = collection.as_keybindings(None).collect::<Vec<_>>();
109        assert_eq!(out_bindings.len(), 2);
110    }
111
112    #[gpui::test]
113    fn test_single_action_many_keys_context(_cx: &mut TestAppContext) {
114        let collection = ActionBindingCollection::default();
115        let collection = collection.with::<TestAction>("enter");
116        let collection = collection.with::<TestAction>("escape");
117        let out_bindings = collection
118            .as_keybindings(Some("test_content"))
119            .collect::<Vec<_>>();
120        assert_eq!(out_bindings.len(), 2);
121    }
122}