Make `workspace::SendKeystrokes` not use layout key equivalents (#47061)

everdrone and Conrad Irwin created

Closes #46509

Release Notes:

- Fixed: `workspace::SendKeystrokes` would not allow remapping keys in
different keyboard layouts

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/vim/src/test.rs            | 119 +++++++++++++++++++++++++++++++++
crates/workspace/src/workspace.rs |   2 
2 files changed, 120 insertions(+), 1 deletion(-)

Detailed changes

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<char, char>> {
+            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);

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()
             })