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