use std::{ cmp::{self}, ops::{Not as _, Range}, rc::Rc, sync::Arc, time::Duration,}; mod ui_components; use anyhow::{Context as _, anyhow};use collections::{HashMap, HashSet};use editor::{CompletionProvider, Editor, EditorEvent};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::OpenKeymap; use crate::{ persistence::KEYBINDING_EDITORS, ui_components::keystroke_input::{ ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording, },}; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>"); actions!( keymap_editor, [ /// Edits the selected key binding. EditBinding, /// Creates a new key binding for the selected action. CreateBinding, /// 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); cx.on_action(|_: &OpenKeymap, cx| { workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| { workspace .with_local_workspace(window, cx, |workspace, window, cx| { let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::<KeymapEditor>()); if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); } else { let keymap_editor = cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx)); workspace.add_item_to_active_pane( Box::new(keymap_editor), None, true, window, cx, ); } }) .detach(); }) }); register_serializable_item::<KeymapEditor>(cx);} 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: false }, 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<6>>, show_hover_menus: bool, /// 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 }); cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| { if !matches!(e, EditorEvent::BufferEdited) { return; } this.on_query_changed(cx); }) .detach(); cx.subscribe(&keystroke_editor, |this, _, _, cx| { if matches!(this.search_mode, SearchMode::Normal) { return; } this.on_query_changed(cx); }) .detach(); cx.spawn({ let workspace = workspace.clone(); async move |this, cx| { let temp_dir = tempfile::tempdir_in(paths::temp_dir())?; let worktree = workspace .update(cx, |ws, cx| { ws.project() .update(cx, |p, cx| p.create_worktree(temp_dir.path(), false, cx)) })? .await?; this.update(cx, |this, _| { this.action_args_temp_dir = Some(temp_dir); this.action_args_temp_dir_worktree = Some(worktree); }) } }) .detach(); let mut this = Self { workspace, keybindings: vec![], keybinding_conflict_state: ConflictState::default(), filter_state: FilterState::default(), search_mode: SearchMode::default(), string_match_candidates: Arc::new(vec![]), matches: vec![], focus_handle: cx.focus_handle(), _keymap_subscription, table_interaction_state, filter_editor, keystroke_editor, selected_index: None, context_menu: None, previous_edit: None, search_query_debounce: None,Lines 1–500 of 3589. Load more
use std::{ cmp::{self}, ops::{Not as _, Range}, rc::Rc, sync::Arc, time::Duration, }; mod ui_components; use anyhow::{Context as _, anyhow}; use collections::{HashMap, HashSet}; use editor::{CompletionProvider, Editor, EditorEvent}; 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::OpenKeymap; use crate::{ persistence::KEYBINDING_EDITORS, ui_components::keystroke_input::{ ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording, }, }; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>"); actions!( keymap_editor, [ /// Edits the selected key binding. EditBinding, /// Creates a new key binding for the selected action. CreateBinding, /// 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); cx.on_action(|_: &OpenKeymap, cx| { workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| { workspace .with_local_workspace(window, cx, |workspace, window, cx| { let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::<KeymapEditor>()); if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); } else { let keymap_editor = cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx)); workspace.add_item_to_active_pane( Box::new(keymap_editor), None, true, window, cx, ); } }) .detach(); }) }); register_serializable_item::<KeymapEditor>(cx); } 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: false }, 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<6>>, show_hover_menus: bool, /// 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 }); cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| { if !matches!(e, EditorEvent::BufferEdited) { return; } this.on_query_changed(cx); }) .detach(); cx.subscribe(&keystroke_editor, |this, _, _, cx| { if matches!(this.search_mode, SearchMode::Normal) { return; } this.on_query_changed(cx); }) .detach(); cx.spawn({ let workspace = workspace.clone(); async move |this, cx| { let temp_dir = tempfile::tempdir_in(paths::temp_dir())?; let worktree = workspace .update(cx, |ws, cx| { ws.project() .update(cx, |p, cx| p.create_worktree(temp_dir.path(), false, cx)) })? .await?; this.update(cx, |this, _| { this.action_args_temp_dir = Some(temp_dir); this.action_args_temp_dir_worktree = Some(worktree); }) } }) .detach(); let mut this = Self { workspace, keybindings: vec![], keybinding_conflict_state: ConflictState::default(), filter_state: FilterState::default(), search_mode: SearchMode::default(), string_match_candidates: Arc::new(vec![]), matches: vec![], focus_handle: cx.focus_handle(), _keymap_subscription, table_interaction_state, filter_editor, keystroke_editor, selected_index: None, context_menu: None, previous_edit: None, search_query_debounce: None,