Skip to main content

gpui/
keymap.rs

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