windows: Fix keystroke & keymap (#36572)

Closes #36300

This PR follows Windows conventions by introducing
`KeybindingKeystroke`, so shortcuts now show up as `ctrl-shift-4`
instead of `ctrl-$`.

It also fixes issues with keyboard layouts: when `use_key_equivalents`
is set to true, keys are remapped based on their virtual key codes. For
example, `ctrl-\` on a standard English layout will be mapped to
`ctrl-ё` on a Russian layout.


Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>
This commit is contained in:
张小白 2025-08-27 03:24:50 +08:00 committed by GitHub
parent b1b60bb7fe
commit fff0ecead1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 3515 additions and 1721 deletions

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, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful,
StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred,
div,
KeyContext, KeybindingKeystroke, Keystroke, 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};
@ -174,7 +174,7 @@ impl FilterState {
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
struct ActionMapping {
keystrokes: Vec<Keystroke>,
keystrokes: Vec<KeybindingKeystroke>,
context: Option<SharedString>,
}
@ -236,7 +236,7 @@ struct ConflictState {
}
type ConflictKeybindMapping = HashMap<
Vec<Keystroke>,
Vec<KeybindingKeystroke>,
Vec<(
Option<gpui::KeyBindingContextPredicate>,
Vec<ConflictOrigin>,
@ -414,12 +414,14 @@ impl Focusable for KeymapEditor {
}
}
/// Helper function to check if two keystroke sequences match exactly
fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool {
fn keystrokes_match_exactly(
keystrokes1: &[KeybindingKeystroke],
keystrokes2: &[KeybindingKeystroke],
) -> bool {
keystrokes1.len() == keystrokes2.len()
&& keystrokes1
.iter()
.zip(keystrokes2)
.all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
&& keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers
})
}
impl KeymapEditor {
@ -509,7 +511,7 @@ impl KeymapEditor {
self.filter_editor.read(cx).text(cx)
}
fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> {
match self.search_mode {
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
SearchMode::Normal => Default::default(),
@ -530,7 +532,7 @@ impl KeymapEditor {
let keystroke_query = keystroke_query
.into_iter()
.map(|keystroke| keystroke.unparse())
.map(|keystroke| keystroke.inner.unparse())
.collect::<Vec<String>>()
.join(" ");
@ -554,7 +556,7 @@ impl KeymapEditor {
async fn update_matches(
this: WeakEntity<Self>,
action_query: String,
keystroke_query: Vec<Keystroke>,
keystroke_query: Vec<KeybindingKeystroke>,
cx: &mut AsyncApp,
) -> anyhow::Result<()> {
let action_query = command_palette::normalize_action_query(&action_query);
@ -603,13 +605,15 @@ impl KeymapEditor {
{
let query = &keystroke_query[query_cursor];
let keystroke = &keystrokes[keystroke_cursor];
let matches =
query.modifiers.is_subset_of(&keystroke.modifiers)
&& ((query.key.is_empty()
|| query.key == keystroke.key)
&& query.key_char.as_ref().is_none_or(
|q_kc| q_kc == &keystroke.key,
));
let matches = query
.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,
));
if matches {
found_count += 1;
query_cursor += 1;
@ -678,7 +682,7 @@ impl KeymapEditor {
.map(KeybindSource::from_meta)
.unwrap_or(KeybindSource::Unknown);
let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
.vim_mode(source == KeybindSource::Vim);
@ -1202,8 +1206,11 @@ impl KeymapEditor {
.read(cx)
.get_scrollbar_offset(Axis::Vertical),
));
cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
.detach_and_notify_err(window, cx);
let keyboard_mapper = cx.keyboard_mapper().clone();
cx.spawn(async move |_, _| {
remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
})
.detach_and_notify_err(window, cx);
}
fn copy_context_to_clipboard(
@ -1422,7 +1429,7 @@ impl ProcessedBinding {
.map(|keybind| keybind.get_action_mapping())
}
fn keystrokes(&self) -> Option<&[Keystroke]> {
fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
self.ui_key_binding()
.map(|binding| binding.keystrokes.as_slice())
}
@ -2220,7 +2227,7 @@ impl KeybindingEditorModal {
Ok(action_arguments)
}
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> {
let new_keystrokes = self
.keybind_editor
.read_with(cx, |editor, _| editor.keystrokes().to_vec());
@ -2316,6 +2323,7 @@ impl KeybindingEditorModal {
}).unwrap_or(Ok(()))?;
let create = self.creating;
let keyboard_mapper = cx.keyboard_mapper().clone();
cx.spawn(async move |this, cx| {
let action_name = existing_keybind.action().name;
@ -2328,6 +2336,7 @@ impl KeybindingEditorModal {
new_action_args.as_deref(),
&fs,
tab_size,
keyboard_mapper.as_ref(),
)
.await
{
@ -2445,11 +2454,21 @@ impl KeybindingEditorModal {
}
}
fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
Keystroke {
modifiers,
key,
..Default::default()
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,
}
}
@ -2992,6 +3011,7 @@ async fn save_keybinding_update(
new_args: Option<&str>,
fs: &Arc<dyn Fs>,
tab_size: usize,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> anyhow::Result<()> {
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
.await
@ -3034,9 +3054,13 @@ async fn save_keybinding_update(
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
let updated_keymap_contents =
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
operation,
keymap_contents,
tab_size,
keyboard_mapper,
)
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
fs.write(
paths::keymap_file().as_path(),
updated_keymap_contents.as_bytes(),
@ -3057,6 +3081,7 @@ async fn remove_keybinding(
existing: ProcessedBinding,
fs: &Arc<dyn Fs>,
tab_size: usize,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> anyhow::Result<()> {
let Some(keystrokes) = existing.keystrokes() else {
anyhow::bail!("Cannot remove a keybinding that does not exist");
@ -3080,9 +3105,13 @@ async fn remove_keybinding(
};
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
let updated_keymap_contents =
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
.context("Failed to update keybinding")?;
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
operation,
keymap_contents,
tab_size,
keyboard_mapper,
)
.context("Failed to update keybinding")?;
fs.write(
paths::keymap_file().as_path(),
updated_keymap_contents.as_bytes(),

View file

@ -1,6 +1,6 @@
use gpui::{
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
};
use ui::{
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult {
}
pub struct KeystrokeInput {
keystrokes: Vec<Keystroke>,
placeholder_keystrokes: Option<Vec<Keystroke>>,
keystrokes: Vec<KeybindingKeystroke>,
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
outer_focus_handle: FocusHandle,
inner_focus_handle: FocusHandle,
intercept_subscription: Option<Subscription>,
@ -70,7 +70,7 @@ impl KeystrokeInput {
const KEYSTROKE_COUNT_MAX: usize = 3;
pub fn new(
placeholder_keystrokes: Option<Vec<Keystroke>>,
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@ -97,7 +97,7 @@ impl KeystrokeInput {
}
}
pub fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
self.keystrokes = keystrokes;
self.keystrokes_changed(cx);
}
@ -106,7 +106,7 @@ impl KeystrokeInput {
self.search = search;
}
pub fn keystrokes(&self) -> &[Keystroke] {
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
&& self.keystrokes.is_empty()
{
@ -116,18 +116,22 @@ impl KeystrokeInput {
&& self
.keystrokes
.last()
.is_some_and(|last| last.key.is_empty())
.is_some_and(|last| last.display_key.is_empty())
{
return &self.keystrokes[..self.keystrokes.len() - 1];
}
&self.keystrokes
}
fn dummy(modifiers: Modifiers) -> Keystroke {
Keystroke {
modifiers,
key: "".to_string(),
key_char: None,
fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
KeybindingKeystroke {
inner: Keystroke {
modifiers,
key: "".to_string(),
key_char: None,
},
display_modifiers: modifiers,
display_key: "".to_string(),
}
}
@ -254,7 +258,7 @@ impl KeystrokeInput {
self.keystrokes_changed(cx);
if let Some(last) = self.keystrokes.last_mut()
&& last.key.is_empty()
&& last.display_key.is_empty()
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
{
if !self.search && !event.modifiers.modified() {
@ -263,13 +267,15 @@ impl KeystrokeInput {
}
if self.search {
if self.previous_modifiers.modified() {
last.modifiers |= event.modifiers;
last.display_modifiers |= event.modifiers;
last.inner.modifiers |= event.modifiers;
} else {
self.keystrokes.push(Self::dummy(event.modifiers));
}
self.previous_modifiers |= event.modifiers;
} else {
last.modifiers = event.modifiers;
last.display_modifiers = event.modifiers;
last.inner.modifiers = event.modifiers;
return;
}
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
@ -297,14 +303,17 @@ impl KeystrokeInput {
return;
}
let mut keystroke = keystroke.clone();
let mut keystroke =
KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref());
if let Some(last) = self.keystrokes.last()
&& last.key.is_empty()
&& last.display_key.is_empty()
&& (!self.search || self.previous_modifiers.modified())
{
let key = keystroke.key.clone();
let display_key = keystroke.display_key.clone();
let inner_key = keystroke.inner.key.clone();
keystroke = last.clone();
keystroke.key = key;
keystroke.display_key = display_key;
keystroke.inner.key = inner_key;
self.keystrokes.pop();
}
@ -324,11 +333,14 @@ impl KeystrokeInput {
self.keystrokes_changed(cx);
if self.search {
self.previous_modifiers = keystroke.modifiers;
self.previous_modifiers = keystroke.display_modifiers;
return;
}
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() {
self.keystrokes.push(Self::dummy(keystroke.modifiers));
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX
&& keystroke.display_modifiers.modified()
{
self.keystrokes
.push(Self::dummy(keystroke.display_modifiers));
}
}
@ -364,7 +376,7 @@ impl KeystrokeInput {
&self.keystrokes
};
keystrokes.iter().map(move |keystroke| {
h_flex().children(ui::render_keystroke(
h_flex().children(ui::render_keybinding_keystroke(
keystroke,
Some(Color::Default),
Some(rems(0.875).into()),
@ -809,9 +821,13 @@ mod tests {
/// Verifies that the keystrokes match the expected strings
#[track_caller]
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
let actual = self
.input
.read_with(&self.cx, |input, _| input.keystrokes.clone());
let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
input
.keystrokes
.iter()
.map(|keystroke| keystroke.inner.clone())
.collect()
});
Self::expect_keystrokes_equal(&actual, expected);
self
}
@ -939,7 +955,7 @@ mod tests {
}
struct KeystrokeUpdateTracker {
initial_keystrokes: Vec<Keystroke>,
initial_keystrokes: Vec<KeybindingKeystroke>,
_subscription: Subscription,
input: Entity<KeystrokeInput>,
received_keystrokes_updated: bool,
@ -983,8 +999,8 @@ mod tests {
);
}
fn keystrokes_str(ks: &[Keystroke]) -> String {
ks.iter().map(|ks| ks.unparse()).join(" ")
fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
ks.iter().map(|ks| ks.inner.unparse()).join(" ")
}
}
}