Detailed changes
@@ -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()
@@ -549,6 +549,7 @@ pub struct Window {
pub(crate) focus: Option<FocusId>,
focus_enabled: bool,
pending_input: Option<PendingInput>,
+ pending_modifiers: Option<Modifiers>,
pending_input_observers: SubscriberSet<(), AnyObserver>,
prompt: Option<RenderablePromptHandle>,
}
@@ -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<Keystroke> = None;
+
+ if let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() {
+ 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::<KeyDownEvent>() {
- 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();
+ 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;
+ if let Some(keystroke) = keystroke {
+ currently_pending.keystrokes.push(keystroke.clone());
+ }
+ for binding in bindings {
+ currently_pending.bindings.push(binding);
+ }
- self.propagate_event = false;
+ 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();
+ }));
- 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)
- }
- }
+ self.window.pending_input = Some(currently_pending);
+ self.pending_input_changed();
- if !bindings.is_empty() {
- self.clear_pending_keystrokes();
+ 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)
}
+ }
- 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;
- }
+ 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;
@@ -30,7 +30,7 @@ impl KeyBinding {
Some(Self::new(key_binding))
}
- fn icon_for_key(keystroke: &Keystroke) -> Option<IconName> {
+ fn icon_for_key(&self, keystroke: &Keystroke) -> Option<IconName> {
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()
@@ -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