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}