mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-28 21:32:39 +00:00
Unbind app menu actions (#20268)
Closes #7544 Release Notes: - Fixed an issue that prevented removing key bindings for actions used in the macOS application menu.
This commit is contained in:
parent
b23835b5a5
commit
50069a2153
3 changed files with 126 additions and 13 deletions
|
@ -1,7 +1,7 @@
|
||||||
use crate::SharedString;
|
use crate::SharedString;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
pub use no_action::NoAction;
|
pub use no_action::{is_no_action, NoAction};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
|
|
||||||
|
@ -321,6 +321,12 @@ macro_rules! __impl_action {
|
||||||
|
|
||||||
mod no_action {
|
mod no_action {
|
||||||
use crate as gpui;
|
use crate as gpui;
|
||||||
|
use std::any::Any as _;
|
||||||
|
|
||||||
actions!(zed, [NoAction]);
|
actions!(zed, [NoAction]);
|
||||||
|
|
||||||
|
/// Returns whether or not this action represents a removed key binding.
|
||||||
|
pub fn is_no_action(action: &dyn gpui::Action) -> bool {
|
||||||
|
action.as_any().type_id() == (NoAction {}).type_id()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,10 @@ mod context;
|
||||||
pub use binding::*;
|
pub use binding::*;
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
|
|
||||||
use crate::{Action, Keystroke, NoAction};
|
use crate::{is_no_action, Action, Keystroke};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::any::{Any, TypeId};
|
use std::any::TypeId;
|
||||||
|
|
||||||
/// An opaque identifier of which version of the keymap is currently active.
|
/// An opaque identifier of which version of the keymap is currently active.
|
||||||
/// The keymap's version is changed whenever bindings are added or removed.
|
/// The keymap's version is changed whenever bindings are added or removed.
|
||||||
|
@ -19,6 +19,7 @@ pub struct KeymapVersion(usize);
|
||||||
pub struct Keymap {
|
pub struct Keymap {
|
||||||
bindings: Vec<KeyBinding>,
|
bindings: Vec<KeyBinding>,
|
||||||
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
||||||
|
no_action_binding_indices: Vec<usize>,
|
||||||
version: KeymapVersion,
|
version: KeymapVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,10 +40,14 @@ impl Keymap {
|
||||||
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
|
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
|
||||||
for binding in bindings {
|
for binding in bindings {
|
||||||
let action_id = binding.action().as_any().type_id();
|
let action_id = binding.action().as_any().type_id();
|
||||||
self.binding_indices_by_action_id
|
if is_no_action(&*binding.action) {
|
||||||
.entry(action_id)
|
self.no_action_binding_indices.push(self.bindings.len());
|
||||||
.or_default()
|
} else {
|
||||||
.push(self.bindings.len());
|
self.binding_indices_by_action_id
|
||||||
|
.entry(action_id)
|
||||||
|
.or_default()
|
||||||
|
.push(self.bindings.len());
|
||||||
|
}
|
||||||
self.bindings.push(binding);
|
self.bindings.push(binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +58,7 @@ impl Keymap {
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.bindings.clear();
|
self.bindings.clear();
|
||||||
self.binding_indices_by_action_id.clear();
|
self.binding_indices_by_action_id.clear();
|
||||||
|
self.no_action_binding_indices.clear();
|
||||||
self.version.0 += 1;
|
self.version.0 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,12 +73,39 @@ impl Keymap {
|
||||||
action: &'a dyn Action,
|
action: &'a dyn Action,
|
||||||
) -> impl 'a + DoubleEndedIterator<Item = &'a KeyBinding> {
|
) -> impl 'a + DoubleEndedIterator<Item = &'a KeyBinding> {
|
||||||
let action_id = action.type_id();
|
let action_id = action.type_id();
|
||||||
self.binding_indices_by_action_id
|
let binding_indices = self
|
||||||
|
.binding_indices_by_action_id
|
||||||
.get(&action_id)
|
.get(&action_id)
|
||||||
.map_or(&[] as _, SmallVec::as_slice)
|
.map_or(&[] as _, SmallVec::as_slice)
|
||||||
.iter()
|
.iter();
|
||||||
.map(|ix| &self.bindings[*ix])
|
|
||||||
.filter(move |binding| binding.action().partial_eq(action))
|
binding_indices.filter_map(|ix| {
|
||||||
|
let binding = &self.bindings[*ix];
|
||||||
|
if !binding.action().partial_eq(action) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for null_ix in &self.no_action_binding_indices {
|
||||||
|
if null_ix > ix {
|
||||||
|
let null_binding = &self.bindings[*null_ix];
|
||||||
|
if null_binding.keystrokes == binding.keystrokes {
|
||||||
|
let null_binding_matches =
|
||||||
|
match (&null_binding.context_predicate, &binding.context_predicate) {
|
||||||
|
(None, _) => true,
|
||||||
|
(Some(_), None) => false,
|
||||||
|
(Some(null_predicate), Some(predicate)) => {
|
||||||
|
null_predicate.is_superset(predicate)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if null_binding_matches {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(binding)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// all bindings for input returns all bindings that might match the input
|
/// all bindings for input returns all bindings that might match the input
|
||||||
|
@ -134,7 +167,7 @@ impl Keymap {
|
||||||
let bindings = bindings
|
let bindings = bindings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map_while(|(binding, _)| {
|
.map_while(|(binding, _)| {
|
||||||
if binding.action.as_any().type_id() == (NoAction {}).type_id() {
|
if is_no_action(&*binding.action) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(binding)
|
Some(binding)
|
||||||
|
@ -162,7 +195,7 @@ impl Keymap {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate as gpui;
|
use crate as gpui;
|
||||||
use gpui::actions;
|
use gpui::{actions, NoAction};
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
keymap_test,
|
keymap_test,
|
||||||
|
@ -241,4 +274,31 @@ mod tests {
|
||||||
.0
|
.0
|
||||||
.is_empty());
|
.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bindings_for_action() {
|
||||||
|
let bindings = [
|
||||||
|
KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")),
|
||||||
|
KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")),
|
||||||
|
KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")),
|
||||||
|
KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")),
|
||||||
|
KeyBinding::new("ctrl-b", NoAction {}, Some("editor")),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut keymap = Keymap::default();
|
||||||
|
keymap.add_bindings(bindings.clone());
|
||||||
|
|
||||||
|
assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]);
|
||||||
|
assert_bindings(&keymap, &ActionBeta {}, &[]);
|
||||||
|
assert_bindings(&keymap, &ActionGamma {}, &["ctrl-c"]);
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||||
|
let actual = keymap
|
||||||
|
.bindings_for_action(action)
|
||||||
|
.map(|binding| binding.keystrokes[0].unparse())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(actual, expected, "{:?}", action);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,6 +269,30 @@ impl KeyBindingContextPredicate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether or not this predicate matches all possible contexts matched by
|
||||||
|
/// the other predicate.
|
||||||
|
pub fn is_superset(&self, other: &Self) -> bool {
|
||||||
|
if self == other {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let KeyBindingContextPredicate::Or(left, right) = self {
|
||||||
|
return left.is_superset(other) || right.is_superset(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
match other {
|
||||||
|
KeyBindingContextPredicate::Child(_, child) => self.is_superset(child),
|
||||||
|
KeyBindingContextPredicate::And(left, right) => {
|
||||||
|
self.is_superset(left) || self.is_superset(right)
|
||||||
|
}
|
||||||
|
KeyBindingContextPredicate::Identifier(_) => false,
|
||||||
|
KeyBindingContextPredicate::Equal(_, _) => false,
|
||||||
|
KeyBindingContextPredicate::NotEqual(_, _) => false,
|
||||||
|
KeyBindingContextPredicate::Not(_) => false,
|
||||||
|
KeyBindingContextPredicate::Or(_, _) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
|
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
|
||||||
type Op = fn(
|
type Op = fn(
|
||||||
KeyBindingContextPredicate,
|
KeyBindingContextPredicate,
|
||||||
|
@ -559,4 +583,27 @@ mod tests {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_superset() {
|
||||||
|
assert_is_superset("editor", "editor", true);
|
||||||
|
assert_is_superset("editor", "workspace", false);
|
||||||
|
|
||||||
|
assert_is_superset("editor", "editor && vim_mode", true);
|
||||||
|
assert_is_superset("editor", "mode == full && editor", true);
|
||||||
|
assert_is_superset("editor && mode == full", "editor", false);
|
||||||
|
|
||||||
|
assert_is_superset("editor", "something > editor", true);
|
||||||
|
assert_is_superset("editor", "editor > menu", false);
|
||||||
|
|
||||||
|
assert_is_superset("foo || bar || baz", "bar", true);
|
||||||
|
assert_is_superset("foo || bar || baz", "quux", false);
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_is_superset(a: &str, b: &str, result: bool) {
|
||||||
|
let a = KeyBindingContextPredicate::parse(a).unwrap();
|
||||||
|
let b = KeyBindingContextPredicate::parse(b).unwrap();
|
||||||
|
assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue