diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b097be90fd..31adef8cd5 100644 --- a/assets/keymaps/default-linux.json +++ b/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" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e33786f1b2..f942c6f8ae 100644 --- a/assets/keymaps/default-macos.json +++ b/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" } }, { diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 67e8f7e7b2..7802671fec 100644 --- a/crates/settings/src/keymap_file.rs +++ b/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 { - 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 for KeybindSource { impl From for KeyBindingMetaIndex { fn from(source: KeybindSource) -> Self { - return source.meta(); + source.meta() } } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 8fdacf7ae8..a0cbdb9680 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/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) -> Option { - indices.next().map(|index| Self { - first_conflict_index: *index, + fn from_iter<'a>(mut indices: impl Iterator) -> Option { + 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, + 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 { + 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, - keybind_mapping: HashMap>, + conflicts: Vec>, + keybind_mapping: HashMap>, + has_user_conflicts: bool, } impl ConflictState { - fn new(key_bindings: &[ProcessedKeybinding]) -> Self { - let mut action_keybind_mapping: HashMap<_, Vec> = HashMap::default(); + fn new(key_bindings: &[ProcessedBinding]) -> Self { + let mut action_keybind_mapping: HashMap<_, Vec> = 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, ) -> Option { 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 { - self.keybind_mapping - .get(action_mapping) - .and_then(|indices| KeybindConflict::from_iter(indices.iter())) + fn conflict_for_idx(&self, idx: usize) -> Option { + 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, focus_handle: FocusHandle, _keymap_subscription: Subscription, - keybindings: Vec, + keybindings: Vec, 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) { - 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) { 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 { + 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, humanized_action_names: &HumanizedActionNameCache, cx: &mut App, - ) -> (Vec, Vec) { + ) -> (Vec, Vec) { 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 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(), - ui_key_binding, + let action_information = ActionInformation::new( action_name, action_arguments, - humanized_action_name, - action_docs, - has_schema: actions_with_schemas.contains(action_name), - context: Some(context), + &actions_with_schemas, + &action_documentation, + &humanized_action_names, + ); + + let index = processed_bindings.len(); + let string_match_candidate = + StringMatchCandidate::new(index, &action_information.humanized_name); + processed_bindings.push(ProcessedBinding::new_mapped( + keystroke_text, + ui_key_binding, + 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, ) { - 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, + cx: &mut Context, + ) -> 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, + ) { + 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, - action_name: &'static str, - humanized_action_name: SharedString, - action_arguments: Option, - action_docs: Option<&'static str>, - has_schema: bool, - context: Option, - 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, + documentation: Option<&'static str>, + has_schema: bool, +} + +impl ActionInformation { + fn new( + action_name: &'static str, + action_arguments: Option, + 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, + 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 { + 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 { + 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
), _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 @@ -1762,7 +2000,7 @@ impl InputError { struct KeybindingEditorModal { creating: bool, - editing_keybind: ProcessedKeybinding, + editing_keybind: ProcessedBinding, editing_keybind_idx: usize, keybind_editor: Entity, context_editor: Entity, @@ -1787,7 +2025,7 @@ impl Focusable for KeybindingEditorModal { impl KeybindingEditorModal { pub fn new( create: bool, - editing_keybind: ProcessedKeybinding, + editing_keybind: ProcessedBinding, editing_keybind_idx: usize, keymap_editor: Entity, action_args_temp_dir: Option<&std::path::Path>, @@ -1805,8 +2043,7 @@ impl KeybindingEditorModal { .label_size(LabelSize::Default); if let Some(context) = editing_keybind - .context - .as_ref() + .context() .and_then(KeybindContextString::local) { input.editor().update(cx, |editor, cx| { @@ -1840,14 +2077,15 @@ impl KeybindingEditorModal { input }); - let action_arguments_editor = editing_keybind.has_schema.then(|| { + let action_arguments_editor = editing_keybind.action().has_schema.then(|| { let arguments = editing_keybind - .action_arguments + .action() + .arguments .as_ref() .map(|args| args.text.clone()); cx.new(|cx| { ActionArgumentsEditor::new( - editing_keybind.action_name, + editing_keybind.action().name, arguments, action_args_temp_dir, workspace.clone(), @@ -1905,7 +2143,7 @@ impl KeybindingEditorModal { }) .transpose()?; - cx.build_action(&self.editing_keybind.action_name, value) + cx.build_action(&self.editing_keybind.action().name, value) .context("Failed to validate action arguments")?; Ok(action_arguments) } @@ -1956,17 +2194,14 @@ impl KeybindingEditorModal { context: new_context.map(SharedString::from), }; - 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) - }; + let conflicting_indices = self + .keymap_editor + .read(cx) + .keybinding_conflict_state + .conflicting_indices_for_mapping( + &action_mapping, + self.creating.not().then_some(self.editing_keybind_idx), + ); conflicting_indices.map(|KeybindConflict { first_conflict_index, @@ -1978,7 +2213,7 @@ impl KeybindingEditorModal { .read(cx) .keybindings .get(first_conflict_index) - .map(|keybind| keybind.action_name); + .map(|keybind| keybind.action().name); let warning_message = match conflicting_action_name { Some(name) => { @@ -2013,7 +2248,7 @@ impl KeybindingEditorModal { let status_toast = StatusToast::new( format!( "Saved edits to the {} action.", - &self.editing_keybind.humanized_action_name + &self.editing_keybind.action().humanized_name ), cx, move |this, _cx| { @@ -2030,7 +2265,7 @@ impl KeybindingEditorModal { .log_err(); cx.spawn(async move |this, cx| { - let action_name = existing_keybind.action_name; + let action_name = existing_keybind.action().name; if let Err(err) = save_keybinding_update( create, @@ -2127,13 +2362,18 @@ impl Render for KeybindingEditorModal { .border_b_1() .border_color(theme.border_variant) .child(Label::new( - self.editing_keybind.humanized_action_name.clone(), + self.editing_keybind.action().humanized_name.clone(), )) - .when_some(self.editing_keybind.action_docs, |this, docs| { - this.child( - Label::new(docs).size(LabelSize::Small).color(Color::Muted), - ) - }), + .when_some( + self.editing_keybind.action().documentation, + |this, docs| { + this.child( + Label::new(docs) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }, + ), ), ) .section( @@ -2296,14 +2536,32 @@ impl ActionArgumentsEditor { ) })?; - let file_name = project::lsp_store::json_language_server_ext::normalized_action_file_name(action_name); + let file_name = + project::lsp_store::json_language_server_ext::normalized_action_file_name( + action_name, + ); - let (buffer, backup_temp_dir) = Self::create_temp_buffer(temp_dir, file_name.clone(), project.clone(), fs, cx).await.context("Failed to create temporary buffer for action arguments. Auto-complete will not work") - ?; + let (buffer, backup_temp_dir) = + Self::create_temp_buffer(temp_dir, file_name.clone(), project.clone(), fs, cx) + .await + .context(concat!( + "Failed to create temporary buffer for action arguments. ", + "Auto-complete will not work" + ))?; let editor = cx.new_window_entity(|window, cx| { let multi_buffer = cx.new(|cx| editor::MultiBuffer::singleton(buffer, cx)); - let mut editor = Editor::new(editor::EditorMode::Full { scale_ui_elements_with_buffer_font_size: true, show_active_line_background: false, sized_by_content: true },multi_buffer, project.upgrade(), window, cx); + let mut editor = Editor::new( + editor::EditorMode::Full { + scale_ui_elements_with_buffer_font_size: true, + show_active_line_background: false, + sized_by_content: true, + }, + multi_buffer, + project.upgrade(), + window, + cx, + ); editor.set_searchable(false); editor.disable_scrollbars_and_minimap(window, cx); editor.set_show_edit_predictions(Some(false), window, cx); @@ -2322,7 +2580,8 @@ impl ActionArgumentsEditor { })?; anyhow::Ok(()) - }.await; + } + .await; if result.is_err() { let json_language = load_json_language(workspace.clone(), cx).await; this.update(cx, |this, cx| { @@ -2334,10 +2593,12 @@ impl ActionArgumentsEditor { } }) // .context("Failed to load JSON language for editing keybinding action arguments input") - }).ok(); + }) + .ok(); this.update(cx, |this, _cx| { this.is_loading = false; - }).ok(); + }) + .ok(); } return result; }) @@ -2582,7 +2843,7 @@ async fn load_keybind_context_language( async fn save_keybinding_update( create: bool, - existing: ProcessedKeybinding, + existing: ProcessedBinding, action_mapping: &ActionMapping, new_args: Option<&str>, fs: &Arc, @@ -2593,37 +2854,31 @@ async fn save_keybinding_update( .context("Failed to load keymap file")?; let existing_keystrokes = existing.keystrokes().unwrap_or_default(); - let existing_context = existing - .context - .as_ref() - .and_then(KeybindContextString::local_str); + let existing_context = existing.context().and_then(KeybindContextString::local_str); let existing_args = existing - .action_arguments + .action() + .arguments .as_ref() .map(|args| args.text.as_ref()); let target = settings::KeybindUpdateTarget { context: existing_context, keystrokes: existing_keystrokes, - action_name: &existing.action_name, + action_name: &existing.action().name, action_arguments: existing_args, }; let source = settings::KeybindUpdateTarget { context: action_mapping.context.as_ref().map(|a| &***a), keystrokes: &action_mapping.keystrokes, - action_name: &existing.action_name, + action_name: &existing.action().name, action_arguments: new_args, }; let operation = if !create { settings::KeybindUpdateOperation::Replace { target, - target_keybind_source: existing - .source - .as_ref() - .map(|(source, _name)| *source) - .unwrap_or(KeybindSource::User), + target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User), source, } } else { @@ -2655,7 +2910,7 @@ async fn save_keybinding_update( } async fn remove_keybinding( - existing: ProcessedKeybinding, + existing: ProcessedBinding, fs: &Arc, tab_size: usize, ) -> anyhow::Result<()> { @@ -2668,22 +2923,16 @@ async fn remove_keybinding( let operation = settings::KeybindUpdateOperation::Remove { target: settings::KeybindUpdateTarget { - context: existing - .context - .as_ref() - .and_then(KeybindContextString::local_str), + context: existing.context().and_then(KeybindContextString::local_str), keystrokes, - action_name: &existing.action_name, + action_name: &existing.action().name, action_arguments: existing - .action_arguments + .action() + .arguments .as_ref() .map(|arguments| arguments.text.as_ref()), }, - target_keybind_source: existing - .source - .as_ref() - .map(|(source, _name)| *source) - .unwrap_or(KeybindSource::User), + target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User), }; let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();