fix KeymapEditor

This commit is contained in:
Junkui Zhang 2025-08-27 02:37:30 +08:00
parent 9615b712ca
commit 8cac7c62dc
5 changed files with 89 additions and 51 deletions

View file

@ -429,8 +429,8 @@ impl App {
} }
/// Get the current keyboard mapper. /// Get the current keyboard mapper.
pub fn keyboard_mapper(&self) -> &dyn PlatformKeyboardMapper { pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
self.keyboard_mapper.as_ref() &self.keyboard_mapper
} }
/// Invokes a handler when the current keyboard layout changes /// Invokes a handler when the current keyboard layout changes

View file

@ -219,31 +219,7 @@ impl Keystroke {
/// Produces a representation of this key that Parse can understand. /// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String { pub fn unparse(&self) -> String {
let mut str = String::new(); unparse(&self.modifiers, &self.key)
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 /// Returns true if this keystroke left
@ -304,6 +280,11 @@ impl KeybindingKeystroke {
display_key: key, display_key: key,
} }
} }
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
unparse(&self.display_modifiers, &self.display_key)
}
} }
fn is_printable_key(key: &str) -> bool { fn is_printable_key(key: &str) -> bool {
@ -668,3 +649,32 @@ fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
}; };
f.write_char(key) f.write_char(key)
} }
#[inline]
fn unparse(modifiers: &Modifiers, key: &str) -> String {
let mut result = String::new();
if modifiers.function {
result.push_str("fn-");
}
if modifiers.control {
result.push_str("ctrl-");
}
if modifiers.alt {
result.push_str("alt-");
}
if modifiers.platform {
#[cfg(target_os = "macos")]
result.push_str("cmd-");
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
result.push_str("super-");
#[cfg(target_os = "windows")]
result.push_str("win-");
}
if modifiers.shift {
result.push_str("shift-");
}
result.push_str(&key);
result
}

View file

@ -398,7 +398,7 @@ impl KeymapFile {
context, context,
use_key_equivalents, use_key_equivalents,
action_input_string.map(SharedString::from), action_input_string.map(SharedString::from),
cx.keyboard_mapper(), cx.keyboard_mapper().as_ref(),
) { ) {
Ok(key_binding) => key_binding, Ok(key_binding) => key_binding,
Err(InvalidKeystrokeError { keystroke }) => { Err(InvalidKeystrokeError { keystroke }) => {
@ -600,6 +600,7 @@ impl KeymapFile {
mut operation: KeybindUpdateOperation<'a>, mut operation: KeybindUpdateOperation<'a>,
mut keymap_contents: String, mut keymap_contents: String,
tab_size: usize, tab_size: usize,
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
) -> Result<String> { ) -> Result<String> {
match operation { match operation {
// if trying to replace a keybinding that is not user-defined, treat it as an add operation // if trying to replace a keybinding that is not user-defined, treat it as an add operation
@ -639,7 +640,7 @@ impl KeymapFile {
.action_value() .action_value()
.context("Failed to generate target action JSON value")?; .context("Failed to generate target action JSON value")?;
let Some((index, keystrokes_str)) = let Some((index, keystrokes_str)) =
find_binding(&keymap, &target, &target_action_value) find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
else { else {
anyhow::bail!("Failed to find keybinding to remove"); anyhow::bail!("Failed to find keybinding to remove");
}; };
@ -674,7 +675,7 @@ impl KeymapFile {
.context("Failed to generate source action JSON value")?; .context("Failed to generate source action JSON value")?;
if let Some((index, keystrokes_str)) = if let Some((index, keystrokes_str)) =
find_binding(&keymap, &target, &target_action_value) find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
{ {
if target.context == source.context { if target.context == source.context {
// if we are only changing the keybinding (common case) // if we are only changing the keybinding (common case)
@ -774,7 +775,7 @@ impl KeymapFile {
} }
let use_key_equivalents = from.and_then(|from| { let use_key_equivalents = from.and_then(|from| {
let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
let (index, _) = find_binding(&keymap, &from, &action_value)?; let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
Some(keymap.0[index].use_key_equivalents) Some(keymap.0[index].use_key_equivalents)
}).unwrap_or(false); }).unwrap_or(false);
if use_key_equivalents { if use_key_equivalents {
@ -801,6 +802,7 @@ impl KeymapFile {
keymap: &'b KeymapFile, keymap: &'b KeymapFile,
target: &KeybindUpdateTarget<'a>, target: &KeybindUpdateTarget<'a>,
target_action_value: &Value, target_action_value: &Value,
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
) -> Option<(usize, &'b str)> { ) -> Option<(usize, &'b str)> {
let target_context_parsed = let target_context_parsed =
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
@ -816,8 +818,11 @@ impl KeymapFile {
for (keystrokes_str, action) in bindings { for (keystrokes_str, action) in bindings {
let Ok(keystrokes) = keystrokes_str let Ok(keystrokes) = keystrokes_str
.split_whitespace() .split_whitespace()
.map(Keystroke::parse) .map(|source| {
.collect::<Result<Vec<_>, _>>() let keystroke = Keystroke::parse(source)?;
Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper))
})
.collect::<Result<Vec<_>, InvalidKeystrokeError>>()
else { else {
continue; continue;
}; };
@ -825,7 +830,7 @@ impl KeymapFile {
|| !keystrokes || !keystrokes
.iter() .iter()
.zip(target.keystrokes) .zip(target.keystrokes)
.all(|(a, b)| a.should_match(b)) .all(|(a, b)| a.inner.should_match(b))
{ {
continue; continue;
} }
@ -840,7 +845,7 @@ impl KeymapFile {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub enum KeybindUpdateOperation<'a> { pub enum KeybindUpdateOperation<'a> {
Replace { Replace {
/// Describes the keybind to create /// Describes the keybind to create
@ -934,7 +939,10 @@ impl<'a> KeybindUpdateTarget<'a> {
fn keystrokes_unparsed(&self) -> String { fn keystrokes_unparsed(&self) -> String {
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8); let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
for keystroke in self.keystrokes { for keystroke in self.keystrokes {
keystrokes.push_str(&keystroke.inner.unparse()); // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
// here is that, we want the user to use `ctrl-shift-4` instread of `ctrl-$`
// by default on Windows.
keystrokes.push_str(&keystroke.unparse());
keystrokes.push(' '); keystrokes.push(' ');
} }
keystrokes.pop(); keystrokes.pop();
@ -952,7 +960,7 @@ impl<'a> KeybindUpdateTarget<'a> {
} }
} }
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum KeybindSource { pub enum KeybindSource {
User, User,
Vim, Vim,
@ -1042,8 +1050,13 @@ mod tests {
operation: KeybindUpdateOperation, operation: KeybindUpdateOperation,
expected: impl ToString, expected: impl ToString,
) { ) {
let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) let result = KeymapFile::update_keybinding(
.expect("Update succeeded"); operation,
input.to_string(),
4,
&gpui::DummyKeyboardMapper,
)
.expect("Update succeeded");
pretty_assertions::assert_eq!(expected.to_string(), result); pretty_assertions::assert_eq!(expected.to_string(), result);
} }

View file

@ -14,9 +14,9 @@ use gpui::{
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
EventEmitter, FocusHandle, Focusable, Global, IsZero, EventEmitter, FocusHandle, Focusable, Global, IsZero,
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
KeyContext, KeybindingKeystroke, Keystroke, MouseButton, Point, ScrollStrategy, KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point,
ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
actions, anchored, deferred, div, TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
}; };
use language::{Language, LanguageConfig, ToOffset as _}; use language::{Language, LanguageConfig, ToOffset as _};
use notifications::status_toast::{StatusToast, ToastIcon}; use notifications::status_toast::{StatusToast, ToastIcon};
@ -1206,8 +1206,11 @@ impl KeymapEditor {
.read(cx) .read(cx)
.get_scrollbar_offset(Axis::Vertical), .get_scrollbar_offset(Axis::Vertical),
)); ));
cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) let keyboard_mapper = cx.keyboard_mapper().clone();
.detach_and_notify_err(window, cx); 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( fn copy_context_to_clipboard(
@ -2320,6 +2323,7 @@ impl KeybindingEditorModal {
}).unwrap_or(Ok(()))?; }).unwrap_or(Ok(()))?;
let create = self.creating; let create = self.creating;
let keyboard_mapper = cx.keyboard_mapper().clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let action_name = existing_keybind.action().name; let action_name = existing_keybind.action().name;
@ -2332,6 +2336,7 @@ impl KeybindingEditorModal {
new_action_args.as_deref(), new_action_args.as_deref(),
&fs, &fs,
tab_size, tab_size,
keyboard_mapper.as_ref(),
) )
.await .await
{ {
@ -3006,6 +3011,7 @@ async fn save_keybinding_update(
new_args: Option<&str>, new_args: Option<&str>,
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let keymap_contents = settings::KeymapFile::load_keymap_file(fs) let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
.await .await
@ -3048,9 +3054,13 @@ async fn save_keybinding_update(
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
let updated_keymap_contents = let updated_keymap_contents = settings::KeymapFile::update_keybinding(
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) operation,
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; keymap_contents,
tab_size,
keyboard_mapper,
)
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
fs.write( fs.write(
paths::keymap_file().as_path(), paths::keymap_file().as_path(),
updated_keymap_contents.as_bytes(), updated_keymap_contents.as_bytes(),
@ -3071,6 +3081,7 @@ async fn remove_keybinding(
existing: ProcessedBinding, existing: ProcessedBinding,
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let Some(keystrokes) = existing.keystrokes() else { let Some(keystrokes) = existing.keystrokes() else {
anyhow::bail!("Cannot remove a keybinding that does not exist"); anyhow::bail!("Cannot remove a keybinding that does not exist");
@ -3094,9 +3105,13 @@ async fn remove_keybinding(
}; };
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
let updated_keymap_contents = let updated_keymap_contents = settings::KeymapFile::update_keybinding(
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) operation,
.context("Failed to update keybinding")?; keymap_contents,
tab_size,
keyboard_mapper,
)
.context("Failed to update keybinding")?;
fs.write( fs.write(
paths::keymap_file().as_path(), paths::keymap_file().as_path(),
updated_keymap_contents.as_bytes(), updated_keymap_contents.as_bytes(),

View file

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