@@ -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