
Closes #ISSUE Fixes a bug that was cherry picked onto stable and preview branches introduced in #35208 whereby modifier keys would show up and not be removable when editing a keybind Release Notes: - (preview only) Keymap Editor: Fixed an issue introduced in v0.197.2 whereby modifier keys would show up and not be removable while recording keystrokes in the keybind edit modal
602 lines
18 KiB
Rust
602 lines
18 KiB
Rust
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
error::Error,
|
|
fmt::{Display, Write},
|
|
};
|
|
|
|
/// A keystroke and associated metadata generated by the platform
|
|
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
|
pub struct Keystroke {
|
|
/// the state of the modifier keys at the time the keystroke was generated
|
|
pub modifiers: Modifiers,
|
|
|
|
/// key is the character printed on the key that was pressed
|
|
/// e.g. for option-s, key is "s"
|
|
/// On layouts that do not have ascii keys (e.g. Thai)
|
|
/// this will be the ASCII-equivalent character (q instead of ๆ),
|
|
/// and the typed character will be present in key_char.
|
|
pub key: String,
|
|
|
|
/// key_char is the character that could have been typed when
|
|
/// this binding was pressed.
|
|
/// e.g. for s this is "s", for option-s "ß", and cmd-s None
|
|
pub key_char: Option<String>,
|
|
}
|
|
|
|
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
|
/// markdown to display it.
|
|
#[derive(Debug)]
|
|
pub struct InvalidKeystrokeError {
|
|
/// The invalid keystroke.
|
|
pub keystroke: String,
|
|
}
|
|
|
|
impl Error for InvalidKeystrokeError {}
|
|
|
|
impl Display for InvalidKeystrokeError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"Invalid keystroke \"{}\". {}",
|
|
self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
|
|
pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
|
|
(`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
|
|
followed by a key, separated by `-`.";
|
|
|
|
impl Keystroke {
|
|
/// When matching a key we cannot know whether the user intended to type
|
|
/// the key_char or the key itself. On some non-US keyboards keys we use in our
|
|
/// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
|
|
/// and on some keyboards the IME handler converts a sequence of keys into a
|
|
/// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
|
|
///
|
|
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
|
/// both possibilities for self against the target.
|
|
pub fn should_match(&self, target: &Keystroke) -> bool {
|
|
#[cfg(not(target_os = "windows"))]
|
|
if let Some(key_char) = self
|
|
.key_char
|
|
.as_ref()
|
|
.filter(|key_char| key_char != &&self.key)
|
|
{
|
|
let ime_modifiers = Modifiers {
|
|
control: self.modifiers.control,
|
|
platform: self.modifiers.platform,
|
|
..Default::default()
|
|
};
|
|
|
|
if &target.key == key_char && target.modifiers == ime_modifiers {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
if let Some(key_char) = self
|
|
.key_char
|
|
.as_ref()
|
|
.filter(|key_char| key_char != &&self.key)
|
|
{
|
|
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
|
if &target.key == key_char && target.modifiers == Modifiers::none() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
target.modifiers == self.modifiers && target.key == self.key
|
|
}
|
|
|
|
/// key syntax is:
|
|
/// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
|
|
/// key_char syntax is only used for generating test events,
|
|
/// secondary means "cmd" on macOS and "ctrl" on other platforms
|
|
/// when matching a key with an key_char set will be matched without it.
|
|
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
|
|
let mut modifiers = Modifiers::none();
|
|
let mut key = None;
|
|
let mut key_char = None;
|
|
|
|
let mut components = source.split('-').peekable();
|
|
while let Some(component) = components.next() {
|
|
if component.eq_ignore_ascii_case("ctrl") {
|
|
modifiers.control = true;
|
|
continue;
|
|
}
|
|
if component.eq_ignore_ascii_case("alt") {
|
|
modifiers.alt = true;
|
|
continue;
|
|
}
|
|
if component.eq_ignore_ascii_case("shift") {
|
|
modifiers.shift = true;
|
|
continue;
|
|
}
|
|
if component.eq_ignore_ascii_case("fn") {
|
|
modifiers.function = true;
|
|
continue;
|
|
}
|
|
if component.eq_ignore_ascii_case("secondary") {
|
|
if cfg!(target_os = "macos") {
|
|
modifiers.platform = true;
|
|
} else {
|
|
modifiers.control = true;
|
|
};
|
|
continue;
|
|
}
|
|
|
|
let is_platform = component.eq_ignore_ascii_case("cmd")
|
|
|| component.eq_ignore_ascii_case("super")
|
|
|| component.eq_ignore_ascii_case("win");
|
|
|
|
if is_platform {
|
|
modifiers.platform = true;
|
|
continue;
|
|
}
|
|
|
|
let mut key_str = component.to_string();
|
|
|
|
if let Some(next) = components.peek() {
|
|
if next.is_empty() && source.ends_with('-') {
|
|
key = Some(String::from("-"));
|
|
break;
|
|
} else if next.len() > 1 && next.starts_with('>') {
|
|
key = Some(key_str);
|
|
key_char = Some(String::from(&next[1..]));
|
|
components.next();
|
|
} else {
|
|
return Err(InvalidKeystrokeError {
|
|
keystroke: source.to_owned(),
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
|
|
// Convert to shift + lowercase char
|
|
modifiers.shift = true;
|
|
key_str.make_ascii_lowercase();
|
|
} else {
|
|
// convert ascii chars to lowercase so that named keys like "tab" and "enter"
|
|
// are accepted case insensitively and stored how we expect so they are matched properly
|
|
key_str.make_ascii_lowercase()
|
|
}
|
|
key = Some(key_str);
|
|
}
|
|
|
|
// Allow for the user to specify a keystroke modifier as the key itself
|
|
// This sets the `key` to the modifier, and disables the modifier
|
|
key = key.or_else(|| {
|
|
use std::mem;
|
|
// std::mem::take clears bool incase its true
|
|
if mem::take(&mut modifiers.shift) {
|
|
Some("shift".to_string())
|
|
} else if mem::take(&mut modifiers.control) {
|
|
Some("control".to_string())
|
|
} else if mem::take(&mut modifiers.alt) {
|
|
Some("alt".to_string())
|
|
} else if mem::take(&mut modifiers.platform) {
|
|
Some("platform".to_string())
|
|
} else if mem::take(&mut modifiers.function) {
|
|
Some("function".to_string())
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
let key = key.ok_or_else(|| InvalidKeystrokeError {
|
|
keystroke: source.to_owned(),
|
|
})?;
|
|
|
|
Ok(Keystroke {
|
|
modifiers,
|
|
key,
|
|
key_char,
|
|
})
|
|
}
|
|
|
|
/// Produces a representation of this key that Parse can understand.
|
|
pub fn unparse(&self) -> String {
|
|
let mut str = String::new();
|
|
if self.modifiers.function {
|
|
str.push_str("fn-");
|
|
}
|
|
if self.modifiers.control {
|
|
str.push_str("ctrl-");
|
|
}
|
|
if self.modifiers.alt {
|
|
str.push_str("alt-");
|
|
}
|
|
if self.modifiers.platform {
|
|
#[cfg(target_os = "macos")]
|
|
str.push_str("cmd-");
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
str.push_str("super-");
|
|
|
|
#[cfg(target_os = "windows")]
|
|
str.push_str("win-");
|
|
}
|
|
if self.modifiers.shift {
|
|
str.push_str("shift-");
|
|
}
|
|
str.push_str(&self.key);
|
|
str
|
|
}
|
|
|
|
/// Returns true if this keystroke left
|
|
/// the ime system in an incomplete state.
|
|
pub fn is_ime_in_progress(&self) -> bool {
|
|
self.key_char.is_none()
|
|
&& (is_printable_key(&self.key) || self.key.is_empty())
|
|
&& !(self.modifiers.platform
|
|
|| self.modifiers.control
|
|
|| self.modifiers.function
|
|
|| self.modifiers.alt)
|
|
}
|
|
|
|
/// Returns a new keystroke with the key_char filled.
|
|
/// This is used for dispatch_keystroke where we want users to
|
|
/// be able to simulate typing "space", etc.
|
|
pub fn with_simulated_ime(mut self) -> Self {
|
|
if self.key_char.is_none()
|
|
&& !self.modifiers.platform
|
|
&& !self.modifiers.control
|
|
&& !self.modifiers.function
|
|
&& !self.modifiers.alt
|
|
{
|
|
self.key_char = match self.key.as_str() {
|
|
"space" => Some(" ".into()),
|
|
"tab" => Some("\t".into()),
|
|
"enter" => Some("\n".into()),
|
|
key if !is_printable_key(key) || key.is_empty() => None,
|
|
key => {
|
|
if self.modifiers.shift {
|
|
Some(key.to_uppercase())
|
|
} else {
|
|
Some(key.into())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self
|
|
}
|
|
}
|
|
|
|
fn is_printable_key(key: &str) -> bool {
|
|
!matches!(
|
|
key,
|
|
"f1" | "f2"
|
|
| "f3"
|
|
| "f4"
|
|
| "f5"
|
|
| "f6"
|
|
| "f7"
|
|
| "f8"
|
|
| "f9"
|
|
| "f10"
|
|
| "f11"
|
|
| "f12"
|
|
| "f13"
|
|
| "f14"
|
|
| "f15"
|
|
| "f16"
|
|
| "f17"
|
|
| "f18"
|
|
| "f19"
|
|
| "f20"
|
|
| "f21"
|
|
| "f22"
|
|
| "f23"
|
|
| "f24"
|
|
| "f25"
|
|
| "f26"
|
|
| "f27"
|
|
| "f28"
|
|
| "f29"
|
|
| "f30"
|
|
| "f31"
|
|
| "f32"
|
|
| "f33"
|
|
| "f34"
|
|
| "f35"
|
|
| "backspace"
|
|
| "delete"
|
|
| "left"
|
|
| "right"
|
|
| "up"
|
|
| "down"
|
|
| "pageup"
|
|
| "pagedown"
|
|
| "insert"
|
|
| "home"
|
|
| "end"
|
|
| "back"
|
|
| "forward"
|
|
| "escape"
|
|
)
|
|
}
|
|
|
|
impl std::fmt::Display for Keystroke {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
if self.modifiers.control {
|
|
#[cfg(target_os = "macos")]
|
|
f.write_char('^')?;
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
write!(f, "ctrl-")?;
|
|
}
|
|
if self.modifiers.alt {
|
|
#[cfg(target_os = "macos")]
|
|
f.write_char('⌥')?;
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
write!(f, "alt-")?;
|
|
}
|
|
if self.modifiers.platform {
|
|
#[cfg(target_os = "macos")]
|
|
f.write_char('⌘')?;
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
f.write_char('❖')?;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
f.write_char('⊞')?;
|
|
}
|
|
if self.modifiers.shift {
|
|
#[cfg(target_os = "macos")]
|
|
f.write_char('⇧')?;
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
write!(f, "shift-")?;
|
|
}
|
|
let key = match self.key.as_str() {
|
|
#[cfg(target_os = "macos")]
|
|
"backspace" => '⌫',
|
|
#[cfg(target_os = "macos")]
|
|
"up" => '↑',
|
|
#[cfg(target_os = "macos")]
|
|
"down" => '↓',
|
|
#[cfg(target_os = "macos")]
|
|
"left" => '←',
|
|
#[cfg(target_os = "macos")]
|
|
"right" => '→',
|
|
#[cfg(target_os = "macos")]
|
|
"tab" => '⇥',
|
|
#[cfg(target_os = "macos")]
|
|
"escape" => '⎋',
|
|
#[cfg(target_os = "macos")]
|
|
"shift" => '⇧',
|
|
#[cfg(target_os = "macos")]
|
|
"control" => '⌃',
|
|
#[cfg(target_os = "macos")]
|
|
"alt" => '⌥',
|
|
#[cfg(target_os = "macos")]
|
|
"platform" => '⌘',
|
|
|
|
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
|
key => return f.write_str(key),
|
|
};
|
|
f.write_char(key)
|
|
}
|
|
}
|
|
|
|
/// The state of the modifier keys at some point in time
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
|
|
pub struct Modifiers {
|
|
/// The control key
|
|
#[serde(default)]
|
|
pub control: bool,
|
|
|
|
/// The alt key
|
|
/// Sometimes also known as the 'meta' key
|
|
#[serde(default)]
|
|
pub alt: bool,
|
|
|
|
/// The shift key
|
|
#[serde(default)]
|
|
pub shift: bool,
|
|
|
|
/// The command key, on macos
|
|
/// the windows key, on windows
|
|
/// the super key, on linux
|
|
#[serde(default)]
|
|
pub platform: bool,
|
|
|
|
/// The function key
|
|
#[serde(default)]
|
|
pub function: bool,
|
|
}
|
|
|
|
impl Modifiers {
|
|
/// Returns whether any modifier key is pressed.
|
|
pub fn modified(&self) -> bool {
|
|
self.control || self.alt || self.shift || self.platform || self.function
|
|
}
|
|
|
|
/// Whether the semantically 'secondary' modifier key is pressed.
|
|
///
|
|
/// On macOS, this is the command key.
|
|
/// On Linux and Windows, this is the control key.
|
|
pub fn secondary(&self) -> bool {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
self.platform
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
self.control
|
|
}
|
|
}
|
|
|
|
/// Returns 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
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with no modifiers.
|
|
pub fn none() -> Modifiers {
|
|
Default::default()
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with just the command key.
|
|
pub fn command() -> Modifiers {
|
|
Modifiers {
|
|
platform: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// A Returns [`Modifiers`] with just the secondary key pressed.
|
|
pub fn secondary_key() -> Modifiers {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
Modifiers {
|
|
platform: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
Modifiers {
|
|
control: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with just the windows key.
|
|
pub fn windows() -> Modifiers {
|
|
Modifiers {
|
|
platform: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with just the super key.
|
|
pub fn super_key() -> Modifiers {
|
|
Modifiers {
|
|
platform: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with just control.
|
|
pub fn control() -> Modifiers {
|
|
Modifiers {
|
|
control: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with just alt.
|
|
pub fn alt() -> Modifiers {
|
|
Modifiers {
|
|
alt: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with just shift.
|
|
pub fn shift() -> Modifiers {
|
|
Modifiers {
|
|
shift: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with command + shift.
|
|
pub fn command_shift() -> Modifiers {
|
|
Modifiers {
|
|
shift: true,
|
|
platform: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Returns [`Modifiers`] with command + shift.
|
|
pub fn control_shift() -> Modifiers {
|
|
Modifiers {
|
|
shift: true,
|
|
control: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
|
|
pub fn is_subset_of(&self, other: &Modifiers) -> bool {
|
|
(*other & *self) == *self
|
|
}
|
|
}
|
|
|
|
impl std::ops::BitOr for Modifiers {
|
|
type Output = Self;
|
|
|
|
fn bitor(mut self, other: Self) -> Self::Output {
|
|
self |= other;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl std::ops::BitOrAssign for Modifiers {
|
|
fn bitor_assign(&mut self, other: Self) {
|
|
self.control |= other.control;
|
|
self.alt |= other.alt;
|
|
self.shift |= other.shift;
|
|
self.platform |= other.platform;
|
|
self.function |= other.function;
|
|
}
|
|
}
|
|
|
|
impl std::ops::BitXor for Modifiers {
|
|
type Output = Self;
|
|
fn bitxor(mut self, rhs: Self) -> Self::Output {
|
|
self ^= rhs;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl std::ops::BitXorAssign for Modifiers {
|
|
fn bitxor_assign(&mut self, other: Self) {
|
|
self.control ^= other.control;
|
|
self.alt ^= other.alt;
|
|
self.shift ^= other.shift;
|
|
self.platform ^= other.platform;
|
|
self.function ^= other.function;
|
|
}
|
|
}
|
|
|
|
impl std::ops::BitAnd for Modifiers {
|
|
type Output = Self;
|
|
fn bitand(mut self, rhs: Self) -> Self::Output {
|
|
self &= rhs;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl std::ops::BitAndAssign for Modifiers {
|
|
fn bitand_assign(&mut self, other: Self) {
|
|
self.control &= other.control;
|
|
self.alt &= other.alt;
|
|
self.shift &= other.shift;
|
|
self.platform &= other.platform;
|
|
self.function &= other.function;
|
|
}
|
|
}
|
|
|
|
/// The state of the capslock key at some point in time
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
|
|
pub struct Capslock {
|
|
/// The capslock key is on
|
|
#[serde(default)]
|
|
pub on: bool,
|
|
}
|