Update keymap context binding behavior of > and ! (#34664)
Now ! means "no ancestors matches this", and > means "any descendent" not "any child". Updates #34570 Co-authored-by: Ben Kunkle <ben@zed.dev> Release Notes: - *Breaking change*. The context predicates in the keymap file now handle ! and > differently. Before this change ! meant "this node does not match", now it means "none of these nodes match". Before this change > meant "child of", now it means "descendent of". We do not expect these changes to break many keymaps, but they may cause subtle changes for complex context queries. --------- Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
parent
f461290ac3
commit
e421fc7a2d
6 changed files with 261 additions and 90 deletions
|
@ -724,7 +724,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "AgentPanel || GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
|
"context": "VimControl || !Editor && !Terminal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
// window related commands (ctrl-w X)
|
// window related commands (ctrl-w X)
|
||||||
"ctrl-w": null,
|
"ctrl-w": null,
|
||||||
|
@ -782,7 +782,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "ChangesList || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
|
"context": "!Editor && !Terminal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
":": "command_palette::Toggle",
|
":": "command_palette::Toggle",
|
||||||
"g /": "pane::DeploySearch"
|
"g /": "pane::DeploySearch"
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub struct Keymap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Index of a binding within a 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);
|
pub struct BindingIndex(usize);
|
||||||
|
|
||||||
impl Keymap {
|
impl Keymap {
|
||||||
|
@ -167,65 +167,60 @@ impl Keymap {
|
||||||
input: &[Keystroke],
|
input: &[Keystroke],
|
||||||
context_stack: &[KeyContext],
|
context_stack: &[KeyContext],
|
||||||
) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) {
|
) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) {
|
||||||
let possibilities = self
|
let mut possibilities = self
|
||||||
.bindings()
|
.bindings()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.rev()
|
.rev()
|
||||||
.filter_map(|(ix, binding)| {
|
.filter_map(|(ix, binding)| {
|
||||||
binding
|
let depth = self.binding_enabled(binding, &context_stack)?;
|
||||||
.match_keystrokes(input)
|
let pending = binding.match_keystrokes(input)?;
|
||||||
.map(|pending| (BindingIndex(ix), binding, pending))
|
Some((depth, BindingIndex(ix), binding, pending))
|
||||||
});
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
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();
|
let mut bindings: SmallVec<[(BindingIndex, KeyBinding, usize); 1]> = SmallVec::new();
|
||||||
|
|
||||||
// (pending, is_no_action, depth, keystrokes)
|
// (pending, is_no_action, depth, keystrokes)
|
||||||
let mut pending_info_opt: Option<(bool, bool, usize, &[Keystroke])> = None;
|
let mut pending_info_opt: Option<(bool, bool, usize, &[Keystroke])> = None;
|
||||||
|
|
||||||
'outer: for (binding_index, binding, pending) in possibilities {
|
'outer: for (depth, binding_index, binding, pending) in possibilities {
|
||||||
for depth in (0..=context_stack.len()).rev() {
|
let is_no_action = is_no_action(&*binding.action);
|
||||||
if self.binding_enabled(binding, &context_stack[0..depth]) {
|
// We only want to consider a binding pending if it has an action
|
||||||
let is_no_action = is_no_action(&*binding.action);
|
// This, however, means that if we have both a NoAction binding and a binding
|
||||||
// We only want to consider a binding pending if it has an action
|
// with an action at the same depth, we should still set is_pending to true.
|
||||||
// This, however, means that if we have both a NoAction binding and a binding
|
if let Some(pending_info) = pending_info_opt.as_mut() {
|
||||||
// with an action at the same depth, we should still set is_pending to true.
|
let (already_pending, pending_is_no_action, pending_depth, pending_keystrokes) =
|
||||||
if let Some(pending_info) = pending_info_opt.as_mut() {
|
*pending_info;
|
||||||
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
|
// 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
|
// 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
|
// binding erroneously setting the pending status to true when a binding with an action
|
||||||
// already set it to false
|
// already set it to false
|
||||||
//
|
//
|
||||||
// We also want to change the pending status if the keystrokes don't match,
|
// 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
|
// meaning it's different keystrokes than the NoAction that set pending to false
|
||||||
if pending
|
if pending
|
||||||
&& !already_pending
|
&& !already_pending
|
||||||
&& pending_is_no_action
|
&& pending_is_no_action
|
||||||
&& (pending_depth == depth
|
&& (pending_depth == depth || pending_keystrokes != binding.keystrokes())
|
||||||
|| pending_keystrokes != binding.keystrokes())
|
{
|
||||||
{
|
pending_info.0 = !is_no_action;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} 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
|
// sort by descending depth
|
||||||
|
@ -245,15 +240,13 @@ impl Keymap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given binding is enabled, given a certain key context.
|
/// Check if the given binding is enabled, given a certain key context.
|
||||||
fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool {
|
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||||
// If binding has a context predicate, it must match the current context,
|
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||||
if let Some(predicate) = &binding.context_predicate {
|
if let Some(predicate) = &binding.context_predicate {
|
||||||
if !predicate.eval(context) {
|
predicate.depth_of(contexts)
|
||||||
return false;
|
} else {
|
||||||
}
|
Some(contexts.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,18 +273,33 @@ mod tests {
|
||||||
keymap.add_bindings(bindings.clone());
|
keymap.add_bindings(bindings.clone());
|
||||||
|
|
||||||
// global bindings are enabled in all contexts
|
// global bindings are enabled in all contexts
|
||||||
assert!(keymap.binding_enabled(&bindings[0], &[]));
|
assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0));
|
||||||
assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]));
|
assert_eq!(
|
||||||
|
keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
|
|
||||||
// contextual bindings are enabled in contexts that match their predicate
|
// contextual bindings are enabled in contexts that match their predicate
|
||||||
assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]));
|
assert_eq!(
|
||||||
assert!(keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]));
|
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_eq!(
|
||||||
assert!(keymap.binding_enabled(
|
keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]),
|
||||||
&bindings[2],
|
None
|
||||||
&[KeyContext::parse("editor mode=full").unwrap()]
|
);
|
||||||
));
|
assert_eq!(
|
||||||
|
keymap.binding_enabled(
|
||||||
|
&bindings[2],
|
||||||
|
&[KeyContext::parse("editor mode=full").unwrap()]
|
||||||
|
),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -178,7 +178,7 @@ pub enum KeyBindingContextPredicate {
|
||||||
NotEqual(SharedString, SharedString),
|
NotEqual(SharedString, SharedString),
|
||||||
/// A predicate that will match a given predicate appearing below another predicate.
|
/// A predicate that will match a given predicate appearing below another predicate.
|
||||||
/// in the element tree
|
/// in the element tree
|
||||||
Child(
|
Descendant(
|
||||||
Box<KeyBindingContextPredicate>,
|
Box<KeyBindingContextPredicate>,
|
||||||
Box<KeyBindingContextPredicate>,
|
Box<KeyBindingContextPredicate>,
|
||||||
),
|
),
|
||||||
|
@ -203,7 +203,7 @@ impl fmt::Display for KeyBindingContextPredicate {
|
||||||
Self::Equal(left, right) => write!(f, "{} == {}", left, right),
|
Self::Equal(left, right) => write!(f, "{} == {}", left, right),
|
||||||
Self::NotEqual(left, right) => write!(f, "{} != {}", left, right),
|
Self::NotEqual(left, right) => write!(f, "{} != {}", left, right),
|
||||||
Self::Not(pred) => write!(f, "!{}", pred),
|
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::And(left, right) => write!(f, "({} && {})", left, right),
|
||||||
Self::Or(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<usize> {
|
||||||
|
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.
|
/// 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 {
|
let Some(context) = contexts.last() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
@ -264,12 +281,38 @@ impl KeyBindingContextPredicate {
|
||||||
.get(left)
|
.get(left)
|
||||||
.map(|value| value != right)
|
.map(|value| value != right)
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
Self::Not(pred) => !pred.eval(contexts),
|
Self::Not(pred) => {
|
||||||
Self::Child(parent, child) => {
|
for i in 0..all_contexts.len() {
|
||||||
parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
|
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 {
|
match other {
|
||||||
KeyBindingContextPredicate::Child(_, child) => self.is_superset(child),
|
KeyBindingContextPredicate::Descendant(_, child) => self.is_superset(child),
|
||||||
KeyBindingContextPredicate::And(left, right) => {
|
KeyBindingContextPredicate::And(left, right) => {
|
||||||
self.is_superset(left) || self.is_superset(right)
|
self.is_superset(left) || self.is_superset(right)
|
||||||
}
|
}
|
||||||
|
@ -375,7 +418,7 @@ impl KeyBindingContextPredicate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_child(self, other: Self) -> Result<Self> {
|
fn new_child(self, other: Self) -> Result<Self> {
|
||||||
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<Self> {
|
fn new_eq(self, other: Self) -> Result<Self> {
|
||||||
|
@ -598,4 +641,122 @@ mod tests {
|
||||||
assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})");
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,14 +132,7 @@ impl KeyContextView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
|
fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
|
||||||
let mut stack = self.context_stack.clone();
|
predicate.depth_of(&self.context_stack).is_some()
|
||||||
while !stack.is_empty() {
|
|
||||||
if predicate.eval(&stack) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
|
fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
|
||||||
|
|
|
@ -3008,7 +3008,7 @@ fn collect_contexts_from_assets() -> Vec<SharedString> {
|
||||||
contexts.insert(ident_a);
|
contexts.insert(ident_a);
|
||||||
contexts.insert(ident_b);
|
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_a);
|
||||||
queue.push(*ctx_b);
|
queue.push(*ctx_b);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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).
|
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:
|
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.
|
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:
|
For example:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -106,9 +104,20 @@ Workspace os=macos
|
||||||
Context expressions can contain the following syntax:
|
Context expressions can contain the following syntax:
|
||||||
|
|
||||||
- `X && Y`, `X || Y` to and/or two conditions
|
- `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)` 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)
|
If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue