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 fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
273 type Op = fn(
274 KeyBindingContextPredicate,
275 KeyBindingContextPredicate,
276 ) -> Result<KeyBindingContextPredicate>;
277
278 let (mut predicate, rest) = Self::parse_primary(source)?;
279 source = rest;
280
281 'parse: loop {
282 for (operator, precedence, constructor) in [
283 (">", PRECEDENCE_CHILD, Self::new_child as Op),
284 ("&&", PRECEDENCE_AND, Self::new_and as Op),
285 ("||", PRECEDENCE_OR, Self::new_or as Op),
286 ("==", PRECEDENCE_EQ, Self::new_eq as Op),
287 ("!=", PRECEDENCE_EQ, Self::new_neq as Op),
288 ] {
289 if source.starts_with(operator) && precedence >= min_precedence {
290 source = skip_whitespace(&source[operator.len()..]);
291 let (right, rest) = Self::parse_expr(source, precedence + 1)?;
292 predicate = constructor(predicate, right)?;
293 source = rest;
294 continue 'parse;
295 }
296 }
297 break;
298 }
299
300 Ok((predicate, source))
301 }
302
303 fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
304 let next = source
305 .chars()
306 .next()
307 .ok_or_else(|| anyhow!("unexpected eof"))?;
308 match next {
309 '(' => {
310 source = skip_whitespace(&source[1..]);
311 let (predicate, rest) = Self::parse_expr(source, 0)?;
312 if let Some(stripped) = rest.strip_prefix(')') {
313 source = skip_whitespace(stripped);
314 Ok((predicate, source))
315 } else {
316 Err(anyhow!("expected a ')'"))
317 }
318 }
319 '!' => {
320 let source = skip_whitespace(&source[1..]);
321 let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?;
322 Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
323 }
324 _ if is_identifier_char(next) => {
325 let len = source
326 .find(|c: char| !is_identifier_char(c) && !is_vim_operator_char(c))
327 .unwrap_or(source.len());
328 let (identifier, rest) = source.split_at(len);
329 source = skip_whitespace(rest);
330 Ok((
331 KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
332 source,
333 ))
334 }
335 _ if is_vim_operator_char(next) => {
336 let (operator, rest) = source.split_at(1);
337 source = skip_whitespace(rest);
338 Ok((
339 KeyBindingContextPredicate::Identifier(operator.to_string().into()),
340 source,
341 ))
342 }
343 _ => Err(anyhow!("unexpected character {next:?}")),
344 }
345 }
346
347 fn new_or(self, other: Self) -> Result<Self> {
348 Ok(Self::Or(Box::new(self), Box::new(other)))
349 }
350
351 fn new_and(self, other: Self) -> Result<Self> {
352 Ok(Self::And(Box::new(self), Box::new(other)))
353 }
354
355 fn new_child(self, other: Self) -> Result<Self> {
356 Ok(Self::Child(Box::new(self), Box::new(other)))
357 }
358
359 fn new_eq(self, other: Self) -> Result<Self> {
360 if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
361 Ok(Self::Equal(left, right))
362 } else {
363 Err(anyhow!("operands must be identifiers"))
364 }
365 }
366
367 fn new_neq(self, other: Self) -> Result<Self> {
368 if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
369 Ok(Self::NotEqual(left, right))
370 } else {
371 Err(anyhow!("operands must be identifiers"))
372 }
373 }
374}
375
376const PRECEDENCE_CHILD: u32 = 1;
377const PRECEDENCE_OR: u32 = 2;
378const PRECEDENCE_AND: u32 = 3;
379const PRECEDENCE_EQ: u32 = 4;
380const PRECEDENCE_NOT: u32 = 5;
381
382fn is_identifier_char(c: char) -> bool {
383 c.is_alphanumeric() || c == '_' || c == '-'
384}
385
386fn is_vim_operator_char(c: char) -> bool {
387 c == '>' || c == '<' || c == '~' || c == '"'
388}
389
390fn skip_whitespace(source: &str) -> &str {
391 let len = source
392 .find(|c: char| !c.is_whitespace())
393 .unwrap_or(source.len());
394 &source[len..]
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate as gpui;
401 use KeyBindingContextPredicate::*;
402
403 #[test]
404 fn test_actions_definition() {
405 {
406 actions!(test, [A, B, C, D, E, F, G]);
407 }
408
409 {
410 actions!(
411 test,
412 [
413 A,
414 B,
415 C,
416 D,
417 E,
418 F,
419 G, // Don't wrap, test the trailing comma
420 ]
421 );
422 }
423 }
424
425 #[test]
426 fn test_parse_context() {
427 let mut expected = KeyContext::default();
428 expected.add("baz");
429 expected.set("foo", "bar");
430 assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected);
431 assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected);
432 assert_eq!(
433 KeyContext::parse(" baz foo = bar baz").unwrap(),
434 expected
435 );
436 assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected);
437 }
438
439 #[test]
440 fn test_parse_identifiers() {
441 // Identifiers
442 assert_eq!(
443 KeyBindingContextPredicate::parse("abc12").unwrap(),
444 Identifier("abc12".into())
445 );
446 assert_eq!(
447 KeyBindingContextPredicate::parse("_1a").unwrap(),
448 Identifier("_1a".into())
449 );
450 }
451
452 #[test]
453 fn test_parse_negations() {
454 assert_eq!(
455 KeyBindingContextPredicate::parse("!abc").unwrap(),
456 Not(Box::new(Identifier("abc".into())))
457 );
458 assert_eq!(
459 KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
460 Not(Box::new(Not(Box::new(Identifier("abc".into())))))
461 );
462 }
463
464 #[test]
465 fn test_parse_equality_operators() {
466 assert_eq!(
467 KeyBindingContextPredicate::parse("a == b").unwrap(),
468 Equal("a".into(), "b".into())
469 );
470 assert_eq!(
471 KeyBindingContextPredicate::parse("c!=d").unwrap(),
472 NotEqual("c".into(), "d".into())
473 );
474 assert_eq!(
475 KeyBindingContextPredicate::parse("c == !d")
476 .unwrap_err()
477 .to_string(),
478 "operands must be identifiers"
479 );
480 }
481
482 #[test]
483 fn test_parse_boolean_operators() {
484 assert_eq!(
485 KeyBindingContextPredicate::parse("a || b").unwrap(),
486 Or(
487 Box::new(Identifier("a".into())),
488 Box::new(Identifier("b".into()))
489 )
490 );
491 assert_eq!(
492 KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
493 Or(
494 Box::new(Identifier("a".into())),
495 Box::new(And(
496 Box::new(Not(Box::new(Identifier("b".into())))),
497 Box::new(Identifier("c".into()))
498 ))
499 )
500 );
501 assert_eq!(
502 KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
503 Or(
504 Box::new(And(
505 Box::new(Identifier("a".into())),
506 Box::new(Identifier("b".into()))
507 )),
508 Box::new(And(
509 Box::new(Identifier("c".into())),
510 Box::new(Identifier("d".into()))
511 ))
512 )
513 );
514 assert_eq!(
515 KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
516 Or(
517 Box::new(And(
518 Box::new(Equal("a".into(), "b".into())),
519 Box::new(Identifier("c".into()))
520 )),
521 Box::new(And(
522 Box::new(Equal("d".into(), "e".into())),
523 Box::new(Identifier("f".into()))
524 ))
525 )
526 );
527 assert_eq!(
528 KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
529 And(
530 Box::new(And(
531 Box::new(And(
532 Box::new(Identifier("a".into())),
533 Box::new(Identifier("b".into()))
534 )),
535 Box::new(Identifier("c".into())),
536 )),
537 Box::new(Identifier("d".into()))
538 ),
539 );
540 }
541
542 #[test]
543 fn test_parse_parenthesized_expressions() {
544 assert_eq!(
545 KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
546 And(
547 Box::new(Identifier("a".into())),
548 Box::new(Or(
549 Box::new(Equal("b".into(), "c".into())),
550 Box::new(NotEqual("d".into(), "e".into())),
551 )),
552 ),
553 );
554 assert_eq!(
555 KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
556 Or(
557 Box::new(Identifier("a".into())),
558 Box::new(Identifier("b".into())),
559 )
560 );
561 }
562}