Refactor KeybindingKeystroke (#37065)

This pull request refactors the `KeybindingKeystroke` struct and related
code to improve platform abstraction. The changes centralize
platform-specific logic within `KeybindingKeystroke` and update its
usage throughout the codebase, making the API more consistent and less
error-prone.



Release Notes:

- N/A
This commit is contained in:
张小白
2025-08-28 08:40:43 +00:00
committed by GitHub
parent 73b38c8306
commit ff03dda90a
11 changed files with 183 additions and 123 deletions
+12 -12
View File
@@ -2588,7 +2588,7 @@ impl Editor {
|| binding
.keystrokes()
.first()
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
.is_some_and(|keystroke| keystroke.modifiers().modified())
}))
}
@@ -7686,16 +7686,16 @@ impl Editor {
.keystroke()
{
modifiers_held = modifiers_held
|| (&accept_keystroke.display_modifiers == modifiers
&& accept_keystroke.display_modifiers.modified());
|| (accept_keystroke.modifiers() == modifiers
&& accept_keystroke.modifiers().modified());
};
if let Some(accept_partial_keystroke) = self
.accept_edit_prediction_keybind(true, window, cx)
.keystroke()
{
modifiers_held = modifiers_held
|| (&accept_partial_keystroke.display_modifiers == modifiers
&& accept_partial_keystroke.display_modifiers.modified());
|| (accept_partial_keystroke.modifiers() == modifiers
&& accept_partial_keystroke.modifiers().modified());
}
if modifiers_held {
@@ -9044,7 +9044,7 @@ impl Editor {
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
let modifiers_color = if *accept_keystroke.modifiers() == window.modifiers() {
Color::Accent
} else {
Color::Muted
@@ -9056,19 +9056,19 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers,
accept_keystroke.modifiers(),
PlatformStyle::platform(),
Some(modifiers_color),
Some(IconSize::XSmall.rems().into()),
true,
)))
.when(is_platform_style_mac, |parent| {
parent.child(accept_keystroke.display_key.clone())
parent.child(accept_keystroke.key().to_string())
})
.when(!is_platform_style_mac, |parent| {
parent.child(
Key::new(
util::capitalize(&accept_keystroke.display_key),
util::capitalize(accept_keystroke.key()),
Some(Color::Default),
)
.size(Some(IconSize::XSmall.rems().into())),
@@ -9249,7 +9249,7 @@ impl Editor {
accept_keystroke.as_ref(),
|el, accept_keystroke| {
el.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers,
accept_keystroke.modifiers(),
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
@@ -9319,7 +9319,7 @@ impl Editor {
.child(completion),
)
.when_some(accept_keystroke, |el, accept_keystroke| {
if !accept_keystroke.display_modifiers.modified() {
if !accept_keystroke.modifiers().modified() {
return el;
}
@@ -9338,7 +9338,7 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.when(is_platform_style_mac, |parent| parent.gap_1())
.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers,
accept_keystroke.modifiers(),
PlatformStyle::platform(),
Some(if !has_completion {
Color::Muted
+1 -1
View File
@@ -638,7 +638,7 @@ mod tests {
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
let actual = keymap
.bindings_for_action(action)
.map(|binding| binding.keystrokes[0].inner.unparse())
.map(|binding| binding.keystrokes[0].inner().unparse())
.collect::<Vec<_>>();
assert_eq!(actual, expected, "{:?}", action);
}
+1 -1
View File
@@ -57,7 +57,7 @@ impl KeyBinding {
.split_whitespace()
.map(|source| {
let keystroke = Keystroke::parse(source)?;
Ok(KeybindingKeystroke::new(
Ok(KeybindingKeystroke::new_with_mapper(
keystroke,
use_key_equivalents,
keyboard_mapper,
+102 -15
View File
@@ -36,11 +36,13 @@ pub struct Keystroke {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct KeybindingKeystroke {
/// The GPUI representation of the keystroke.
pub inner: Keystroke,
inner: Keystroke,
/// The modifiers to display.
pub display_modifiers: Modifiers,
#[cfg(target_os = "windows")]
display_modifiers: Modifiers,
/// The key to display.
pub display_key: String,
#[cfg(target_os = "windows")]
display_key: String,
}
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
@@ -262,8 +264,17 @@ impl Keystroke {
}
impl KeybindingKeystroke {
/// Create a new keybinding keystroke from the given keystroke
pub fn new(
#[cfg(target_os = "windows")]
pub(crate) fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
KeybindingKeystroke {
inner,
display_modifiers,
display_key,
}
}
/// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper.
pub fn new_with_mapper(
inner: Keystroke,
use_key_equivalents: bool,
keyboard_mapper: &dyn PlatformKeyboardMapper,
@@ -271,19 +282,95 @@ impl KeybindingKeystroke {
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: keystroke,
display_modifiers: modifiers,
display_key: key,
/// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping.
pub fn from_keystroke(keystroke: Keystroke) -> Self {
#[cfg(target_os = "windows")]
{
let key = keystroke.key.clone();
let modifiers = keystroke.modifiers;
KeybindingKeystroke {
inner: keystroke,
display_modifiers: modifiers,
display_key: key,
}
}
#[cfg(not(target_os = "windows"))]
{
KeybindingKeystroke { inner: keystroke }
}
}
/// Returns the GPUI representation of the keystroke.
pub fn inner(&self) -> &Keystroke {
&self.inner
}
/// Returns the modifiers.
///
/// Platform-specific behavior:
/// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
/// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
/// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
pub fn modifiers(&self) -> &Modifiers {
#[cfg(target_os = "windows")]
{
&self.display_modifiers
}
#[cfg(not(target_os = "windows"))]
{
&self.inner.modifiers
}
}
/// Returns the key.
///
/// Platform-specific behavior:
/// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
/// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
pub fn key(&self) -> &str {
#[cfg(target_os = "windows")]
{
&self.display_key
}
#[cfg(not(target_os = "windows"))]
{
&self.inner.key
}
}
/// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
pub fn set_modifiers(&mut self, modifiers: Modifiers) {
self.inner.modifiers = modifiers;
#[cfg(target_os = "windows")]
{
self.display_modifiers = modifiers;
}
}
/// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
pub fn set_key(&mut self, key: String) {
#[cfg(target_os = "windows")]
{
self.display_key = key.clone();
}
self.inner.key = key;
}
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
unparse(&self.display_modifiers, &self.display_key)
#[cfg(target_os = "windows")]
{
unparse(&self.display_modifiers, &self.display_key)
}
#[cfg(not(target_os = "windows"))]
{
unparse(&self.inner.modifiers, &self.inner.key)
}
}
/// Removes the key_char
pub fn remove_key_char(&mut self) {
self.inner.key_char = None;
}
}
@@ -350,8 +437,8 @@ impl std::fmt::Display for Keystroke {
impl std::fmt::Display for KeybindingKeystroke {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
display_modifiers(&self.display_modifiers, f)?;
display_key(&self.display_key, f)
display_modifiers(self.modifiers(), f)?;
display_key(self.key(), f)
}
}
+5 -5
View File
@@ -354,19 +354,19 @@ impl MacPlatform {
let mut mask = NSEventModifierFlags::empty();
for (modifier, flag) in &[
(
keystroke.display_modifiers.platform,
keystroke.modifiers().platform,
NSEventModifierFlags::NSCommandKeyMask,
),
(
keystroke.display_modifiers.control,
keystroke.modifiers().control,
NSEventModifierFlags::NSControlKeyMask,
),
(
keystroke.display_modifiers.alt,
keystroke.modifiers().alt,
NSEventModifierFlags::NSAlternateKeyMask,
),
(
keystroke.display_modifiers.shift,
keystroke.modifiers().shift,
NSEventModifierFlags::NSShiftKeyMask,
),
] {
@@ -379,7 +379,7 @@ impl MacPlatform {
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector,
ns_string(key_to_native(&keystroke.display_key).as_ref()),
ns_string(key_to_native(keystroke.key()).as_ref()),
)
.autorelease();
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
+14 -18
View File
@@ -83,11 +83,7 @@ impl PlatformKeyboardMapper for WindowsKeyboardMapper {
..keystroke.modifiers
};
KeybindingKeystroke {
inner: keystroke,
display_modifiers: modifiers,
display_key: key,
}
KeybindingKeystroke::new(keystroke, modifiers, key)
}
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
@@ -335,9 +331,9 @@ mod tests {
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.display_key, "a");
assert_eq!(mapped.display_modifiers, Modifiers::control());
assert_eq!(*mapped.inner(), keystroke);
assert_eq!(mapped.key(), "a");
assert_eq!(*mapped.modifiers(), Modifiers::control());
// Shifted case, ctrl-$
let keystroke = Keystroke {
@@ -346,9 +342,9 @@ mod tests {
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
assert_eq!(*mapped.inner(), keystroke);
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
// Shifted case, but shift is true
let keystroke = Keystroke {
@@ -357,9 +353,9 @@ mod tests {
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
assert_eq!(mapped.inner().modifiers, Modifiers::control());
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
// Windows style
let keystroke = Keystroke {
@@ -368,9 +364,9 @@ mod tests {
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.inner.key, "$");
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
assert_eq!(mapped.inner().modifiers, Modifiers::control());
assert_eq!(mapped.inner().key, "$");
assert_eq!(mapped.key(), "4");
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
}
}
+7 -3
View File
@@ -820,7 +820,11 @@ impl KeymapFile {
.split_whitespace()
.map(|source| {
let keystroke = Keystroke::parse(source)?;
Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper))
Ok(KeybindingKeystroke::new_with_mapper(
keystroke,
false,
keyboard_mapper,
))
})
.collect::<Result<Vec<_>, InvalidKeystrokeError>>()
else {
@@ -830,7 +834,7 @@ impl KeymapFile {
|| !keystrokes
.iter()
.zip(target.keystrokes)
.all(|(a, b)| a.inner.should_match(b))
.all(|(a, b)| a.inner().should_match(b))
{
continue;
}
@@ -1065,7 +1069,7 @@ mod tests {
keystrokes
.split(' ')
.map(|s| {
KeybindingKeystroke::new(
KeybindingKeystroke::new_with_mapper(
Keystroke::parse(s).expect("Keystrokes valid"),
false,
&DummyKeyboardMapper,
+15 -35
View File
@@ -14,9 +14,9 @@ use gpui::{
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
EventEmitter, FocusHandle, Focusable, Global, IsZero,
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point,
ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy,
ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,
actions, anchored, deferred, div,
};
use language::{Language, LanguageConfig, ToOffset as _};
use notifications::status_toast::{StatusToast, ToastIcon};
@@ -420,7 +420,7 @@ fn keystrokes_match_exactly(
) -> bool {
keystrokes1.len() == keystrokes2.len()
&& keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers
k1.inner().key == k2.inner().key && k1.inner().modifiers == k2.inner().modifiers
})
}
@@ -532,7 +532,7 @@ impl KeymapEditor {
let keystroke_query = keystroke_query
.into_iter()
.map(|keystroke| keystroke.inner.unparse())
.map(|keystroke| keystroke.inner().unparse())
.collect::<Vec<String>>()
.join(" ");
@@ -606,13 +606,13 @@ impl KeymapEditor {
let query = &keystroke_query[query_cursor];
let keystroke = &keystrokes[keystroke_cursor];
let matches = query
.inner
.inner()
.modifiers
.is_subset_of(&keystroke.inner.modifiers)
&& ((query.inner.key.is_empty()
|| query.inner.key == keystroke.inner.key)
&& query.inner.key_char.as_ref().is_none_or(
|q_kc| q_kc == &keystroke.inner.key,
.is_subset_of(&keystroke.inner().modifiers)
&& ((query.inner().key.is_empty()
|| query.inner().key == keystroke.inner().key)
&& query.inner().key_char.as_ref().is_none_or(
|q_kc| q_kc == &keystroke.inner().key,
));
if matches {
found_count += 1;
@@ -2256,12 +2256,10 @@ impl KeybindingEditorModal {
let fs = self.fs.clone();
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
let new_keystrokes = self
.validate_keystrokes(cx)
.map_err(InputError::error)?
.into_iter()
.map(remove_key_char)
.collect::<Vec<_>>();
let mut new_keystrokes = self.validate_keystrokes(cx).map_err(InputError::error)?;
new_keystrokes
.iter_mut()
.for_each(|ks| ks.remove_key_char());
let new_context = self.validate_context(cx).map_err(InputError::error)?;
let new_action_args = self
@@ -2454,24 +2452,6 @@ impl KeybindingEditorModal {
}
}
fn remove_key_char(
KeybindingKeystroke {
inner,
display_modifiers,
display_key,
}: KeybindingKeystroke,
) -> KeybindingKeystroke {
KeybindingKeystroke {
inner: Keystroke {
modifiers: inner.modifiers,
key: inner.key,
key_char: None,
},
display_modifiers,
display_key,
}
}
impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors();
@@ -116,7 +116,7 @@ impl KeystrokeInput {
&& self
.keystrokes
.last()
.is_some_and(|last| last.display_key.is_empty())
.is_some_and(|last| last.key().is_empty())
{
return &self.keystrokes[..self.keystrokes.len() - 1];
}
@@ -124,15 +124,11 @@ impl KeystrokeInput {
}
fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
KeybindingKeystroke {
inner: Keystroke {
modifiers,
key: "".to_string(),
key_char: None,
},
display_modifiers: modifiers,
display_key: "".to_string(),
}
KeybindingKeystroke::from_keystroke(Keystroke {
modifiers,
key: "".to_string(),
key_char: None,
})
}
fn keystrokes_changed(&self, cx: &mut Context<Self>) {
@@ -258,7 +254,7 @@ impl KeystrokeInput {
self.keystrokes_changed(cx);
if let Some(last) = self.keystrokes.last_mut()
&& last.display_key.is_empty()
&& last.key().is_empty()
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
{
if !self.search && !event.modifiers.modified() {
@@ -267,15 +263,14 @@ impl KeystrokeInput {
}
if self.search {
if self.previous_modifiers.modified() {
last.display_modifiers |= event.modifiers;
last.inner.modifiers |= event.modifiers;
let modifiers = *last.modifiers() | event.modifiers;
last.set_modifiers(modifiers);
} else {
self.keystrokes.push(Self::dummy(event.modifiers));
}
self.previous_modifiers |= event.modifiers;
} else {
last.display_modifiers = event.modifiers;
last.inner.modifiers = event.modifiers;
last.set_modifiers(event.modifiers);
return;
}
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
@@ -303,10 +298,13 @@ impl KeystrokeInput {
return;
}
let keystroke =
KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref());
let keystroke = KeybindingKeystroke::new_with_mapper(
keystroke.clone(),
false,
cx.keyboard_mapper().as_ref(),
);
if let Some(last) = self.keystrokes.last()
&& last.display_key.is_empty()
&& last.key().is_empty()
&& (!self.search || self.previous_modifiers.modified())
{
self.keystrokes.pop();
@@ -825,7 +823,7 @@ mod tests {
input
.keystrokes
.iter()
.map(|keystroke| keystroke.inner.clone())
.map(|keystroke| keystroke.inner().clone())
.collect()
});
Self::expect_keystrokes_equal(&actual, expected);
@@ -1094,7 +1092,7 @@ mod tests {
}
fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
ks.iter().map(|ks| ks.inner.unparse()).join(" ")
ks.iter().map(|ks| ks.inner().unparse()).join(" ")
}
}
}
+7 -12
View File
@@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding {
"KEY_BINDING-{}",
self.keystrokes
.iter()
.map(|k| k.display_key.to_string())
.map(|k| k.key().to_string())
.collect::<Vec<_>>()
.join(" ")
)
@@ -165,8 +165,8 @@ pub fn render_keybinding_keystroke(
if use_text {
let element = Key::new(
keystroke_text(
&keystroke.display_modifiers,
&keystroke.display_key,
keystroke.modifiers(),
keystroke.key(),
platform_style,
vim_mode,
),
@@ -178,18 +178,13 @@ pub fn render_keybinding_keystroke(
} else {
let mut elements = Vec::new();
elements.extend(render_modifiers(
&keystroke.display_modifiers,
keystroke.modifiers(),
platform_style,
color,
size,
true,
));
elements.push(render_key(
&keystroke.display_key,
color,
platform_style,
size,
));
elements.push(render_key(keystroke.key(), color, platform_style, size));
elements
}
}
@@ -418,8 +413,8 @@ pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &A
.iter()
.map(|keystroke| {
keystroke_text(
&keystroke.display_modifiers,
&keystroke.display_key,
keystroke.modifiers(),
keystroke.key(),
platform_style,
vim_enabled,
)
+1 -1
View File
@@ -4738,7 +4738,7 @@ mod tests {
// and key strokes contain the given key
bindings
.into_iter()
.any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)),
.any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
"On {} Failed to find {} with key binding {}",
line,
action.name(),