diff --git a/assets/icons/equal.svg b/assets/icons/equal.svg
new file mode 100644
index 0000000000..9b3a151a12
--- /dev/null
+++ b/assets/icons/equal.svg
@@ -0,0 +1 @@
+
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index b2ec768435..b29a8b78e6 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -107,6 +107,7 @@ pub enum IconName {
Ellipsis,
EllipsisVertical,
Envelope,
+ Equal,
Eraser,
Escape,
Exit,
diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs
index 4526b7fcc8..c83a4c2423 100644
--- a/crates/settings_ui/src/keybindings.rs
+++ b/crates/settings_ui/src/keybindings.rs
@@ -66,6 +66,8 @@ actions!(
ToggleConflictFilter,
/// Toggle Keystroke search
ToggleKeystrokeSearch,
+ /// Toggles exact matching for keystroke search
+ ToggleExactKeystrokeMatching,
]
);
@@ -176,14 +178,16 @@ impl KeymapEventChannel {
enum SearchMode {
#[default]
Normal,
- KeyStroke,
+ KeyStroke {
+ exact_match: bool,
+ },
}
impl SearchMode {
fn invert(&self) -> Self {
match self {
- SearchMode::Normal => SearchMode::KeyStroke,
- SearchMode::KeyStroke => SearchMode::Normal,
+ SearchMode::Normal => SearchMode::KeyStroke { exact_match: false },
+ SearchMode::KeyStroke { .. } => SearchMode::Normal,
}
}
}
@@ -204,7 +208,11 @@ impl FilterState {
}
}
-type ActionMapping = (SharedString, Option);
+#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
+struct ActionMapping {
+ keystroke_text: SharedString,
+ context: Option,
+}
#[derive(Default)]
struct ConflictState {
@@ -257,6 +265,12 @@ impl ConflictState {
})
}
+ fn will_conflict(&self, action_mapping: ActionMapping) -> Option> {
+ self.action_keybind_mapping
+ .get(&action_mapping)
+ .and_then(|indices| indices.is_empty().not().then_some(indices.clone()))
+ }
+
fn has_conflict(&self, candidate_idx: &usize) -> bool {
self.conflicts.contains(candidate_idx)
}
@@ -375,7 +389,7 @@ impl KeymapEditor {
fn current_keystroke_query(&self, cx: &App) -> Vec {
match self.search_mode {
- SearchMode::KeyStroke => self
+ SearchMode::KeyStroke { .. } => self
.keystroke_editor
.read(cx)
.keystrokes()
@@ -432,17 +446,27 @@ impl KeymapEditor {
}
match this.search_mode {
- SearchMode::KeyStroke => {
+ SearchMode::KeyStroke { exact_match } => {
matches.retain(|item| {
this.keybindings[item.candidate_id]
.keystrokes()
.is_some_and(|keystrokes| {
- keystroke_query.iter().all(|key| {
- keystrokes.iter().any(|keystroke| {
- keystroke.key == key.key
- && keystroke.modifiers == key.modifiers
+ if exact_match {
+ keystroke_query.len() == keystrokes.len()
+ && keystroke_query.iter().zip(keystrokes).all(
+ |(query, keystroke)| {
+ query.key == keystroke.key
+ && query.modifiers == keystroke.modifiers
+ },
+ )
+ } else {
+ keystroke_query.iter().all(|key| {
+ keystrokes.iter().any(|keystroke| {
+ keystroke.key == key.key
+ && keystroke.modifiers == key.modifiers
+ })
})
- })
+ }
})
});
}
@@ -699,7 +723,12 @@ impl KeymapEditor {
window: &mut Window,
cx: &mut Context,
) {
+ let weak = cx.weak_entity();
self.context_menu = self.selected_binding().map(|selected_binding| {
+ let key_strokes = selected_binding
+ .keystrokes()
+ .map(Vec::from)
+ .unwrap_or_default();
let selected_binding_has_no_context = selected_binding
.context
.as_ref()
@@ -727,6 +756,22 @@ impl KeymapEditor {
"Copy Context",
Box::new(CopyContext),
)
+ .entry("Show matching keybindings", None, {
+ let weak = weak.clone();
+ let key_strokes = key_strokes.clone();
+
+ move |_, cx| {
+ weak.update(cx, |this, cx| {
+ this.filter_state = FilterState::All;
+ this.search_mode = SearchMode::KeyStroke { exact_match: true };
+
+ this.keystroke_editor.update(cx, |editor, cx| {
+ editor.set_keystrokes(key_strokes.clone(), cx);
+ });
+ })
+ .ok();
+ }
+ })
});
let context_menu_handle = context_menu.focus_handle(cx);
@@ -943,17 +988,32 @@ impl KeymapEditor {
// Update the keystroke editor to turn the `search` bool on
self.keystroke_editor.update(cx, |keystroke_editor, cx| {
- keystroke_editor.set_search_mode(self.search_mode == SearchMode::KeyStroke);
+ keystroke_editor
+ .set_search_mode(matches!(self.search_mode, SearchMode::KeyStroke { .. }));
cx.notify();
});
match self.search_mode {
- SearchMode::KeyStroke => {
+ SearchMode::KeyStroke { .. } => {
window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
}
SearchMode::Normal => {}
}
}
+
+ fn toggle_exact_keystroke_matching(
+ &mut self,
+ _: &ToggleExactKeystrokeMatching,
+ _: &mut Window,
+ cx: &mut Context,
+ ) {
+ let SearchMode::KeyStroke { exact_match } = &mut self.search_mode else {
+ return;
+ };
+
+ *exact_match = !(*exact_match);
+ self.on_query_changed(cx);
+ }
}
#[derive(Clone)]
@@ -970,13 +1030,14 @@ struct ProcessedKeybinding {
impl ProcessedKeybinding {
fn get_action_mapping(&self) -> ActionMapping {
- (
- self.keystroke_text.clone(),
- self.context
+ ActionMapping {
+ keystroke_text: self.keystroke_text.clone(),
+ context: self
+ .context
.as_ref()
.and_then(|context| context.local())
.cloned(),
- )
+ }
}
fn keystrokes(&self) -> Option<&[Keystroke]> {
@@ -1061,6 +1122,7 @@ impl Render for KeymapEditor {
.on_action(cx.listener(Self::copy_context_to_clipboard))
.on_action(cx.listener(Self::toggle_conflict_filter))
.on_action(cx.listener(Self::toggle_keystroke_search))
+ .on_action(cx.listener(Self::toggle_exact_keystroke_matching))
.size_full()
.p_2()
.gap_1()
@@ -1103,7 +1165,10 @@ impl Render for KeymapEditor {
cx,
)
})
- .toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
+ .toggle_state(matches!(
+ self.search_mode,
+ SearchMode::KeyStroke { .. }
+ ))
.on_click(|_, window, cx| {
window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
}),
@@ -1141,19 +1206,43 @@ impl Render for KeymapEditor {
)
}),
)
- .when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
- this.child(
- div()
- .map(|this| {
- if self.keybinding_conflict_state.any_conflicts() {
- this.pr(rems_from_px(54.))
- } else {
- this.pr_7()
- }
- })
- .child(self.keystroke_editor.clone()),
- )
- }),
+ .when_some(
+ match self.search_mode {
+ SearchMode::Normal => None,
+ SearchMode::KeyStroke { exact_match } => Some(exact_match),
+ },
+ |this, exact_match| {
+ this.child(
+ h_flex()
+ .map(|this| {
+ if self.keybinding_conflict_state.any_conflicts() {
+ this.pr(rems_from_px(54.))
+ } else {
+ this.pr_7()
+ }
+ })
+ .child(self.keystroke_editor.clone())
+ .child(
+ div().p_1().child(
+ IconButton::new(
+ "keystrokes-exact-match",
+ IconName::Equal,
+ )
+ .shape(IconButtonShape::Square)
+ .toggle_state(exact_match)
+ .on_click(
+ cx.listener(|_, _, window, cx| {
+ window.dispatch_action(
+ ToggleExactKeystrokeMatching.boxed_clone(),
+ cx,
+ );
+ }),
+ ),
+ ),
+ ),
+ )
+ },
+ ),
)
.child(
Table::new()
@@ -1650,20 +1739,23 @@ impl KeybindingEditorModal {
Ok(input) => input,
};
- let action_mapping: ActionMapping = (
- ui::text_for_keystrokes(&new_keystrokes, cx).into(),
- new_context
- .as_ref()
- .map(Into::into)
- .or_else(|| existing_keybind.get_action_mapping().1),
- );
+ let action_mapping = ActionMapping {
+ keystroke_text: ui::text_for_keystrokes(&new_keystrokes, cx).into(),
+ context: new_context.as_ref().map(Into::into),
+ };
- if let Some(conflicting_indices) = self
- .keymap_editor
- .read(cx)
- .keybinding_conflict_state
- .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
- {
+ let conflicting_indices = if self.creating {
+ self.keymap_editor
+ .read(cx)
+ .keybinding_conflict_state
+ .will_conflict(action_mapping)
+ } else {
+ self.keymap_editor
+ .read(cx)
+ .keybinding_conflict_state
+ .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
+ };
+ if let Some(conflicting_indices) = conflicting_indices {
let first_conflicting_index = conflicting_indices[0];
let conflicting_action_name = self
.keymap_editor
@@ -1739,10 +1831,11 @@ impl KeybindingEditorModal {
.log_err();
} else {
this.update(cx, |this, cx| {
- let action_mapping = (
- ui::text_for_keystrokes(new_keystrokes.as_slice(), cx).into(),
- new_context.map(SharedString::from),
- );
+ let action_mapping = ActionMapping {
+ keystroke_text: ui::text_for_keystrokes(new_keystrokes.as_slice(), cx)
+ .into(),
+ context: new_context.map(SharedString::from),
+ };
this.keymap_editor.update(cx, |keymap, cx| {
keymap.previous_edit = Some(PreviousEdit::Keybinding {
@@ -2221,6 +2314,11 @@ impl KeystrokeInput {
}
}
+ fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) {
+ self.keystrokes = keystrokes;
+ self.keystrokes_changed(cx);
+ }
+
fn dummy(modifiers: Modifiers) -> Keystroke {
return Keystroke {
modifiers,
@@ -2438,14 +2536,11 @@ impl KeystrokeInput {
fn clear_keystrokes(
&mut self,
_: &ClearKeystrokes,
- window: &mut Window,
+ _window: &mut Window,
cx: &mut Context,
) {
- if !self.outer_focus_handle.is_focused(window) {
- return;
- }
self.keystrokes.clear();
- cx.notify();
+ self.keystrokes_changed(cx);
}
}