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