Skip to main content

gpui/keymap/
collection.rs

1use crate::{Action, DummyKeyboardMapper, KeyBinding, KeyBindingContextPredicate, SharedString};
2use std::{any::TypeId, collections::HashMap};
3
4/// A set of actions and the keystrokes that trigger each one, to be bound together in one key
5/// context.
6///
7/// Use it when one action has several keystrokes, or when a group of bindings shares a context.
8/// Build it with [`with`](Self::with), then turn it into [`KeyBinding`]s with
9/// [`as_keybindings`](Self::as_keybindings) and pass them to
10/// [`App::bind_keys`](crate::App::bind_keys).
11///
12/// # Examples
13///
14/// ```standalone_crate
15/// use gpui::{ActionBindingCollection, actions};
16///
17/// actions!(collection_docs, [Submit]);
18///
19/// let bindings = ActionBindingCollection::default()
20///     .with::<Submit>("enter")
21///     .with::<Submit>("ctrl-enter");
22///
23/// // One binding per keystroke, all in the `Form` context.
24/// let bindings: Vec<_> = bindings.as_keybindings(Some("Form")).collect();
25/// assert_eq!(bindings.len(), 2);
26/// ```
27#[derive(Default)]
28pub struct ActionBindingCollection {
29    entries: HashMap<TypeId, ActionBindingEntry>,
30}
31
32#[derive(Debug)]
33struct ActionBindingEntry {
34    // The action used to build Keybinding
35    action: Box<dyn Action>,
36    // The keystrokes that trigger the action
37    // NOTE: could limit allocations by utilizing smallvec
38    keystrokes: Vec<SharedString>,
39}
40
41impl ActionBindingCollection {
42    /// Adds `keystrokes` as a trigger for the action type `A`, and returns the collection.
43    ///
44    /// The action is built with `A::default()` once per type, so `A` must be a type you can
45    /// construct with `Default`, such as an action declared with [`actions!`](crate::actions).
46    /// Keystrokes aren't parsed here. They are parsed by [`as_keybindings`](Self::as_keybindings),
47    /// which panics on an invalid one.
48    pub fn with<A: Action + Default>(mut self, keystrokes: impl Into<SharedString>) -> Self {
49        let action_id = TypeId::of::<A>();
50        let entry = self.entries.entry(action_id);
51        let entry = entry.or_insert_with(|| ActionBindingEntry {
52            action: Box::new(A::default()),
53            keystrokes: Vec::with_capacity(5),
54        });
55        entry.keystrokes.push(keystrokes.into());
56        self
57    }
58
59    /// Returns one [`KeyBinding`] for each action and keystroke in the collection, all in `context`.
60    ///
61    /// Pass `None` for `context` to make the bindings apply everywhere. Elements that declare a
62    /// matching [`key_context`](crate::InteractiveElement::key_context) receive the actions.
63    /// The order of the bindings is unspecified, because the collection is a hash map keyed by
64    /// action type. Bindings for different actions on the same keystrokes therefore have no
65    /// defined precedence.
66    ///
67    /// # Panics
68    ///
69    /// Panics if a keystroke fails to parse, or if `context` is not a valid key context
70    /// predicate.
71    pub fn as_keybindings(&self, context: Option<&str>) -> impl Iterator<Item = KeyBinding> {
72        let context_predicate =
73            context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
74        let iter = self.entries.iter();
75        let iter = iter.map(move |(_, entry)| {
76            let action = &entry.action;
77            let context_predicate = context_predicate.clone();
78            entry.keystrokes.iter().map(move |keystrokes| {
79                let keybinding = KeyBinding::load(
80                    keystrokes.as_str(),
81                    action.boxed_clone(),
82                    context_predicate.clone(),
83                    false,
84                    None,
85                    &DummyKeyboardMapper,
86                );
87                keybinding.expect("failed to load keybinding")
88            })
89        });
90        iter.flatten()
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::TestAppContext;
98
99    actions!([TestAction]);
100
101    #[gpui::test]
102    fn test_default_empty(_cx: &mut TestAppContext) {
103        assert!(ActionBindingCollection::default().entries.is_empty());
104    }
105
106    #[gpui::test]
107    fn test_single_binding(_cx: &mut TestAppContext) {
108        let collection = ActionBindingCollection::default();
109        let collection = collection.with::<TestAction>("enter");
110
111        let entry = collection.entries.get(&TypeId::of::<TestAction>());
112        let entry = entry.map(|entry| &entry.keystrokes);
113        assert_eq!(entry, Some(&vec![SharedString::new_static("enter")]));
114
115        let out_bindings = collection.as_keybindings(None).collect::<Vec<_>>();
116        assert_eq!(out_bindings.len(), 1);
117    }
118
119    #[gpui::test]
120    fn test_single_action_many_keys(_cx: &mut TestAppContext) {
121        let collection = ActionBindingCollection::default();
122        let collection = collection.with::<TestAction>("enter");
123        let collection = collection.with::<TestAction>("escape");
124
125        let entry = collection.entries.get(&TypeId::of::<TestAction>());
126        let entry = entry.map(|entry| &entry.keystrokes);
127        assert_eq!(
128            entry,
129            Some(&vec![
130                SharedString::new_static("enter"),
131                SharedString::new_static("escape")
132            ])
133        );
134
135        let out_bindings = collection.as_keybindings(None).collect::<Vec<_>>();
136        assert_eq!(out_bindings.len(), 2);
137    }
138
139    #[gpui::test]
140    fn test_single_action_many_keys_context(_cx: &mut TestAppContext) {
141        let collection = ActionBindingCollection::default();
142        let collection = collection.with::<TestAction>("enter");
143        let collection = collection.with::<TestAction>("escape");
144        let out_bindings = collection
145            .as_keybindings(Some("test_content"))
146            .collect::<Vec<_>>();
147        assert_eq!(out_bindings.len(), 2);
148    }
149}