Skip to main content

gpui/keymap/
context.rs

1use crate::SharedString;
2use anyhow::{Context as _, Result};
3use std::fmt;
4
5/// The names and values that describe one element, for matching key bindings.
6///
7/// An element sets its context with [`InteractiveElement::key_context`](crate::InteractiveElement::key_context),
8/// and GPUI collects the contexts along the focus path into a stack. A binding's
9/// [predicate](KeyBindingContextPredicate) is then matched against that stack. A context holds
10/// identifiers, such as `Editor`, and key-value pairs, such as `mode=full`.
11///
12/// # Examples
13///
14/// ```
15/// # use gpui::{KeyContext, SharedString};
16/// let context = KeyContext::parse("Editor mode=full").unwrap();
17/// assert!(context.contains("Editor"));
18/// assert_eq!(context.get("mode"), Some(&SharedString::from("full")));
19/// assert_eq!(context.get("Editor"), None);
20/// ```
21#[derive(Clone, Default, Eq, PartialEq, Hash)]
22pub struct KeyContext(Vec<ContextEntry>);
23
24#[derive(Clone, Debug, Eq, PartialEq, Hash)]
25/// One identifier or key-value pair in a [`KeyContext`].
26pub struct ContextEntry {
27    /// The identifier, or the key of a key-value pair.
28    pub key: SharedString,
29    /// The value of a key-value pair, or `None` for a plain identifier.
30    pub value: Option<SharedString>,
31}
32
33impl<'a> TryFrom<&'a str> for KeyContext {
34    type Error = anyhow::Error;
35
36    fn try_from(value: &'a str) -> Result<Self> {
37        Self::parse(value)
38    }
39}
40
41impl KeyContext {
42    /// Creates an empty context that already has an `os` entry for the current platform.
43    ///
44    /// The value is `macos`, `linux`, `windows` or `unknown`, so a binding can use
45    /// `os == macos` to apply on one platform only.
46    pub fn new_with_defaults() -> Self {
47        let mut context = Self::default();
48        #[cfg(target_os = "macos")]
49        context.set("os", "macos");
50        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
51        context.set("os", "linux");
52        #[cfg(target_os = "windows")]
53        context.set("os", "windows");
54        #[cfg(not(any(
55            target_os = "macos",
56            target_os = "linux",
57            target_os = "freebsd",
58            target_os = "windows"
59        )))]
60        context.set("os", "unknown");
61        context
62    }
63
64    /// Returns the first identifier without a value, which names the element in
65    /// the usual context convention. Returns `None` if there is no identifier.
66    pub fn primary(&self) -> Option<&ContextEntry> {
67        self.0.iter().find(|p| p.value.is_none())
68    }
69
70    /// Returns every entry except the one [`primary`](Self::primary) returns.
71    pub fn secondary(&self) -> impl Iterator<Item = &ContextEntry> {
72        let primary = self.primary();
73        self.0.iter().filter(move |&p| Some(p) != primary)
74    }
75
76    /// Parses a context such as `Editor` or `Editor mode=full`.
77    ///
78    /// The text is a list of entries separated by whitespace. Each entry is either an identifier,
79    /// such as `Editor`, or a key-value pair written `key=value` or `key = value`. Identifier
80    /// characters are letters, digits, `_` and `-`. An entry whose key is already present is
81    /// ignored, so the first one wins. Returns an error for text that isn't a list of such
82    /// entries, such as `.` or `=insert`.
83    ///
84    /// This takes entries only. Operators such as `&&` and `>` aren't part of this format. Use
85    /// [`KeyBindingContextPredicate::parse`] to write a predicate.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// # use gpui::{KeyContext, SharedString};
91    /// let context = KeyContext::parse("Panel Editor mode = insert").unwrap();
92    /// assert!(context.contains("Panel"));
93    /// assert_eq!(context.primary().unwrap().key, SharedString::from("Panel"));
94    /// ```
95    pub fn parse(source: &str) -> Result<Self> {
96        let mut context = Self::default();
97        let source = skip_whitespace(source);
98        Self::parse_expr(source, &mut context)?;
99        Ok(context)
100    }
101
102    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
103        if source.is_empty() {
104            return Ok(());
105        }
106
107        let key = source
108            .chars()
109            .take_while(|c| is_identifier_char(*c))
110            .collect::<String>();
111        // Without this, an unexpected character makes no progress and recurses forever.
112        anyhow::ensure!(
113            !key.is_empty(),
114            "expected a context identifier at {source:?}"
115        );
116        source = skip_whitespace(&source[key.len()..]);
117        if let Some(suffix) = source.strip_prefix('=') {
118            source = skip_whitespace(suffix);
119            let value = source
120                .chars()
121                .take_while(|c| is_identifier_char(*c))
122                .collect::<String>();
123            source = skip_whitespace(&source[value.len()..]);
124            context.set(key, value);
125        } else {
126            context.add(key);
127        }
128
129        Self::parse_expr(source, context)
130    }
131
132    /// Returns whether this context has no entries.
133    pub fn is_empty(&self) -> bool {
134        self.0.is_empty()
135    }
136
137    /// Removes every entry.
138    pub fn clear(&mut self) {
139        self.0.clear();
140    }
141
142    /// Adds each entry of `other` whose key this context doesn't already have.
143    pub fn extend(&mut self, other: &Self) {
144        for entry in &other.0 {
145            if !self.contains(&entry.key) {
146                self.0.push(entry.clone());
147            }
148        }
149    }
150
151    /// Adds an identifier, unless this context already has an entry with that key.
152    pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
153        let key = identifier.into();
154
155        if !self.contains(&key) {
156            self.0.push(ContextEntry { key, value: None })
157        }
158    }
159
160    /// Adds a key-value pair, unless this context already has an entry with that key.
161    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
162        let key = key.into();
163        if !self.contains(&key) {
164            self.0.push(ContextEntry {
165                key,
166                value: Some(value.into()),
167            })
168        }
169    }
170
171    /// Returns whether this context has an entry with `key`, whether or not that entry has a value.
172    pub fn contains(&self, key: &str) -> bool {
173        self.0.iter().any(|entry| entry.key.as_ref() == key)
174    }
175
176    /// Returns the value of the first entry with `key`. Returns `None` if there is no such entry,
177    /// or if the entry is a plain identifier.
178    pub fn get(&self, key: &str) -> Option<&SharedString> {
179        self.0
180            .iter()
181            .find(|entry| entry.key.as_ref() == key)?
182            .value
183            .as_ref()
184    }
185}
186
187impl fmt::Debug for KeyContext {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        let mut entries = self.0.iter().peekable();
190        while let Some(entry) = entries.next() {
191            if let Some(ref value) = entry.value {
192                write!(f, "{}={}", entry.key, value)?;
193            } else {
194                write!(f, "{}", entry.key)?;
195            }
196            if entries.peek().is_some() {
197                write!(f, " ")?;
198            }
199        }
200        Ok(())
201    }
202}
203
204/// A condition on the key context stack that decides whether a binding applies.
205///
206/// A predicate is written in a small language, such as `Editor && mode == insert`, and is
207/// evaluated against the [`KeyContext`]s on the focus path. Build one with
208/// [`KeyBindingContextPredicate::parse`].
209///
210/// `Display` writes the predicate in the same syntax. It adds parentheses around `||` inside `&&`
211/// and around negated groups. Descendant expressions are written without parentheses around their
212/// operands, so add parentheses yourself when combining `>` with `&&` or `||` in text that must
213/// round-trip through [`parse`](Self::parse).
214///
215/// # Examples
216///
217/// ```
218/// # use gpui::{KeyBindingContextPredicate, KeyContext};
219/// let predicate = KeyBindingContextPredicate::parse("Editor && mode == insert").unwrap();
220/// assert_eq!(predicate.to_string(), "Editor && mode == insert");
221///
222/// let stack = [
223///     KeyContext::parse("Workspace").unwrap(),
224///     KeyContext::parse("Editor mode=insert").unwrap(),
225/// ];
226/// assert_eq!(predicate.depth_of(&stack), Some(2));
227/// ```
228#[derive(Clone, Debug, Eq, PartialEq, Hash)]
229pub enum KeyBindingContextPredicate {
230    /// Matches a context that has an entry named by the identifier, with or without a value.
231    Identifier(SharedString),
232    /// Matches a context whose entry for the key has the given value. Written `key == value`.
233    Equal(SharedString, SharedString),
234    /// Matches unless the context's entry for the key has the given value. Written `key != value`.
235    /// Also matches when the context has no entry for the key.
236    NotEqual(SharedString, SharedString),
237    /// Matches when the first predicate matches an ancestor context and the second matches the
238    /// contexts beneath it. Written `parent > child`.
239    Descendant(
240        Box<KeyBindingContextPredicate>,
241        Box<KeyBindingContextPredicate>,
242    ),
243    /// Matches when the inner predicate doesn't match. Written `!inner`.
244    Not(Box<KeyBindingContextPredicate>),
245    /// Matches when both predicates match. Written `left && right`.
246    And(
247        Box<KeyBindingContextPredicate>,
248        Box<KeyBindingContextPredicate>,
249    ),
250    /// Matches when either predicate matches. Written `left || right`.
251    Or(
252        Box<KeyBindingContextPredicate>,
253        Box<KeyBindingContextPredicate>,
254    ),
255}
256
257impl fmt::Display for KeyBindingContextPredicate {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        match self {
260            Self::Identifier(name) => write!(f, "{name}"),
261            Self::Equal(left, right) => write!(f, "{left} == {right}"),
262            Self::NotEqual(left, right) => write!(f, "{left} != {right}"),
263            Self::Descendant(parent, child) => write!(f, "{parent} > {child}"),
264            Self::Not(pred) => match pred.as_ref() {
265                Self::Identifier(name) => write!(f, "!{name}"),
266                _ => write!(f, "!({pred})"),
267            },
268            Self::And(..) => self.fmt_joined(f, " && ", LogicalOperator::And, |node| {
269                matches!(node, Self::Or(..))
270            }),
271            Self::Or(..) => self.fmt_joined(f, " || ", LogicalOperator::Or, |node| {
272                matches!(node, Self::And(..))
273            }),
274        }
275    }
276}
277
278impl KeyBindingContextPredicate {
279    /// Parses a predicate such as `Editor && !ReadOnly` or `Panel > Editor`.
280    ///
281    /// The grammar, from the tightest-binding rule to the loosest, is:
282    ///
283    /// - **Atoms.** An identifier, such as `Editor`, matches a context with that name. An identifier
284    ///   is letters, digits, `_` and `-`. The characters `>`, `<`, `~`, `"` and `?` can also
285    ///   appear after the first character, so write `Panel > Editor` with spaces. The text
286    ///   `Panel>Editor` is one identifier.
287    /// - **Parentheses.** `( ... )` groups a predicate.
288    /// - **Negation.** `!` applies to the atom or parenthesized group after it, so `!a && b` is
289    ///   `(!a) && b`. Use `!(a && b)` to negate a group.
290    /// - **Equality.** `key == value` and `key != value` compare the value of an entry. Both sides
291    ///   must be plain identifiers, so `mode == "full"` is an error.
292    /// - **`&&`** binds tighter than **`||`**. So `a || b && c` is `a || (b && c)`.
293    /// - **Descendant.** `parent > child` binds loosest, so `a && b > c` is `(a && b) > c`. It is
294    ///   left-associative: `A > B > C` is `(A > B) > C`.
295    ///
296    /// The keymap guide has the same rules as a table.
297    ///
298    /// # Errors
299    ///
300    /// Returns an error for an unbalanced parenthesis, a missing operand, an operator the grammar
301    /// doesn't have, a `==` or `!=` whose operands aren't identifiers, or any leftover text.
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// # use gpui::KeyBindingContextPredicate;
307    /// let predicate = KeyBindingContextPredicate::parse("Pane > (Editor || Terminal) && !ReadOnly")
308    ///     .unwrap();
309    /// assert_eq!(predicate.to_string(), "Pane > (Editor || Terminal) && !ReadOnly");
310    /// assert!(KeyBindingContextPredicate::parse("mode == \"full\"").is_err());
311    /// ```
312    ///
313    /// See [contexts](crate::_documentation::concepts::contexts) and
314    /// [keyboard input](crate::_documentation::guides::keyboard) for when contexts apply.
315    pub fn parse(source: &str) -> Result<Self> {
316        let source = skip_whitespace(source);
317        let (predicate, rest) = Self::parse_expr(source, 0)?;
318        if let Some(next) = rest.chars().next() {
319            anyhow::bail!("unexpected character '{next:?}'");
320        } else {
321            Ok(predicate)
322        }
323    }
324
325    /// Returns the deepest depth at which the predicate matches `contexts`, or `None` if it
326    /// matches none.
327    ///
328    /// A depth `d` means the predicate matches the first `d` contexts, with the `d`th as the
329    /// deepest one that identifiers and `==` look at. The keymap uses this depth to rank bindings:
330    /// a deeper match wins. A predicate that matches only at a shallower depth is returned at that
331    /// depth.
332    ///
333    /// # Examples
334    ///
335    /// ```
336    /// # use gpui::{KeyBindingContextPredicate, KeyContext};
337    /// let predicate = KeyBindingContextPredicate::parse("Workspace").unwrap();
338    /// let stack = [
339    ///     KeyContext::parse("Workspace").unwrap(),
340    ///     KeyContext::parse("Editor").unwrap(),
341    /// ];
342    /// assert_eq!(predicate.depth_of(&stack), Some(1));
343    /// assert_eq!(predicate.depth_of(&stack[1..]), None);
344    /// ```
345    pub fn depth_of(&self, contexts: &[KeyContext]) -> Option<usize> {
346        for depth in (0..=contexts.len()).rev() {
347            let context_slice = &contexts[0..depth];
348            if self.eval_inner(context_slice, contexts) {
349                return Some(depth);
350            }
351        }
352        None
353    }
354
355    /// Returns whether the predicate matches the whole of `contexts`, which is ordered from the
356    /// root to the focused element.
357    #[allow(unused)]
358    pub fn eval(&self, contexts: &[KeyContext]) -> bool {
359        self.eval_inner(contexts, contexts)
360    }
361
362    /// Returns whether the predicate matches the prefix `contexts`, ordered from the root to the
363    /// focused element. `all_contexts` is the full stack, which `!` checks against.
364    ///
365    /// Callers that want the whole stack use [`eval`](Self::eval). [`depth_of`](Self::depth_of)
366    /// calls this with each prefix.
367    pub fn eval_inner(&self, contexts: &[KeyContext], all_contexts: &[KeyContext]) -> bool {
368        let Some(context) = contexts.last() else {
369            return false;
370        };
371        match self {
372            Self::Identifier(name) => context.contains(name),
373            Self::Equal(left, right) => context
374                .get(left)
375                .map(|value| value == right)
376                .unwrap_or(false),
377            Self::NotEqual(left, right) => context
378                .get(left)
379                .map(|value| value != right)
380                .unwrap_or(true),
381            Self::Not(pred) => {
382                for i in 0..all_contexts.len() {
383                    if pred.eval_inner(&all_contexts[..=i], all_contexts) {
384                        return false;
385                    }
386                }
387                true
388            }
389            // Workspace > Pane > Editor
390            //
391            // Pane > (Pane > Editor) // should match?
392            // (Pane > Pane) > Editor // should not match?
393            // Pane > !Workspace <-- should match?
394            // !Workspace        <-- shouldn't match?
395            Self::Descendant(parent, child) => {
396                for i in 0..contexts.len() - 1 {
397                    // [Workspace >  Pane], [Editor]
398                    if parent.eval_inner(&contexts[..=i], all_contexts) {
399                        if !child.eval_inner(&contexts[i + 1..], &contexts[i + 1..]) {
400                            return false;
401                        }
402                        return true;
403                    }
404                }
405                false
406            }
407            Self::And(left, right) => {
408                left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts)
409            }
410            Self::Or(left, right) => {
411                left.eval_inner(contexts, all_contexts) || right.eval_inner(contexts, all_contexts)
412            }
413        }
414    }
415
416    /// Returns whether every context that `other` matches is also matched by `self`.
417    ///
418    /// The check is conservative. `true` is certain, but `false` means only that this check
419    /// couldn't show it. The keymap uses it to check whether a disabling binding's context covers
420    /// the context of the binding it disables.
421    ///
422    /// # Examples
423    ///
424    /// ```
425    /// # use gpui::KeyBindingContextPredicate;
426    /// let editor = KeyBindingContextPredicate::parse("Editor").unwrap();
427    /// let editor_and_vim = KeyBindingContextPredicate::parse("Editor && vim_mode").unwrap();
428    /// assert!(editor.is_superset(&editor_and_vim));
429    /// assert!(!editor_and_vim.is_superset(&editor));
430    /// ```
431    pub fn is_superset(&self, other: &Self) -> bool {
432        if self == other {
433            return true;
434        }
435
436        if let KeyBindingContextPredicate::Or(left, right) = self {
437            return left.is_superset(other) || right.is_superset(other);
438        }
439
440        match other {
441            KeyBindingContextPredicate::Descendant(_, child) => self.is_superset(child),
442            KeyBindingContextPredicate::And(left, right) => {
443                self.is_superset(left) || self.is_superset(right)
444            }
445            KeyBindingContextPredicate::Identifier(_) => false,
446            KeyBindingContextPredicate::Equal(_, _) => false,
447            KeyBindingContextPredicate::NotEqual(_, _) => false,
448            KeyBindingContextPredicate::Not(_) => false,
449            KeyBindingContextPredicate::Or(_, _) => false,
450        }
451    }
452
453    fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
454        type Op = fn(
455            KeyBindingContextPredicate,
456            KeyBindingContextPredicate,
457        ) -> Result<KeyBindingContextPredicate>;
458
459        let (mut predicate, rest) = Self::parse_primary(source)?;
460        source = rest;
461
462        'parse: loop {
463            for (operator, precedence, constructor) in [
464                (">", PRECEDENCE_CHILD, Self::new_child as Op),
465                ("&&", PRECEDENCE_AND, Self::new_and as Op),
466                ("||", PRECEDENCE_OR, Self::new_or as Op),
467                ("==", PRECEDENCE_EQ, Self::new_eq as Op),
468                ("!=", PRECEDENCE_EQ, Self::new_neq as Op),
469            ] {
470                if source.starts_with(operator) && precedence >= min_precedence {
471                    source = skip_whitespace(&source[operator.len()..]);
472                    let (right, rest) = Self::parse_expr(source, precedence + 1)?;
473                    predicate = constructor(predicate, right)?;
474                    source = rest;
475                    continue 'parse;
476                }
477            }
478            break;
479        }
480
481        Ok((predicate, source))
482    }
483
484    fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
485        let next = source.chars().next().context("unexpected end")?;
486        match next {
487            '(' => {
488                source = skip_whitespace(&source[1..]);
489                let (predicate, rest) = Self::parse_expr(source, 0)?;
490                let stripped = rest.strip_prefix(')').context("expected a ')'")?;
491                source = skip_whitespace(stripped);
492                Ok((predicate, source))
493            }
494            '!' => {
495                let source = skip_whitespace(&source[1..]);
496                let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?;
497                Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
498            }
499            _ if is_identifier_char(next) => {
500                let len = source
501                    .find(|c: char| !is_identifier_char(c) && !is_vim_operator_char(c))
502                    .unwrap_or(source.len());
503                let (identifier, rest) = source.split_at(len);
504                source = skip_whitespace(rest);
505                Ok((
506                    KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
507                    source,
508                ))
509            }
510            _ if is_vim_operator_char(next) => {
511                let (operator, rest) = source.split_at(1);
512                source = skip_whitespace(rest);
513                Ok((
514                    KeyBindingContextPredicate::Identifier(operator.to_string().into()),
515                    source,
516                ))
517            }
518            _ => anyhow::bail!("unexpected character '{next:?}'"),
519        }
520    }
521
522    fn new_or(self, other: Self) -> Result<Self> {
523        Ok(Self::Or(Box::new(self), Box::new(other)))
524    }
525
526    fn new_and(self, other: Self) -> Result<Self> {
527        Ok(Self::And(Box::new(self), Box::new(other)))
528    }
529
530    fn new_child(self, other: Self) -> Result<Self> {
531        Ok(Self::Descendant(Box::new(self), Box::new(other)))
532    }
533
534    fn new_eq(self, other: Self) -> Result<Self> {
535        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
536            Ok(Self::Equal(left, right))
537        } else {
538            anyhow::bail!("operands of == must be identifiers");
539        }
540    }
541
542    fn new_neq(self, other: Self) -> Result<Self> {
543        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
544            Ok(Self::NotEqual(left, right))
545        } else {
546            anyhow::bail!("operands of != must be identifiers");
547        }
548    }
549
550    fn fmt_joined(
551        &self,
552        f: &mut fmt::Formatter<'_>,
553        separator: &str,
554        operator: LogicalOperator,
555        needs_parens: impl Fn(&Self) -> bool + Copy,
556    ) -> fmt::Result {
557        let mut first = true;
558        self.fmt_joined_inner(f, separator, operator, needs_parens, &mut first)
559    }
560
561    fn fmt_joined_inner(
562        &self,
563        f: &mut fmt::Formatter<'_>,
564        separator: &str,
565        operator: LogicalOperator,
566        needs_parens: impl Fn(&Self) -> bool + Copy,
567        first: &mut bool,
568    ) -> fmt::Result {
569        match (operator, self) {
570            (LogicalOperator::And, Self::And(left, right))
571            | (LogicalOperator::Or, Self::Or(left, right)) => {
572                left.fmt_joined_inner(f, separator, operator, needs_parens, first)?;
573                right.fmt_joined_inner(f, separator, operator, needs_parens, first)
574            }
575            (_, node) => {
576                if !*first {
577                    f.write_str(separator)?;
578                }
579                *first = false;
580
581                if needs_parens(node) {
582                    write!(f, "({node})")
583                } else {
584                    write!(f, "{node}")
585                }
586            }
587        }
588    }
589}
590
591#[derive(Clone, Copy)]
592enum LogicalOperator {
593    And,
594    Or,
595}
596
597const PRECEDENCE_CHILD: u32 = 1;
598const PRECEDENCE_OR: u32 = 2;
599const PRECEDENCE_AND: u32 = 3;
600const PRECEDENCE_EQ: u32 = 4;
601const PRECEDENCE_NOT: u32 = 5;
602
603fn is_identifier_char(c: char) -> bool {
604    c.is_alphanumeric() || c == '_' || c == '-'
605}
606
607fn is_vim_operator_char(c: char) -> bool {
608    c == '>' || c == '<' || c == '~' || c == '"' || c == '?'
609}
610
611fn skip_whitespace(source: &str) -> &str {
612    let len = source
613        .find(|c: char| !c.is_whitespace())
614        .unwrap_or(source.len());
615    &source[len..]
616}
617
618#[cfg(test)]
619mod tests {
620    use core::slice;
621
622    use super::*;
623    use crate as gpui;
624    use KeyBindingContextPredicate::*;
625
626    #[test]
627    fn test_actions_definition() {
628        {
629            actions!(test_only, [A, B, C, D, E, F, G]);
630        }
631
632        {
633            actions!(
634                test_only,
635                [
636                    H, I, J, K, L, M, N, // Don't wrap, test the trailing comma
637                ]
638            );
639        }
640    }
641
642    #[test]
643    fn test_parse_context() {
644        let mut expected = KeyContext::default();
645        expected.add("baz");
646        expected.set("foo", "bar");
647        assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected);
648        assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected);
649        assert_eq!(
650            KeyContext::parse("  baz foo   =   bar baz").unwrap(),
651            expected
652        );
653        assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected);
654    }
655
656    #[test]
657    fn test_parse_malformed_context() {
658        for source in [
659            ".",
660            "Editor && enabled",
661            "=insert",
662            "mode==insert",
663            "mode=\"insert\"",
664        ] {
665            assert!(KeyContext::parse(source).is_err(), "accepted {source:?}");
666        }
667        assert!(KeyContext::parse(" \n ").unwrap().is_empty());
668    }
669
670    #[test]
671    fn test_parse_identifiers() {
672        // Identifiers
673        assert_eq!(
674            KeyBindingContextPredicate::parse("abc12").unwrap(),
675            Identifier("abc12".into())
676        );
677        assert_eq!(
678            KeyBindingContextPredicate::parse("_1a").unwrap(),
679            Identifier("_1a".into())
680        );
681    }
682
683    #[test]
684    fn test_parse_negations() {
685        assert_eq!(
686            KeyBindingContextPredicate::parse("!abc").unwrap(),
687            Not(Box::new(Identifier("abc".into())))
688        );
689        assert_eq!(
690            KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
691            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
692        );
693    }
694
695    #[test]
696    fn test_parse_equality_operators() {
697        assert_eq!(
698            KeyBindingContextPredicate::parse("a == b").unwrap(),
699            Equal("a".into(), "b".into())
700        );
701        assert_eq!(
702            KeyBindingContextPredicate::parse("c!=d").unwrap(),
703            NotEqual("c".into(), "d".into())
704        );
705        assert_eq!(
706            KeyBindingContextPredicate::parse("c == !d")
707                .unwrap_err()
708                .to_string(),
709            "operands of == must be identifiers"
710        );
711    }
712
713    #[test]
714    fn test_parse_boolean_operators() {
715        assert_eq!(
716            KeyBindingContextPredicate::parse("a || b").unwrap(),
717            Or(
718                Box::new(Identifier("a".into())),
719                Box::new(Identifier("b".into()))
720            )
721        );
722        assert_eq!(
723            KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
724            Or(
725                Box::new(Identifier("a".into())),
726                Box::new(And(
727                    Box::new(Not(Box::new(Identifier("b".into())))),
728                    Box::new(Identifier("c".into()))
729                ))
730            )
731        );
732        assert_eq!(
733            KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
734            Or(
735                Box::new(And(
736                    Box::new(Identifier("a".into())),
737                    Box::new(Identifier("b".into()))
738                )),
739                Box::new(And(
740                    Box::new(Identifier("c".into())),
741                    Box::new(Identifier("d".into()))
742                ))
743            )
744        );
745        assert_eq!(
746            KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
747            Or(
748                Box::new(And(
749                    Box::new(Equal("a".into(), "b".into())),
750                    Box::new(Identifier("c".into()))
751                )),
752                Box::new(And(
753                    Box::new(Equal("d".into(), "e".into())),
754                    Box::new(Identifier("f".into()))
755                ))
756            )
757        );
758        assert_eq!(
759            KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
760            And(
761                Box::new(And(
762                    Box::new(And(
763                        Box::new(Identifier("a".into())),
764                        Box::new(Identifier("b".into()))
765                    )),
766                    Box::new(Identifier("c".into())),
767                )),
768                Box::new(Identifier("d".into()))
769            ),
770        );
771    }
772
773    #[test]
774    fn test_parse_parenthesized_expressions() {
775        assert_eq!(
776            KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
777            And(
778                Box::new(Identifier("a".into())),
779                Box::new(Or(
780                    Box::new(Equal("b".into(), "c".into())),
781                    Box::new(NotEqual("d".into(), "e".into())),
782                )),
783            ),
784        );
785        assert_eq!(
786            KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
787            Or(
788                Box::new(Identifier("a".into())),
789                Box::new(Identifier("b".into())),
790            )
791        );
792    }
793
794    #[test]
795    fn test_is_superset() {
796        assert_is_superset("editor", "editor", true);
797        assert_is_superset("editor", "workspace", false);
798
799        assert_is_superset("editor", "editor && vim_mode", true);
800        assert_is_superset("editor", "mode == full && editor", true);
801        assert_is_superset("editor && mode == full", "editor", false);
802
803        assert_is_superset("editor", "something > editor", true);
804        assert_is_superset("editor", "editor > menu", false);
805
806        assert_is_superset("foo || bar || baz", "bar", true);
807        assert_is_superset("foo || bar || baz", "quux", false);
808
809        #[track_caller]
810        fn assert_is_superset(a: &str, b: &str, result: bool) {
811            let a = KeyBindingContextPredicate::parse(a).unwrap();
812            let b = KeyBindingContextPredicate::parse(b).unwrap();
813            assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})");
814        }
815    }
816
817    #[test]
818    fn test_child_operator() {
819        let predicate = KeyBindingContextPredicate::parse("parent > child").unwrap();
820
821        let parent_context = KeyContext::try_from("parent").unwrap();
822        let child_context = KeyContext::try_from("child").unwrap();
823
824        let contexts = vec![parent_context.clone(), child_context.clone()];
825        assert!(predicate.eval(&contexts));
826
827        let grandparent_context = KeyContext::try_from("grandparent").unwrap();
828
829        let contexts = vec![
830            grandparent_context,
831            parent_context.clone(),
832            child_context.clone(),
833        ];
834        assert!(predicate.eval(&contexts));
835
836        let other_context = KeyContext::try_from("other").unwrap();
837
838        let contexts = vec![other_context.clone(), child_context.clone()];
839        assert!(!predicate.eval(&contexts));
840
841        let contexts = vec![parent_context.clone(), other_context, child_context.clone()];
842        assert!(predicate.eval(&contexts));
843
844        assert!(!predicate.eval(&[]));
845        assert!(!predicate.eval(slice::from_ref(&child_context)));
846        assert!(!predicate.eval(&[parent_context]));
847
848        let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap();
849        assert!(!zany_predicate.eval(slice::from_ref(&child_context)));
850        assert!(zany_predicate.eval(&[child_context.clone(), child_context]));
851    }
852
853    #[test]
854    fn test_not_operator() {
855        let not_predicate = KeyBindingContextPredicate::parse("!editor").unwrap();
856        let editor_context = KeyContext::try_from("editor").unwrap();
857        let workspace_context = KeyContext::try_from("workspace").unwrap();
858        let parent_context = KeyContext::try_from("parent").unwrap();
859        let child_context = KeyContext::try_from("child").unwrap();
860
861        assert!(not_predicate.eval(slice::from_ref(&workspace_context)));
862        assert!(!not_predicate.eval(slice::from_ref(&editor_context)));
863        assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()]));
864        assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()]));
865
866        let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap();
867        assert!(complex_not.eval(slice::from_ref(&workspace_context)));
868        assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()]));
869
870        let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap();
871        let mut mode_context = KeyContext::default();
872        mode_context.set("mode", "full");
873        assert!(!not_mode_predicate.eval(&[mode_context.clone()]));
874
875        let mut other_mode_context = KeyContext::default();
876        other_mode_context.set("mode", "partial");
877        assert!(not_mode_predicate.eval(&[other_mode_context]));
878
879        let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap();
880        assert!(not_descendant.eval(slice::from_ref(&parent_context)));
881        assert!(not_descendant.eval(slice::from_ref(&child_context)));
882        assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
883
884        let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap();
885        assert!(!not_descendant.eval(slice::from_ref(&parent_context)));
886        assert!(!not_descendant.eval(slice::from_ref(&child_context)));
887        assert!(!not_descendant.eval(&[parent_context, child_context]));
888
889        let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap();
890        assert!(double_not.eval(slice::from_ref(&editor_context)));
891        assert!(!double_not.eval(slice::from_ref(&workspace_context)));
892
893        // Test complex descendant cases
894        let workspace_context = KeyContext::try_from("Workspace").unwrap();
895        let pane_context = KeyContext::try_from("Pane").unwrap();
896        let editor_context = KeyContext::try_from("Editor").unwrap();
897
898        // Workspace > Pane > Editor
899        let workspace_pane_editor = vec![
900            workspace_context.clone(),
901            pane_context.clone(),
902            editor_context.clone(),
903        ];
904
905        // Pane > (Pane > Editor) - should not match
906        let pane_pane_editor = KeyBindingContextPredicate::parse("Pane > (Pane > Editor)").unwrap();
907        assert!(!pane_pane_editor.eval(&workspace_pane_editor));
908
909        let workspace_pane_editor_predicate =
910            KeyBindingContextPredicate::parse("Workspace > Pane > Editor").unwrap();
911        assert!(workspace_pane_editor_predicate.eval(&workspace_pane_editor));
912
913        // (Pane > Pane) > Editor - should not match
914        let pane_pane_then_editor =
915            KeyBindingContextPredicate::parse("(Pane > Pane) > Editor").unwrap();
916        assert!(!pane_pane_then_editor.eval(&workspace_pane_editor));
917
918        // Pane > !Workspace - should match
919        let pane_not_workspace = KeyBindingContextPredicate::parse("Pane > !Workspace").unwrap();
920        assert!(pane_not_workspace.eval(&[pane_context.clone(), editor_context.clone()]));
921        assert!(!pane_not_workspace.eval(&[pane_context.clone(), workspace_context.clone()]));
922
923        // !Workspace - shouldn't match when Workspace is in the context
924        let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap();
925        assert!(!not_workspace.eval(slice::from_ref(&workspace_context)));
926        assert!(not_workspace.eval(slice::from_ref(&pane_context)));
927        assert!(not_workspace.eval(slice::from_ref(&editor_context)));
928        assert!(!not_workspace.eval(&workspace_pane_editor));
929    }
930
931    // MARK: - Display
932
933    #[test]
934    fn test_context_display() {
935        fn ident(s: &str) -> Box<KeyBindingContextPredicate> {
936            Box::new(Identifier(SharedString::new(s)))
937        }
938        fn eq(a: &str, b: &str) -> Box<KeyBindingContextPredicate> {
939            Box::new(Equal(SharedString::new(a), SharedString::new(b)))
940        }
941        fn not_eq(a: &str, b: &str) -> Box<KeyBindingContextPredicate> {
942            Box::new(NotEqual(SharedString::new(a), SharedString::new(b)))
943        }
944        fn and(
945            a: Box<KeyBindingContextPredicate>,
946            b: Box<KeyBindingContextPredicate>,
947        ) -> Box<KeyBindingContextPredicate> {
948            Box::new(And(a, b))
949        }
950        fn or(
951            a: Box<KeyBindingContextPredicate>,
952            b: Box<KeyBindingContextPredicate>,
953        ) -> Box<KeyBindingContextPredicate> {
954            Box::new(Or(a, b))
955        }
956        fn descendant(
957            a: Box<KeyBindingContextPredicate>,
958            b: Box<KeyBindingContextPredicate>,
959        ) -> Box<KeyBindingContextPredicate> {
960            Box::new(Descendant(a, b))
961        }
962        fn not(a: Box<KeyBindingContextPredicate>) -> Box<KeyBindingContextPredicate> {
963            Box::new(Not(a))
964        }
965
966        let test_cases = [
967            (ident("a"), "a"),
968            (eq("a", "b"), "a == b"),
969            (not_eq("a", "b"), "a != b"),
970            (descendant(ident("a"), ident("b")), "a > b"),
971            (not(ident("a")), "!a"),
972            (not_eq("a", "b"), "a != b"),
973            (descendant(ident("a"), ident("b")), "a > b"),
974            (not(and(ident("a"), ident("b"))), "!(a && b)"),
975            (not(or(ident("a"), ident("b"))), "!(a || b)"),
976            (and(ident("a"), ident("b")), "a && b"),
977            (and(and(ident("a"), ident("b")), ident("c")), "a && b && c"),
978            (or(ident("a"), ident("b")), "a || b"),
979            (or(or(ident("a"), ident("b")), ident("c")), "a || b || c"),
980            (or(ident("a"), and(ident("b"), ident("c"))), "a || (b && c)"),
981            (
982                and(
983                    and(
984                        and(ident("a"), eq("b", "c")),
985                        not(descendant(ident("d"), ident("e"))),
986                    ),
987                    eq("f", "g"),
988                ),
989                "a && b == c && !(d > e) && f == g",
990            ),
991            (
992                and(and(ident("a"), or(ident("b"), ident("c"))), ident("d")),
993                "a && (b || c) && d",
994            ),
995            (
996                or(or(ident("a"), and(ident("b"), ident("c"))), ident("d")),
997                "a || (b && c) || d",
998            ),
999        ];
1000
1001        for (predicate, expected) in test_cases {
1002            let actual = predicate.to_string();
1003            assert_eq!(actual, expected);
1004            let parsed = KeyBindingContextPredicate::parse(&actual).unwrap();
1005            assert_eq!(parsed, *predicate);
1006        }
1007    }
1008}