Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/gpui/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ macro_rules! actions {
/// Multiple arguments separated by commas may be specified in `#[action(...)]`:
///
/// - `namespace = some_namespace` sets the namespace. In Zed this is required.
/// `namespace = crate` uses the name of the crate being compiled.
///
/// - `name = "ActionName"` overrides the action's name. This must not contain `::`.
///
Expand Down
28 changes: 28 additions & 0 deletions crates/gpui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ impl Application {
self
}

/// Run without the library-default key bindings registered via
/// [`crate::keybinding!`]. Applications that own their entire keymap (like
/// Zed) can use this to ensure no bindings exist except the ones they load
/// themselves. Individual defaults can instead be masked by binding
/// [`crate::NoAction`] over their keystrokes.
pub fn without_default_key_bindings(self) -> Self {
self.0.borrow_mut().default_key_bindings_enabled = false;
self
}

/// Start the application. The provided callback will be called once the
/// app is fully launched.
pub fn run<F>(self, on_finish_launching: F)
Expand All @@ -230,6 +240,7 @@ impl Application {
let platform = self.0.borrow().platform.clone();
platform.run(Box::new(move || {
let cx = &mut *this.borrow_mut();
cx.load_default_key_bindings();
on_finish_launching(cx);
}));
}
Expand All @@ -251,6 +262,7 @@ impl Application {
let platform = self.0.borrow().platform.clone();
platform.run(Box::new(move || {
let cx = &mut *this.borrow_mut();
cx.load_default_key_bindings();
on_finish_launching(cx);
}));
ApplicationHandle { app: self.0 }
Expand Down Expand Up @@ -691,6 +703,7 @@ pub struct App {
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
pub(crate) focus_handles: Arc<FocusMap>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) default_key_bindings_enabled: bool,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
pub(crate) global_action_listeners:
Expand Down Expand Up @@ -810,6 +823,7 @@ impl App {
window_handles: FxHashMap::default(),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
keymap: Rc::new(RefCell::new(Keymap::default())),
default_key_bindings_enabled: true,
keyboard_layout,
keyboard_mapper,
global_action_listeners: Default::default(),
Expand Down Expand Up @@ -2071,6 +2085,20 @@ impl App {
self.pending_effects.push_back(Effect::RefreshWindows);
}

/// Load the library-default key bindings registered via
/// [`crate::keybinding!`], unless the application opted out with
/// [`crate::Application::without_default_key_bindings`]. Called at the
/// beginning of `run` (and at test/headless app construction), before any
/// user bindings, so user keymaps shadow the defaults via
/// declaration-order precedence.
pub(crate) fn load_default_key_bindings(&mut self) {
if self.default_key_bindings_enabled {
self.keymap
.borrow_mut()
.add_bindings(crate::DefaultKeyBinding::load_all());
}
}

/// Clear all key bindings in the app.
pub fn clear_key_bindings(&mut self) {
self.keymap.borrow_mut().clear();
Expand Down
1 change: 1 addition & 0 deletions crates/gpui/src/app/bench_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
let asset_source = Arc::new(());
let http_client = http_client::FakeHttpClient::with_404_response();
let app = App::new_app(platform, asset_source, http_client);
app.borrow_mut().load_default_key_bindings();

Self {
app,
Expand Down
1 change: 1 addition & 0 deletions crates/gpui/src/app/headless_app_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ impl HeadlessAppContext {
let http_client = http_client::FakeHttpClient::with_404_response();
let app = App::new_app(platform, asset_source, http_client);
app.borrow_mut().mode = GpuiMode::test();
app.borrow_mut().load_default_key_bindings();

Self {
app,
Expand Down
1 change: 1 addition & 0 deletions crates/gpui/src/app/test_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl TestApp {

let app = App::new_app(platform.clone(), asset_source, http_client);
app.borrow_mut().mode = GpuiMode::test();
app.borrow_mut().load_default_key_bindings();

Self {
app,
Expand Down
1 change: 1 addition & 0 deletions crates/gpui/src/app/test_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ impl TestAppContext {

let app = App::new_app(platform.clone(), asset_source, http_client);
app.borrow_mut().mode = GpuiMode::test();
app.borrow_mut().load_default_key_bindings();

Self {
app,
Expand Down
1 change: 1 addition & 0 deletions crates/gpui/src/app/visual_test_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ impl VisualTestAppContext {

let mut app = App::new_app(platform.clone(), asset_source, http_client);
app.borrow_mut().mode = GpuiMode::test();
app.borrow_mut().load_default_key_bindings();

Self {
app,
Expand Down
54 changes: 54 additions & 0 deletions crates/gpui/src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ impl Keymap {
self.version.0 += 1;
}

/// Remove all library-default bindings registered via [`crate::keybinding!`],
/// preserving the relative order of the remaining bindings.
pub fn remove_default_bindings(&mut self) {
if !self.bindings.iter().any(|binding| binding.default) {
return;
}
let bindings = std::mem::take(&mut self.bindings);
self.binding_indices_by_action_id.clear();
self.disabled_binding_indices.clear();
// Re-adding rebuilds the index structures, which refer to bindings by
// position and would otherwise be invalidated by removal.
self.add_bindings(bindings.into_iter().filter(|binding| !binding.default));
}

/// Iterate over all bindings, in the order they were added.
pub fn bindings(&self) -> impl DoubleEndedIterator<Item = &KeyBinding> + ExactSizeIterator {
self.bindings.iter()
Expand Down Expand Up @@ -300,6 +314,46 @@ mod tests {
[ActionAlpha, ActionBeta, ActionGamma, ActionDelta,]
);

crate::keybinding!(
"ctrl-alt-shift-f19",
KeybindingMacroTest,
"GpuiKeybindingMacroTest"
);

#[test]
fn keybinding_macro_registers_action_and_default_binding() {
use crate::{Action as _, TestAppContext};

assert_eq!(KeybindingMacroTest.name(), "gpui::KeybindingMacroTest");

let mut cx = TestAppContext::single();
cx.update(|cx| {
{
let keymap = cx.keymap.borrow();
let bindings: Vec<_> = keymap.bindings_for_action(&KeybindingMacroTest).collect();
assert_eq!(bindings.len(), 1);
assert!(bindings[0].is_default());
assert!(bindings[0].context_predicate.is_some());
}

cx.bind_keys([KeyBinding::new(
"ctrl-alt-shift-f18",
KeybindingMacroTest,
None,
)]);

let mut keymap = cx.keymap.borrow_mut();
keymap.remove_default_bindings();
let bindings: Vec<_> = keymap.bindings_for_action(&KeybindingMacroTest).collect();
assert_eq!(
bindings.len(),
1,
"defaults are removed, user bindings survive"
);
assert!(!bindings[0].is_default());
});
}

#[test]
fn test_keymap() {
let bindings = [
Expand Down
127 changes: 127 additions & 0 deletions crates/gpui/src/keymap/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub struct KeyBinding {
pub(crate) meta: Option<KeyBindingMetaIndex>,
/// The json input string used when building the keybinding, if any
pub(crate) action_input: Option<SharedString>,
/// Whether this is a library default registered via [`crate::keybinding!`].
pub(crate) default: bool,
}

impl Clone for KeyBinding {
Expand All @@ -24,6 +26,7 @@ impl Clone for KeyBinding {
context_predicate: self.context_predicate.clone(),
meta: self.meta,
action_input: self.action_input.clone(),
default: self.default,
}
}
}
Expand Down Expand Up @@ -71,9 +74,15 @@ impl KeyBinding {
context_predicate,
meta: None,
action_input,
default: false,
})
}

/// Whether this is a library default registered via [`crate::keybinding!`].
pub fn is_default(&self) -> bool {
self.default
}

/// Set the metadata for this binding.
pub fn with_meta(mut self, meta: KeyBindingMetaIndex) -> Self {
self.meta = Some(meta);
Expand Down Expand Up @@ -141,3 +150,121 @@ impl std::fmt::Debug for KeyBinding {
/// associated with the binding, such as the source of the binding.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyBindingMetaIndex(pub u32);

/// A default key binding registered at link time via [`crate::keybinding!`].
/// Collected into the keymap when an [`crate::App`] is created, before any
/// user bindings, so user bindings take precedence via declaration order.
///
/// This type is public so the macro can construct it in other crates; it is
/// not intended to be used directly.
#[doc(hidden)]
pub struct DefaultKeyBinding {
/// The keystrokes to bind, in the same format as [`KeyBinding::new`].
pub keystrokes: &'static str,
/// An optional key context predicate to scope the binding.
pub context: Option<&'static str>,
/// Builds the action this binding dispatches.
pub build_action: fn() -> Box<dyn Action>,
}

inventory::collect!(DefaultKeyBinding);

impl DefaultKeyBinding {
/// Construct a registration record. Used by [`crate::keybinding!`].
#[doc(hidden)]
pub const fn new(
keystrokes: &'static str,
context: Option<&'static str>,
build_action: fn() -> Box<dyn Action>,
) -> Self {
DefaultKeyBinding {
keystrokes,
context,
build_action,
}
}

/// Load all registered default key bindings, marking them as defaults.
/// Invalid registrations are programmer errors in the registering crate:
/// they panic in debug builds and are logged and skipped in release.
pub(crate) fn load_all() -> Vec<KeyBinding> {
let mut bindings = Vec::new();
for registration in inventory::iter::<DefaultKeyBinding> {
let action = (registration.build_action)();
let context_predicate = match registration.context {
Some(context) => match KeyBindingContextPredicate::parse(context) {
Ok(predicate) => Some(Rc::new(predicate)),
Err(error) => {
gpui_util::debug_panic!(
"invalid context {:?} in default key binding for {}: {}",
context,
action.name(),
error
);
continue;
}
},
None => None,
};
match KeyBinding::load(
registration.keystrokes,
action,
context_predicate,
false,
None,
&DummyKeyboardMapper,
) {
Ok(mut binding) => {
binding.default = true;
bindings.push(binding);
}
Err(error) => {
gpui_util::debug_panic!(
"invalid keystrokes {:?} in default key binding: {}",
registration.keystrokes,
error
);
}
}
}
bindings
}
}

/// Declares an action and registers a default key binding for it, in one step.
///
/// The action is registered under the crate's namespace (like `actions!` with
/// the crate name as the namespace), and the binding is added to every
/// [`crate::App`]'s keymap at creation time — before any user bindings, so
/// user keymaps shadow it via declaration-order precedence. Applications can
/// opt out of all library defaults with
/// [`crate::Application::without_default_key_bindings`], or mask individual
/// ones by binding [`crate::NoAction`] over them.
///
/// ```ignore
/// keybinding!("enter", Confirm);
/// keybinding!("escape", Cancel, "TextInput"); // scoped to a key context
/// ```
#[macro_export]
macro_rules! keybinding {
($keystrokes:literal, $name:ident) => {
gpui::keybinding!(@impl $keystrokes, $name, ::std::option::Option::None);
};
($keystrokes:literal, $name:ident, $context:literal) => {
gpui::keybinding!(@impl $keystrokes, $name, ::std::option::Option::Some($context));
};
(@impl $keystrokes:literal, $name:ident, $context:expr) => {
#[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, gpui::Action)]
#[action(namespace = crate)]
pub struct $name;

const _: () = {
fn build_action() -> ::std::boxed::Box<dyn gpui::Action> {
::std::boxed::Box::new($name)
}
gpui::private::inventory::submit! {
gpui::DefaultKeyBinding::new($keystrokes, $context, build_action)
}
};
};
}
12 changes: 10 additions & 2 deletions crates/gpui_macros/src/derive_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,16 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
return Err(meta.error("'namespace' argument specified multiple times"));
}
meta.input.parse::<Token![=]>()?;
let ident: Ident = meta.input.parse()?;
namespace = Some(ident.to_string());
if meta.input.peek(Token![crate]) {
// `namespace = crate` uses the name of the crate being compiled.
meta.input.parse::<Token![crate]>()?;
namespace = Some(std::env::var("CARGO_CRATE_NAME").map_err(|_| {
meta.error("CARGO_CRATE_NAME must be set to use 'namespace = crate'")
})?);
} else {
let ident: Ident = meta.input.parse()?;
namespace = Some(ident.to_string());
}
} else if meta.path.is_ident("no_json") {
if no_json {
return Err(meta.error("'no_json' argument specified multiple times"));
Expand Down
7 changes: 5 additions & 2 deletions crates/zed/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,14 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

fn build_application() -> Application {
let platform = gpui_platform::current_platform(false);
if std::env::var("ZED_EXPERIMENTAL_A11Y").as_deref() == Ok("1") {
let application = if std::env::var("ZED_EXPERIMENTAL_A11Y").as_deref() == Ok("1") {
Application::with_platform(platform)
} else {
Application::new_inaccessible(platform)
}
};
// Zed owns its entire keymap via its keymap JSON files, so opt out of any
// library-default key bindings registered with `gpui::keybinding!`.
application.without_default_key_bindings()
}

fn files_not_created_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
Expand Down
Loading