keymap_ui: Dim keybinds that are overridden by other keybinds (#34952)

Finn Evers created

This change dims rows in the keymap editor for which the corresponding
keybind is overridden by other keybinds coming from higher priority
sources.

Release Notes:

- N/A

Change summary

assets/keymaps/default-linux.json     |   5 
assets/keymaps/default-macos.json     |   5 
crates/settings/src/keymap_file.rs    |  30 
crates/settings_ui/src/keybindings.rs | 776 ++++++++++++++++++----------
4 files changed, 530 insertions(+), 286 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -1137,7 +1137,10 @@
       "alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
       "alt-c": "keymap_editor::ToggleConflictFilter",
       "enter": "keymap_editor::EditBinding",
-      "alt-enter": "keymap_editor::CreateBinding"
+      "alt-enter": "keymap_editor::CreateBinding",
+      "ctrl-c": "keymap_editor::CopyAction",
+      "ctrl-shift-c": "keymap_editor::CopyContext",
+      "ctrl-t": "keymap_editor::ShowMatchingKeybinds"
     }
   },
   {

assets/keymaps/default-macos.json 🔗

@@ -1239,7 +1239,10 @@
       "cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
       "cmd-alt-c": "keymap_editor::ToggleConflictFilter",
       "enter": "keymap_editor::EditBinding",
-      "alt-enter": "keymap_editor::CreateBinding"
+      "alt-enter": "keymap_editor::CreateBinding",
+      "cmd-c": "keymap_editor::CopyAction",
+      "cmd-shift-c": "keymap_editor::CopyContext",
+      "cmd-t": "keymap_editor::ShowMatchingKeybinds"
     }
   },
   {

crates/settings/src/keymap_file.rs 🔗

@@ -959,19 +959,21 @@ impl<'a> KeybindUpdateTarget<'a> {
     }
 }
 
-#[derive(Clone, Copy, PartialEq, Eq)]
+#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
 pub enum KeybindSource {
     User,
-    Default,
-    Base,
     Vim,
+    Base,
+    #[default]
+    Default,
+    Unknown,
 }
 
 impl KeybindSource {
-    const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(0);
-    const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(1);
-    const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(2);
-    const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(3);
+    const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Base as u32);
+    const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Default as u32);
+    const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Vim as u32);
+    const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::User as u32);
 
     pub fn name(&self) -> &'static str {
         match self {
@@ -979,6 +981,7 @@ impl KeybindSource {
             KeybindSource::Default => "Default",
             KeybindSource::Base => "Base",
             KeybindSource::Vim => "Vim",
+            KeybindSource::Unknown => "Unknown",
         }
     }
 
@@ -988,21 +991,18 @@ impl KeybindSource {
             KeybindSource::Default => Self::DEFAULT,
             KeybindSource::Base => Self::BASE,
             KeybindSource::Vim => Self::VIM,
+            KeybindSource::Unknown => KeyBindingMetaIndex(*self as u32),
         }
     }
 
     pub fn from_meta(index: KeyBindingMetaIndex) -> Self {
-        Self::try_from_meta(index).unwrap()
-    }
-
-    pub fn try_from_meta(index: KeyBindingMetaIndex) -> Result<Self> {
-        Ok(match index {
+        match index {
             Self::USER => KeybindSource::User,
             Self::BASE => KeybindSource::Base,
             Self::DEFAULT => KeybindSource::Default,
             Self::VIM => KeybindSource::Vim,
-            _ => anyhow::bail!("Invalid keybind source {:?}", index),
-        })
+            _ => KeybindSource::Unknown,
+        }
     }
 }
 
@@ -1014,7 +1014,7 @@ impl From<KeyBindingMetaIndex> for KeybindSource {
 
 impl From<KeybindSource> for KeyBindingMetaIndex {
     fn from(source: KeybindSource) -> Self {
-        return source.meta();
+        source.meta()
     }
 }
 

crates/settings_ui/src/keybindings.rs 🔗

@@ -1,4 +1,5 @@
 use std::{
+    cmp::{self},
     ops::{Not as _, Range},
     sync::Arc,
     time::Duration,
@@ -20,15 +21,13 @@ use language::{Language, LanguageConfig, ToOffset as _};
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::Project;
 use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets};
-
-use util::ResultExt;
-
 use ui::{
     ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
     Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString,
     Styled as _, Tooltip, Window, prelude::*,
 };
 use ui_input::SingleLineInput;
+use util::ResultExt;
 use workspace::{
     Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
     register_serializable_item,
@@ -68,6 +67,8 @@ actions!(
         ToggleKeystrokeSearch,
         /// Toggles exact matching for keystroke search
         ToggleExactKeystrokeMatching,
+        /// Shows matching keystrokes for the currently selected binding
+        ShowMatchingKeybinds
     ]
 );
 
@@ -192,76 +193,134 @@ struct KeybindConflict {
 }
 
 impl KeybindConflict {
-    fn from_iter<'a>(mut indices: impl Iterator<Item = &'a usize>) -> Option<Self> {
-        indices.next().map(|index| Self {
-            first_conflict_index: *index,
+    fn from_iter<'a>(mut indices: impl Iterator<Item = &'a ConflictOrigin>) -> Option<Self> {
+        indices.next().map(|origin| Self {
+            first_conflict_index: origin.index,
             remaining_conflict_amount: indices.count(),
         })
     }
 }
 
+#[derive(Clone, Copy, PartialEq)]
+struct ConflictOrigin {
+    override_source: KeybindSource,
+    overridden_source: Option<KeybindSource>,
+    index: usize,
+}
+
+impl ConflictOrigin {
+    fn new(source: KeybindSource, index: usize) -> Self {
+        Self {
+            override_source: source,
+            index,
+            overridden_source: None,
+        }
+    }
+
+    fn with_overridden_source(self, source: KeybindSource) -> Self {
+        Self {
+            overridden_source: Some(source),
+            ..self
+        }
+    }
+
+    fn get_conflict_with(&self, other: &Self) -> Option<Self> {
+        if self.override_source == KeybindSource::User
+            && other.override_source == KeybindSource::User
+        {
+            Some(
+                Self::new(KeybindSource::User, other.index)
+                    .with_overridden_source(self.override_source),
+            )
+        } else if self.override_source > other.override_source {
+            Some(other.with_overridden_source(self.override_source))
+        } else {
+            None
+        }
+    }
+
+    fn is_user_keybind_conflict(&self) -> bool {
+        self.override_source == KeybindSource::User
+            && self.overridden_source == Some(KeybindSource::User)
+    }
+}
+
 #[derive(Default)]
 struct ConflictState {
-    conflicts: Vec<usize>,
-    keybind_mapping: HashMap<ActionMapping, Vec<usize>>,
+    conflicts: Vec<Option<ConflictOrigin>>,
+    keybind_mapping: HashMap<ActionMapping, Vec<ConflictOrigin>>,
+    has_user_conflicts: bool,
 }
 
 impl ConflictState {
-    fn new(key_bindings: &[ProcessedKeybinding]) -> Self {
-        let mut action_keybind_mapping: HashMap<_, Vec<usize>> = HashMap::default();
+    fn new(key_bindings: &[ProcessedBinding]) -> Self {
+        let mut action_keybind_mapping: HashMap<_, Vec<ConflictOrigin>> = HashMap::default();
 
-        key_bindings
+        let mut largest_index = 0;
+        for (index, binding) in key_bindings
             .iter()
             .enumerate()
-            .filter(|(_, binding)| {
-                binding.keystrokes().is_some()
-                    && binding
-                        .source
-                        .as_ref()
-                        .is_some_and(|source| matches!(source.0, KeybindSource::User))
-            })
-            .for_each(|(index, binding)| {
-                action_keybind_mapping
-                    .entry(binding.get_action_mapping())
-                    .or_default()
-                    .push(index);
-            });
+            .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information()))
+        {
+            action_keybind_mapping
+                .entry(binding.get_action_mapping())
+                .or_default()
+                .push(ConflictOrigin::new(binding.source, index));
+            largest_index = index;
+        }
+
+        let mut conflicts = vec![None; largest_index + 1];
+        let mut has_user_conflicts = false;
+
+        for indices in action_keybind_mapping.values_mut() {
+            indices.sort_unstable_by_key(|origin| origin.override_source);
+            let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else {
+                continue;
+            };
+
+            for origin in indices.iter() {
+                conflicts[origin.index] =
+                    origin.get_conflict_with(if origin == fst { &snd } else { &fst })
+            }
+
+            has_user_conflicts |= fst.override_source == KeybindSource::User
+                && snd.override_source == KeybindSource::User;
+        }
 
         Self {
-            conflicts: action_keybind_mapping
-                .values()
-                .filter(|indices| indices.len() > 1)
-                .flatten()
-                .copied()
-                .collect(),
+            conflicts,
             keybind_mapping: action_keybind_mapping,
+            has_user_conflicts,
         }
     }
 
     fn conflicting_indices_for_mapping(
         &self,
         action_mapping: &ActionMapping,
-        keybind_idx: usize,
+        keybind_idx: Option<usize>,
     ) -> Option<KeybindConflict> {
         self.keybind_mapping
             .get(action_mapping)
             .and_then(|indices| {
-                KeybindConflict::from_iter(indices.iter().filter(|&idx| *idx != keybind_idx))
+                KeybindConflict::from_iter(
+                    indices
+                        .iter()
+                        .filter(|&conflict| Some(conflict.index) != keybind_idx),
+                )
             })
     }
 
-    fn will_conflict(&self, action_mapping: &ActionMapping) -> Option<KeybindConflict> {
-        self.keybind_mapping
-            .get(action_mapping)
-            .and_then(|indices| KeybindConflict::from_iter(indices.iter()))
+    fn conflict_for_idx(&self, idx: usize) -> Option<ConflictOrigin> {
+        self.conflicts.get(idx).copied().flatten()
     }
 
-    fn has_conflict(&self, candidate_idx: &usize) -> bool {
-        self.conflicts.contains(candidate_idx)
+    fn has_user_conflict(&self, candidate_idx: usize) -> bool {
+        self.conflict_for_idx(candidate_idx)
+            .is_some_and(|conflict| conflict.is_user_keybind_conflict())
     }
 
-    fn any_conflicts(&self) -> bool {
-        !self.conflicts.is_empty()
+    fn any_user_binding_conflicts(&self) -> bool {
+        self.has_user_conflicts
     }
 }
 
@@ -269,7 +328,7 @@ struct KeymapEditor {
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
     _keymap_subscription: Subscription,
-    keybindings: Vec<ProcessedKeybinding>,
+    keybindings: Vec<ProcessedBinding>,
     keybinding_conflict_state: ConflictState,
     filter_state: FilterState,
     search_mode: SearchMode,
@@ -426,24 +485,6 @@ impl KeymapEditor {
         }
     }
 
-    fn filter_on_selected_binding_keystrokes(&mut self, cx: &mut Context<Self>) {
-        let Some(selected_binding) = self.selected_binding() else {
-            return;
-        };
-
-        let keystrokes = selected_binding
-            .keystrokes()
-            .map(Vec::from)
-            .unwrap_or_default();
-
-        self.filter_state = FilterState::All;
-        self.search_mode = SearchMode::KeyStroke { exact_match: true };
-
-        self.keystroke_editor.update(cx, |editor, cx| {
-            editor.set_keystrokes(keystrokes, cx);
-        });
-    }
-
     fn on_query_changed(&mut self, cx: &mut Context<Self>) {
         let action_query = self.current_action_query(cx);
         let keystroke_query = self.current_keystroke_query(cx);
@@ -505,7 +546,7 @@ impl KeymapEditor {
                 FilterState::Conflicts => {
                     matches.retain(|candidate| {
                         this.keybinding_conflict_state
-                            .has_conflict(&candidate.candidate_id)
+                            .has_user_conflict(candidate.candidate_id)
                     });
                 }
                 FilterState::All => {}
@@ -551,20 +592,11 @@ impl KeymapEditor {
             }
 
             if action_query.is_empty() {
-                // apply default sort
-                // sorts by source precedence, and alphabetically by action name within each source
-                matches.sort_by_key(|match_item| {
-                    let keybind = &this.keybindings[match_item.candidate_id];
-                    let source = keybind.source.as_ref().map(|s| s.0);
-                    use KeybindSource::*;
-                    let source_precedence = match source {
-                        Some(User) => 0,
-                        Some(Vim) => 1,
-                        Some(Base) => 2,
-                        Some(Default) => 3,
-                        None => 4,
-                    };
-                    return (source_precedence, keybind.action_name);
+                matches.sort_by(|item1, item2| {
+                    let binding1 = &this.keybindings[item1.candidate_id];
+                    let binding2 = &this.keybindings[item2.candidate_id];
+
+                    binding1.cmp(binding2)
                 });
             }
             this.selected_index.take();
@@ -574,11 +606,11 @@ impl KeymapEditor {
         })
     }
 
-    fn has_conflict(&self, row_index: usize) -> bool {
-        self.matches
-            .get(row_index)
-            .map(|candidate| candidate.candidate_id)
-            .is_some_and(|id| self.keybinding_conflict_state.has_conflict(&id))
+    fn get_conflict(&self, row_index: usize) -> Option<ConflictOrigin> {
+        self.matches.get(row_index).and_then(|candidate| {
+            self.keybinding_conflict_state
+                .conflict_for_idx(candidate.candidate_id)
+        })
     }
 
     fn process_bindings(
@@ -586,7 +618,7 @@ impl KeymapEditor {
         zed_keybind_context_language: Arc<Language>,
         humanized_action_names: &HumanizedActionNameCache,
         cx: &mut App,
-    ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
+    ) -> (Vec<ProcessedBinding>, Vec<StringMatchCandidate>) {
         let key_bindings_ptr = cx.key_bindings();
         let lock = key_bindings_ptr.borrow();
         let key_bindings = lock.bindings();
@@ -606,14 +638,12 @@ impl KeymapEditor {
         for key_binding in key_bindings {
             let source = key_binding
                 .meta()
-                .map(settings::KeybindSource::try_from_meta)
-                .and_then(|source| source.log_err());
+                .map(KeybindSource::from_meta)
+                .unwrap_or(KeybindSource::Unknown);
 
             let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
-            let ui_key_binding = Some(
-                ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
-                    .vim_mode(source == Some(settings::KeybindSource::Vim)),
-            );
+            let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
+                .vim_mode(source == KeybindSource::Vim);
 
             let context = key_binding
                 .predicate()
@@ -625,48 +655,46 @@ impl KeymapEditor {
                 })
                 .unwrap_or(KeybindContextString::Global);
 
-            let source = source.map(|source| (source, source.name().into()));
-
             let action_name = key_binding.action().name();
             unmapped_action_names.remove(&action_name);
+
             let action_arguments = key_binding
                 .action_input()
                 .map(|arguments| SyntaxHighlightedText::new(arguments, json_language.clone()));
-            let action_docs = action_documentation.get(action_name).copied();
+            let action_information = ActionInformation::new(
+                action_name,
+                action_arguments,
+                &actions_with_schemas,
+                &action_documentation,
+                &humanized_action_names,
+            );
 
             let index = processed_bindings.len();
-            let humanized_action_name = humanized_action_names.get(action_name);
-            let string_match_candidate = StringMatchCandidate::new(index, &humanized_action_name);
-            processed_bindings.push(ProcessedKeybinding {
-                keystroke_text: keystroke_text.into(),
+            let string_match_candidate =
+                StringMatchCandidate::new(index, &action_information.humanized_name);
+            processed_bindings.push(ProcessedBinding::new_mapped(
+                keystroke_text,
                 ui_key_binding,
-                action_name,
-                action_arguments,
-                humanized_action_name,
-                action_docs,
-                has_schema: actions_with_schemas.contains(action_name),
-                context: Some(context),
+                context,
                 source,
-            });
+                action_information,
+            ));
             string_match_candidates.push(string_match_candidate);
         }
 
-        let empty = SharedString::new_static("");
         for action_name in unmapped_action_names.into_iter() {
             let index = processed_bindings.len();
-            let humanized_action_name = humanized_action_names.get(action_name);
-            let string_match_candidate = StringMatchCandidate::new(index, &humanized_action_name);
-            processed_bindings.push(ProcessedKeybinding {
-                keystroke_text: empty.clone(),
-                ui_key_binding: None,
+            let action_information = ActionInformation::new(
                 action_name,
-                action_arguments: None,
-                humanized_action_name,
-                action_docs: action_documentation.get(action_name).copied(),
-                has_schema: actions_with_schemas.contains(action_name),
-                context: None,
-                source: None,
-            });
+                None,
+                &actions_with_schemas,
+                &action_documentation,
+                &humanized_action_names,
+            );
+            let string_match_candidate =
+                StringMatchCandidate::new(index, &action_information.humanized_name);
+
+            processed_bindings.push(ProcessedBinding::Unmapped(action_information));
             string_match_candidates.push(string_match_candidate);
         }
 
@@ -728,8 +756,9 @@ impl KeymapEditor {
                             let scroll_position =
                                 this.matches.iter().enumerate().find_map(|(index, item)| {
                                     let binding = &this.keybindings[item.candidate_id];
-                                    if binding.get_action_mapping() == action_mapping
-                                        && binding.action_name == action_name
+                                    if binding.get_action_mapping().is_some_and(|binding_mapping| {
+                                        binding_mapping == action_mapping
+                                    }) && binding.action().name == action_name
                                     {
                                         Some(index)
                                     } else {
@@ -799,12 +828,12 @@ impl KeymapEditor {
             .map(|r#match| r#match.candidate_id)
     }
 
-    fn selected_keybind_and_index(&self) -> Option<(&ProcessedKeybinding, usize)> {
+    fn selected_keybind_and_index(&self) -> Option<(&ProcessedBinding, usize)> {
         self.selected_keybind_index()
             .map(|keybind_index| (&self.keybindings[keybind_index], keybind_index))
     }
 
-    fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
+    fn selected_binding(&self) -> Option<&ProcessedBinding> {
         self.selected_keybind_index()
             .and_then(|keybind_index| self.keybindings.get(keybind_index))
     }
@@ -832,15 +861,13 @@ impl KeymapEditor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let weak = cx.weak_entity();
         self.context_menu = self.selected_binding().map(|selected_binding| {
             let selected_binding_has_no_context = selected_binding
-                .context
-                .as_ref()
+                .context()
                 .and_then(KeybindContextString::local)
                 .is_none();
 
-            let selected_binding_is_unbound = selected_binding.keystrokes().is_none();
+            let selected_binding_is_unbound = selected_binding.is_unbound();
 
             let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
                 menu.context(self.focus_handle.clone())
@@ -863,14 +890,11 @@ impl KeymapEditor {
                         Box::new(CopyContext),
                     )
                     .separator()
-                    .entry("Show Matching Keybindings", None, {
-                        move |_, cx| {
-                            weak.update(cx, |this, cx| {
-                                this.filter_on_selected_binding_keystrokes(cx);
-                            })
-                            .ok();
-                        }
-                    })
+                    .action_disabled_when(
+                        selected_binding_has_no_context,
+                        "Show Matching Keybindings",
+                        Box::new(ShowMatchingKeybinds),
+                    )
             });
 
             let context_menu_handle = context_menu.focus_handle(cx);
@@ -898,10 +922,98 @@ impl KeymapEditor {
         self.context_menu.is_some()
     }
 
+    fn create_row_button(
+        &self,
+        index: usize,
+        conflict: Option<ConflictOrigin>,
+        cx: &mut Context<Self>,
+    ) -> IconButton {
+        if self.filter_state != FilterState::Conflicts
+            && let Some(conflict) = conflict
+        {
+            if conflict.is_user_keybind_conflict() {
+                base_button_style(index, IconName::Warning)
+                    .icon_color(Color::Warning)
+                    .tooltip(|window, cx| {
+                        Tooltip::with_meta(
+                            "View conflicts",
+                            Some(&ToggleConflictFilter),
+                            "Use alt+click to show all conflicts",
+                            window,
+                            cx,
+                        )
+                    })
+                    .on_click(cx.listener(move |this, click: &ClickEvent, window, cx| {
+                        if click.modifiers().alt {
+                            this.set_filter_state(FilterState::Conflicts, cx);
+                        } else {
+                            this.select_index(index, None, window, cx);
+                            this.open_edit_keybinding_modal(false, window, cx);
+                            cx.stop_propagation();
+                        }
+                    }))
+            } else if self.search_mode.exact_match() {
+                base_button_style(index, IconName::Info)
+                    .tooltip(|window, cx| {
+                        Tooltip::with_meta(
+                            "Edit this binding",
+                            Some(&ShowMatchingKeybinds),
+                            "This binding is overridden by other bindings.",
+                            window,
+                            cx,
+                        )
+                    })
+                    .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
+                        this.select_index(index, None, window, cx);
+                        this.open_edit_keybinding_modal(false, window, cx);
+                        cx.stop_propagation();
+                    }))
+            } else {
+                base_button_style(index, IconName::Info)
+                    .tooltip(|window, cx| {
+                        Tooltip::with_meta(
+                            "Show matching keybinds",
+                            Some(&ShowMatchingKeybinds),
+                            "This binding is overridden by other bindings.\nUse alt+click to edit this binding",
+                            window,
+                            cx,
+                        )
+                    })
+                    .on_click(cx.listener(move |this, click: &ClickEvent, window, cx| {
+                        if click.modifiers().alt {
+                            this.select_index(index, None, window, cx);
+                            this.open_edit_keybinding_modal(false, window, cx);
+                            cx.stop_propagation();
+                        } else {
+                            this.show_matching_keystrokes(&Default::default(), window, cx);
+                        }
+                    }))
+            }
+        } else {
+            base_button_style(index, IconName::Pencil)
+                .visible_on_hover(if self.selected_index == Some(index) {
+                    "".into()
+                } else if self.show_hover_menus {
+                    row_group_id(index)
+                } else {
+                    "never-show".into()
+                })
+                .when(
+                    self.show_hover_menus && !self.context_menu_deployed(),
+                    |this| this.tooltip(Tooltip::for_action_title("Edit Keybinding", &EditBinding)),
+                )
+                .on_click(cx.listener(move |this, _, window, cx| {
+                    this.select_index(index, None, window, cx);
+                    this.open_edit_keybinding_modal(false, window, cx);
+                    cx.stop_propagation();
+                }))
+        }
+    }
+
     fn render_no_matches_hint(&self, _window: &mut Window, _cx: &App) -> AnyElement {
         let hint = match (self.filter_state, &self.search_mode) {
             (FilterState::Conflicts, _) => {
-                if self.keybinding_conflict_state.any_conflicts() {
+                if self.keybinding_conflict_state.any_user_binding_conflicts() {
                     "No conflicting keybinds found that match the provided query"
                 } else {
                     "No conflicting keybinds found"
@@ -982,20 +1094,22 @@ impl KeymapEditor {
         let keybind = keybind.clone();
         let keymap_editor = cx.entity();
 
+        let keystroke = keybind.keystroke_text().cloned().unwrap_or_default();
         let arguments = keybind
-            .action_arguments
+            .action()
+            .arguments
             .as_ref()
             .map(|arguments| arguments.text.clone());
         let context = keybind
-            .context
-            .as_ref()
+            .context()
             .map(|context| context.local_str().unwrap_or("global"));
-        let source = keybind.source.as_ref().map(|source| source.1.clone());
+        let action = keybind.action().name;
+        let source = keybind.keybind_source().map(|source| source.name());
 
         telemetry::event!(
             "Edit Keybinding Modal Opened",
-            keystroke = keybind.keystroke_text,
-            action = keybind.action_name,
+            keystroke = keystroke,
+            action = action,
             source = source,
             context = context,
             arguments = arguments,
@@ -1063,7 +1177,7 @@ impl KeymapEditor {
     ) {
         let context = self
             .selected_binding()
-            .and_then(|binding| binding.context.as_ref())
+            .and_then(|binding| binding.context())
             .and_then(KeybindContextString::local_str)
             .map(|context| context.to_string());
         let Some(context) = context else {
@@ -1082,7 +1196,7 @@ impl KeymapEditor {
     ) {
         let action = self
             .selected_binding()
-            .map(|binding| binding.action_name.to_string());
+            .map(|binding| binding.action().name.to_string());
         let Some(action) = action else {
             return;
         };
@@ -1142,6 +1256,29 @@ impl KeymapEditor {
         *exact_match = !(*exact_match);
         self.on_query_changed(cx);
     }
+
+    fn show_matching_keystrokes(
+        &mut self,
+        _: &ShowMatchingKeybinds,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(selected_binding) = self.selected_binding() else {
+            return;
+        };
+
+        let keystrokes = selected_binding
+            .keystrokes()
+            .map(Vec::from)
+            .unwrap_or_default();
+
+        self.filter_state = FilterState::All;
+        self.search_mode = SearchMode::KeyStroke { exact_match: true };
+
+        self.keystroke_editor.update(cx, |editor, cx| {
+            editor.set_keystrokes(keystrokes, cx);
+        });
+    }
 }
 
 struct HumanizedActionNameCache {
@@ -1168,35 +1305,134 @@ impl HumanizedActionNameCache {
 }
 
 #[derive(Clone)]
-struct ProcessedKeybinding {
+struct KeybindInformation {
     keystroke_text: SharedString,
-    ui_key_binding: Option<ui::KeyBinding>,
-    action_name: &'static str,
-    humanized_action_name: SharedString,
-    action_arguments: Option<SyntaxHighlightedText>,
-    action_docs: Option<&'static str>,
-    has_schema: bool,
-    context: Option<KeybindContextString>,
-    source: Option<(KeybindSource, SharedString)>,
+    ui_binding: ui::KeyBinding,
+    context: KeybindContextString,
+    source: KeybindSource,
 }
 
-impl ProcessedKeybinding {
+impl KeybindInformation {
     fn get_action_mapping(&self) -> ActionMapping {
         ActionMapping {
-            keystrokes: self.keystrokes().map(Vec::from).unwrap_or_default(),
-            context: self
-                .context
-                .as_ref()
-                .and_then(|context| context.local())
-                .cloned(),
+            keystrokes: self.ui_binding.keystrokes.clone(),
+            context: self.context.local().cloned(),
         }
     }
+}
+
+#[derive(Clone)]
+struct ActionInformation {
+    name: &'static str,
+    humanized_name: SharedString,
+    arguments: Option<SyntaxHighlightedText>,
+    documentation: Option<&'static str>,
+    has_schema: bool,
+}
+
+impl ActionInformation {
+    fn new(
+        action_name: &'static str,
+        action_arguments: Option<SyntaxHighlightedText>,
+        actions_with_schemas: &HashSet<&'static str>,
+        action_documentation: &HashMap<&'static str, &'static str>,
+        action_name_cache: &HumanizedActionNameCache,
+    ) -> Self {
+        Self {
+            humanized_name: action_name_cache.get(action_name),
+            has_schema: actions_with_schemas.contains(action_name),
+            arguments: action_arguments,
+            documentation: action_documentation.get(action_name).copied(),
+            name: action_name,
+        }
+    }
+}
+
+#[derive(Clone)]
+enum ProcessedBinding {
+    Mapped(KeybindInformation, ActionInformation),
+    Unmapped(ActionInformation),
+}
+
+impl ProcessedBinding {
+    fn new_mapped(
+        keystroke_text: impl Into<SharedString>,
+        ui_key_binding: ui::KeyBinding,
+        context: KeybindContextString,
+        source: KeybindSource,
+        action_information: ActionInformation,
+    ) -> Self {
+        Self::Mapped(
+            KeybindInformation {
+                keystroke_text: keystroke_text.into(),
+                ui_binding: ui_key_binding,
+                context,
+                source,
+            },
+            action_information,
+        )
+    }
+
+    fn is_unbound(&self) -> bool {
+        matches!(self, Self::Unmapped(_))
+    }
+
+    fn get_action_mapping(&self) -> Option<ActionMapping> {
+        self.keybind_information()
+            .map(|keybind| keybind.get_action_mapping())
+    }
 
     fn keystrokes(&self) -> Option<&[Keystroke]> {
-        self.ui_key_binding
-            .as_ref()
+        self.ui_key_binding()
             .map(|binding| binding.keystrokes.as_slice())
     }
+
+    fn keybind_information(&self) -> Option<&KeybindInformation> {
+        match self {
+            Self::Mapped(keybind_information, _) => Some(keybind_information),
+            Self::Unmapped(_) => None,
+        }
+    }
+
+    fn keybind_source(&self) -> Option<KeybindSource> {
+        self.keybind_information().map(|keybind| keybind.source)
+    }
+
+    fn context(&self) -> Option<&KeybindContextString> {
+        self.keybind_information().map(|keybind| &keybind.context)
+    }
+
+    fn ui_key_binding(&self) -> Option<&ui::KeyBinding> {
+        self.keybind_information()
+            .map(|keybind| &keybind.ui_binding)
+    }
+
+    fn keystroke_text(&self) -> Option<&SharedString> {
+        self.keybind_information()
+            .map(|binding| &binding.keystroke_text)
+    }
+
+    fn action(&self) -> &ActionInformation {
+        match self {
+            Self::Mapped(_, action) | Self::Unmapped(action) => action,
+        }
+    }
+
+    fn cmp(&self, other: &Self) -> cmp::Ordering {
+        match (self, other) {
+            (Self::Mapped(keybind1, action1), Self::Mapped(keybind2, action2)) => {
+                match keybind1.source.cmp(&keybind2.source) {
+                    cmp::Ordering::Equal => action1.humanized_name.cmp(&action2.humanized_name),
+                    ordering => ordering,
+                }
+            }
+            (Self::Mapped(_, _), Self::Unmapped(_)) => cmp::Ordering::Less,
+            (Self::Unmapped(_), Self::Mapped(_, _)) => cmp::Ordering::Greater,
+            (Self::Unmapped(action1), Self::Unmapped(action2)) => {
+                action1.humanized_name.cmp(&action2.humanized_name)
+            }
+        }
+    }
 }
 
 #[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
@@ -1275,6 +1511,7 @@ impl Render for KeymapEditor {
             .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))
+            .on_action(cx.listener(Self::show_matching_keystrokes))
             .on_mouse_move(cx.listener(|this, _, _window, _cx| {
                 this.show_hover_menus = true;
             }))
@@ -1335,9 +1572,12 @@ impl Render for KeymapEditor {
                             .child(
                                 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
                                     .shape(ui::IconButtonShape::Square)
-                                    .when(self.keybinding_conflict_state.any_conflicts(), |this| {
-                                        this.indicator(Indicator::dot().color(Color::Warning))
-                                    })
+                                    .when(
+                                        self.keybinding_conflict_state.any_user_binding_conflicts(),
+                                        |this| {
+                                            this.indicator(Indicator::dot().color(Color::Warning))
+                                        },
+                                    )
                                     .tooltip({
                                         let filter_state = self.filter_state;
                                         let focus_handle = focus_handle.clone();
@@ -1377,7 +1617,10 @@ impl Render for KeymapEditor {
                             this.child(
                                 h_flex()
                                     .map(|this| {
-                                        if self.keybinding_conflict_state.any_conflicts() {
+                                        if self
+                                            .keybinding_conflict_state
+                                            .any_user_binding_conflicts()
+                                        {
                                             this.pr(rems_from_px(54.))
                                         } else {
                                             this.pr_7()
@@ -1457,73 +1700,21 @@ impl Render for KeymapEditor {
                                 .filter_map(|index| {
                                     let candidate_id = this.matches.get(index)?.candidate_id;
                                     let binding = &this.keybindings[candidate_id];
-                                    let action_name = binding.action_name;
+                                    let action_name = binding.action().name;
+                                    let conflict = this.get_conflict(index);
+                                    let is_overridden = conflict.is_some_and(|conflict| {
+                                        !conflict.is_user_keybind_conflict()
+                                    });
 
-                                    let icon = if this.filter_state != FilterState::Conflicts
-                                        && this.has_conflict(index)
-                                    {
-                                        base_button_style(index, IconName::Warning)
-                                            .icon_color(Color::Warning)
-                                            .tooltip(|window, cx| {
-                                                Tooltip::with_meta(
-                                                    "View conflicts",
-                                                    Some(&ToggleConflictFilter),
-                                                    "Use alt+click to show all conflicts",
-                                                    window,
-                                                    cx,
-                                                )
-                                            })
-                                            .on_click(cx.listener(
-                                                move |this, click: &ClickEvent, window, cx| {
-                                                    if click.modifiers().alt {
-                                                        this.set_filter_state(
-                                                            FilterState::Conflicts,
-                                                            cx,
-                                                        );
-                                                    } else {
-                                                        this.select_index(index, None, window, cx);
-                                                        this.open_edit_keybinding_modal(
-                                                            false, window, cx,
-                                                        );
-                                                        cx.stop_propagation();
-                                                    }
-                                                },
-                                            ))
-                                            .into_any_element()
-                                    } else {
-                                        base_button_style(index, IconName::Pencil)
-                                            .visible_on_hover(
-                                                if this.selected_index == Some(index) {
-                                                    "".into()
-                                                } else if this.show_hover_menus {
-                                                    row_group_id(index)
-                                                } else {
-                                                    "never-show".into()
-                                                },
-                                            )
-                                            .when(
-                                                this.show_hover_menus && !context_menu_deployed,
-                                                |this| {
-                                                    this.tooltip(Tooltip::for_action_title(
-                                                        "Edit Keybinding",
-                                                        &EditBinding,
-                                                    ))
-                                                },
-                                            )
-                                            .on_click(cx.listener(move |this, _, window, cx| {
-                                                this.select_index(index, None, window, cx);
-                                                this.open_edit_keybinding_modal(false, window, cx);
-                                                cx.stop_propagation();
-                                            }))
-                                            .into_any_element()
-                                    };
+                                    let icon = this.create_row_button(index, conflict, cx);
 
                                     let action = div()
                                         .id(("keymap action", index))
                                         .child({
                                             if action_name != gpui::NoAction.name() {
                                                 binding
-                                                    .humanized_action_name
+                                                    .action()
+                                                    .humanized_name
                                                     .clone()
                                                     .into_any_element()
                                             } else {
@@ -1534,11 +1725,14 @@ impl Render for KeymapEditor {
                                             }
                                         })
                                         .when(
-                                            !context_menu_deployed && this.show_hover_menus,
+                                            !context_menu_deployed
+                                                && this.show_hover_menus
+                                                && !is_overridden,
                                             |this| {
                                                 this.tooltip({
-                                                    let action_name = binding.action_name;
-                                                    let action_docs = binding.action_docs;
+                                                    let action_name = binding.action().name;
+                                                    let action_docs =
+                                                        binding.action().documentation;
                                                     move |_, cx| {
                                                         let action_tooltip =
                                                             Tooltip::new(action_name);
@@ -1552,14 +1746,19 @@ impl Render for KeymapEditor {
                                             },
                                         )
                                         .into_any_element();
-                                    let keystrokes = binding.ui_key_binding.clone().map_or(
-                                        binding.keystroke_text.clone().into_any_element(),
+                                    let keystrokes = binding.ui_key_binding().cloned().map_or(
+                                        binding
+                                            .keystroke_text()
+                                            .cloned()
+                                            .unwrap_or_default()
+                                            .into_any_element(),
                                         IntoElement::into_any_element,
                                     );
-                                    let action_arguments = match binding.action_arguments.clone() {
+                                    let action_arguments = match binding.action().arguments.clone()
+                                    {
                                         Some(arguments) => arguments.into_any_element(),
                                         None => {
-                                            if binding.has_schema {
+                                            if binding.action().has_schema {
                                                 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
                                                     .into_any_element()
                                             } else {
@@ -1567,7 +1766,7 @@ impl Render for KeymapEditor {
                                             }
                                         }
                                     };
-                                    let context = binding.context.clone().map_or(
+                                    let context = binding.context().cloned().map_or(
                                         gpui::Empty.into_any_element(),
                                         |context| {
                                             let is_local = context.local().is_some();
@@ -1578,6 +1777,7 @@ impl Render for KeymapEditor {
                                                 .when(
                                                     is_local
                                                         && !context_menu_deployed
+                                                        && !is_overridden
                                                         && this.show_hover_menus,
                                                     |this| {
                                                         this.tooltip(Tooltip::element({
@@ -1591,13 +1791,12 @@ impl Render for KeymapEditor {
                                         },
                                     );
                                     let source = binding
-                                        .source
-                                        .clone()
-                                        .map(|(_source, name)| name)
+                                        .keybind_source()
+                                        .map(|source| source.name())
                                         .unwrap_or_default()
                                         .into_any_element();
                                     Some([
-                                        icon,
+                                        icon.into_any_element(),
                                         action,
                                         action_arguments,
                                         keystrokes,
@@ -1610,51 +1809,90 @@ impl Render for KeymapEditor {
                     )
                     .map_row(cx.processor(
                         |this, (row_index, row): (usize, Stateful<Div>), _window, cx| {
-                            let is_conflict = this.has_conflict(row_index);
+                        let conflict = this.get_conflict(row_index);
                             let is_selected = this.selected_index == Some(row_index);
 
                             let row_id = row_group_id(row_index);
 
-                            let row = row
-                                .on_any_mouse_down(cx.listener(
-                                    move |this,
-                                          mouse_down_event: &gpui::MouseDownEvent,
-                                          window,
-                                          cx| {
-                                        match mouse_down_event.button {
-                                            MouseButton::Right => {
+                            div()
+                                .id(("keymap-row-wrapper", row_index))
+                                .child(
+                                    row.id(row_id.clone())
+                                        .on_any_mouse_down(cx.listener(
+                                            move |this,
+                                                  mouse_down_event: &gpui::MouseDownEvent,
+                                                  window,
+                                                  cx| {
+                                                match mouse_down_event.button {
+                                                    MouseButton::Right => {
+                                                        this.select_index(
+                                                            row_index, None, window, cx,
+                                                        );
+                                                        this.create_context_menu(
+                                                            mouse_down_event.position,
+                                                            window,
+                                                            cx,
+                                                        );
+                                                    }
+                                                    _ => {}
+                                                }
+                                            },
+                                        ))
+                                        .on_click(cx.listener(
+                                            move |this, event: &ClickEvent, window, cx| {
                                                 this.select_index(row_index, None, window, cx);
-                                                this.create_context_menu(
-                                                    mouse_down_event.position,
-                                                    window,
-                                                    cx,
-                                                );
-                                            }
-                                            _ => {}
-                                        }
-                                    },
-                                ))
-                                .on_click(cx.listener(
-                                    move |this, event: &ClickEvent, window, cx| {
-                                        this.select_index(row_index, None, window, cx);
-                                        if event.up.click_count == 2 {
-                                            this.open_edit_keybinding_modal(false, window, cx);
-                                        }
-                                    },
-                                ))
-                                .group(row_id)
+                                                if event.up.click_count == 2 {
+                                                    this.open_edit_keybinding_modal(
+                                                        false, window, cx,
+                                                    );
+                                                }
+                                            },
+                                        ))
+                                        .group(row_id)
+                                        .when(
+                                            conflict.is_some_and(|conflict| {
+                                                !conflict.is_user_keybind_conflict()
+                                            }),
+                                            |row| {
+                                                const OVERRIDDEN_OPACITY: f32 = 0.5;
+                                                row.opacity(OVERRIDDEN_OPACITY)
+                                            },
+                                        )
+                                        .when_some(
+                                            conflict.filter(|conflict| {
+                                                !this.context_menu_deployed() &&
+                                                !conflict.is_user_keybind_conflict()
+                                            }),
+                                            |row, conflict| {
+                                                let overriding_binding = this.keybindings.get(conflict.index);
+                                                let context = overriding_binding.and_then(|binding| {
+                                                    match conflict.override_source {
+                                                        KeybindSource::User  => Some("your keymap"),
+                                                        KeybindSource::Vim => Some("the vim keymap"),
+                                                        KeybindSource::Base => Some("your base keymap"),
+                                                        _ => {
+                                                            log::error!("Unexpected override from the {} keymap", conflict.override_source.name());
+                                                            None
+                                                        }
+                                                    }.map(|source| format!("This keybinding is overridden by the '{}' binding from {}.", binding.action().humanized_name, source))
+                                                }).unwrap_or_else(|| "This binding is overridden.".to_string());
+
+                                                row.tooltip(Tooltip::text(context))},
+                                        ),
+                                )
                                 .border_2()
-                                .when(is_conflict, |row| {
-                                    row.bg(cx.theme().status().error_background)
-                                })
+                                .when(
+                                    conflict.is_some_and(|conflict| {
+                                        conflict.is_user_keybind_conflict()
+                                    }),
+                                    |row| row.bg(cx.theme().status().error_background),
+                                )
                                 .when(is_selected, |row| {
                                     row.border_color(cx.theme().colors().panel_focused_border)
-                                        .border_2()
-                                });
-
-                            row.into_any_element()
-                        },
-                    )),
+                                })
+                                .into_any_element()
+                        }),
+                    ),
             )
             .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
                 // This ensures that the menu is not dismissed in cases where scroll events