From de9d2c8f627a4e216b5e8293126322a02e875ef8 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:09:47 +0800 Subject: [PATCH] fix(keymap): let the code editor's multi-cursor chords beat the pane keys gpui-component now binds secondary-d, secondary-shift-l and secondary-alt-up/down in the Input context for multiple cursors. A context-free binding ranks as deep as the focused context and ties go to the one added last, so tty7's SplitRight and FocusPaneUp/Down took those keys inside the editor. Re-add the editor bindings after tty7's table; other text fields have no handler for them and fall through to the pane keys. --- src/ui/keymap.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/ui/keymap.rs b/src/ui/keymap.rs index e28d7b91..14497b41 100644 --- a/src/ui/keymap.rs +++ b/src/ui/keymap.rs @@ -70,6 +70,28 @@ fn fixed_bindings() -> Vec { let search = Some(crate::ui::search::KEY_CONTEXT); bindings.push(KeyBinding::new("tab", SearchNextTab, search)); bindings.push(KeyBinding::new("shift-tab", SearchPrevTab, search)); + // The code editor's multi-cursor chords, laid down again after tty7's + // own table. gpui ranks a context-free binding as deep as the focused + // context, and breaks the tie by whichever was added last — so the copies + // gpui-component installs lose ⌘D to `SplitRight` and ⌘⌥↑/↓ to + // `FocusPaneUp`/`Down`, whose handlers sit on the workspace root and take + // the key before the editor is asked. Re-added here they win inside an + // `Input`; only the code editor handles them, so in any other text field + // gpui falls through to the pane binding next in line. + { + use gpui_component::input::{ + AddCursorAbove, AddCursorBelow, SelectAllOccurrences, SelectNextOccurrence, + }; + let input = Some("Input"); + bindings.push(KeyBinding::new("secondary-d", SelectNextOccurrence, input)); + bindings.push(KeyBinding::new( + "secondary-shift-l", + SelectAllOccurrences, + input, + )); + bindings.push(KeyBinding::new("secondary-alt-up", AddCursorAbove, input)); + bindings.push(KeyBinding::new("secondary-alt-down", AddCursorBelow, input)); + } bindings } @@ -2833,4 +2855,52 @@ mod gpui_tests { assert_eq!(backspace(cx), inherited, "nor may a rebind"); }); } + + /// The code editor's multi-cursor chords sit on keys tty7 ships for panes + /// (⌘D split, ⌘⌥↑/↓ focus). `fixed_bindings` re-adds them after the + /// pane table so they rank first inside an `Input`; any other text field + /// has no handler for them, and gpui falls through to the pane binding + /// listed after. A terminal never sees them. + #[gpui::test] + fn editor_multi_cursor_chords_outrank_the_pane_bindings(cx: &mut TestAppContext) { + use gpui::Action as _; + use gpui_component::input::{ + AddCursorAbove, AddCursorBelow, SelectAllOccurrences, SelectNextOccurrence, + }; + cx.update(|cx| { + running_on_json(cx, "{}"); + let in_input = |keys: &str| -> Vec<&'static str> { + let input = [Keystroke::parse(keys).expect("the keystroke parses")]; + let context = [ + gpui::KeyContext::parse("Workspace").expect("the context parses"), + gpui::KeyContext::parse("Input").expect("the context parses"), + ]; + cx.key_bindings() + .borrow() + .bindings_for_input(&input, &context) + .0 + .iter() + .map(|b| b.action().name()) + .collect() + }; + let cases = [ + ("secondary-d", SelectNextOccurrence::name_for_type()), + ("secondary-shift-l", SelectAllOccurrences::name_for_type()), + ("secondary-alt-up", AddCursorAbove::name_for_type()), + ("secondary-alt-down", AddCursorBelow::name_for_type()), + ]; + for (keys, editor_action) in cases { + let hits = in_input(keys); + assert_eq!(hits.first(), Some(&editor_action), "{keys} in the editor"); + } + if cfg!(target_os = "macos") { + assert!(in_input("secondary-d").contains(&SplitRight::name_for_type())); + assert_eq!( + fired(cx, "secondary-d").first(), + Some(&SplitRight::name_for_type()), + "a terminal still splits on ⌘D" + ); + } + }); + } }