context.rs

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