context.rs

  1use crate::SharedString;
  2use anyhow::{anyhow, Result};
  3use smallvec::SmallVec;
  4use std::fmt;
  5
  6/// A datastructure for resolving whether an action should be dispatched
  7/// at this point in the element tree. Contains a set of identifiers
  8/// and/or key value pairs representing the current context for the
  9/// keymap.
 10#[derive(Clone, Default, Eq, PartialEq, Hash)]
 11pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
 12
 13#[derive(Clone, Debug, Eq, PartialEq, Hash)]
 14/// An entry in a KeyContext
 15pub struct ContextEntry {
 16    /// The key (or name if no value)
 17    pub key: SharedString,
 18    /// The value
 19    pub value: Option<SharedString>,
 20}
 21
 22impl<'a> TryFrom<&'a str> for KeyContext {
 23    type Error = anyhow::Error;
 24
 25    fn try_from(value: &'a str) -> Result<Self> {
 26        Self::parse(value)
 27    }
 28}
 29
 30impl KeyContext {
 31    /// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`.
 32    pub fn new_with_defaults() -> Self {
 33        let mut context = Self::default();
 34        #[cfg(target_os = "macos")]
 35        context.set("os", "macos");
 36        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 37        context.set("os", "linux");
 38        #[cfg(target_os = "windows")]
 39        context.set("os", "windows");
 40        #[cfg(not(any(
 41            target_os = "macos",
 42            target_os = "linux",
 43            target_os = "freebsd",
 44            target_os = "windows"
 45        )))]
 46        context.set("os", "unknown");
 47        context
 48    }
 49
 50    /// Returns the primary context entry (usually the name of the component)
 51    pub fn primary(&self) -> Option<&ContextEntry> {
 52        self.0.iter().find(|p| p.value.is_none())
 53    }
 54
 55    /// Returns everything except the primary context entry.
 56    pub fn secondary(&self) -> impl Iterator<Item = &ContextEntry> {
 57        let primary = self.primary();
 58        self.0.iter().filter(move |&p| Some(p) != primary)
 59    }
 60
 61    /// Parse a key context from a string.
 62    /// The key context format is very simple:
 63    /// - either a single identifier, such as `StatusBar`
 64    /// - or a key value pair, such as `mode = visible`
 65    /// - separated by whitespace, such as `StatusBar mode = visible`
 66    pub fn parse(source: &str) -> Result<Self> {
 67        let mut context = Self::default();
 68        let source = skip_whitespace(source);
 69        Self::parse_expr(source, &mut context)?;
 70        Ok(context)
 71    }
 72
 73    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
 74        if source.is_empty() {
 75            return Ok(());
 76        }
 77
 78        let key = source
 79            .chars()
 80            .take_while(|c| is_identifier_char(*c))
 81            .collect::<String>();
 82        source = skip_whitespace(&source[key.len()..]);
 83        if let Some(suffix) = source.strip_prefix('=') {
 84            source = skip_whitespace(suffix);
 85            let value = source
 86                .chars()
 87                .take_while(|c| is_identifier_char(*c))
 88                .collect::<String>();
 89            source = skip_whitespace(&source[value.len()..]);
 90            context.set(key, value);
 91        } else {
 92            context.add(key);
 93        }
 94
 95        Self::parse_expr(source, context)
 96    }
 97
 98    /// Check if this context is empty.
 99    pub fn is_empty(&self) -> bool {
100        self.0.is_empty()
101    }
102
103    /// Clear this context.
104    pub fn clear(&mut self) {
105        self.0.clear();
106    }
107
108    /// Extend this context with another context.
109    pub fn extend(&mut self, other: &Self) {
110        for entry in &other.0 {
111            if !self.contains(&entry.key) {
112                self.0.push(entry.clone());
113            }
114        }
115    }
116
117    /// Add an identifier to this context, if it's not already in this context.
118    pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
119        let key = identifier.into();
120
121        if !self.contains(&key) {
122            self.0.push(ContextEntry { key, value: None })
123        }
124    }
125
126    /// Set a key value pair in this context, if it's not already set.
127    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
128        let key = key.into();
129        if !self.contains(&key) {
130            self.0.push(ContextEntry {
131                key,
132                value: Some(value.into()),
133            })
134        }
135    }
136
137    /// Check if this context contains a given identifier or key.
138    pub fn contains(&self, key: &str) -> bool {
139        self.0.iter().any(|entry| entry.key.as_ref() == key)
140    }
141
142    /// Get the associated value for a given identifier or key.
143    pub fn get(&self, key: &str) -> Option<&SharedString> {
144        self.0
145            .iter()
146            .find(|entry| entry.key.as_ref() == key)?
147            .value
148            .as_ref()
149    }
150}
151
152impl fmt::Debug for KeyContext {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        let mut entries = self.0.iter().peekable();
155        while let Some(entry) = entries.next() {
156            if let Some(ref value) = entry.value {
157                write!(f, "{}={}", entry.key, value)?;
158            } else {
159                write!(f, "{}", entry.key)?;
160            }
161            if entries.peek().is_some() {
162                write!(f, " ")?;
163            }
164        }
165        Ok(())
166    }
167}
168
169/// A datastructure for resolving whether an action should be dispatched
170/// Representing a small language for describing which contexts correspond
171/// to which actions.
172#[derive(Clone, Debug, Eq, PartialEq, Hash)]
173pub enum KeyBindingContextPredicate {
174    /// A predicate that will match a given identifier.
175    Identifier(SharedString),
176    /// A predicate that will match a given key-value pair.
177    Equal(SharedString, SharedString),
178    /// A predicate that will match a given key-value pair not being present.
179    NotEqual(SharedString, SharedString),
180    /// A predicate that will match a given predicate appearing below another predicate.
181    /// in the element tree
182    Child(
183        Box<KeyBindingContextPredicate>,
184        Box<KeyBindingContextPredicate>,
185    ),
186    /// Predicate that will invert another predicate.
187    Not(Box<KeyBindingContextPredicate>),
188    /// A predicate that will match if both of its children match.
189    And(
190        Box<KeyBindingContextPredicate>,
191        Box<KeyBindingContextPredicate>,
192    ),
193    /// A predicate that will match if either of its children match.
194    Or(
195        Box<KeyBindingContextPredicate>,
196        Box<KeyBindingContextPredicate>,
197    ),
198}
199
200impl fmt::Display for KeyBindingContextPredicate {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        match self {
203            Self::Identifier(name) => write!(f, "{}", name),
204            Self::Equal(left, right) => write!(f, "{} == {}", left, right),
205            Self::NotEqual(left, right) => write!(f, "{} != {}", left, right),
206            Self::Not(pred) => write!(f, "!{}", pred),
207            Self::Child(parent, child) => write!(f, "{} > {}", parent, child),
208            Self::And(left, right) => write!(f, "({} && {})", left, right),
209            Self::Or(left, right) => write!(f, "({} || {})", left, right),
210        }
211    }
212}
213
214impl KeyBindingContextPredicate {
215    /// Parse a string in the same format as the keymap's context field.
216    ///
217    /// A basic equivalence check against a set of identifiers can performed by
218    /// simply writing a string:
219    ///
220    /// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar`
221    ///
222    /// You can also specify a key-value pair:
223    ///
224    /// `mode == visible` -> A predicate that will match a context with the key `mode`
225    ///                      with the value `visible`
226    ///
227    /// And a logical operations combining these two checks:
228    ///
229    /// `StatusBar && mode == visible` -> A predicate that will match a context with the
230    ///                                   identifier `StatusBar` and the key `mode`
231    ///                                   with the value `visible`
232    ///
233    ///
234    /// There is also a special child `>` operator that will match a predicate that is
235    /// below another predicate:
236    ///
237    /// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar`
238    ///                                  and a child context that has the key `mode` with the
239    ///                                  value `visible`
240    ///
241    /// This syntax supports `!=`, `||` and `&&` as logical operators.
242    /// You can also preface an operation or check with a `!` to negate it.
243    pub fn parse(source: &str) -> Result<Self> {
244        let source = skip_whitespace(source);
245        let (predicate, rest) = Self::parse_expr(source, 0)?;
246        if let Some(next) = rest.chars().next() {
247            Err(anyhow!("unexpected character '{next:?}'"))
248        } else {
249            Ok(predicate)
250        }
251    }
252
253    /// Eval a predicate against a set of contexts, arranged from lowest to highest.
254    pub fn eval(&self, contexts: &[KeyContext]) -> bool {
255        let Some(context) = contexts.last() else {
256            return false;
257        };
258        match self {
259            Self::Identifier(name) => context.contains(name),
260            Self::Equal(left, right) => context
261                .get(left)
262                .map(|value| value == right)
263                .unwrap_or(false),
264            Self::NotEqual(left, right) => context
265                .get(left)
266                .map(|value| value != right)
267                .unwrap_or(true),
268            Self::Not(pred) => !pred.eval(contexts),
269            Self::Child(parent, child) => {
270                parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
271            }
272            Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
273            Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
274        }
275    }
276
277    /// Returns whether or not this predicate matches all possible contexts matched by
278    /// the other predicate.
279    pub fn is_superset(&self, other: &Self) -> bool {
280        if self == other {
281            return true;
282        }
283
284        if let KeyBindingContextPredicate::Or(left, right) = self {
285            return left.is_superset(other) || right.is_superset(other);
286        }
287
288        match other {
289            KeyBindingContextPredicate::Child(_, child) => self.is_superset(child),
290            KeyBindingContextPredicate::And(left, right) => {
291                self.is_superset(left) || self.is_superset(right)
292            }
293            KeyBindingContextPredicate::Identifier(_) => false,
294            KeyBindingContextPredicate::Equal(_, _) => false,
295            KeyBindingContextPredicate::NotEqual(_, _) => false,
296            KeyBindingContextPredicate::Not(_) => false,
297            KeyBindingContextPredicate::Or(_, _) => false,
298        }
299    }
300
301    fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
302        type Op = fn(
303            KeyBindingContextPredicate,
304            KeyBindingContextPredicate,
305        ) -> Result<KeyBindingContextPredicate>;
306
307        let (mut predicate, rest) = Self::parse_primary(source)?;
308        source = rest;
309
310        'parse: loop {
311            for (operator, precedence, constructor) in [
312                (">", PRECEDENCE_CHILD, Self::new_child as Op),
313                ("&&", PRECEDENCE_AND, Self::new_and as Op),
314                ("||", PRECEDENCE_OR, Self::new_or as Op),
315                ("==", PRECEDENCE_EQ, Self::new_eq as Op),
316                ("!=", PRECEDENCE_EQ, Self::new_neq as Op),
317            ] {
318                if source.starts_with(operator) && precedence >= min_precedence {
319                    source = skip_whitespace(&source[operator.len()..]);
320                    let (right, rest) = Self::parse_expr(source, precedence + 1)?;
321                    predicate = constructor(predicate, right)?;
322                    source = rest;
323                    continue 'parse;
324                }
325            }
326            break;
327        }
328
329        Ok((predicate, source))
330    }
331
332    fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
333        let next = source
334            .chars()
335            .next()
336            .ok_or_else(|| anyhow!("unexpected end"))?;
337        match next {
338            '(' => {
339                source = skip_whitespace(&source[1..]);
340                let (predicate, rest) = Self::parse_expr(source, 0)?;
341                if let Some(stripped) = rest.strip_prefix(')') {
342                    source = skip_whitespace(stripped);
343                    Ok((predicate, source))
344                } else {
345                    Err(anyhow!("expected a ')'"))
346                }
347            }
348            '!' => {
349                let source = skip_whitespace(&source[1..]);
350                let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?;
351                Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
352            }
353            _ if is_identifier_char(next) => {
354                let len = source
355                    .find(|c: char| !is_identifier_char(c) && !is_vim_operator_char(c))
356                    .unwrap_or(source.len());
357                let (identifier, rest) = source.split_at(len);
358                source = skip_whitespace(rest);
359                Ok((
360                    KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
361                    source,
362                ))
363            }
364            _ if is_vim_operator_char(next) => {
365                let (operator, rest) = source.split_at(1);
366                source = skip_whitespace(rest);
367                Ok((
368                    KeyBindingContextPredicate::Identifier(operator.to_string().into()),
369                    source,
370                ))
371            }
372            _ => Err(anyhow!("unexpected character '{next:?}'")),
373        }
374    }
375
376    fn new_or(self, other: Self) -> Result<Self> {
377        Ok(Self::Or(Box::new(self), Box::new(other)))
378    }
379
380    fn new_and(self, other: Self) -> Result<Self> {
381        Ok(Self::And(Box::new(self), Box::new(other)))
382    }
383
384    fn new_child(self, other: Self) -> Result<Self> {
385        Ok(Self::Child(Box::new(self), Box::new(other)))
386    }
387
388    fn new_eq(self, other: Self) -> Result<Self> {
389        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
390            Ok(Self::Equal(left, right))
391        } else {
392            Err(anyhow!("operands of == must be identifiers"))
393        }
394    }
395
396    fn new_neq(self, other: Self) -> Result<Self> {
397        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
398            Ok(Self::NotEqual(left, right))
399        } else {
400            Err(anyhow!("operands of != must be identifiers"))
401        }
402    }
403}
404
405const PRECEDENCE_CHILD: u32 = 1;
406const PRECEDENCE_OR: u32 = 2;
407const PRECEDENCE_AND: u32 = 3;
408const PRECEDENCE_EQ: u32 = 4;
409const PRECEDENCE_NOT: u32 = 5;
410
411fn is_identifier_char(c: char) -> bool {
412    c.is_alphanumeric() || c == '_' || c == '-'
413}
414
415fn is_vim_operator_char(c: char) -> bool {
416    c == '>' || c == '<' || c == '~' || c == '"'
417}
418
419fn skip_whitespace(source: &str) -> &str {
420    let len = source
421        .find(|c: char| !c.is_whitespace())
422        .unwrap_or(source.len());
423    &source[len..]
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate as gpui;
430    use KeyBindingContextPredicate::*;
431
432    #[test]
433    fn test_actions_definition() {
434        {
435            actions!(test, [A, B, C, D, E, F, G]);
436        }
437
438        {
439            actions!(
440                test,
441                [
442                A,
443                B,
444                C,
445                D,
446                E,
447                F,
448                G, // Don't wrap, test the trailing comma
449            ]
450            );
451        }
452    }
453
454    #[test]
455    fn test_parse_context() {
456        let mut expected = KeyContext::default();
457        expected.add("baz");
458        expected.set("foo", "bar");
459        assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected);
460        assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected);
461        assert_eq!(
462            KeyContext::parse("  baz foo   =   bar baz").unwrap(),
463            expected
464        );
465        assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected);
466    }
467
468    #[test]
469    fn test_parse_identifiers() {
470        // Identifiers
471        assert_eq!(
472            KeyBindingContextPredicate::parse("abc12").unwrap(),
473            Identifier("abc12".into())
474        );
475        assert_eq!(
476            KeyBindingContextPredicate::parse("_1a").unwrap(),
477            Identifier("_1a".into())
478        );
479    }
480
481    #[test]
482    fn test_parse_negations() {
483        assert_eq!(
484            KeyBindingContextPredicate::parse("!abc").unwrap(),
485            Not(Box::new(Identifier("abc".into())))
486        );
487        assert_eq!(
488            KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
489            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
490        );
491    }
492
493    #[test]
494    fn test_parse_equality_operators() {
495        assert_eq!(
496            KeyBindingContextPredicate::parse("a == b").unwrap(),
497            Equal("a".into(), "b".into())
498        );
499        assert_eq!(
500            KeyBindingContextPredicate::parse("c!=d").unwrap(),
501            NotEqual("c".into(), "d".into())
502        );
503        assert_eq!(
504            KeyBindingContextPredicate::parse("c == !d")
505                .unwrap_err()
506                .to_string(),
507            "operands of == must be identifiers"
508        );
509    }
510
511    #[test]
512    fn test_parse_boolean_operators() {
513        assert_eq!(
514            KeyBindingContextPredicate::parse("a || b").unwrap(),
515            Or(
516                Box::new(Identifier("a".into())),
517                Box::new(Identifier("b".into()))
518            )
519        );
520        assert_eq!(
521            KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
522            Or(
523                Box::new(Identifier("a".into())),
524                Box::new(And(
525                    Box::new(Not(Box::new(Identifier("b".into())))),
526                    Box::new(Identifier("c".into()))
527                ))
528            )
529        );
530        assert_eq!(
531            KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
532            Or(
533                Box::new(And(
534                    Box::new(Identifier("a".into())),
535                    Box::new(Identifier("b".into()))
536                )),
537                Box::new(And(
538                    Box::new(Identifier("c".into())),
539                    Box::new(Identifier("d".into()))
540                ))
541            )
542        );
543        assert_eq!(
544            KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
545            Or(
546                Box::new(And(
547                    Box::new(Equal("a".into(), "b".into())),
548                    Box::new(Identifier("c".into()))
549                )),
550                Box::new(And(
551                    Box::new(Equal("d".into(), "e".into())),
552                    Box::new(Identifier("f".into()))
553                ))
554            )
555        );
556        assert_eq!(
557            KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
558            And(
559                Box::new(And(
560                    Box::new(And(
561                        Box::new(Identifier("a".into())),
562                        Box::new(Identifier("b".into()))
563                    )),
564                    Box::new(Identifier("c".into())),
565                )),
566                Box::new(Identifier("d".into()))
567            ),
568        );
569    }
570
571    #[test]
572    fn test_parse_parenthesized_expressions() {
573        assert_eq!(
574            KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
575            And(
576                Box::new(Identifier("a".into())),
577                Box::new(Or(
578                    Box::new(Equal("b".into(), "c".into())),
579                    Box::new(NotEqual("d".into(), "e".into())),
580                )),
581            ),
582        );
583        assert_eq!(
584            KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
585            Or(
586                Box::new(Identifier("a".into())),
587                Box::new(Identifier("b".into())),
588            )
589        );
590    }
591
592    #[test]
593    fn test_is_superset() {
594        assert_is_superset("editor", "editor", true);
595        assert_is_superset("editor", "workspace", false);
596
597        assert_is_superset("editor", "editor && vim_mode", true);
598        assert_is_superset("editor", "mode == full && editor", true);
599        assert_is_superset("editor && mode == full", "editor", false);
600
601        assert_is_superset("editor", "something > editor", true);
602        assert_is_superset("editor", "editor > menu", false);
603
604        assert_is_superset("foo || bar || baz", "bar", true);
605        assert_is_superset("foo || bar || baz", "quux", false);
606
607        #[track_caller]
608        fn assert_is_superset(a: &str, b: &str, result: bool) {
609            let a = KeyBindingContextPredicate::parse(a).unwrap();
610            let b = KeyBindingContextPredicate::parse(b).unwrap();
611            assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})");
612        }
613    }
614}