diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index d48c9358c4..64682d69d3 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -94,6 +94,27 @@ impl Keystroke { } } + //Allow for the user to specify a keystroke modifier as the key itself + //This sets the `key` to the modifier, and disables the modifier + if key.is_none() { + if shift { + key = Some("shift".to_string()); + shift = false; + } else if control { + key = Some("control".to_string()); + control = false; + } else if alt { + key = Some("alt".to_string()); + alt = false; + } else if platform { + key = Some("platform".to_string()); + platform = false; + } else if function { + key = Some("function".to_string()); + function = false; + } + } + let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?; Ok(Keystroke { @@ -186,6 +207,10 @@ impl std::fmt::Display for Keystroke { "right" => '→', "tab" => '⇥', "escape" => '⎋', + "shift" => '⇧', + "control" => '⌃', + "alt" => '⌥', + "platform" => '⌘', key => { if key.len() == 1 { key.chars().next().unwrap().to_ascii_uppercase() @@ -241,6 +266,15 @@ impl Modifiers { } } + /// How many modifier keys are pressed + pub fn number_of_modifiers(&self) -> u8 { + self.control as u8 + + self.alt as u8 + + self.shift as u8 + + self.platform as u8 + + self.function as u8 + } + /// helper method for Modifiers with no modifiers pub fn none() -> Modifiers { Default::default() diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index b1031e084d..289e0af16c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -549,6 +549,7 @@ pub struct Window { pub(crate) focus: Option, focus_enabled: bool, pending_input: Option, + pending_modifiers: Option, pending_input_observers: SubscriberSet<(), AnyObserver>, prompt: Option, } @@ -823,6 +824,7 @@ impl Window { focus: None, focus_enabled: true, pending_input: None, + pending_modifiers: None, pending_input_observers: SubscriberSet::new(), prompt: None, }) @@ -3161,70 +3163,129 @@ impl<'a> WindowContext<'a> { .dispatch_tree .dispatch_path(node_id); + let mut bindings: SmallVec<[KeyBinding; 1]> = SmallVec::new(); + let mut pending = false; + let mut keystroke: Option = None; + + if let Some(event) = event.downcast_ref::() { + if let Some(previous) = self.window.pending_modifiers.take() { + if event.modifiers.number_of_modifiers() == 0 { + let key = match previous { + modifiers if modifiers.shift => Some("shift"), + modifiers if modifiers.control => Some("control"), + modifiers if modifiers.alt => Some("alt"), + modifiers if modifiers.platform => Some("platform"), + modifiers if modifiers.function => Some("function"), + _ => None, + }; + if let Some(key) = key { + let key = Keystroke { + key: key.to_string(), + ime_key: None, + modifiers: Modifiers::default(), + }; + let KeymatchResult { + bindings: modifier_bindings, + pending: pending_bindings, + } = self + .window + .rendered_frame + .dispatch_tree + .dispatch_key(&key, &dispatch_path); + + keystroke = Some(key); + bindings = modifier_bindings; + pending = pending_bindings; + } + } + } else if event.modifiers.number_of_modifiers() == 1 { + self.window.pending_modifiers = Some(event.modifiers); + } + if keystroke.is_none() { + self.finish_dispatch_key_event(event, dispatch_path); + return; + } + } + if let Some(key_down_event) = event.downcast_ref::() { - let KeymatchResult { bindings, pending } = self + self.window.pending_modifiers.take(); + let KeymatchResult { + bindings: key_down_bindings, + pending: key_down_pending, + } = self .window .rendered_frame .dispatch_tree .dispatch_key(&key_down_event.keystroke, &dispatch_path); - if pending { - let mut currently_pending = self.window.pending_input.take().unwrap_or_default(); - if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus - { - currently_pending = PendingInput::default(); - } - currently_pending.focus = self.window.focus; - currently_pending - .keystrokes - .push(key_down_event.keystroke.clone()); - for binding in bindings { - currently_pending.bindings.push(binding); - } + keystroke = Some(key_down_event.keystroke.clone()); - currently_pending.timer = Some(self.spawn(|mut cx| async move { - cx.background_executor.timer(Duration::from_secs(1)).await; - cx.update(move |cx| { - cx.clear_pending_keystrokes(); - let Some(currently_pending) = cx.window.pending_input.take() else { - return; - }; - cx.pending_input_changed(); - cx.replay_pending_input(currently_pending); - }) - .log_err(); - })); + bindings = key_down_bindings; + pending = key_down_pending; + } - self.window.pending_input = Some(currently_pending); - self.pending_input_changed(); - - self.propagate_event = false; - - return; - } else if let Some(currently_pending) = self.window.pending_input.take() { - self.pending_input_changed(); - if bindings - .iter() - .all(|binding| !currently_pending.used_by_binding(binding)) - { - self.replay_pending_input(currently_pending) - } + if pending { + let mut currently_pending = self.window.pending_input.take().unwrap_or_default(); + if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus { + currently_pending = PendingInput::default(); } - - if !bindings.is_empty() { - self.clear_pending_keystrokes(); + currently_pending.focus = self.window.focus; + if let Some(keystroke) = keystroke { + currently_pending.keystrokes.push(keystroke.clone()); } - - self.propagate_event = true; for binding in bindings { - self.dispatch_action_on_node(node_id, binding.action.as_ref()); - if !self.propagate_event { - self.dispatch_keystroke_observers(event, Some(binding.action)); - return; - } + currently_pending.bindings.push(binding); + } + + currently_pending.timer = Some(self.spawn(|mut cx| async move { + cx.background_executor.timer(Duration::from_secs(1)).await; + cx.update(move |cx| { + cx.clear_pending_keystrokes(); + let Some(currently_pending) = cx.window.pending_input.take() else { + return; + }; + cx.replay_pending_input(currently_pending); + cx.pending_input_changed(); + }) + .log_err(); + })); + + self.window.pending_input = Some(currently_pending); + self.pending_input_changed(); + + self.propagate_event = false; + return; + } else if let Some(currently_pending) = self.window.pending_input.take() { + self.pending_input_changed(); + if bindings + .iter() + .all(|binding| !currently_pending.used_by_binding(binding)) + { + self.replay_pending_input(currently_pending) } } + if !bindings.is_empty() { + self.clear_pending_keystrokes(); + } + + self.propagate_event = true; + for binding in bindings { + self.dispatch_action_on_node(node_id, binding.action.as_ref()); + if !self.propagate_event { + self.dispatch_keystroke_observers(event, Some(binding.action)); + return; + } + } + + self.finish_dispatch_key_event(event, dispatch_path) + } + + fn finish_dispatch_key_event( + &mut self, + event: &dyn Any, + dispatch_path: SmallVec<[DispatchNodeId; 32]>, + ) { self.dispatch_key_down_up_event(event, &dispatch_path); if !self.propagate_event { return; diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 44f05ffecc..1e4983f5ad 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -30,7 +30,7 @@ impl KeyBinding { Some(Self::new(key_binding)) } - fn icon_for_key(keystroke: &Keystroke) -> Option { + fn icon_for_key(&self, keystroke: &Keystroke) -> Option { match keystroke.key.as_str() { "left" => Some(IconName::ArrowLeft), "right" => Some(IconName::ArrowRight), @@ -45,6 +45,11 @@ impl KeyBinding { "escape" => Some(IconName::Escape), "pagedown" => Some(IconName::PageDown), "pageup" => Some(IconName::PageUp), + "shift" if self.platform_style == PlatformStyle::Mac => Some(IconName::Shift), + "control" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control), + "platform" if self.platform_style == PlatformStyle::Mac => Some(IconName::Command), + "function" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control), + "alt" if self.platform_style == PlatformStyle::Mac => Some(IconName::Option), _ => None, } } @@ -80,7 +85,7 @@ impl RenderOnce for KeyBinding { .gap(Spacing::Small.rems(cx)) .flex_none() .children(self.key_binding.keystrokes().iter().map(|keystroke| { - let key_icon = Self::icon_for_key(keystroke); + let key_icon = self.icon_for_key(keystroke); h_flex() .flex_none() diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 5933309544..882638fa3f 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -50,12 +50,12 @@ Zed has the ability to match against not just a single keypress, but a sequence Each key press is a sequence of modifiers followed by a key. The modifiers are: - `ctrl-` The control key -- `cmd-` On macOS, this is the command key -- `alt-` On macOS, this is the option key +* `cmd-`, `win-` or `super-` for the platform modifier (Command on macOS, Windows key on Windows, and the Super key on Linux). +- `alt-` for alt (option on macOS) - `shift-` The shift key - `fn-` The function key -The keys can be any single unicode codepoint that your keyboard generates (for example `a`, `0`, `£` or `ç`). +The keys can be any single unicode codepoint that your keyboard generates (for example `a`, `0`, `£` or `ç`), or any named key (`tab`, `f1`, `shift`, or `cmd`). A few examples: @@ -64,10 +64,15 @@ A few examples: "cmd-k cmd-s": "zed::OpenKeymap", // matches ⌘-k then ⌘-s "space e": "editor::Complete", // type space then e "ç": "editor::Complete", // matches ⌥-c + "shift shift": "file_finder::Toggle", // matches pressing and releasing shift twice } ``` -NOTE: Keys on a keyboard are not always the same as the character they generate. For example `shift-e` actually types `E` (or `alt-c` types `ç`). Zed allows you to match against either the key and its modifiers or the character it generates. This means you can specify `alt-c` or `ç`, but not `alt-ç`. It is usually better to specify the key and its modifiers, as this will work better on different keyboard layouts. +The `shift-` modifier can only be used in combination with a letter to indicate the uppercase version. For example `shift-g` matches typing `G`. Although on many keyboards shift is used to type punctuation characters like `(`, the keypress is not considered to be modified and so `shift-(` does not match. + +The `alt-` modifier can be used on many layouts to generate a different key. For example on macOS US keyboard the combination `alt-c` types `ç`. You can match against either in your keymap file, though by convention Zed spells this combination as `alt-c`. + +It is possible to match against typing a modifier key on its own. For example `shift shift` can be used to implement JetBrains search everywhere shortcut. In this case the binding happens on key release instead of key press. ### Remapping keys