From ebda6b8a94e8aa8baefc7534f8c940feac500be9 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 6 Aug 2025 11:16:05 -0500 Subject: [PATCH] keymap_ui: Show matching bindings (#35732) Closes #ISSUE Adds a bit of text in the keybind editing modal when there are existing keystrokes with the same key, with the ability for the user to click the text and have the keymap editor search be updated to show only bindings with those keystrokes Release Notes: - Keymap Editor: Added a warning to the keybind editing modal when existing bindings have the same keystrokes. Clicking the warning will close the modal and show bindings with the entered keystrokes in the keymap editor. This behavior was previously possible with the `keymap_editor::ShowMatchingKeybinds` action in the Keymap Editor, and is now present in the keybind editing modal as well. --- crates/settings_ui/src/keybindings.rs | 107 ++++++++++++++++++++------ 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 81c461fed6..60e527677a 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -374,6 +374,14 @@ impl Focusable for KeymapEditor { } } } +/// Helper function to check if two keystroke sequences match exactly +fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool { + keystrokes1.len() == keystrokes2.len() + && keystrokes1 + .iter() + .zip(keystrokes2) + .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers) +} impl KeymapEditor { fn new(workspace: WeakEntity, window: &mut Window, cx: &mut Context) -> Self { @@ -549,13 +557,7 @@ impl KeymapEditor { .keystrokes() .is_some_and(|keystrokes| { if exact_match { - keystroke_query.len() == keystrokes.len() - && keystroke_query.iter().zip(keystrokes).all( - |(query, keystroke)| { - query.key == keystroke.key - && query.modifiers == keystroke.modifiers - }, - ) + keystrokes_match_exactly(&keystroke_query, keystrokes) } else if keystroke_query.len() > keystrokes.len() { return false; } else { @@ -2340,8 +2342,50 @@ impl KeybindingEditorModal { self.save_or_display_error(cx); } - fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent) + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } + + fn get_matching_bindings_count(&self, cx: &Context) -> usize { + let current_keystrokes = self.keybind_editor.read(cx).keystrokes().to_vec(); + + if current_keystrokes.is_empty() { + return 0; + } + + self.keymap_editor + .read(cx) + .keybindings + .iter() + .enumerate() + .filter(|(idx, binding)| { + // Don't count the binding we're currently editing + if !self.creating && *idx == self.editing_keybind_idx { + return false; + } + + binding + .keystrokes() + .map(|keystrokes| keystrokes_match_exactly(keystrokes, ¤t_keystrokes)) + .unwrap_or(false) + }) + .count() + } + + fn show_matching_bindings(&mut self, _window: &mut Window, cx: &mut Context) { + let keystrokes = self.keybind_editor.read(cx).keystrokes().to_vec(); + + // Dismiss the modal + cx.emit(DismissEvent); + + // Update the keymap editor to show matching keystrokes + self.keymap_editor.update(cx, |editor, cx| { + editor.filter_state = FilterState::All; + editor.search_mode = SearchMode::KeyStroke { exact_match: true }; + editor.keystroke_editor.update(cx, |keystroke_editor, cx| { + keystroke_editor.set_keystrokes(keystrokes, cx); + }); + }); } } @@ -2356,6 +2400,7 @@ fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { impl Render for KeybindingEditorModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = cx.theme().colors(); + let matching_bindings_count = self.get_matching_bindings_count(cx); v_flex() .w(rems(34.)) @@ -2427,19 +2472,37 @@ impl Render for KeybindingEditorModal { ), ) .footer( - ModalFooter::new().end_slot( - h_flex() - .gap_1() - .child( - Button::new("cancel", "Cancel") - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), - ) - .child(Button::new("save-btn", "Save").on_click(cx.listener( - |this, _event, _window, cx| { - this.save_or_display_error(cx); - }, - ))), - ), + ModalFooter::new() + .start_slot( + div().when(matching_bindings_count > 0, |this| { + this.child( + Button::new("show_matching", format!( + "There {} {} {} with the same keystrokes. Click to view", + if matching_bindings_count == 1 { "is" } else { "are" }, + matching_bindings_count, + if matching_bindings_count == 1 { "binding" } else { "bindings" } + )) + .style(ButtonStyle::Transparent) + .color(Color::Accent) + .on_click(cx.listener(|this, _, window, cx| { + this.show_matching_bindings(window, cx); + })) + ) + }) + ) + .end_slot( + h_flex() + .gap_1() + .child( + Button::new("cancel", "Cancel") + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), + ) + .child(Button::new("save-btn", "Save").on_click(cx.listener( + |this, _event, _window, cx| { + this.save_or_display_error(cx); + }, + ))), + ), ), ) }