This commit is contained in:
Junkui Zhang 2025-08-20 02:25:14 +08:00
parent cb8b7b03cc
commit fa3abe789c
8 changed files with 190 additions and 324 deletions

View file

@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
});
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
});
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
let keymap = match os {
"macos" => &KEYMAP_MACOS,
"linux" | "freebsd" => &KEYMAP_LINUX,
"windows" => &KEYMAP_WINDOWS,
_ => unreachable!("Not a valid OS: {}", os),
};

View file

@ -3,8 +3,8 @@ use std::rc::Rc;
use collections::HashMap;
use crate::{
Action, AsKeystroke, InvalidKeystrokeError, KeyBindingContextPredicate, KeybindingKeystroke,
Keystroke, SharedString,
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
};
use smallvec::SmallVec;
@ -35,7 +35,16 @@ impl KeyBinding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
let context_predicate =
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
Self::load(
keystrokes,
Box::new(action),
context_predicate,
false,
None,
None,
&DummyKeyboardMapper,
)
.unwrap()
}
/// Load a keybinding from the given raw data.
@ -43,12 +52,14 @@ impl KeyBinding {
keystrokes: &str,
action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
use_key_equivalents: bool,
key_equivalents: Option<&HashMap<char, char>>,
action_input: Option<SharedString>,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
.map(|source| Keystroke::parse(source).map(|keystroke| keystroke.into_shifted()))
.map(Keystroke::parse)
.collect::<std::result::Result<_, _>>()?;
if let Some(equivalents) = key_equivalents {
@ -63,7 +74,9 @@ impl KeyBinding {
let keystrokes = keystrokes
.into_iter()
.map(KeybindingKeystroke::new)
.map(|keystroke| {
KeybindingKeystroke::new(keystroke, use_key_equivalents, keyboard_mapper)
})
.collect();
Ok(Self {

View file

@ -11,13 +11,22 @@ pub trait PlatformKeyboardLayout {
/// A trait for platform-specific keyboard mappings
pub trait PlatformKeyboardMapper {
/// Map a key equivalent to its platform-specific representation
fn map_key_equivalent(&self, keystroke: Keystroke) -> KeybindingKeystroke;
fn map_key_equivalent(
&self,
keystroke: Keystroke,
use_key_equivalents: bool,
) -> KeybindingKeystroke;
}
pub(crate) struct DummyKeyboardMapper;
/// A dummy implementation of the platform keyboard mapper
pub struct DummyKeyboardMapper;
impl PlatformKeyboardMapper for DummyKeyboardMapper {
fn map_key_equivalent(&self, keystroke: Keystroke) -> KeybindingKeystroke {
KeybindingKeystroke::new(keystroke)
fn map_key_equivalent(
&self,
keystroke: Keystroke,
_use_key_equivalents: bool,
) -> KeybindingKeystroke {
KeybindingKeystroke::from_keystroke(keystroke)
}
}

View file

@ -5,6 +5,8 @@ use std::{
fmt::{Display, Write},
};
use crate::PlatformKeyboardMapper;
/// TODO:
pub trait AsKeystroke {
/// TODO:
@ -281,30 +283,23 @@ impl Keystroke {
}
self
}
/// TODO:
pub fn into_shifted(self) -> Self {
let Keystroke {
modifiers,
key,
key_char,
} = self;
let (key, modifiers) = into_shifted_key(key, modifiers);
Self {
key,
modifiers,
key_char,
}
}
}
impl KeybindingKeystroke {
/// Create a new keybinding keystroke from the given keystroke
pub fn new(inner: Keystroke) -> Self {
let key = inner.key.clone();
let modifiers = inner.modifiers;
pub fn new(
inner: Keystroke,
use_key_equivalents: bool,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> Self {
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
}
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
let key = keystroke.key.clone();
let modifiers = keystroke.modifiers;
KeybindingKeystroke {
inner,
inner: keystroke,
modifiers,
key,
}
@ -608,191 +603,6 @@ impl AsKeystroke for KeybindingKeystroke {
}
}
fn to_unshifted_key(key: &str, modifiers: &Modifiers) -> (String, Modifiers) {
let mut modifiers = modifiers.clone();
match key {
"~" => {
modifiers.shift = true;
("`".to_string(), modifiers)
}
"!" => {
modifiers.shift = true;
("1".to_string(), modifiers)
}
"@" => {
modifiers.shift = true;
("2".to_string(), modifiers)
}
"#" => {
modifiers.shift = true;
("3".to_string(), modifiers)
}
"$" => {
modifiers.shift = true;
("4".to_string(), modifiers)
}
"%" => {
modifiers.shift = true;
("5".to_string(), modifiers)
}
"^" => {
modifiers.shift = true;
("6".to_string(), modifiers)
}
"&" => {
modifiers.shift = true;
("7".to_string(), modifiers)
}
"*" => {
modifiers.shift = true;
("8".to_string(), modifiers)
}
"(" => {
modifiers.shift = true;
("9".to_string(), modifiers)
}
")" => {
modifiers.shift = true;
("0".to_string(), modifiers)
}
"_" => {
modifiers.shift = true;
("-".to_string(), modifiers)
}
"+" => {
modifiers.shift = true;
("=".to_string(), modifiers)
}
"{" => {
modifiers.shift = true;
("[".to_string(), modifiers)
}
"}" => {
modifiers.shift = true;
("]".to_string(), modifiers)
}
"|" => {
modifiers.shift = true;
("\\".to_string(), modifiers)
}
":" => {
modifiers.shift = true;
(";".to_string(), modifiers)
}
"\"" => {
modifiers.shift = true;
("'".to_string(), modifiers)
}
"<" => {
modifiers.shift = true;
(",".to_string(), modifiers)
}
">" => {
modifiers.shift = true;
(">".to_string(), modifiers)
}
"?" => {
modifiers.shift = true;
("/".to_string(), modifiers)
}
_ => (key.to_string(), modifiers),
}
}
fn into_shifted_key(key: String, mut modifiers: Modifiers) -> (String, Modifiers) {
if !modifiers.shift {
(key, modifiers)
} else {
match key.as_str() {
"`" => {
modifiers.shift = false;
("~".to_string(), modifiers)
}
"1" => {
modifiers.shift = false;
("!".to_string(), modifiers)
}
"2" => {
modifiers.shift = false;
("@".to_string(), modifiers)
}
"3" => {
modifiers.shift = false;
("#".to_string(), modifiers)
}
"4" => {
modifiers.shift = false;
("$".to_string(), modifiers)
}
"5" => {
modifiers.shift = false;
("%".to_string(), modifiers)
}
"6" => {
modifiers.shift = false;
("^".to_string(), modifiers)
}
"7" => {
modifiers.shift = false;
("&".to_string(), modifiers)
}
"8" => {
modifiers.shift = false;
("*".to_string(), modifiers)
}
"9" => {
modifiers.shift = false;
("(".to_string(), modifiers)
}
"0" => {
modifiers.shift = false;
(")".to_string(), modifiers)
}
"-" => {
modifiers.shift = false;
("_".to_string(), modifiers)
}
"=" => {
modifiers.shift = false;
("+".to_string(), modifiers)
}
"[" => {
modifiers.shift = false;
("{".to_string(), modifiers)
}
"]" => {
modifiers.shift = false;
("}".to_string(), modifiers)
}
"\\" => {
modifiers.shift = false;
("|".to_string(), modifiers)
}
";" => {
modifiers.shift = false;
(":".to_string(), modifiers)
}
"'" => {
modifiers.shift = false;
("\"".to_string(), modifiers)
}
"," => {
modifiers.shift = false;
("<".to_string(), modifiers)
}
"." => {
modifiers.shift = false;
(">".to_string(), modifiers)
}
"/" => {
modifiers.shift = false;
("?".to_string(), modifiers)
}
_ => (key, modifiers),
}
}
}
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if modifiers.control {
#[cfg(target_os = "macos")]
@ -858,45 +668,3 @@ fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
};
f.write_char(key)
}
#[cfg(test)]
mod tests {
use crate::{KeybindingKeystroke, Keystroke, Modifiers};
#[test]
fn test_parsing_keystroke_on_windows() {
// On windows, users prefer to use "ctrl-shift-key", so here we support
// both "ctrl-$" and "ctrl-shift-4"
let source = "ctrl-$";
let keystroke = Keystroke::parse(source).unwrap();
assert_eq!(keystroke.modifiers, Modifiers::control());
assert_eq!(keystroke.key, "$");
assert_eq!(keystroke.key_char, None);
let keystroke_display = KeybindingKeystroke::new(keystroke.clone());
assert_eq!(keystroke_display.inner, keystroke);
assert_eq!(keystroke_display.key, "4");
assert_eq!(keystroke_display.modifiers, Modifiers::control_shift());
let source = "ctrl-shift-4";
let keystroke = Keystroke::parse(source).unwrap();
assert_eq!(keystroke.modifiers, Modifiers::control_shift());
assert_eq!(keystroke.key, "4");
assert_eq!(keystroke.key_char, None);
let keystroke = keystroke.into_shifted();
assert_eq!(keystroke.modifiers, Modifiers::control());
assert_eq!(keystroke.key, "$");
let keystroke_display = KeybindingKeystroke::new(keystroke.clone());
assert_eq!(
keystroke_display.inner,
Keystroke {
modifiers: Modifiers::control(),
key: "$".to_string(),
key_char: None
}
);
assert_eq!(keystroke_display.key, "4");
assert_eq!(keystroke_display.modifiers, Modifiers::control_shift());
}
}

View file

@ -1,4 +1,5 @@
use anyhow::Result;
use collections::HashMap;
use windows::Win32::UI::{
Input::KeyboardAndMouse::{
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
@ -19,7 +20,11 @@ pub(crate) struct WindowsKeyboardLayout {
name: String,
}
pub(crate) struct WindowsKeyboardMapper;
pub(crate) struct WindowsKeyboardMapper {
key_to_vkey: HashMap<String, (u16, bool)>,
vkey_to_key: HashMap<u16, String>,
vkey_to_shifted: HashMap<u16, String>,
}
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
fn id(&self) -> &str {
@ -32,9 +37,14 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
}
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
fn map_key_equivalent(&self, mut keystroke: Keystroke) -> KeybindingKeystroke {
let Some((vkey, shifted_key)) = key_needs_processing(&keystroke.key) else {
return KeybindingKeystroke::new(keystroke);
fn map_key_equivalent(
&self,
mut keystroke: Keystroke,
use_key_equivalents: bool,
) -> KeybindingKeystroke {
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
else {
return KeybindingKeystroke::from_keystroke(keystroke);
};
if shifted_key && keystroke.modifiers.shift {
log::warn!(
@ -46,31 +56,22 @@ impl PlatformKeyboardMapper for WindowsKeyboardMapper {
let shift = shifted_key || keystroke.modifiers.shift;
keystroke.modifiers.shift = false;
let Some(key) = get_key_from_vkey(vkey) else {
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
log::error!(
"Failed to map key equivalent '{:?}' to a valid key",
keystroke
);
return KeybindingKeystroke::new(keystroke);
return KeybindingKeystroke::from_keystroke(keystroke);
};
keystroke.key = if shift {
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
if scan_code == 0 {
log::error!(
"Failed to map keystroke {:?} with virtual key '{:?}' to a scan code",
keystroke,
vkey
);
return KeybindingKeystroke::new(keystroke);
}
let Some(shifted_key) = get_shifted_key(vkey, scan_code) else {
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
log::error!(
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
keystroke,
vkey
);
return KeybindingKeystroke::new(keystroke);
return KeybindingKeystroke::from_keystroke(keystroke);
};
shifted_key
} else {
@ -113,7 +114,36 @@ impl WindowsKeyboardLayout {
impl WindowsKeyboardMapper {
pub(crate) fn new() -> Self {
Self
let mut key_to_vkey = HashMap::default();
let mut vkey_to_key = HashMap::default();
let mut vkey_to_shifted = HashMap::default();
for vkey in CANDIDATE_VKEYS {
if let Some(key) = get_key_from_vkey(*vkey) {
key_to_vkey.insert(key.clone(), (vkey.0, false));
vkey_to_key.insert(vkey.0, key);
}
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
if scan_code == 0 {
continue;
}
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
vkey_to_shifted.insert(vkey.0, shifted_key);
}
}
Self {
key_to_vkey,
vkey_to_key,
vkey_to_shifted,
}
}
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
if use_key_equivalents {
key_needs_processing(key)
} else {
self.key_to_vkey.get(key).cloned()
}
}
}
@ -210,54 +240,81 @@ pub(crate) fn generate_key_char(
}
}
fn key_needs_processing(key: &str) -> Option<(VIRTUAL_KEY, bool)> {
fn key_needs_processing(key: &str) -> Option<(u16, bool)> {
match key {
"`" => Some((VK_OEM_3, false)),
"~" => Some((VK_OEM_3, true)),
"1" => Some((VK_1, false)),
"!" => Some((VK_1, true)),
"2" => Some((VK_2, false)),
"@" => Some((VK_2, true)),
"3" => Some((VK_3, false)),
"#" => Some((VK_3, true)),
"4" => Some((VK_4, false)),
"$" => Some((VK_4, true)),
"5" => Some((VK_5, false)),
"%" => Some((VK_5, true)),
"6" => Some((VK_6, false)),
"^" => Some((VK_6, true)),
"7" => Some((VK_7, false)),
"&" => Some((VK_7, true)),
"8" => Some((VK_8, false)),
"*" => Some((VK_8, true)),
"9" => Some((VK_9, false)),
"(" => Some((VK_9, true)),
"0" => Some((VK_0, false)),
")" => Some((VK_0, true)),
"-" => Some((VK_OEM_MINUS, false)),
"_" => Some((VK_OEM_MINUS, true)),
"=" => Some((VK_OEM_PLUS, false)),
"+" => Some((VK_OEM_PLUS, true)),
"[" => Some((VK_OEM_4, false)),
"{" => Some((VK_OEM_4, true)),
"]" => Some((VK_OEM_6, false)),
"}" => Some((VK_OEM_6, true)),
"\\" => Some((VK_OEM_5, false)),
"|" => Some((VK_OEM_5, true)),
";" => Some((VK_OEM_1, false)),
":" => Some((VK_OEM_1, true)),
"'" => Some((VK_OEM_7, false)),
"\"" => Some((VK_OEM_7, true)),
"," => Some((VK_OEM_COMMA, false)),
"<" => Some((VK_OEM_COMMA, true)),
"." => Some((VK_OEM_PERIOD, false)),
">" => Some((VK_OEM_PERIOD, true)),
"/" => Some((VK_OEM_2, false)),
"?" => Some((VK_OEM_2, true)),
"`" => Some((VK_OEM_3.0, false)),
"~" => Some((VK_OEM_3.0, true)),
"1" => Some((VK_1.0, false)),
"!" => Some((VK_1.0, true)),
"2" => Some((VK_2.0, false)),
"@" => Some((VK_2.0, true)),
"3" => Some((VK_3.0, false)),
"#" => Some((VK_3.0, true)),
"4" => Some((VK_4.0, false)),
"$" => Some((VK_4.0, true)),
"5" => Some((VK_5.0, false)),
"%" => Some((VK_5.0, true)),
"6" => Some((VK_6.0, false)),
"^" => Some((VK_6.0, true)),
"7" => Some((VK_7.0, false)),
"&" => Some((VK_7.0, true)),
"8" => Some((VK_8.0, false)),
"*" => Some((VK_8.0, true)),
"9" => Some((VK_9.0, false)),
"(" => Some((VK_9.0, true)),
"0" => Some((VK_0.0, false)),
")" => Some((VK_0.0, true)),
"-" => Some((VK_OEM_MINUS.0, false)),
"_" => Some((VK_OEM_MINUS.0, true)),
"=" => Some((VK_OEM_PLUS.0, false)),
"+" => Some((VK_OEM_PLUS.0, true)),
"[" => Some((VK_OEM_4.0, false)),
"{" => Some((VK_OEM_4.0, true)),
"]" => Some((VK_OEM_6.0, false)),
"}" => Some((VK_OEM_6.0, true)),
"\\" => Some((VK_OEM_5.0, false)),
"|" => Some((VK_OEM_5.0, true)),
";" => Some((VK_OEM_1.0, false)),
":" => Some((VK_OEM_1.0, true)),
"'" => Some((VK_OEM_7.0, false)),
"\"" => Some((VK_OEM_7.0, true)),
"," => Some((VK_OEM_COMMA.0, false)),
"<" => Some((VK_OEM_COMMA.0, true)),
"." => Some((VK_OEM_PERIOD.0, false)),
">" => Some((VK_OEM_PERIOD.0, true)),
"/" => Some((VK_OEM_2.0, false)),
"?" => Some((VK_OEM_2.0, true)),
_ => None,
}
}
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
VK_OEM_3,
VK_OEM_MINUS,
VK_OEM_PLUS,
VK_OEM_4,
VK_OEM_5,
VK_OEM_6,
VK_OEM_1,
VK_OEM_7,
VK_OEM_COMMA,
VK_OEM_PERIOD,
VK_OEM_2,
VK_OEM_102,
VK_OEM_8,
VK_ABNT_C1,
VK_0,
VK_1,
VK_2,
VK_3,
VK_4,
VK_5,
VK_6,
VK_7,
VK_8,
VK_9,
];
#[cfg(test)]
mod tests {
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
@ -272,7 +329,7 @@ mod tests {
key: "a".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone());
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.key, "a");
assert_eq!(mapped.modifiers, Modifiers::control());
@ -283,7 +340,7 @@ mod tests {
key: "$".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone());
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.key, "4");
assert_eq!(mapped.modifiers, Modifiers::control_shift());
@ -294,7 +351,7 @@ mod tests {
key: "$".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone());
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.key, "4");
assert_eq!(mapped.modifiers, Modifiers::control_shift());
@ -305,7 +362,7 @@ mod tests {
key: "4".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone());
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.inner.key, "$");
assert_eq!(mapped.key, "4");

View file

@ -279,6 +279,7 @@ impl KeymapFile {
keystrokes,
action,
context_predicate.clone(),
*use_key_equivalents,
key_equivalents,
cx,
);
@ -337,6 +338,7 @@ impl KeymapFile {
keystrokes: &str,
action: &KeymapAction,
context: Option<Rc<KeyBindingContextPredicate>>,
use_key_equivalents: bool,
key_equivalents: Option<&HashMap<char, char>>,
cx: &App,
) -> std::result::Result<KeyBinding, String> {
@ -405,8 +407,10 @@ impl KeymapFile {
keystrokes,
action,
context,
use_key_equivalents,
key_equivalents,
action_input_string.map(SharedString::from),
cx.keyboard_mapper(),
) {
Ok(key_binding) => key_binding,
Err(InvalidKeystrokeError { keystroke }) => {
@ -1021,7 +1025,7 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
#[cfg(test)]
mod tests {
use gpui::{KeybindingKeystroke, Keystroke};
use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke};
use unindent::Unindent;
use crate::{
@ -1059,7 +1063,13 @@ mod tests {
fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
keystrokes
.split(' ')
.map(|s| KeybindingKeystroke::new(Keystroke::parse(s).expect("Keystrokes valid")))
.map(|s| {
KeybindingKeystroke::new(
Keystroke::parse(s).expect("Keystrokes valid"),
false,
&DummyKeyboardMapper,
)
})
.collect()
}

View file

@ -89,9 +89,12 @@ pub fn default_settings() -> Cow<'static, str> {
#[cfg(target_os = "macos")]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
#[cfg(target_os = "windows")]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json";
pub fn default_keymap() -> Cow<'static, str> {
asset_str::<SettingsAssets>(DEFAULT_KEYMAP_PATH)
}

View file

@ -301,7 +301,8 @@ impl KeystrokeInput {
return;
}
let mut keystroke = KeybindingKeystroke::new(keystroke.clone());
let mut keystroke =
KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper());
if let Some(last) = self.keystrokes.last()
&& last.key.is_empty()
&& (!self.search || self.previous_modifiers.modified())