use std::{ cell::RefCell, cmp::{self}, ops::{Not as _, Range}, rc::Rc, sync::Arc, time::{Duration, Instant},}; mod action_completion_provider;mod ui_components; use anyhow::{Context as _, anyhow};use collections::{HashMap, HashSet};use editor::{CompletionProvider, Editor, EditorEvent, EditorMode, SizingBehavior};use fs::Fs;use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{ Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,};use language::{Language, LanguageConfig, ToOffset as _};use notifications::status_toast::{StatusToast, ToastIcon};use project::{CompletionDisplayOptions, Project};use settings::{ BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size,};use ui::{ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section, SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, Tooltip, Window, prelude::*,};use ui_input::InputField;use util::ResultExt;use workspace::{ Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _, register_serializable_item,}; pub use ui_components::*;use zed_actions::{ChangeKeybinding, OpenKeymap}; use crate::{ action_completion_provider::ActionCompletionProvider, persistence::KEYBINDING_EDITORS, ui_components::keystroke_input::{ ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording, },}; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");const COLS: usize = 6; actions!( keymap_editor, [ /// Edits the selected key binding. EditBinding, /// Creates a new key binding for the selected action. CreateBinding, /// Creates a new key binding from scratch, prompting for the action. OpenCreateKeybindingModal, /// Deletes the selected key binding. DeleteBinding, /// Copies the action name to clipboard. CopyAction, /// Copies the context predicate to clipboard. CopyContext, /// Toggles Conflict Filtering ToggleConflictFilter, /// Toggle Keystroke search ToggleKeystrokeSearch, /// Toggles exact matching for keystroke search ToggleExactKeystrokeMatching, /// Shows matching keystrokes for the currently selected binding ShowMatchingKeybinds ]); pub fn init(cx: &mut App) { let keymap_event_channel = KeymapEventChannel::new(); cx.set_global(keymap_event_channel); fn open_keymap_editor( filter: Option<String>, workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) { let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::<KeymapEditor>()); let keymap_editor = if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); existing } else { let keymap_editor = cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx)); workspace.add_item_to_active_pane( Box::new(keymap_editor.clone()), None, true, window, cx, ); keymap_editor }; if let Some(filter) = filter { keymap_editor.update(cx, |editor, cx| { editor.filter_editor.update(cx, |editor, cx| { editor.clear(window, cx); editor.insert(&filter, window, cx); }); if !editor.has_binding_for(&filter) { open_binding_modal_after_loading(cx) } }) } } cx.observe_new(|workspace: &mut Workspace, _window, _cx| { workspace .register_action(|workspace, _: &OpenKeymap, window, cx| { open_keymap_editor(None, workspace, window, cx); }) .register_action(|workspace, action: &ChangeKeybinding, window, cx| { open_keymap_editor(Some(action.action.clone()), workspace, window, cx); }); }) .detach(); register_serializable_item::<KeymapEditor>(cx);} fn open_binding_modal_after_loading(cx: &mut Context<KeymapEditor>) { let started_at = Instant::now(); let observer = Rc::new(RefCell::new(None)); let handle = { let observer = Rc::clone(&observer); cx.observe(&cx.entity(), move |editor, _, cx| { let subscription = observer.borrow_mut().take(); if started_at.elapsed().as_secs() > 10 { return; } if !editor.matches.is_empty() { editor.selected_index = Some(0); cx.dispatch_action(&CreateBinding); return; } *observer.borrow_mut() = subscription; }) }; *observer.borrow_mut() = Some(handle);} pub struct KeymapEventChannel {} impl Global for KeymapEventChannel {} impl KeymapEventChannel { fn new() -> Self { Self {} } pub fn trigger_keymap_changed(cx: &mut App) { let Some(_event_channel) = cx.try_global::<Self>() else { // don't panic if no global defined. This usually happens in tests return; }; cx.update_global(|_event_channel: &mut Self, _| { /* triggers observers in KeymapEditors */ }); }} #[derive(Default, PartialEq)]enum SearchMode { #[default] Normal, KeyStroke { exact_match: bool, },} impl SearchMode { fn invert(&self) -> Self { match self { SearchMode::Normal => SearchMode::KeyStroke { exact_match: true }, SearchMode::KeyStroke { .. } => SearchMode::Normal, } } fn exact_match(&self) -> bool { match self { SearchMode::Normal => false, SearchMode::KeyStroke { exact_match } => *exact_match, } }} #[derive(Default, PartialEq, Copy, Clone)]enum FilterState { #[default] All, Conflicts,} impl FilterState { fn invert(&self) -> Self { match self { FilterState::All => FilterState::Conflicts, FilterState::Conflicts => FilterState::All, } }} #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]struct ActionMapping { keystrokes: Rc<[KeybindingKeystroke]>, context: Option<SharedString>,} #[derive(Debug)]struct KeybindConflict { first_conflict_index: usize, remaining_conflict_amount: usize,} #[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<Option<ConflictOrigin>>, keybind_mapping: ConflictKeybindMapping, has_user_conflicts: bool,} type ConflictKeybindMapping = HashMap< Rc<[KeybindingKeystroke]>, Vec<( Option<gpui::KeyBindingContextPredicate>, Vec<ConflictOrigin>, )>,>; impl ConflictState { fn new(key_bindings: &[ProcessedBinding]) -> Self { let mut action_keybind_mapping = ConflictKeybindMapping::default(); let mut largest_index = 0; for (index, binding) in key_bindings .iter() .enumerate() .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information())) { let mapping = binding.get_action_mapping(); let predicate = mapping .context .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); let entry = action_keybind_mapping .entry(mapping.keystrokes.clone()) .or_default(); let origin = ConflictOrigin::new(binding.source, index); if let Some((_, origins)) = entry .iter_mut() .find(|(other_predicate, _)| match (&predicate, other_predicate) { (None, None) => true, (Some(a), Some(b)) => normalized_ctx_eq(a, b), _ => false, }) { origins.push(origin); } else { entry.push((predicate, vec![origin])); } largest_index = index; } let mut conflicts = vec![None; largest_index + 1]; let mut has_user_conflicts = false; for entries in action_keybind_mapping.values_mut() { for (_, indices) in entries.iter_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, keybind_mapping: action_keybind_mapping, has_user_conflicts, } } fn conflicting_indices_for_mapping( &self, action_mapping: &ActionMapping, keybind_idx: Option<usize>, ) -> Option<KeybindConflict> { let ActionMapping { keystrokes, context, } = action_mapping; let predicate = context .as_deref() .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); self.keybind_mapping.get(keystrokes).and_then(|entries| { entries .iter() .find_map(|(other_predicate, indices)| { match (&predicate, other_predicate) { (None, None) => true, (Some(pred), Some(other)) => normalized_ctx_eq(pred, other), _ => false, } .then_some(indices) }) .and_then(|indices| { let mut indices = indices .iter() .filter(|&conflict| Some(conflict.index) != keybind_idx); indices.next().map(|origin| KeybindConflict { first_conflict_index: origin.index, remaining_conflict_amount: indices.count(), }) }) }) } fn conflict_for_idx(&self, idx: usize) -> Option<ConflictOrigin> { self.conflicts.get(idx).copied().flatten() } 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_user_binding_conflicts(&self) -> bool { self.has_user_conflicts }} struct KeymapEditor { workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, _keymap_subscription: Subscription, keybindings: Vec<ProcessedBinding>, keybinding_conflict_state: ConflictState, filter_state: FilterState, search_mode: SearchMode, search_query_debounce: Option<Task<()>>, // corresponds 1 to 1 with keybindings string_match_candidates: Arc<Vec<StringMatchCandidate>>, matches: Vec<StringMatch>, table_interaction_state: Entity<TableInteractionState>, filter_editor: Entity<Editor>, keystroke_editor: Entity<KeystrokeInput>, selected_index: Option<usize>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, previous_edit: Option<PreviousEdit>, humanized_action_names: HumanizedActionNameCache, current_widths: Entity<TableColumnWidths>, show_hover_menus: bool, actions_with_schemas: HashSet<&'static str>, /// In order for the JSON LSP to run in the actions arguments editor, we /// require a backing file In order to avoid issues (primarily log spam) /// with drop order between the buffer, file, worktree, etc, we create a /// temporary directory for these backing files in the keymap editor struct /// instead of here. This has the added benefit of only having to create a /// worktree and directory once, although the perf improvement is negligible. action_args_temp_dir_worktree: Option<Entity<project::Worktree>>, action_args_temp_dir: Option<tempfile::TempDir>,} enum PreviousEdit { /// When deleting, we want to maintain the same scroll position ScrollBarOffset(Point<Pixels>), /// When editing or creating, because the new keybinding could be in a different position in the sort order /// we store metadata about the new binding (either the modified version or newly created one) /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches /// this metadata, we set the selected index to it and scroll to it, /// and if we don't find it, we scroll to 0 and don't set a selected index Keybinding { action_mapping: ActionMapping, action_name: &'static str, /// The scrollbar position to fallback to if we don't find the keybinding during a refresh /// this can happen if there's a filter applied to the search and the keybinding modification /// filters the binding from the search results fallback: Point<Pixels>, },} impl EventEmitter<()> for KeymapEditor {} impl Focusable for KeymapEditor { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { if self.selected_index.is_some() { self.focus_handle.clone() } else { self.filter_editor.focus_handle(cx) } }}/// Helper function to check if two keystroke sequences match exactlyfn keystrokes_match_exactly( keystrokes1: &[KeybindingKeystroke], keystrokes2: &[KeybindingKeystroke],) -> bool { keystrokes1.len() == keystrokes2.len() && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { k1.inner().key == k2.inner().key && k1.inner().modifiers == k2.inner().modifiers })} impl KeymapEditor { fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self { let _keymap_subscription = cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed); let table_interaction_state = cx.new(|cx| { TableInteractionState::new(cx) .with_custom_scrollbar(ui::Scrollbars::for_settings::<editor::EditorSettings>()) }); let keystroke_editor = cx.new(|cx| { let mut keystroke_editor = KeystrokeInput::new(None, window, cx); keystroke_editor.set_search(true); keystroke_editor }); let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter action names…", window, cx); editor }); Lines 1–500 of 3960. Load more
use std::{ cell::RefCell, cmp::{self}, ops::{Not as _, Range}, rc::Rc, sync::Arc, time::{Duration, Instant}, }; mod action_completion_provider; mod ui_components; use anyhow::{Context as _, anyhow}; use collections::{HashMap, HashSet}; use editor::{CompletionProvider, Editor, EditorEvent, EditorMode, SizingBehavior}; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; use project::{CompletionDisplayOptions, Project}; use settings::{ BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size, }; use ui::{ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section, SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, Tooltip, Window, prelude::*, }; use ui_input::InputField; use util::ResultExt; use workspace::{ Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _, register_serializable_item, }; pub use ui_components::*; use zed_actions::{ChangeKeybinding, OpenKeymap}; use crate::{ action_completion_provider::ActionCompletionProvider, persistence::KEYBINDING_EDITORS, ui_components::keystroke_input::{ ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording, }, }; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>"); const COLS: usize = 6; actions!( keymap_editor, [ /// Edits the selected key binding. EditBinding, /// Creates a new key binding for the selected action. CreateBinding, /// Creates a new key binding from scratch, prompting for the action. OpenCreateKeybindingModal, /// Deletes the selected key binding. DeleteBinding, /// Copies the action name to clipboard. CopyAction, /// Copies the context predicate to clipboard. CopyContext, /// Toggles Conflict Filtering ToggleConflictFilter, /// Toggle Keystroke search ToggleKeystrokeSearch, /// Toggles exact matching for keystroke search ToggleExactKeystrokeMatching, /// Shows matching keystrokes for the currently selected binding ShowMatchingKeybinds ] ); pub fn init(cx: &mut App) { let keymap_event_channel = KeymapEventChannel::new(); cx.set_global(keymap_event_channel); fn open_keymap_editor( filter: Option<String>, workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) { let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::<KeymapEditor>()); let keymap_editor = if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); existing } else { let keymap_editor = cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx)); workspace.add_item_to_active_pane( Box::new(keymap_editor.clone()), None, true, window, cx, ); keymap_editor }; if let Some(filter) = filter { keymap_editor.update(cx, |editor, cx| { editor.filter_editor.update(cx, |editor, cx| { editor.clear(window, cx); editor.insert(&filter, window, cx); }); if !editor.has_binding_for(&filter) { open_binding_modal_after_loading(cx) } }) } } cx.observe_new(|workspace: &mut Workspace, _window, _cx| { workspace .register_action(|workspace, _: &OpenKeymap, window, cx| { open_keymap_editor(None, workspace, window, cx); }) .register_action(|workspace, action: &ChangeKeybinding, window, cx| { open_keymap_editor(Some(action.action.clone()), workspace, window, cx); }); }) .detach(); register_serializable_item::<KeymapEditor>(cx); } fn open_binding_modal_after_loading(cx: &mut Context<KeymapEditor>) { let started_at = Instant::now(); let observer = Rc::new(RefCell::new(None)); let handle = { let observer = Rc::clone(&observer); cx.observe(&cx.entity(), move |editor, _, cx| { let subscription = observer.borrow_mut().take(); if started_at.elapsed().as_secs() > 10 { return; } if !editor.matches.is_empty() { editor.selected_index = Some(0); cx.dispatch_action(&CreateBinding); return; } *observer.borrow_mut() = subscription; }) }; *observer.borrow_mut() = Some(handle); } pub struct KeymapEventChannel {} impl Global for KeymapEventChannel {} impl KeymapEventChannel { fn new() -> Self { Self {} } pub fn trigger_keymap_changed(cx: &mut App) { let Some(_event_channel) = cx.try_global::<Self>() else { // don't panic if no global defined. This usually happens in tests return; }; cx.update_global(|_event_channel: &mut Self, _| { /* triggers observers in KeymapEditors */ }); } } #[derive(Default, PartialEq)] enum SearchMode { #[default] Normal, KeyStroke { exact_match: bool, }, } impl SearchMode { fn invert(&self) -> Self { match self { SearchMode::Normal => SearchMode::KeyStroke { exact_match: true }, SearchMode::KeyStroke { .. } => SearchMode::Normal, } } fn exact_match(&self) -> bool { match self { SearchMode::Normal => false, SearchMode::KeyStroke { exact_match } => *exact_match, } } } #[derive(Default, PartialEq, Copy, Clone)] enum FilterState { #[default] All, Conflicts, } impl FilterState { fn invert(&self) -> Self { match self { FilterState::All => FilterState::Conflicts, FilterState::Conflicts => FilterState::All, } } } #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] struct ActionMapping { keystrokes: Rc<[KeybindingKeystroke]>, context: Option<SharedString>, } #[derive(Debug)] struct KeybindConflict { first_conflict_index: usize, remaining_conflict_amount: usize, } #[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<Option<ConflictOrigin>>, keybind_mapping: ConflictKeybindMapping, has_user_conflicts: bool, } type ConflictKeybindMapping = HashMap< Rc<[KeybindingKeystroke]>, Vec<( Option<gpui::KeyBindingContextPredicate>, Vec<ConflictOrigin>, )>, >; impl ConflictState { fn new(key_bindings: &[ProcessedBinding]) -> Self { let mut action_keybind_mapping = ConflictKeybindMapping::default(); let mut largest_index = 0; for (index, binding) in key_bindings .iter() .enumerate() .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information())) { let mapping = binding.get_action_mapping(); let predicate = mapping .context .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); let entry = action_keybind_mapping .entry(mapping.keystrokes.clone()) .or_default(); let origin = ConflictOrigin::new(binding.source, index); if let Some((_, origins)) = entry .iter_mut() .find(|(other_predicate, _)| match (&predicate, other_predicate) { (None, None) => true, (Some(a), Some(b)) => normalized_ctx_eq(a, b), _ => false, }) { origins.push(origin); } else { entry.push((predicate, vec![origin])); } largest_index = index; } let mut conflicts = vec![None; largest_index + 1]; let mut has_user_conflicts = false; for entries in action_keybind_mapping.values_mut() { for (_, indices) in entries.iter_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, keybind_mapping: action_keybind_mapping, has_user_conflicts, } } fn conflicting_indices_for_mapping( &self, action_mapping: &ActionMapping, keybind_idx: Option<usize>, ) -> Option<KeybindConflict> { let ActionMapping { keystrokes, context, } = action_mapping; let predicate = context .as_deref() .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); self.keybind_mapping.get(keystrokes).and_then(|entries| { entries .iter() .find_map(|(other_predicate, indices)| { match (&predicate, other_predicate) { (None, None) => true, (Some(pred), Some(other)) => normalized_ctx_eq(pred, other), _ => false, } .then_some(indices) }) .and_then(|indices| { let mut indices = indices .iter() .filter(|&conflict| Some(conflict.index) != keybind_idx); indices.next().map(|origin| KeybindConflict { first_conflict_index: origin.index, remaining_conflict_amount: indices.count(), }) }) }) } fn conflict_for_idx(&self, idx: usize) -> Option<ConflictOrigin> { self.conflicts.get(idx).copied().flatten() } 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_user_binding_conflicts(&self) -> bool { self.has_user_conflicts } } struct KeymapEditor { workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, _keymap_subscription: Subscription, keybindings: Vec<ProcessedBinding>, keybinding_conflict_state: ConflictState, filter_state: FilterState, search_mode: SearchMode, search_query_debounce: Option<Task<()>>, // corresponds 1 to 1 with keybindings string_match_candidates: Arc<Vec<StringMatchCandidate>>, matches: Vec<StringMatch>, table_interaction_state: Entity<TableInteractionState>, filter_editor: Entity<Editor>, keystroke_editor: Entity<KeystrokeInput>, selected_index: Option<usize>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, previous_edit: Option<PreviousEdit>, humanized_action_names: HumanizedActionNameCache, current_widths: Entity<TableColumnWidths>, show_hover_menus: bool, actions_with_schemas: HashSet<&'static str>, /// In order for the JSON LSP to run in the actions arguments editor, we /// require a backing file In order to avoid issues (primarily log spam) /// with drop order between the buffer, file, worktree, etc, we create a /// temporary directory for these backing files in the keymap editor struct /// instead of here. This has the added benefit of only having to create a /// worktree and directory once, although the perf improvement is negligible. action_args_temp_dir_worktree: Option<Entity<project::Worktree>>, action_args_temp_dir: Option<tempfile::TempDir>, } enum PreviousEdit { /// When deleting, we want to maintain the same scroll position ScrollBarOffset(Point<Pixels>), /// When editing or creating, because the new keybinding could be in a different position in the sort order /// we store metadata about the new binding (either the modified version or newly created one) /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches /// this metadata, we set the selected index to it and scroll to it, /// and if we don't find it, we scroll to 0 and don't set a selected index Keybinding { action_mapping: ActionMapping, action_name: &'static str, /// The scrollbar position to fallback to if we don't find the keybinding during a refresh /// this can happen if there's a filter applied to the search and the keybinding modification /// filters the binding from the search results fallback: Point<Pixels>, }, } impl EventEmitter<()> for KeymapEditor {} impl Focusable for KeymapEditor { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { if self.selected_index.is_some() { self.focus_handle.clone() } else { self.filter_editor.focus_handle(cx) } } } /// Helper function to check if two keystroke sequences match exactly fn keystrokes_match_exactly( keystrokes1: &[KeybindingKeystroke], keystrokes2: &[KeybindingKeystroke], ) -> bool { keystrokes1.len() == keystrokes2.len() && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { k1.inner().key == k2.inner().key && k1.inner().modifiers == k2.inner().modifiers }) } impl KeymapEditor { fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self { let _keymap_subscription = cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed); let table_interaction_state = cx.new(|cx| { TableInteractionState::new(cx) .with_custom_scrollbar(ui::Scrollbars::for_settings::<editor::EditorSettings>()) }); let keystroke_editor = cx.new(|cx| { let mut keystroke_editor = KeystrokeInput::new(None, window, cx); keystroke_editor.set_search(true); keystroke_editor }); let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter action names…", window, cx); editor });