Skip to main content

gpui/
keymap.rs

1mod binding;
2mod collection;
3mod context;
4
5pub use binding::*;
6pub use collection::*;
7pub use context::*;
8
9use crate::{Action, AsKeystroke, Keystroke, Unbind, is_no_action, is_unbind};
10use collections::{HashSet, TypeIdHashMap};
11use smallvec::SmallVec;
12
13/// An identifier for one state of a [`Keymap`].
14///
15/// The version changes whenever bindings are added or the keymap is cleared. Store the version
16/// with a cached result, and recompute the result when the version differs.
17#[derive(Copy, Clone, Eq, PartialEq, Default)]
18pub struct KeymapVersion(usize);
19
20/// The set of key bindings for the application, and the rules for choosing among them.
21///
22/// Usually you don't build a `Keymap` yourself. [`App`](crate::App) holds one, and
23/// [`App::bind_keys`](crate::App::bind_keys) adds to it. It is also the place where the
24/// precedence rules are implemented, so this is the reference for how a keystroke resolves.
25///
26/// # Examples
27///
28/// ```standalone_crate
29/// use gpui::{Action, KeyBinding, KeyContext, Keymap, Keystroke, actions};
30///
31/// actions!(keymap_docs, [Save, Format]);
32///
33/// let keymap = Keymap::new(vec![
34///     KeyBinding::new("cmd-s", Format, Some("Workspace")),
35///     KeyBinding::new("cmd-s", Save, Some("Editor")),
36/// ]);
37///
38/// // In an editor inside a workspace, the deeper `Editor` binding wins.
39/// let typed = [Keystroke::parse("cmd-s").unwrap()];
40/// let context = [
41///     KeyContext::parse("Workspace").unwrap(),
42///     KeyContext::parse("Editor").unwrap(),
43/// ];
44/// let (bindings, pending) = keymap.bindings_for_input(&typed, &context);
45/// assert_eq!(bindings[0].action().name(), "keymap_docs::Save");
46/// assert!(!pending);
47/// ```
48///
49/// See [keyboard input](crate::_documentation::concepts::input_dispatch) for the full precedence
50/// rules as they apply during dispatch.
51#[derive(Default)]
52pub struct Keymap {
53    bindings: Vec<KeyBinding>,
54    binding_indices_by_action_id: TypeIdHashMap<SmallVec<[usize; 3]>>,
55    disabled_binding_indices: Vec<usize>,
56    version: KeymapVersion,
57}
58
59/// The position of a binding in a [`Keymap`], counted in the order bindings were added.
60///
61/// A larger index was added later, so it takes precedence over a smaller one at the same depth.
62#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
63pub struct BindingIndex(usize);
64
65fn disabled_binding_matches_context(disabled_binding: &KeyBinding, binding: &KeyBinding) -> bool {
66    match (
67        &disabled_binding.context_predicate,
68        &binding.context_predicate,
69    ) {
70        (None, _) => true,
71        (Some(_), None) => false,
72        (Some(disabled_predicate), Some(predicate)) => disabled_predicate.is_superset(predicate),
73    }
74}
75
76fn binding_is_unbound(disabled_binding: &KeyBinding, binding: &KeyBinding) -> bool {
77    disabled_binding.keystrokes == binding.keystrokes
78        && disabled_binding
79            .action()
80            .as_any()
81            .downcast_ref::<Unbind>()
82            .is_some_and(|unbind| unbind.0.as_ref() == binding.action.name())
83}
84
85impl Keymap {
86    /// Creates a keymap that contains `bindings`, in the order given.
87    pub fn new(bindings: Vec<KeyBinding>) -> Self {
88        let mut this = Self::default();
89        this.add_bindings(bindings);
90        this
91    }
92
93    /// Returns the current version. See [`KeymapVersion`].
94    pub fn version(&self) -> KeymapVersion {
95        self.version
96    }
97
98    /// Adds `bindings` after the existing ones and changes the [`version`](Self::version).
99    ///
100    /// A binding added later wins over an earlier one at the same depth. Add the user's bindings
101    /// after the defaults so that they take precedence.
102    pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
103        for binding in bindings {
104            let action_id = binding.action().as_any().type_id();
105            if is_no_action(&*binding.action) || is_unbind(&*binding.action) {
106                self.disabled_binding_indices.push(self.bindings.len());
107            } else {
108                self.binding_indices_by_action_id
109                    .entry(action_id)
110                    .or_default()
111                    .push(self.bindings.len());
112            }
113            self.bindings.push(binding);
114        }
115
116        self.version.0 += 1;
117    }
118
119    /// Removes every binding and changes the [`version`](Self::version).
120    pub fn clear(&mut self) {
121        self.bindings.clear();
122        self.binding_indices_by_action_id.clear();
123        self.disabled_binding_indices.clear();
124        self.version.0 += 1;
125    }
126
127    /// Iterates over all bindings, in the order they were added.
128    ///
129    /// The iterator includes [`NoAction`](crate::NoAction) and [`Unbind`](crate::Unbind) entries.
130    pub fn bindings(&self) -> impl DoubleEndedIterator<Item = &KeyBinding> + ExactSizeIterator {
131        self.bindings.iter()
132    }
133
134    /// Iterates over the bindings that run `action`, in the order they were added.
135    ///
136    /// Bindings disabled by a later [`NoAction`](crate::NoAction) or [`Unbind`](crate::Unbind) are
137    /// left out. This ignores key contexts, so a binding is included even when it can't fire in the
138    /// current focus. To find the bindings that apply now, use
139    /// [`Window::bindings_for_action`](crate::Window::bindings_for_action). For a shortcut label,
140    /// use the last binding, which was added most recently.
141    pub fn bindings_for_action<'a>(
142        &'a self,
143        action: &'a dyn Action,
144    ) -> impl 'a + DoubleEndedIterator<Item = &'a KeyBinding> {
145        let action_id = action.type_id();
146        let binding_indices = self
147            .binding_indices_by_action_id
148            .get(&action_id)
149            .map_or(&[] as _, SmallVec::as_slice)
150            .iter();
151
152        binding_indices.filter_map(|ix| {
153            let binding = &self.bindings[*ix];
154            if !binding.action().partial_eq(action) {
155                return None;
156            }
157
158            for disabled_ix in &self.disabled_binding_indices {
159                if disabled_ix > ix {
160                    let disabled_binding = &self.bindings[*disabled_ix];
161                    if disabled_binding.keystrokes != binding.keystrokes {
162                        continue;
163                    }
164
165                    if is_no_action(&*disabled_binding.action) {
166                        // Like `bindings_for_input`, a null only hides bindings from
167                        // equal or weaker sources (a larger meta is weaker).
168                        let source = binding.meta.map_or(0, |meta| meta.0);
169                        let removal_source = disabled_binding.meta.map_or(0, |meta| meta.0);
170                        if source >= removal_source
171                            && disabled_binding_matches_context(disabled_binding, binding)
172                        {
173                            return None;
174                        }
175                    } else if is_unbind(&*disabled_binding.action)
176                        && disabled_binding_matches_context(disabled_binding, binding)
177                        && binding_is_unbound(disabled_binding, binding)
178                    {
179                        return None;
180                    }
181                }
182            }
183
184            Some(binding)
185        })
186    }
187
188    /// Returns every binding that matches all of `input`, ignoring key contexts.
189    ///
190    /// The bindings are in precedence order: the most recently added comes first. Bindings that
191    /// need more keystrokes than `input` are left out. `NoAction` and `Unbind` entries that match
192    /// are included, but their effect on other bindings is not applied.
193    pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
194        self.bindings()
195            .rev()
196            .filter(|binding| {
197                binding
198                    .match_keystrokes(input)
199                    .is_some_and(|pending| !pending)
200            })
201            .cloned()
202            .collect()
203    }
204
205    /// Returns the bindings that match `input` in `context_stack`, and whether a longer input
206    /// could still match a binding.
207    ///
208    /// The first value holds the matching bindings, highest precedence first. The second value is
209    /// `true` when some binding needs more keystrokes than `input`, so the caller should wait for
210    /// the next keystroke.
211    ///
212    /// Precedence works as follows:
213    ///
214    /// - **Depth.** A binding's depth is the deepest point in `context_stack` where its predicate
215    ///   matches. A binding in a deeper context wins over one in a shallower context. A binding
216    ///   without a context is as deep as the stack, so it is at least as deep as any
217    ///   context-specific binding. At equal depth, the order rule below decides.
218    /// - **Order.** At the same depth, the binding added later wins.
219    /// - **`NoAction`.** A winning [`NoAction`](crate::NoAction) removes the lower-precedence
220    ///   bindings for the same keystrokes, unless they come from a stronger source. See
221    ///   [`KeyBindingMetaIndex`](crate::KeyBindingMetaIndex).
222    /// - **`Unbind`.** A winning [`Unbind`](crate::Unbind) removes the lower-precedence binding for
223    ///   the same keystrokes that runs the action it names, whatever that binding's context.
224    ///
225    /// Several bindings can be returned, because a match that isn't suppressed stays in the list.
226    ///
227    /// # Examples
228    ///
229    /// ```standalone_crate
230    /// use gpui::{Action, KeyBinding, KeyContext, Keymap, Keystroke, actions};
231    ///
232    /// actions!(input_docs, [Save, Format]);
233    ///
234    /// let keymap = Keymap::new(vec![
235    ///     KeyBinding::new("cmd-s", Format, Some("Workspace")),
236    ///     KeyBinding::new("cmd-s", Save, Some("Editor")),
237    /// ]);
238    ///
239    /// // The `Editor` binding is at depth 2 and the `Workspace` binding at depth 1.
240    /// let (bindings, _) = keymap.bindings_for_input(
241    ///     &[Keystroke::parse("cmd-s").unwrap()],
242    ///     &[
243    ///         KeyContext::parse("Workspace").unwrap(),
244    ///         KeyContext::parse("Editor").unwrap(),
245    ///     ],
246    /// );
247    /// assert_eq!(bindings[0].action().name(), "input_docs::Save");
248    /// ```
249    pub fn bindings_for_input(
250        &self,
251        input: &[impl AsKeystroke],
252        context_stack: &[KeyContext],
253    ) -> (SmallVec<[KeyBinding; 1]>, bool) {
254        let (bindings, pending) = self.resolve_bindings_for_input(
255            input,
256            context_stack,
257            self.bindings().enumerate().rev(),
258        );
259        (
260            bindings
261                .into_iter()
262                .map(|(_, binding)| binding.clone())
263                .collect(),
264            pending,
265        )
266    }
267
268    /// Same as `bindings_for_input`, but only looks at `keymap_bindings` instead of the whole
269    /// keymap, and returns each binding with its keymap index.
270    ///
271    /// `keymap_bindings` must be in reverse keymap order and include every binding that could
272    /// match `input`. For example, leaving out a `null` binding would make the binding it disables
273    /// look like it wins. `bindings_for_input` passes the whole keymap, which always meets this but
274    /// goes through every binding on every call. Passing a pre-filtered list is cheaper when
275    /// resolving many inputs against it.
276    fn resolve_bindings_for_input<'a>(
277        &self,
278        input: &[impl AsKeystroke],
279        context_stack: &[KeyContext],
280        keymap_bindings: impl IntoIterator<Item = (usize, &'a KeyBinding)>,
281    ) -> (SmallVec<[(BindingIndex, &'a KeyBinding); 1]>, bool) {
282        let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
283        let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new();
284
285        for (ix, binding) in keymap_bindings {
286            let Some(depth) = self.binding_enabled(binding, context_stack) else {
287                continue;
288            };
289            let Some(pending) = binding.match_keystrokes(input) else {
290                continue;
291            };
292
293            if !pending {
294                matched_bindings.push((depth, BindingIndex(ix), binding));
295            } else {
296                pending_bindings.push((BindingIndex(ix), binding));
297            }
298        }
299
300        matched_bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| {
301            depth_b.cmp(depth_a).then(ix_b.cmp(ix_a))
302        });
303
304        let mut bindings: SmallVec<[_; 1]> = SmallVec::new();
305        let mut first_binding_index = None;
306        let mut unbound_bindings: Vec<&KeyBinding> = Vec::new();
307        // A `NoAction` binding suppresses out-ranked bindings from sources with
308        // equal or weaker precedence, while bindings from stronger sources (a
309        // smaller meta, e.g. a user binding vs a base keymap null) still apply.
310        // Bindings without a meta are treated as user bindings.
311        let mut no_action_meta: Option<u32> = None;
312
313        for (_, ix, binding) in matched_bindings {
314            let meta = binding.meta.map_or(0, |meta| meta.0);
315            if is_no_action(&*binding.action) {
316                no_action_meta = Some(no_action_meta.map_or(meta, |existing| existing.min(meta)));
317                continue;
318            }
319
320            if no_action_meta.is_some_and(|no_action_meta| meta >= no_action_meta) {
321                continue;
322            }
323
324            if is_unbind(&*binding.action) {
325                unbound_bindings.push(binding);
326                continue;
327            }
328
329            if unbound_bindings
330                .iter()
331                .any(|disabled_binding| binding_is_unbound(disabled_binding, binding))
332            {
333                continue;
334            }
335
336            bindings.push((ix, binding));
337            first_binding_index.get_or_insert(ix);
338        }
339
340        let mut pending = HashSet::default();
341        for (ix, binding) in pending_bindings.into_iter().rev() {
342            if let Some(binding_ix) = first_binding_index
343                && binding_ix > ix
344            {
345                continue;
346            }
347            if is_no_action(&*binding.action) || is_unbind(&*binding.action) {
348                pending.remove(&&binding.keystrokes);
349                continue;
350            }
351            pending.insert(&binding.keystrokes);
352        }
353
354        (bindings, !pending.is_empty())
355    }
356    /// Check if the given binding is enabled, given a certain key context.
357    /// Returns the deepest depth at which the binding matches, or None if it doesn't match.
358    fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
359        if let Some(predicate) = &binding.context_predicate {
360            predicate.depth_of(contexts)
361        } else {
362            Some(contexts.len())
363        }
364    }
365
366    /// Returns the bindings that `input` could still complete with more keystrokes, highest
367    /// precedence first, for the given `context_stack`.
368    ///
369    /// Use it to list the shortcuts available after a prefix, such as after `ctrl-k`. Bindings
370    /// that match `input` exactly, and `NoAction` and `Unbind` entries, are not included.
371    pub fn possible_next_bindings_for_input(
372        &self,
373        input: &[Keystroke],
374        context_stack: &[KeyContext],
375    ) -> Vec<KeyBinding> {
376        // Keeps `null` and `Unbind` bindings, which can disable candidates.
377        let bindings_extending_input = self
378            .bindings()
379            .enumerate()
380            .rev()
381            .filter(|(_, binding)| {
382                binding
383                    .match_keystrokes(input)
384                    .is_some_and(|pending| pending)
385            })
386            .collect::<Vec<_>>();
387
388        let mut full_input = SmallVec::<[&Keystroke; 4]>::new();
389        let mut candidates = bindings_extending_input
390            .iter()
391            .filter(|(_, binding)| !is_no_action(&*binding.action) && !is_unbind(&*binding.action))
392            .filter_map(|&(ix, binding)| {
393                let depth = self.binding_enabled(binding, context_stack)?;
394                // Use the typed input rather than the binding's own keystrokes. A typed keystroke
395                // can match through its physical key, like `alt-s`, or the character it produced,
396                // like `ß`.
397                full_input.clear();
398                full_input.extend(input.iter());
399                full_input.extend(
400                    binding.keystrokes[input.len()..]
401                        .iter()
402                        .map(AsKeystroke::as_keystroke),
403                );
404                let (dispatched, _) = self.resolve_bindings_for_input(
405                    &full_input,
406                    context_stack,
407                    bindings_extending_input.iter().copied(),
408                );
409                dispatched
410                    .iter()
411                    .any(|(dispatched_ix, _)| *dispatched_ix == BindingIndex(ix))
412                    .then_some((depth, ix, binding))
413            })
414            .collect::<Vec<_>>();
415        candidates.sort_unstable_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| {
416            depth_b.cmp(depth_a).then(ix_b.cmp(ix_a))
417        });
418
419        candidates
420            .into_iter()
421            .map(|(_, _, binding)| binding.clone())
422            .collect()
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate as gpui;
430    use gpui::{NoAction, Unbind};
431
432    actions!(
433        test_only,
434        [ActionAlpha, ActionBeta, ActionGamma, ActionDelta,]
435    );
436
437    #[test]
438    fn test_keymap() {
439        let bindings = [
440            KeyBinding::new("ctrl-a", ActionAlpha {}, None),
441            KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")),
442            KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")),
443        ];
444
445        let mut keymap = Keymap::default();
446        keymap.add_bindings(bindings.clone());
447
448        // global bindings are enabled in all contexts
449        assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0));
450        assert_eq!(
451            keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]),
452            Some(1)
453        );
454
455        // contextual bindings are enabled in contexts that match their predicate
456        assert_eq!(
457            keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]),
458            None
459        );
460        assert_eq!(
461            keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]),
462            Some(1)
463        );
464
465        assert_eq!(
466            keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]),
467            None
468        );
469        assert_eq!(
470            keymap.binding_enabled(
471                &bindings[2],
472                &[KeyContext::parse("editor mode=full").unwrap()]
473            ),
474            Some(1)
475        );
476    }
477
478    #[test]
479    fn test_depth_precedence() {
480        let bindings = [
481            KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")),
482            KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor")),
483        ];
484
485        let mut keymap = Keymap::default();
486        keymap.add_bindings(bindings);
487
488        let (result, pending) = keymap.bindings_for_input(
489            &[Keystroke::parse("ctrl-a").unwrap()],
490            &[
491                KeyContext::parse("pane").unwrap(),
492                KeyContext::parse("editor").unwrap(),
493            ],
494        );
495
496        assert!(!pending);
497        assert_eq!(result.len(), 2);
498        assert!(result[0].action.partial_eq(&ActionGamma {}));
499        assert!(result[1].action.partial_eq(&ActionBeta {}));
500    }
501
502    #[test]
503    fn test_keymap_disabled() {
504        let bindings = [
505            KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")),
506            KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")),
507            KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")),
508            KeyBinding::new("ctrl-b", NoAction {}, None),
509        ];
510
511        let mut keymap = Keymap::default();
512        keymap.add_bindings(bindings);
513
514        // binding is only enabled in a specific context
515        assert!(
516            keymap
517                .bindings_for_input(
518                    &[Keystroke::parse("ctrl-a").unwrap()],
519                    &[KeyContext::parse("barf").unwrap()],
520                )
521                .0
522                .is_empty()
523        );
524        assert!(
525            !keymap
526                .bindings_for_input(
527                    &[Keystroke::parse("ctrl-a").unwrap()],
528                    &[KeyContext::parse("editor").unwrap()],
529                )
530                .0
531                .is_empty()
532        );
533
534        // binding is disabled in a more specific context
535        assert!(
536            keymap
537                .bindings_for_input(
538                    &[Keystroke::parse("ctrl-a").unwrap()],
539                    &[KeyContext::parse("editor mode=full").unwrap()],
540                )
541                .0
542                .is_empty()
543        );
544
545        // binding is globally disabled
546        assert!(
547            keymap
548                .bindings_for_input(
549                    &[Keystroke::parse("ctrl-b").unwrap()],
550                    &[KeyContext::parse("barf").unwrap()],
551                )
552                .0
553                .is_empty()
554        );
555    }
556
557    #[test]
558    /// Tests for https://github.com/zed-industries/zed/issues/30259
559    fn test_multiple_keystroke_binding_disabled() {
560        let bindings = [
561            KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
562            KeyBinding::new("space w w", NoAction {}, Some("editor")),
563        ];
564
565        let mut keymap = Keymap::default();
566        keymap.add_bindings(bindings);
567
568        let space = || Keystroke::parse("space").unwrap();
569        let w = || Keystroke::parse("w").unwrap();
570
571        let space_w = [space(), w()];
572        let space_w_w = [space(), w(), w()];
573
574        let workspace_context = || [KeyContext::parse("workspace").unwrap()];
575
576        let editor_workspace_context = || {
577            [
578                KeyContext::parse("workspace").unwrap(),
579                KeyContext::parse("editor").unwrap(),
580            ]
581        };
582
583        // Ensure `space` results in pending input on the workspace, but not editor
584        let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context());
585        assert!(space_workspace.0.is_empty());
586        assert!(space_workspace.1);
587
588        let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
589        assert!(space_editor.0.is_empty());
590        assert!(!space_editor.1);
591
592        // Ensure `space w` results in pending input on the workspace, but not editor
593        let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context());
594        assert!(space_w_workspace.0.is_empty());
595        assert!(space_w_workspace.1);
596
597        let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context());
598        assert!(space_w_editor.0.is_empty());
599        assert!(!space_w_editor.1);
600
601        // Ensure `space w w` results in the binding in the workspace, but not in the editor
602        let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context());
603        assert!(!space_w_w_workspace.0.is_empty());
604        assert!(!space_w_w_workspace.1);
605
606        let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context());
607        assert!(space_w_w_editor.0.is_empty());
608        assert!(!space_w_w_editor.1);
609
610        // Now test what happens if we have another binding defined AFTER the NoAction
611        // that should result in pending
612        let bindings = [
613            KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
614            KeyBinding::new("space w w", NoAction {}, Some("editor")),
615            KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
616        ];
617        let mut keymap = Keymap::default();
618        keymap.add_bindings(bindings);
619
620        let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
621        assert!(space_editor.0.is_empty());
622        assert!(space_editor.1);
623
624        // Now test what happens if we have another binding defined BEFORE the NoAction
625        // that should result in pending
626        let bindings = [
627            KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
628            KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
629            KeyBinding::new("space w w", NoAction {}, Some("editor")),
630        ];
631        let mut keymap = Keymap::default();
632        keymap.add_bindings(bindings);
633
634        let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
635        assert!(space_editor.0.is_empty());
636        assert!(space_editor.1);
637
638        // Now test what happens if we have another binding defined at a higher context
639        // that should result in pending
640        let bindings = [
641            KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
642            KeyBinding::new("space w x", ActionAlpha {}, Some("workspace")),
643            KeyBinding::new("space w w", NoAction {}, Some("editor")),
644        ];
645        let mut keymap = Keymap::default();
646        keymap.add_bindings(bindings);
647
648        let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
649        assert!(space_editor.0.is_empty());
650        assert!(space_editor.1);
651    }
652
653    #[test]
654    fn test_override_multikey() {
655        let bindings = [
656            KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
657            KeyBinding::new("ctrl-w", NoAction {}, Some("editor")),
658        ];
659
660        let mut keymap = Keymap::default();
661        keymap.add_bindings(bindings);
662
663        // Ensure `space` results in pending input on the workspace, but not editor
664        let (result, pending) = keymap.bindings_for_input(
665            &[Keystroke::parse("ctrl-w").unwrap()],
666            &[KeyContext::parse("editor").unwrap()],
667        );
668        assert!(result.is_empty());
669        assert!(pending);
670
671        let bindings = [
672            KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
673            KeyBinding::new("ctrl-w", ActionBeta {}, Some("editor")),
674        ];
675
676        let mut keymap = Keymap::default();
677        keymap.add_bindings(bindings);
678
679        // Ensure `space` results in pending input on the workspace, but not editor
680        let (result, pending) = keymap.bindings_for_input(
681            &[Keystroke::parse("ctrl-w").unwrap()],
682            &[KeyContext::parse("editor").unwrap()],
683        );
684        assert_eq!(result.len(), 1);
685        assert!(!pending);
686    }
687
688    #[test]
689    fn test_simple_disable() {
690        let bindings = [
691            KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")),
692            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")),
693        ];
694
695        let mut keymap = Keymap::default();
696        keymap.add_bindings(bindings);
697
698        // Ensure `space` results in pending input on the workspace, but not editor
699        let (result, pending) = keymap.bindings_for_input(
700            &[Keystroke::parse("ctrl-x").unwrap()],
701            &[KeyContext::parse("editor").unwrap()],
702        );
703        assert!(result.is_empty());
704        assert!(!pending);
705    }
706
707    #[test]
708    fn test_disable_weaker_sources_only() {
709        const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(0);
710        const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(1);
711        const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(2);
712        const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(3);
713
714        let editor_context = || [KeyContext::parse("editor").unwrap()];
715        let ctrl_x = || [Keystroke::parse("ctrl-x").unwrap()];
716
717        // A base keymap null disables a default binding in the same context.
718        let mut keymap = Keymap::default();
719        keymap.add_bindings([
720            KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT),
721            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE),
722        ]);
723        let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context());
724        assert!(result.is_empty());
725
726        // A user binding is not affected by base keymap or default nulls.
727        let mut keymap = Keymap::default();
728        keymap.add_bindings([
729            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(DEFAULT),
730            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE),
731            KeyBinding::new("ctrl-x", ActionBeta {}, None).with_meta(USER),
732        ]);
733        let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context());
734        assert_eq!(result.len(), 1);
735        assert!(result[0].action.partial_eq(&ActionBeta {}));
736
737        // A user binding at a shallower context is not disabled by a deeper
738        // base keymap null.
739        let mut keymap = Keymap::default();
740        keymap.add_bindings([
741            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE),
742            KeyBinding::new("ctrl-x", ActionBeta {}, Some("workspace")).with_meta(USER),
743        ]);
744        let (result, _) = keymap.bindings_for_input(
745            &ctrl_x(),
746            &[
747                KeyContext::parse("workspace").unwrap(),
748                KeyContext::parse("editor").unwrap(),
749            ],
750        );
751        assert_eq!(result.len(), 1);
752        assert!(result[0].action.partial_eq(&ActionBeta {}));
753
754        // A vim binding survives a base keymap null, and a user null disables
755        // everything.
756        let mut keymap = Keymap::default();
757        keymap.add_bindings([
758            KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT),
759            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE),
760            KeyBinding::new("ctrl-x", ActionGamma {}, Some("editor")).with_meta(VIM),
761        ]);
762        let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context());
763        assert_eq!(result.len(), 1);
764        assert!(result[0].action.partial_eq(&ActionGamma {}));
765
766        let mut keymap = Keymap::default();
767        keymap.add_bindings([
768            KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT),
769            KeyBinding::new("ctrl-x", ActionGamma {}, Some("editor")).with_meta(VIM),
770            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(USER),
771        ]);
772        let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context());
773        assert!(result.is_empty());
774    }
775
776    #[test]
777    fn test_bindings_for_action_disable_weaker_sources_only() {
778        // A base keymap null (meta 2) listed after the binding hides only
779        // bindings from equal or weaker sources, matching `bindings_for_input`.
780        for (source, expected) in [
781            (None, 1),
782            (Some(KeyBindingMetaIndex(0)), 1),
783            (Some(KeyBindingMetaIndex(1)), 1),
784            (Some(KeyBindingMetaIndex(2)), 0),
785            (Some(KeyBindingMetaIndex(3)), 0),
786        ] {
787            let mut binding = KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor"));
788            if let Some(source) = source {
789                binding.set_meta(source);
790            }
791            let mut keymap = Keymap::default();
792            keymap.add_bindings([
793                binding,
794                KeyBinding::new("ctrl-x", NoAction {}, Some("editor"))
795                    .with_meta(KeyBindingMetaIndex(2)),
796            ]);
797
798            let (candidates, _) = keymap.bindings_for_input(
799                &[Keystroke::parse("ctrl-x").unwrap()],
800                &[KeyContext::parse("editor").unwrap()],
801            );
802            assert_eq!(candidates.len(), expected, "{source:?}");
803            assert_eq!(
804                keymap.bindings_for_action(&ActionAlpha {}).count(),
805                expected,
806                "{source:?}"
807            );
808        }
809    }
810
811    #[test]
812    fn test_fail_to_disable() {
813        // disabled at the wrong level
814        let bindings = [
815            KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")),
816            KeyBinding::new("ctrl-x", NoAction {}, Some("workspace")),
817        ];
818
819        let mut keymap = Keymap::default();
820        keymap.add_bindings(bindings);
821
822        // Ensure `space` results in pending input on the workspace, but not editor
823        let (result, pending) = keymap.bindings_for_input(
824            &[Keystroke::parse("ctrl-x").unwrap()],
825            &[
826                KeyContext::parse("workspace").unwrap(),
827                KeyContext::parse("editor").unwrap(),
828            ],
829        );
830        assert_eq!(result.len(), 1);
831        assert!(!pending);
832    }
833
834    #[test]
835    fn test_disable_deeper() {
836        let bindings = [
837            KeyBinding::new("ctrl-x", ActionAlpha {}, Some("workspace")),
838            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")),
839        ];
840
841        let mut keymap = Keymap::default();
842        keymap.add_bindings(bindings);
843
844        // Ensure `space` results in pending input on the workspace, but not editor
845        let (result, pending) = keymap.bindings_for_input(
846            &[Keystroke::parse("ctrl-x").unwrap()],
847            &[
848                KeyContext::parse("workspace").unwrap(),
849                KeyContext::parse("editor").unwrap(),
850            ],
851        );
852        assert_eq!(result.len(), 0);
853        assert!(!pending);
854    }
855
856    #[test]
857    fn test_pending_match_enabled() {
858        let bindings = [
859            KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
860            KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")),
861        ];
862        let mut keymap = Keymap::default();
863        keymap.add_bindings(bindings);
864
865        let matched = keymap.bindings_for_input(
866            &[Keystroke::parse("ctrl-x")].map(Result::unwrap),
867            &[
868                KeyContext::parse("Workspace"),
869                KeyContext::parse("Pane"),
870                KeyContext::parse("Editor vim_mode=normal"),
871            ]
872            .map(Result::unwrap),
873        );
874        assert_eq!(matched.0.len(), 1);
875        assert!(matched.0[0].action.partial_eq(&ActionBeta));
876        assert!(matched.1);
877    }
878
879    #[test]
880    fn test_pending_match_enabled_extended() {
881        let bindings = [
882            KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
883            KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")),
884        ];
885        let mut keymap = Keymap::default();
886        keymap.add_bindings(bindings);
887
888        let matched = keymap.bindings_for_input(
889            &[Keystroke::parse("ctrl-x")].map(Result::unwrap),
890            &[
891                KeyContext::parse("Workspace"),
892                KeyContext::parse("Pane"),
893                KeyContext::parse("Editor vim_mode=normal"),
894            ]
895            .map(Result::unwrap),
896        );
897        assert_eq!(matched.0.len(), 1);
898        assert!(matched.0[0].action.partial_eq(&ActionBeta));
899        assert!(!matched.1);
900        let bindings = [
901            KeyBinding::new("ctrl-x", ActionBeta, Some("Workspace")),
902            KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")),
903        ];
904        let mut keymap = Keymap::default();
905        keymap.add_bindings(bindings);
906
907        let matched = keymap.bindings_for_input(
908            &[Keystroke::parse("ctrl-x")].map(Result::unwrap),
909            &[
910                KeyContext::parse("Workspace"),
911                KeyContext::parse("Pane"),
912                KeyContext::parse("Editor vim_mode=normal"),
913            ]
914            .map(Result::unwrap),
915        );
916        assert_eq!(matched.0.len(), 1);
917        assert!(matched.0[0].action.partial_eq(&ActionBeta));
918        assert!(!matched.1);
919    }
920
921    #[test]
922    fn test_overriding_prefix() {
923        let bindings = [
924            KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")),
925            KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
926        ];
927        let mut keymap = Keymap::default();
928        keymap.add_bindings(bindings);
929
930        let matched = keymap.bindings_for_input(
931            &[Keystroke::parse("ctrl-x")].map(Result::unwrap),
932            &[
933                KeyContext::parse("Workspace"),
934                KeyContext::parse("Pane"),
935                KeyContext::parse("Editor vim_mode=normal"),
936            ]
937            .map(Result::unwrap),
938        );
939        assert_eq!(matched.0.len(), 1);
940        assert!(matched.0[0].action.partial_eq(&ActionBeta));
941        assert!(!matched.1);
942    }
943
944    #[test]
945    fn test_context_precedence_with_same_source() {
946        // Test case: User has both Workspace and Editor bindings for the same key
947        // Editor binding should take precedence over Workspace binding
948        let bindings = [
949            KeyBinding::new("cmd-r", ActionAlpha {}, Some("Workspace")),
950            KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")),
951        ];
952
953        let mut keymap = Keymap::default();
954        keymap.add_bindings(bindings);
955
956        // Test with context stack: [Workspace, Editor] (Editor is deeper)
957        let (result, _) = keymap.bindings_for_input(
958            &[Keystroke::parse("cmd-r").unwrap()],
959            &[
960                KeyContext::parse("Workspace").unwrap(),
961                KeyContext::parse("Editor").unwrap(),
962            ],
963        );
964
965        // Both bindings should be returned, but Editor binding should be first (highest precedence)
966        assert_eq!(result.len(), 2);
967        assert!(result[0].action.partial_eq(&ActionBeta {})); // Editor binding first
968        assert!(result[1].action.partial_eq(&ActionAlpha {})); // Workspace binding second
969    }
970
971    #[test]
972    fn test_bindings_for_action() {
973        let bindings = [
974            KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")),
975            KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")),
976            KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")),
977            KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")),
978            KeyBinding::new("ctrl-b", NoAction {}, Some("editor")),
979        ];
980
981        let mut keymap = Keymap::default();
982        keymap.add_bindings(bindings);
983
984        assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]);
985        assert_bindings(&keymap, &ActionBeta {}, &[]);
986        assert_bindings(&keymap, &ActionGamma {}, &["ctrl-c"]);
987
988        #[track_caller]
989        fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
990            let actual = keymap
991                .bindings_for_action(action)
992                .map(|binding| binding.keystrokes[0].inner().unparse())
993                .collect::<Vec<_>>();
994            assert_eq!(actual, expected, "{:?}", action);
995        }
996    }
997
998    #[test]
999    fn test_targeted_unbind_ignores_target_context() {
1000        let bindings = [
1001            KeyBinding::new("tab", ActionAlpha {}, Some("Editor")),
1002            KeyBinding::new("tab", ActionBeta {}, Some("Editor && showing_completions")),
1003            KeyBinding::new(
1004                "tab",
1005                Unbind("test_only::ActionAlpha".into()),
1006                Some("Editor && edit_prediction"),
1007            ),
1008        ];
1009
1010        let mut keymap = Keymap::default();
1011        keymap.add_bindings(bindings);
1012
1013        let (result, pending) = keymap.bindings_for_input(
1014            &[Keystroke::parse("tab").unwrap()],
1015            &[KeyContext::parse("Editor showing_completions edit_prediction").unwrap()],
1016        );
1017
1018        assert!(!pending);
1019        assert_eq!(result.len(), 1);
1020        assert!(result[0].action.partial_eq(&ActionBeta {}));
1021    }
1022
1023    #[test]
1024    fn test_bindings_for_action_keeps_binding_for_narrower_targeted_unbind() {
1025        let bindings = [
1026            KeyBinding::new("tab", ActionAlpha {}, Some("Editor")),
1027            KeyBinding::new(
1028                "tab",
1029                Unbind("test_only::ActionAlpha".into()),
1030                Some("Editor && edit_prediction"),
1031            ),
1032            KeyBinding::new("tab", ActionBeta {}, Some("Editor && showing_completions")),
1033        ];
1034
1035        let mut keymap = Keymap::default();
1036        keymap.add_bindings(bindings);
1037
1038        assert_bindings(&keymap, &ActionAlpha {}, &["tab"]);
1039        assert_bindings(&keymap, &ActionBeta {}, &["tab"]);
1040
1041        #[track_caller]
1042        fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
1043            let actual = keymap
1044                .bindings_for_action(action)
1045                .map(|binding| binding.keystrokes[0].inner().unparse())
1046                .collect::<Vec<_>>();
1047            assert_eq!(actual, expected, "{:?}", action);
1048        }
1049    }
1050
1051    #[test]
1052    fn test_bindings_for_action_removes_binding_for_broader_targeted_unbind() {
1053        let bindings = [
1054            KeyBinding::new("tab", ActionAlpha {}, Some("Editor && edit_prediction")),
1055            KeyBinding::new(
1056                "tab",
1057                Unbind("test_only::ActionAlpha".into()),
1058                Some("Editor"),
1059            ),
1060        ];
1061
1062        let mut keymap = Keymap::default();
1063        keymap.add_bindings(bindings);
1064
1065        assert!(keymap.bindings_for_action(&ActionAlpha {}).next().is_none());
1066    }
1067
1068    #[test]
1069    fn test_source_precedence_sorting() {
1070        // KeybindSource precedence: User (0) > Vim (1) > Base (2) > Default (3)
1071        // Test that user keymaps take precedence over default keymaps at the same context depth
1072        let mut keymap = Keymap::default();
1073
1074        // Add a default keymap binding first
1075        let mut default_binding = KeyBinding::new("cmd-r", ActionAlpha {}, Some("Editor"));
1076        default_binding.set_meta(KeyBindingMetaIndex(3)); // Default source
1077        keymap.add_bindings([default_binding]);
1078
1079        // Add a user keymap binding
1080        let mut user_binding = KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor"));
1081        user_binding.set_meta(KeyBindingMetaIndex(0)); // User source
1082        keymap.add_bindings([user_binding]);
1083
1084        // Test with Editor context stack
1085        let (result, _) = keymap.bindings_for_input(
1086            &[Keystroke::parse("cmd-r").unwrap()],
1087            &[KeyContext::parse("Editor").unwrap()],
1088        );
1089
1090        // User binding should take precedence over default binding
1091        assert_eq!(result.len(), 2);
1092        assert!(result[0].action.partial_eq(&ActionBeta {}));
1093        assert!(result[1].action.partial_eq(&ActionAlpha {}));
1094    }
1095
1096    #[test]
1097    fn test_possible_next_bindings_exclude_null_bindings() {
1098        let keymap = Keymap::new(vec![
1099            KeyBinding::new("ctrl-k a", ActionAlpha {}, Some("Editor")),
1100            KeyBinding::new("ctrl-k b", ActionBeta {}, Some("Editor")),
1101            KeyBinding::new("ctrl-k a", NoAction {}, Some("Editor")),
1102        ]);
1103
1104        let bindings = keymap.possible_next_bindings_for_input(
1105            &[Keystroke::parse("ctrl-k").unwrap()],
1106            &[KeyContext::parse("Editor").unwrap()],
1107        );
1108        assert_eq!(
1109            describe_bindings(&bindings),
1110            vec![("ctrl-k b".to_string(), "test_only::ActionBeta")]
1111        );
1112    }
1113
1114    #[test]
1115    fn test_possible_next_bindings_exclude_unbound_bindings() {
1116        let keymap = Keymap::new(vec![
1117            KeyBinding::new("ctrl-k a", ActionAlpha {}, Some("Editor")),
1118            KeyBinding::new("ctrl-k b", ActionBeta {}, Some("Editor")),
1119            KeyBinding::new(
1120                "ctrl-k a",
1121                Unbind("test_only::ActionAlpha".into()),
1122                Some("Editor"),
1123            ),
1124        ]);
1125
1126        let bindings = keymap.possible_next_bindings_for_input(
1127            &[Keystroke::parse("ctrl-k").unwrap()],
1128            &[KeyContext::parse("Editor").unwrap()],
1129        );
1130        assert_eq!(
1131            describe_bindings(&bindings),
1132            vec![("ctrl-k b".to_string(), "test_only::ActionBeta")]
1133        );
1134    }
1135
1136    #[test]
1137    fn test_possible_next_bindings_keep_user_binding_over_base_keymap_null() {
1138        const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(0);
1139        const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(2);
1140        const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(3);
1141
1142        let mut keymap = Keymap::default();
1143        keymap.add_bindings([
1144            KeyBinding::new("ctrl-k a", ActionAlpha {}, Some("Editor")).with_meta(DEFAULT),
1145            KeyBinding::new("ctrl-k a", NoAction {}, Some("Editor")).with_meta(BASE),
1146            KeyBinding::new("ctrl-k a", ActionBeta {}, Some("Editor")).with_meta(USER),
1147        ]);
1148
1149        let bindings = keymap.possible_next_bindings_for_input(
1150            &[Keystroke::parse("ctrl-k").unwrap()],
1151            &[KeyContext::parse("Editor").unwrap()],
1152        );
1153        assert_eq!(
1154            describe_bindings(&bindings),
1155            vec![("ctrl-k a".to_string(), "test_only::ActionBeta")]
1156        );
1157    }
1158
1159    #[test]
1160    fn test_possible_next_bindings_apply_disabling_by_context() {
1161        let keymap = Keymap::new(vec![
1162            KeyBinding::new("ctrl-k a", ActionAlpha {}, Some("Workspace")),
1163            KeyBinding::new("ctrl-k a", ActionBeta {}, Some("Editor")),
1164            KeyBinding::new("ctrl-k b", ActionGamma {}, Some("Editor")),
1165            // Only removes the editor binding, so the workspace binding still dispatches.
1166            KeyBinding::new(
1167                "ctrl-k a",
1168                Unbind("test_only::ActionBeta".into()),
1169                Some("Editor"),
1170            ),
1171            // Doesn't apply because `Terminal` isn't in the context stack.
1172            KeyBinding::new("ctrl-k b", NoAction {}, Some("Terminal")),
1173        ]);
1174
1175        let bindings = keymap.possible_next_bindings_for_input(
1176            &[Keystroke::parse("ctrl-k").unwrap()],
1177            &[
1178                KeyContext::parse("Workspace").unwrap(),
1179                KeyContext::parse("Editor").unwrap(),
1180            ],
1181        );
1182        assert_eq!(
1183            describe_bindings(&bindings),
1184            vec![
1185                ("ctrl-k b".to_string(), "test_only::ActionGamma"),
1186                ("ctrl-k a".to_string(), "test_only::ActionAlpha"),
1187            ]
1188        );
1189    }
1190
1191    #[test]
1192    fn test_possible_next_bindings_exclude_longer_chord_disabled_in_deeper_context() {
1193        let keymap = Keymap::new(vec![
1194            KeyBinding::new("space w w", ActionAlpha {}, Some("Workspace")),
1195            KeyBinding::new("space w x", ActionBeta {}, Some("Workspace")),
1196            KeyBinding::new("space w w", NoAction {}, Some("AgentPanel")),
1197        ]);
1198        let context_stack = [
1199            KeyContext::parse("Workspace").unwrap(),
1200            KeyContext::parse("AgentPanel").unwrap(),
1201        ];
1202
1203        for input in ["space", "space w"] {
1204            let input = input
1205                .split(' ')
1206                .map(|keystroke| Keystroke::parse(keystroke).unwrap())
1207                .collect::<Vec<_>>();
1208            let bindings = keymap.possible_next_bindings_for_input(&input, &context_stack);
1209            assert_eq!(
1210                describe_bindings(&bindings),
1211                vec![("space w x".to_string(), "test_only::ActionBeta")]
1212            );
1213        }
1214    }
1215
1216    #[test]
1217    fn test_possible_next_bindings_exclude_disabled_bindings_with_typed_input() {
1218        // Option-S reports both its physical key and the character it produced, so typed input
1219        // matches bindings written with either.
1220        let typed_input = [Keystroke::parse("alt-s->ß").unwrap()];
1221        let keymap = Keymap::new(vec![
1222            KeyBinding::new("ß a", ActionAlpha {}, None),
1223            KeyBinding::new("alt-s a", NoAction {}, None),
1224        ]);
1225        assert!(
1226            keymap
1227                .possible_next_bindings_for_input(&typed_input, &[])
1228                .is_empty()
1229        );
1230
1231        // Cross-check against dispatch of the same typed input.
1232        let typed_then_a = [
1233            Keystroke::parse("alt-s->ß").unwrap(),
1234            Keystroke::parse("a").unwrap(),
1235        ];
1236        let (dispatched, _) = keymap.bindings_for_input(&typed_then_a, &[]);
1237        assert!(dispatched.is_empty());
1238    }
1239
1240    fn describe_bindings(bindings: &[KeyBinding]) -> Vec<(String, &'static str)> {
1241        bindings
1242            .iter()
1243            .map(|binding| {
1244                (
1245                    binding
1246                        .keystrokes()
1247                        .iter()
1248                        .map(|keystroke| keystroke.inner().unparse())
1249                        .collect::<Vec<_>>()
1250                        .join(" "),
1251                    binding.action().name(),
1252                )
1253            })
1254            .collect()
1255    }
1256}