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.
This commit is contained in:
Ben Kunkle 2025-08-06 11:16:05 -05:00 committed by GitHub
parent 55b4df4d9f
commit ebda6b8a94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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 { impl KeymapEditor {
fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
@ -549,13 +557,7 @@ impl KeymapEditor {
.keystrokes() .keystrokes()
.is_some_and(|keystrokes| { .is_some_and(|keystrokes| {
if exact_match { if exact_match {
keystroke_query.len() == keystrokes.len() keystrokes_match_exactly(&keystroke_query, keystrokes)
&& keystroke_query.iter().zip(keystrokes).all(
|(query, keystroke)| {
query.key == keystroke.key
&& query.modifiers == keystroke.modifiers
},
)
} else if keystroke_query.len() > keystrokes.len() { } else if keystroke_query.len() > keystrokes.len() {
return false; return false;
} else { } else {
@ -2340,8 +2342,50 @@ impl KeybindingEditorModal {
self.save_or_display_error(cx); self.save_or_display_error(cx);
} }
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) { fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent) cx.emit(DismissEvent);
}
fn get_matching_bindings_count(&self, cx: &Context<Self>) -> 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, &current_keystrokes))
.unwrap_or(false)
})
.count()
}
fn show_matching_bindings(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
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 { impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors(); let theme = cx.theme().colors();
let matching_bindings_count = self.get_matching_bindings_count(cx);
v_flex() v_flex()
.w(rems(34.)) .w(rems(34.))
@ -2427,19 +2472,37 @@ impl Render for KeybindingEditorModal {
), ),
) )
.footer( .footer(
ModalFooter::new().end_slot( ModalFooter::new()
h_flex() .start_slot(
.gap_1() div().when(matching_bindings_count > 0, |this| {
.child( this.child(
Button::new("cancel", "Cancel") Button::new("show_matching", format!(
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), "There {} {} {} with the same keystrokes. Click to view",
) if matching_bindings_count == 1 { "is" } else { "are" },
.child(Button::new("save-btn", "Save").on_click(cx.listener( matching_bindings_count,
|this, _event, _window, cx| { if matching_bindings_count == 1 { "binding" } else { "bindings" }
this.save_or_display_error(cx); ))
}, .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);
},
))),
),
), ),
) )
} }