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}