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