mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
keymap: Allow modifiers as keys (#12047)
It is sometimes desirable to allow modifers to serve as keys themselves for the purposes of keybinds. For example, the popular keybind in jetbrains IDEs `shift shift` which opens the file finder. This change treats modifers in the keymaps as keys themselves if they are not accompanied by a key they are modifying. Further this change wires up they key dispatcher to treat modifer change events as key presses which are considered for matching against keybinds. Release Notes: - Fixes #6460 --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
7e694d1bcf
commit
0b1a589183
@ -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))?;
|
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
|
||||||
|
|
||||||
Ok(Keystroke {
|
Ok(Keystroke {
|
||||||
@ -186,6 +207,10 @@ impl std::fmt::Display for Keystroke {
|
|||||||
"right" => '→',
|
"right" => '→',
|
||||||
"tab" => '⇥',
|
"tab" => '⇥',
|
||||||
"escape" => '⎋',
|
"escape" => '⎋',
|
||||||
|
"shift" => '⇧',
|
||||||
|
"control" => '⌃',
|
||||||
|
"alt" => '⌥',
|
||||||
|
"platform" => '⌘',
|
||||||
key => {
|
key => {
|
||||||
if key.len() == 1 {
|
if key.len() == 1 {
|
||||||
key.chars().next().unwrap().to_ascii_uppercase()
|
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
|
/// helper method for Modifiers with no modifiers
|
||||||
pub fn none() -> Modifiers {
|
pub fn none() -> Modifiers {
|
||||||
Default::default()
|
Default::default()
|
||||||
|
@ -549,6 +549,7 @@ pub struct Window {
|
|||||||
pub(crate) focus: Option<FocusId>,
|
pub(crate) focus: Option<FocusId>,
|
||||||
focus_enabled: bool,
|
focus_enabled: bool,
|
||||||
pending_input: Option<PendingInput>,
|
pending_input: Option<PendingInput>,
|
||||||
|
pending_modifiers: Option<Modifiers>,
|
||||||
pending_input_observers: SubscriberSet<(), AnyObserver>,
|
pending_input_observers: SubscriberSet<(), AnyObserver>,
|
||||||
prompt: Option<RenderablePromptHandle>,
|
prompt: Option<RenderablePromptHandle>,
|
||||||
}
|
}
|
||||||
@ -823,6 +824,7 @@ impl Window {
|
|||||||
focus: None,
|
focus: None,
|
||||||
focus_enabled: true,
|
focus_enabled: true,
|
||||||
pending_input: None,
|
pending_input: None,
|
||||||
|
pending_modifiers: None,
|
||||||
pending_input_observers: SubscriberSet::new(),
|
pending_input_observers: SubscriberSet::new(),
|
||||||
prompt: None,
|
prompt: None,
|
||||||
})
|
})
|
||||||
@ -3161,70 +3163,129 @@ impl<'a> WindowContext<'a> {
|
|||||||
.dispatch_tree
|
.dispatch_tree
|
||||||
.dispatch_path(node_id);
|
.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>() {
|
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
|
.window
|
||||||
.rendered_frame
|
.rendered_frame
|
||||||
.dispatch_tree
|
.dispatch_tree
|
||||||
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
|
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
|
||||||
|
|
||||||
if pending {
|
keystroke = Some(key_down_event.keystroke.clone());
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
currently_pending.timer = Some(self.spawn(|mut cx| async move {
|
bindings = key_down_bindings;
|
||||||
cx.background_executor.timer(Duration::from_secs(1)).await;
|
pending = key_down_pending;
|
||||||
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();
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.window.pending_input = Some(currently_pending);
|
if pending {
|
||||||
self.pending_input_changed();
|
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
|
||||||
|
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus {
|
||||||
self.propagate_event = false;
|
currently_pending = PendingInput::default();
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
currently_pending.focus = self.window.focus;
|
||||||
if !bindings.is_empty() {
|
if let Some(keystroke) = keystroke {
|
||||||
self.clear_pending_keystrokes();
|
currently_pending.keystrokes.push(keystroke.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.propagate_event = true;
|
|
||||||
for binding in bindings {
|
for binding in bindings {
|
||||||
self.dispatch_action_on_node(node_id, binding.action.as_ref());
|
currently_pending.bindings.push(binding);
|
||||||
if !self.propagate_event {
|
}
|
||||||
self.dispatch_keystroke_observers(event, Some(binding.action));
|
|
||||||
return;
|
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);
|
self.dispatch_key_down_up_event(event, &dispatch_path);
|
||||||
if !self.propagate_event {
|
if !self.propagate_event {
|
||||||
return;
|
return;
|
||||||
|
@ -30,7 +30,7 @@ impl KeyBinding {
|
|||||||
Some(Self::new(key_binding))
|
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() {
|
match keystroke.key.as_str() {
|
||||||
"left" => Some(IconName::ArrowLeft),
|
"left" => Some(IconName::ArrowLeft),
|
||||||
"right" => Some(IconName::ArrowRight),
|
"right" => Some(IconName::ArrowRight),
|
||||||
@ -45,6 +45,11 @@ impl KeyBinding {
|
|||||||
"escape" => Some(IconName::Escape),
|
"escape" => Some(IconName::Escape),
|
||||||
"pagedown" => Some(IconName::PageDown),
|
"pagedown" => Some(IconName::PageDown),
|
||||||
"pageup" => Some(IconName::PageUp),
|
"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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,7 +85,7 @@ impl RenderOnce for KeyBinding {
|
|||||||
.gap(Spacing::Small.rems(cx))
|
.gap(Spacing::Small.rems(cx))
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
.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()
|
h_flex()
|
||||||
.flex_none()
|
.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:
|
Each key press is a sequence of modifiers followed by a key. The modifiers are:
|
||||||
|
|
||||||
- `ctrl-` The control key
|
- `ctrl-` The control key
|
||||||
- `cmd-` On macOS, this is the command key
|
* `cmd-`, `win-` or `super-` for the platform modifier (Command on macOS, Windows key on Windows, and the Super key on Linux).
|
||||||
- `alt-` On macOS, this is the option key
|
- `alt-` for alt (option on macOS)
|
||||||
- `shift-` The shift key
|
- `shift-` The shift key
|
||||||
- `fn-` The function 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:
|
A few examples:
|
||||||
|
|
||||||
@ -64,10 +64,15 @@ A few examples:
|
|||||||
"cmd-k cmd-s": "zed::OpenKeymap", // matches ⌘-k then ⌘-s
|
"cmd-k cmd-s": "zed::OpenKeymap", // matches ⌘-k then ⌘-s
|
||||||
"space e": "editor::Complete", // type space then e
|
"space e": "editor::Complete", // type space then e
|
||||||
"ç": "editor::Complete", // matches ⌥-c
|
"ç": "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
|
### Remapping keys
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user