diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 89a71e59e6..2ef282c21e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -724,7 +724,7 @@ } }, { - "context": "AgentPanel || GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel", + "context": "VimControl || !Editor && !Terminal", "bindings": { // window related commands (ctrl-w X) "ctrl-w": null, @@ -782,7 +782,7 @@ } }, { - "context": "ChangesList || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome", + "context": "!Editor && !Terminal", "bindings": { ":": "command_palette::Toggle", "g /": "pane::DeploySearch" diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index b5dbab15c7..174dbc80f0 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -24,7 +24,7 @@ pub struct Keymap { } /// Index of a binding within a keymap. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct BindingIndex(usize); impl Keymap { @@ -167,65 +167,60 @@ impl Keymap { input: &[Keystroke], context_stack: &[KeyContext], ) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) { - let possibilities = self + let mut possibilities = self .bindings() .enumerate() .rev() .filter_map(|(ix, binding)| { - binding - .match_keystrokes(input) - .map(|pending| (BindingIndex(ix), binding, pending)) - }); + let depth = self.binding_enabled(binding, &context_stack)?; + let pending = binding.match_keystrokes(input)?; + Some((depth, BindingIndex(ix), binding, pending)) + }) + .collect::>(); + possibilities.sort_by(|(depth_a, ix_a, _, _), (depth_b, ix_b, _, _)| { + depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) + }); let mut bindings: SmallVec<[(BindingIndex, KeyBinding, usize); 1]> = SmallVec::new(); // (pending, is_no_action, depth, keystrokes) let mut pending_info_opt: Option<(bool, bool, usize, &[Keystroke])> = None; - 'outer: for (binding_index, binding, pending) in possibilities { - for depth in (0..=context_stack.len()).rev() { - if self.binding_enabled(binding, &context_stack[0..depth]) { - let is_no_action = is_no_action(&*binding.action); - // We only want to consider a binding pending if it has an action - // This, however, means that if we have both a NoAction binding and a binding - // with an action at the same depth, we should still set is_pending to true. - if let Some(pending_info) = pending_info_opt.as_mut() { - let ( - already_pending, - pending_is_no_action, - pending_depth, - pending_keystrokes, - ) = *pending_info; + 'outer: for (depth, binding_index, binding, pending) in possibilities { + let is_no_action = is_no_action(&*binding.action); + // We only want to consider a binding pending if it has an action + // This, however, means that if we have both a NoAction binding and a binding + // with an action at the same depth, we should still set is_pending to true. + if let Some(pending_info) = pending_info_opt.as_mut() { + let (already_pending, pending_is_no_action, pending_depth, pending_keystrokes) = + *pending_info; - // We only want to change the pending status if it's not already pending AND if - // the existing pending status was set by a NoAction binding. This avoids a NoAction - // binding erroneously setting the pending status to true when a binding with an action - // already set it to false - // - // We also want to change the pending status if the keystrokes don't match, - // meaning it's different keystrokes than the NoAction that set pending to false - if pending - && !already_pending - && pending_is_no_action - && (pending_depth == depth - || pending_keystrokes != binding.keystrokes()) - { - pending_info.0 = !is_no_action; - } - } else { - pending_info_opt = Some(( - pending && !is_no_action, - is_no_action, - depth, - binding.keystrokes(), - )); - } - - if !pending { - bindings.push((binding_index, binding.clone(), depth)); - continue 'outer; - } + // We only want to change the pending status if it's not already pending AND if + // the existing pending status was set by a NoAction binding. This avoids a NoAction + // binding erroneously setting the pending status to true when a binding with an action + // already set it to false + // + // We also want to change the pending status if the keystrokes don't match, + // meaning it's different keystrokes than the NoAction that set pending to false + if pending + && !already_pending + && pending_is_no_action + && (pending_depth == depth || pending_keystrokes != binding.keystrokes()) + { + pending_info.0 = !is_no_action; } + } else { + pending_info_opt = Some(( + pending && !is_no_action, + is_no_action, + depth, + binding.keystrokes(), + )); + } + + if !pending { + bindings.push((binding_index, binding.clone(), depth)); + continue 'outer; } } // sort by descending depth @@ -245,15 +240,13 @@ impl Keymap { } /// Check if the given binding is enabled, given a certain key context. - fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool { - // If binding has a context predicate, it must match the current context, + /// Returns the deepest depth at which the binding matches, or None if it doesn't match. + fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { if let Some(predicate) = &binding.context_predicate { - if !predicate.eval(context) { - return false; - } + predicate.depth_of(contexts) + } else { + Some(contexts.len()) } - - true } } @@ -280,18 +273,33 @@ mod tests { keymap.add_bindings(bindings.clone()); // global bindings are enabled in all contexts - assert!(keymap.binding_enabled(&bindings[0], &[])); - assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()])); + assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0)); + assert_eq!( + keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]), + Some(1) + ); // contextual bindings are enabled in contexts that match their predicate - assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()])); - assert!(keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()])); + assert_eq!( + keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]), + None + ); + assert_eq!( + keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]), + Some(1) + ); - assert!(!keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()])); - assert!(keymap.binding_enabled( - &bindings[2], - &[KeyContext::parse("editor mode=full").unwrap()] - )); + assert_eq!( + keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]), + None + ); + assert_eq!( + keymap.binding_enabled( + &bindings[2], + &[KeyContext::parse("editor mode=full").unwrap()] + ), + Some(1) + ); } #[test] diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index eaad060982..f4b878ae77 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -178,7 +178,7 @@ pub enum KeyBindingContextPredicate { NotEqual(SharedString, SharedString), /// A predicate that will match a given predicate appearing below another predicate. /// in the element tree - Child( + Descendant( Box, Box, ), @@ -203,7 +203,7 @@ impl fmt::Display for KeyBindingContextPredicate { Self::Equal(left, right) => write!(f, "{} == {}", left, right), Self::NotEqual(left, right) => write!(f, "{} != {}", left, right), Self::Not(pred) => write!(f, "!{}", pred), - Self::Child(parent, child) => write!(f, "{} > {}", parent, child), + Self::Descendant(parent, child) => write!(f, "{} > {}", parent, child), Self::And(left, right) => write!(f, "({} && {})", left, right), Self::Or(left, right) => write!(f, "({} || {})", left, right), } @@ -249,8 +249,25 @@ impl KeyBindingContextPredicate { } } + /// Find the deepest depth at which the predicate matches. + pub fn depth_of(&self, contexts: &[KeyContext]) -> Option { + for depth in (0..=contexts.len()).rev() { + let context_slice = &contexts[0..depth]; + if self.eval_inner(context_slice, contexts) { + return Some(depth); + } + } + None + } + /// Eval a predicate against a set of contexts, arranged from lowest to highest. - pub fn eval(&self, contexts: &[KeyContext]) -> bool { + #[allow(unused)] + pub(crate) fn eval(&self, contexts: &[KeyContext]) -> bool { + self.eval_inner(contexts, contexts) + } + + /// Eval a predicate against a set of contexts, arranged from lowest to highest. + pub fn eval_inner(&self, contexts: &[KeyContext], all_contexts: &[KeyContext]) -> bool { let Some(context) = contexts.last() else { return false; }; @@ -264,12 +281,38 @@ impl KeyBindingContextPredicate { .get(left) .map(|value| value != right) .unwrap_or(true), - Self::Not(pred) => !pred.eval(contexts), - Self::Child(parent, child) => { - parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts) + Self::Not(pred) => { + for i in 0..all_contexts.len() { + if pred.eval_inner(&all_contexts[..=i], all_contexts) { + return false; + } + } + return true; + } + // Workspace > Pane > Editor + // + // Pane > (Pane > Editor) // should match? + // (Pane > Pane) > Editor // should not match? + // Pane > !Workspace <-- should match? + // !Workspace <-- shouldn't match? + Self::Descendant(parent, child) => { + for i in 0..contexts.len() - 1 { + // [Workspace > Pane], [Editor] + if parent.eval_inner(&contexts[..=i], all_contexts) { + if !child.eval_inner(&contexts[i + 1..], &contexts[i + 1..]) { + return false; + } + return true; + } + } + return false; + } + Self::And(left, right) => { + left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts) + } + Self::Or(left, right) => { + left.eval_inner(contexts, all_contexts) || right.eval_inner(contexts, all_contexts) } - Self::And(left, right) => left.eval(contexts) && right.eval(contexts), - Self::Or(left, right) => left.eval(contexts) || right.eval(contexts), } } @@ -285,7 +328,7 @@ impl KeyBindingContextPredicate { } match other { - KeyBindingContextPredicate::Child(_, child) => self.is_superset(child), + KeyBindingContextPredicate::Descendant(_, child) => self.is_superset(child), KeyBindingContextPredicate::And(left, right) => { self.is_superset(left) || self.is_superset(right) } @@ -375,7 +418,7 @@ impl KeyBindingContextPredicate { } fn new_child(self, other: Self) -> Result { - Ok(Self::Child(Box::new(self), Box::new(other))) + Ok(Self::Descendant(Box::new(self), Box::new(other))) } fn new_eq(self, other: Self) -> Result { @@ -598,4 +641,122 @@ mod tests { assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})"); } } + + #[test] + fn test_child_operator() { + let predicate = KeyBindingContextPredicate::parse("parent > child").unwrap(); + + let parent_context = KeyContext::try_from("parent").unwrap(); + let child_context = KeyContext::try_from("child").unwrap(); + + let contexts = vec![parent_context.clone(), child_context.clone()]; + assert!(predicate.eval(&contexts)); + + let grandparent_context = KeyContext::try_from("grandparent").unwrap(); + + let contexts = vec![ + grandparent_context, + parent_context.clone(), + child_context.clone(), + ]; + assert!(predicate.eval(&contexts)); + + let other_context = KeyContext::try_from("other").unwrap(); + + let contexts = vec![other_context.clone(), child_context.clone()]; + assert!(!predicate.eval(&contexts)); + + let contexts = vec![ + parent_context.clone(), + other_context.clone(), + child_context.clone(), + ]; + assert!(predicate.eval(&contexts)); + + assert!(!predicate.eval(&[])); + assert!(!predicate.eval(&[child_context.clone()])); + assert!(!predicate.eval(&[parent_context])); + + let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); + assert!(!zany_predicate.eval(&[child_context.clone()])); + assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); + } + + #[test] + fn test_not_operator() { + let not_predicate = KeyBindingContextPredicate::parse("!editor").unwrap(); + let editor_context = KeyContext::try_from("editor").unwrap(); + let workspace_context = KeyContext::try_from("workspace").unwrap(); + let parent_context = KeyContext::try_from("parent").unwrap(); + let child_context = KeyContext::try_from("child").unwrap(); + + assert!(not_predicate.eval(&[workspace_context.clone()])); + assert!(!not_predicate.eval(&[editor_context.clone()])); + assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); + assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); + + let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); + assert!(complex_not.eval(&[workspace_context.clone()])); + assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); + + let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); + let mut mode_context = KeyContext::default(); + mode_context.set("mode", "full"); + assert!(!not_mode_predicate.eval(&[mode_context.clone()])); + + let mut other_mode_context = KeyContext::default(); + other_mode_context.set("mode", "partial"); + assert!(not_mode_predicate.eval(&[other_mode_context])); + + let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); + assert!(not_descendant.eval(&[parent_context.clone()])); + assert!(not_descendant.eval(&[child_context.clone()])); + assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); + + let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); + assert!(!not_descendant.eval(&[parent_context.clone()])); + assert!(!not_descendant.eval(&[child_context.clone()])); + assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); + + let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); + assert!(double_not.eval(&[editor_context.clone()])); + assert!(!double_not.eval(&[workspace_context.clone()])); + + // Test complex descendant cases + let workspace_context = KeyContext::try_from("Workspace").unwrap(); + let pane_context = KeyContext::try_from("Pane").unwrap(); + let editor_context = KeyContext::try_from("Editor").unwrap(); + + // Workspace > Pane > Editor + let workspace_pane_editor = vec![ + workspace_context.clone(), + pane_context.clone(), + editor_context.clone(), + ]; + + // Pane > (Pane > Editor) - should not match + let pane_pane_editor = KeyBindingContextPredicate::parse("Pane > (Pane > Editor)").unwrap(); + assert!(!pane_pane_editor.eval(&workspace_pane_editor)); + + let workspace_pane_editor_predicate = + KeyBindingContextPredicate::parse("Workspace > Pane > Editor").unwrap(); + assert!(workspace_pane_editor_predicate.eval(&workspace_pane_editor)); + + // (Pane > Pane) > Editor - should not match + let pane_pane_then_editor = + KeyBindingContextPredicate::parse("(Pane > Pane) > Editor").unwrap(); + assert!(!pane_pane_then_editor.eval(&workspace_pane_editor)); + + // Pane > !Workspace - should match + let pane_not_workspace = KeyBindingContextPredicate::parse("Pane > !Workspace").unwrap(); + assert!(pane_not_workspace.eval(&[pane_context.clone(), editor_context.clone()])); + assert!(!pane_not_workspace.eval(&[pane_context.clone(), workspace_context.clone()])); + + // !Workspace - shouldn't match when Workspace is in the context + let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); + assert!(!not_workspace.eval(&[workspace_context.clone()])); + assert!(not_workspace.eval(&[pane_context.clone()])); + assert!(not_workspace.eval(&[editor_context.clone()])); + assert!(!not_workspace.eval(&workspace_pane_editor)); + } } diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index c933872d8c..88131781ec 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -132,14 +132,7 @@ impl KeyContextView { } fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool { - let mut stack = self.context_stack.clone(); - while !stack.is_empty() { - if predicate.eval(&stack) { - return true; - } - stack.pop(); - } - false + predicate.depth_of(&self.context_stack).is_some() } fn action_matches(&self, a: &Option>, b: &dyn Action) -> bool { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 29927c4196..1a5bb7b459 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -3008,7 +3008,7 @@ fn collect_contexts_from_assets() -> Vec { contexts.insert(ident_a); contexts.insert(ident_b); } - gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => { + gpui::KeyBindingContextPredicate::Descendant(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index b2beaf9ffb..90aa400bb4 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -24,7 +24,7 @@ The file contains a JSON array of objects with `"bindings"`. If no `"context"` i Within each binding section a [key sequence](#keybinding-syntax) is mapped to an [action](#actions). If conflicts are detected they are resolved as [described below](#precedence). -If you are using a non-QWERTY, Latin-character keyboard, you may want to set `use_layout_keys` to `true`. See [Non-QWERTY keyboards](#non-qwerty-keyboards) for more information. +If you are using a non-QWERTY, Latin-character keyboard, you may want to set `use_key_equivalents` to `true`. See [Non-QWERTY keyboards](#non-qwerty-keyboards) for more information. For example: @@ -87,8 +87,6 @@ If a binding group has a `"context"` key it will be matched against the currentl Zed's contexts make up a tree, with the root being `Workspace`. Workspaces contain Panes and Panels, and Panes contain Editors, etc. The easiest way to see what contexts are active at a given moment is the key context view, which you can get to with `dev: Open Key Context View` in the command palette. -Contexts can contain extra attributes in addition to the name, so that you can (for example) match only in markdown files with `"context": "Editor && extension==md"`. It's worth noting that you can only use attributes at the level they are defined. - For example: ``` @@ -106,9 +104,20 @@ Workspace os=macos Context expressions can contain the following syntax: - `X && Y`, `X || Y` to and/or two conditions -- `!X` to negate a condition +- `!X` to check that a condition is false - `(X)` for grouping -- `X > Y` to match if a parent in the tree matches X and this layer matches Y. +- `X > Y` to match if an ancestor in the tree matches X and this layer matches Y. + +For example: + +- `"context": "Editor"` - matches any editor (including inline inputs) +- `"context": "Editor && mode=full"` - matches the main editors used for editing code +- `"context": "!Editor && !Terminal"` - matches anywhere except where an Editor or Terminal is focused +- `"context": "os=macos > Editor"` - matches any editor on macOS. + +It's worth noting that attributes are only available on the node they are defined on. This means that if you want to (for example) only enable a keybinding when the debugger is stopped in vim normal mode, you need to do `debugger_stopped > vim_mode == normal`. + +Note: Before Zed v0.197.x, the ! operator only looked at one node at a time, and `>` meant "parent" not "ancestor". This meant that `!Editor` would match the context `Workspace > Pane > Editor`, because (confusingly) the Pane matches `!Editor`, and that `os=macos > Editor` did not match the context `Workspace > Pane > Editor` because of the intermediate `Pane` node. If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts)