From c93773cfe5d940e48ea01fc38a38d95b9134f91a Mon Sep 17 00:00:00 2001 From: everdrone Date: Tue, 27 Jan 2026 04:28:00 +0100 Subject: [PATCH] Make `workspace::SendKeystrokes` not use layout key equivalents (#47061) Closes #46509 Release Notes: - Fixed: `workspace::SendKeystrokes` would not allow remapping keys in different keyboard layouts --------- Co-authored-by: Conrad Irwin --- crates/vim/src/test.rs | 119 ++++++++++++++++++++++++++++++ crates/workspace/src/workspace.rs | 2 +- 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 98548ff387e678ca59263e5b3f4f859c39e5f779..7cbd998972f658e9af7ff3013700594178006a10 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -2540,6 +2540,125 @@ async fn test_deactivate(cx: &mut gpui::TestAppContext) { }); } +// workspace::SendKeystrokes should pass literal keystrokes without triggering vim motions. +// When sending `" _ x`, the `_` should select the blackhole register, not trigger +// vim::StartOfLineDownward. +#[gpui::test] +async fn test_send_keystrokes_underscore_is_literal_46509(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Bind a key to send `" _ x` which should: + // `"` - start register selection + // `_` - select blackhole register (NOT vim::StartOfLineDownward) + // `x` - delete character into blackhole register + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "g x", + workspace::SendKeystrokes("\" _ x".to_string()), + Some("VimControl"), + )]) + }); + + cx.set_state("helˇlo", Mode::Normal); + + cx.simulate_keystrokes("g x"); + cx.run_until_parked(); + + cx.assert_state("helˇo", Mode::Normal); +} + +#[gpui::test] +async fn test_send_keystrokes_no_key_equivalent_mapping_46509(cx: &mut gpui::TestAppContext) { + use collections::HashMap; + use gpui::{KeybindingKeystroke, Keystroke, PlatformKeyboardMapper}; + + // create a mock Danish keyboard mapper + // on Danish keyboards, the macOS key equivalents mapping includes: '{' -> 'Æ' and '}' -> 'Ø' + // this means the `{` character is produced by the key labeled `Æ` (with shift modifier) + struct DanishKeyboardMapper; + impl PlatformKeyboardMapper for DanishKeyboardMapper { + fn map_key_equivalent( + &self, + mut keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke { + if use_key_equivalents { + if keystroke.key == "{" { + keystroke.key = "Æ".to_string(); + } + if keystroke.key == "}" { + keystroke.key = "Ø".to_string(); + } + } + KeybindingKeystroke::from_keystroke(keystroke) + } + + fn get_key_equivalents(&self) -> Option<&HashMap> { + None + } + } + + let mapper = DanishKeyboardMapper; + + let keystroke_brace = Keystroke::parse("{").unwrap(); + let mapped_with_bug = mapper.map_key_equivalent(keystroke_brace.clone(), true); + assert_eq!( + mapped_with_bug.key(), + "Æ", + "BUG: With use_key_equivalents=true, {{ is mapped to Æ on Danish keyboard" + ); + + // Fixed behavior, where the literal `{` character is preserved + let mapped_fixed = mapper.map_key_equivalent(keystroke_brace.clone(), false); + assert_eq!( + mapped_fixed.key(), + "{", + "FIX: With use_key_equivalents=false, {{ stays as {{" + ); + + // Same applies to } + let keystroke_close = Keystroke::parse("}").unwrap(); + let mapped_close_bug = mapper.map_key_equivalent(keystroke_close.clone(), true); + assert_eq!(mapped_close_bug.key(), "Ø"); + let mapped_close_fixed = mapper.map_key_equivalent(keystroke_close.clone(), false); + assert_eq!(mapped_close_fixed.key(), "}"); + + let mut cx = VimTestContext::new(cx, true).await; + + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "g p", + workspace::SendKeystrokes("{".to_string()), + Some("vim_mode == normal"), + )]) + }); + + cx.set_state( + indoc! {" + first paragraph + + second paragraphˇ + + third paragraph + "}, + Mode::Normal, + ); + + cx.simulate_keystrokes("g p"); + cx.run_until_parked(); + + cx.assert_state( + indoc! {" + first paragraph + ˇ + second paragraph + + third paragraph + "}, + Mode::Normal, + ); +} + #[gpui::test] async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) { VimTestContext::init(cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bcb34e65cd98645113e17a539a36e4fb5fd95995..9c27841b65f59d73497cc6611c418fb1b3656c04 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2722,7 +2722,7 @@ impl Workspace { .flat_map(|k| Keystroke::parse(k).log_err()) .map(|k| { cx.keyboard_mapper() - .map_key_equivalent(k, true) + .map_key_equivalent(k, false) .inner() .clone() })