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:
parent
b1b60bb7fe
commit
fff0ecead1
25 changed files with 3515 additions and 1721 deletions
|
@ -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(),
|
||||
|
|
|
@ -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(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue