1use std::{
2 ops::{Not as _, Range},
3 sync::Arc,
4};
5
6use anyhow::{Context as _, anyhow};
7use collections::{HashMap, HashSet};
8use editor::{CompletionProvider, Editor, EditorEvent};
9use feature_flags::FeatureFlagViewExt;
10use fs::Fs;
11use fuzzy::{StringMatch, StringMatchCandidate};
12use gpui::{
13 Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context,
14 DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero,
15 KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
16 ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div,
17};
18use language::{Language, LanguageConfig, ToOffset as _};
19use notifications::status_toast::{StatusToast, ToastIcon};
20use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
21
22use util::ResultExt;
23
24use ui::{
25 ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Modal,
26 ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, Styled as _,
27 Tooltip, Window, prelude::*,
28};
29use ui_input::SingleLineInput;
30use workspace::{
31 Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
32 register_serializable_item,
33};
34
35use crate::{
36 SettingsUiFeatureFlag,
37 keybindings::persistence::KEYBINDING_EDITORS,
38 ui_components::table::{Table, TableInteractionState},
39};
40
41const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
42
43actions!(
44 zed,
45 [
46 /// Opens the keymap editor.
47 OpenKeymapEditor
48 ]
49);
50
51const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
52actions!(
53 keymap_editor,
54 [
55 /// Edits the selected key binding.
56 EditBinding,
57 /// Creates a new key binding for the selected action.
58 CreateBinding,
59 /// Deletes the selected key binding.
60 DeleteBinding,
61 /// Copies the action name to clipboard.
62 CopyAction,
63 /// Copies the context predicate to clipboard.
64 CopyContext,
65 /// Toggles Conflict Filtering
66 ToggleConflictFilter,
67 /// Toggle Keystroke search
68 ToggleKeystrokeSearch,
69 /// Toggles exact matching for keystroke search
70 ToggleExactKeystrokeMatching,
71 ]
72);
73
74actions!(
75 keystroke_input,
76 [
77 /// Starts recording keystrokes
78 StartRecording,
79 /// Stops recording keystrokes
80 StopRecording,
81 /// Clears the recorded keystrokes
82 ClearKeystrokes,
83 ]
84);
85
86pub fn init(cx: &mut App) {
87 let keymap_event_channel = KeymapEventChannel::new();
88 cx.set_global(keymap_event_channel);
89
90 cx.on_action(|_: &OpenKeymapEditor, cx| {
91 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
92 workspace
93 .with_local_workspace(window, cx, |workspace, window, cx| {
94 let existing = workspace
95 .active_pane()
96 .read(cx)
97 .items()
98 .find_map(|item| item.downcast::<KeymapEditor>());
99
100 if let Some(existing) = existing {
101 workspace.activate_item(&existing, true, true, window, cx);
102 } else {
103 let keymap_editor =
104 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
105 workspace.add_item_to_active_pane(
106 Box::new(keymap_editor),
107 None,
108 true,
109 window,
110 cx,
111 );
112 }
113 })
114 .detach();
115 })
116 });
117
118 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
119 let Some(window) = window else { return };
120
121 let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
122
123 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
124 filter.hide_action_types(&keymap_ui_actions);
125 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
126 });
127
128 cx.observe_flag::<SettingsUiFeatureFlag, _>(
129 window,
130 move |is_enabled, _workspace, _, cx| {
131 if is_enabled {
132 command_palette_hooks::CommandPaletteFilter::update_global(
133 cx,
134 |filter, _cx| {
135 filter.show_action_types(keymap_ui_actions.iter());
136 filter.show_namespace(KEYMAP_EDITOR_NAMESPACE);
137 },
138 );
139 } else {
140 command_palette_hooks::CommandPaletteFilter::update_global(
141 cx,
142 |filter, _cx| {
143 filter.hide_action_types(&keymap_ui_actions);
144 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
145 },
146 );
147 }
148 },
149 )
150 .detach();
151 })
152 .detach();
153
154 register_serializable_item::<KeymapEditor>(cx);
155}
156
157pub struct KeymapEventChannel {}
158
159impl Global for KeymapEventChannel {}
160
161impl KeymapEventChannel {
162 fn new() -> Self {
163 Self {}
164 }
165
166 pub fn trigger_keymap_changed(cx: &mut App) {
167 let Some(_event_channel) = cx.try_global::<Self>() else {
168 // don't panic if no global defined. This usually happens in tests
169 return;
170 };
171 cx.update_global(|_event_channel: &mut Self, _| {
172 /* triggers observers in KeymapEditors */
173 });
174 }
175}
176
177#[derive(Default, PartialEq)]
178enum SearchMode {
179 #[default]
180 Normal,
181 KeyStroke {
182 exact_match: bool,
183 },
184}
185
186impl SearchMode {
187 fn invert(&self) -> Self {
188 match self {
189 SearchMode::Normal => SearchMode::KeyStroke { exact_match: false },
190 SearchMode::KeyStroke { .. } => SearchMode::Normal,
191 }
192 }
193}
194
195#[derive(Default, PartialEq, Copy, Clone)]
196enum FilterState {
197 #[default]
198 All,
199 Conflicts,
200}
201
202impl FilterState {
203 fn invert(&self) -> Self {
204 match self {
205 FilterState::All => FilterState::Conflicts,
206 FilterState::Conflicts => FilterState::All,
207 }
208 }
209}
210
211#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
212struct ActionMapping {
213 keystroke_text: SharedString,
214 context: Option<SharedString>,
215}
216
217#[derive(Default)]
218struct ConflictState {
219 conflicts: Vec<usize>,
220 action_keybind_mapping: HashMap<ActionMapping, Vec<usize>>,
221}
222
223impl ConflictState {
224 fn new(key_bindings: &[ProcessedKeybinding]) -> Self {
225 let mut action_keybind_mapping: HashMap<_, Vec<usize>> = HashMap::default();
226
227 key_bindings
228 .iter()
229 .enumerate()
230 .filter(|(_, binding)| {
231 !binding.keystroke_text.is_empty()
232 && binding
233 .source
234 .as_ref()
235 .is_some_and(|source| matches!(source.0, KeybindSource::User))
236 })
237 .for_each(|(index, binding)| {
238 action_keybind_mapping
239 .entry(binding.get_action_mapping())
240 .or_default()
241 .push(index);
242 });
243
244 Self {
245 conflicts: action_keybind_mapping
246 .values()
247 .filter(|indices| indices.len() > 1)
248 .flatten()
249 .copied()
250 .collect(),
251 action_keybind_mapping,
252 }
253 }
254
255 fn conflicting_indices_for_mapping(
256 &self,
257 action_mapping: ActionMapping,
258 keybind_idx: usize,
259 ) -> Option<Vec<usize>> {
260 self.action_keybind_mapping
261 .get(&action_mapping)
262 .and_then(|indices| {
263 let mut indices = indices.iter().filter(|&idx| *idx != keybind_idx).peekable();
264 indices.peek().is_some().then(|| indices.copied().collect())
265 })
266 }
267
268 fn will_conflict(&self, action_mapping: ActionMapping) -> Option<Vec<usize>> {
269 self.action_keybind_mapping
270 .get(&action_mapping)
271 .and_then(|indices| indices.is_empty().not().then_some(indices.clone()))
272 }
273
274 fn has_conflict(&self, candidate_idx: &usize) -> bool {
275 self.conflicts.contains(candidate_idx)
276 }
277
278 fn any_conflicts(&self) -> bool {
279 !self.conflicts.is_empty()
280 }
281}
282
283struct KeymapEditor {
284 workspace: WeakEntity<Workspace>,
285 focus_handle: FocusHandle,
286 _keymap_subscription: Subscription,
287 keybindings: Vec<ProcessedKeybinding>,
288 keybinding_conflict_state: ConflictState,
289 filter_state: FilterState,
290 search_mode: SearchMode,
291 // corresponds 1 to 1 with keybindings
292 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
293 matches: Vec<StringMatch>,
294 table_interaction_state: Entity<TableInteractionState>,
295 filter_editor: Entity<Editor>,
296 keystroke_editor: Entity<KeystrokeInput>,
297 selected_index: Option<usize>,
298 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
299 previous_edit: Option<PreviousEdit>,
300 humanized_action_names: HashMap<&'static str, SharedString>,
301}
302
303enum PreviousEdit {
304 /// When deleting, we want to maintain the same scroll position
305 ScrollBarOffset(Point<Pixels>),
306 /// When editing or creating, because the new keybinding could be in a different position in the sort order
307 /// we store metadata about the new binding (either the modified version or newly created one)
308 /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches
309 /// this metadata, we set the selected index to it and scroll to it,
310 /// and if we don't find it, we scroll to 0 and don't set a selected index
311 Keybinding {
312 action_mapping: ActionMapping,
313 action_name: &'static str,
314 /// The scrollbar position to fallback to if we don't find the keybinding during a refresh
315 /// this can happen if there's a filter applied to the search and the keybinding modification
316 /// filters the binding from the search results
317 fallback: Point<Pixels>,
318 },
319}
320
321impl EventEmitter<()> for KeymapEditor {}
322
323impl Focusable for KeymapEditor {
324 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
325 return self.filter_editor.focus_handle(cx);
326 }
327}
328
329impl KeymapEditor {
330 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
331 let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(Self::on_keymap_changed);
332 let table_interaction_state = TableInteractionState::new(window, cx);
333
334 let keystroke_editor = cx.new(|cx| {
335 let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
336 keystroke_editor.highlight_on_focus = false;
337 keystroke_editor
338 });
339
340 let filter_editor = cx.new(|cx| {
341 let mut editor = Editor::single_line(window, cx);
342 editor.set_placeholder_text("Filter action names…", cx);
343 editor
344 });
345
346 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
347 if !matches!(e, EditorEvent::BufferEdited) {
348 return;
349 }
350
351 this.on_query_changed(cx);
352 })
353 .detach();
354
355 cx.subscribe(&keystroke_editor, |this, _, _, cx| {
356 if matches!(this.search_mode, SearchMode::Normal) {
357 return;
358 }
359
360 this.on_query_changed(cx);
361 })
362 .detach();
363
364 let humanized_action_names =
365 HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| {
366 (
367 action_name,
368 command_palette::humanize_action_name(action_name).into(),
369 )
370 }));
371
372 let mut this = Self {
373 workspace,
374 keybindings: vec![],
375 keybinding_conflict_state: ConflictState::default(),
376 filter_state: FilterState::default(),
377 search_mode: SearchMode::default(),
378 string_match_candidates: Arc::new(vec![]),
379 matches: vec![],
380 focus_handle: cx.focus_handle(),
381 _keymap_subscription,
382 table_interaction_state,
383 filter_editor,
384 keystroke_editor,
385 selected_index: None,
386 context_menu: None,
387 previous_edit: None,
388 humanized_action_names,
389 };
390
391 this.on_keymap_changed(cx);
392
393 this
394 }
395
396 fn current_action_query(&self, cx: &App) -> String {
397 self.filter_editor.read(cx).text(cx)
398 }
399
400 fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
401 match self.search_mode {
402 SearchMode::KeyStroke { .. } => self
403 .keystroke_editor
404 .read(cx)
405 .keystrokes()
406 .iter()
407 .cloned()
408 .collect(),
409 SearchMode::Normal => Default::default(),
410 }
411 }
412
413 fn on_query_changed(&self, cx: &mut Context<Self>) {
414 let action_query = self.current_action_query(cx);
415 let keystroke_query = self.current_keystroke_query(cx);
416
417 cx.spawn(async move |this, cx| {
418 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
419 this.update(cx, |this, cx| {
420 this.scroll_to_item(0, ScrollStrategy::Top, cx)
421 })
422 })
423 .detach();
424 }
425
426 async fn update_matches(
427 this: WeakEntity<Self>,
428 action_query: String,
429 keystroke_query: Vec<Keystroke>,
430 cx: &mut AsyncApp,
431 ) -> anyhow::Result<()> {
432 let action_query = command_palette::normalize_action_query(&action_query);
433 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
434 (this.string_match_candidates.clone(), this.keybindings.len())
435 })?;
436 let executor = cx.background_executor().clone();
437 let mut matches = fuzzy::match_strings(
438 &string_match_candidates,
439 &action_query,
440 true,
441 true,
442 keybind_count,
443 &Default::default(),
444 executor,
445 )
446 .await;
447 this.update(cx, |this, cx| {
448 match this.filter_state {
449 FilterState::Conflicts => {
450 matches.retain(|candidate| {
451 this.keybinding_conflict_state
452 .has_conflict(&candidate.candidate_id)
453 });
454 }
455 FilterState::All => {}
456 }
457
458 match this.search_mode {
459 SearchMode::KeyStroke { exact_match } => {
460 matches.retain(|item| {
461 this.keybindings[item.candidate_id]
462 .keystrokes()
463 .is_some_and(|keystrokes| {
464 if exact_match {
465 keystroke_query.len() == keystrokes.len()
466 && keystroke_query.iter().zip(keystrokes).all(
467 |(query, keystroke)| {
468 query.key == keystroke.key
469 && query.modifiers == keystroke.modifiers
470 },
471 )
472 } else {
473 let key_press_query =
474 KeyPressIterator::new(keystroke_query.as_slice());
475 let mut last_match_idx = 0;
476
477 key_press_query.into_iter().all(|key| {
478 let key_presses = KeyPressIterator::new(keystrokes);
479 key_presses.into_iter().enumerate().any(
480 |(index, keystroke)| {
481 if last_match_idx > index || keystroke != key {
482 return false;
483 }
484
485 last_match_idx = index;
486 true
487 },
488 )
489 })
490 }
491 })
492 });
493 }
494 SearchMode::Normal => {}
495 }
496
497 if action_query.is_empty() {
498 // apply default sort
499 // sorts by source precedence, and alphabetically by action name within each source
500 matches.sort_by_key(|match_item| {
501 let keybind = &this.keybindings[match_item.candidate_id];
502 let source = keybind.source.as_ref().map(|s| s.0);
503 use KeybindSource::*;
504 let source_precedence = match source {
505 Some(User) => 0,
506 Some(Vim) => 1,
507 Some(Base) => 2,
508 Some(Default) => 3,
509 None => 4,
510 };
511 return (source_precedence, keybind.action_name);
512 });
513 }
514 this.selected_index.take();
515 this.matches = matches;
516 cx.notify();
517 })
518 }
519
520 fn has_conflict(&self, row_index: usize) -> bool {
521 self.matches
522 .get(row_index)
523 .map(|candidate| candidate.candidate_id)
524 .is_some_and(|id| self.keybinding_conflict_state.has_conflict(&id))
525 }
526
527 fn process_bindings(
528 json_language: Arc<Language>,
529 zed_keybind_context_language: Arc<Language>,
530 cx: &mut App,
531 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
532 let key_bindings_ptr = cx.key_bindings();
533 let lock = key_bindings_ptr.borrow();
534 let key_bindings = lock.bindings();
535 let mut unmapped_action_names =
536 HashSet::from_iter(cx.all_action_names().into_iter().copied());
537 let action_documentation = cx.action_documentation();
538 let mut generator = KeymapFile::action_schema_generator();
539 let action_schema = HashMap::from_iter(
540 cx.action_schemas(&mut generator)
541 .into_iter()
542 .filter_map(|(name, schema)| schema.map(|schema| (name, schema))),
543 );
544
545 let mut processed_bindings = Vec::new();
546 let mut string_match_candidates = Vec::new();
547
548 for key_binding in key_bindings {
549 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
550
551 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
552 let ui_key_binding = Some(
553 ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
554 .vim_mode(source == Some(settings::KeybindSource::Vim)),
555 );
556
557 let context = key_binding
558 .predicate()
559 .map(|predicate| {
560 KeybindContextString::Local(
561 predicate.to_string().into(),
562 zed_keybind_context_language.clone(),
563 )
564 })
565 .unwrap_or(KeybindContextString::Global);
566
567 let source = source.map(|source| (source, source.name().into()));
568
569 let action_name = key_binding.action().name();
570 unmapped_action_names.remove(&action_name);
571 let action_arguments = key_binding
572 .action_input()
573 .map(|arguments| SyntaxHighlightedText::new(arguments, json_language.clone()));
574 let action_docs = action_documentation.get(action_name).copied();
575
576 let index = processed_bindings.len();
577 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
578 processed_bindings.push(ProcessedKeybinding {
579 keystroke_text: keystroke_text.into(),
580 ui_key_binding,
581 action_name,
582 action_arguments,
583 action_docs,
584 action_schema: action_schema.get(action_name).cloned(),
585 context: Some(context),
586 source,
587 });
588 string_match_candidates.push(string_match_candidate);
589 }
590
591 let empty = SharedString::new_static("");
592 for action_name in unmapped_action_names.into_iter() {
593 let index = processed_bindings.len();
594 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
595 processed_bindings.push(ProcessedKeybinding {
596 keystroke_text: empty.clone(),
597 ui_key_binding: None,
598 action_name,
599 action_arguments: None,
600 action_docs: action_documentation.get(action_name).copied(),
601 action_schema: action_schema.get(action_name).cloned(),
602 context: None,
603 source: None,
604 });
605 string_match_candidates.push(string_match_candidate);
606 }
607
608 (processed_bindings, string_match_candidates)
609 }
610
611 fn on_keymap_changed(&mut self, cx: &mut Context<KeymapEditor>) {
612 let workspace = self.workspace.clone();
613 cx.spawn(async move |this, cx| {
614 let json_language = load_json_language(workspace.clone(), cx).await;
615 let zed_keybind_context_language =
616 load_keybind_context_language(workspace.clone(), cx).await;
617
618 let (action_query, keystroke_query) = this.update(cx, |this, cx| {
619 let (key_bindings, string_match_candidates) =
620 Self::process_bindings(json_language, zed_keybind_context_language, cx);
621
622 this.keybinding_conflict_state = ConflictState::new(&key_bindings);
623
624 if !this.keybinding_conflict_state.any_conflicts() {
625 this.filter_state = FilterState::All;
626 }
627
628 this.keybindings = key_bindings;
629 this.string_match_candidates = Arc::new(string_match_candidates);
630 this.matches = this
631 .string_match_candidates
632 .iter()
633 .enumerate()
634 .map(|(ix, candidate)| StringMatch {
635 candidate_id: ix,
636 score: 0.0,
637 positions: vec![],
638 string: candidate.string.clone(),
639 })
640 .collect();
641 (
642 this.current_action_query(cx),
643 this.current_keystroke_query(cx),
644 )
645 })?;
646 // calls cx.notify
647 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
648 this.update(cx, |this, cx| {
649 if let Some(previous_edit) = this.previous_edit.take() {
650 match previous_edit {
651 // should remove scroll from process_query
652 PreviousEdit::ScrollBarOffset(offset) => {
653 this.table_interaction_state.update(cx, |table, _| {
654 table.set_scrollbar_offset(Axis::Vertical, offset)
655 })
656 // set selected index and scroll
657 }
658 PreviousEdit::Keybinding {
659 action_mapping,
660 action_name,
661 fallback,
662 } => {
663 let scroll_position =
664 this.matches.iter().enumerate().find_map(|(index, item)| {
665 let binding = &this.keybindings[item.candidate_id];
666 if binding.get_action_mapping() == action_mapping
667 && binding.action_name == action_name
668 {
669 Some(index)
670 } else {
671 None
672 }
673 });
674
675 if let Some(scroll_position) = scroll_position {
676 this.scroll_to_item(scroll_position, ScrollStrategy::Top, cx);
677 this.selected_index = Some(scroll_position);
678 } else {
679 this.table_interaction_state.update(cx, |table, _| {
680 table.set_scrollbar_offset(Axis::Vertical, fallback)
681 });
682 }
683 cx.notify();
684 }
685 }
686 }
687 })
688 })
689 .detach_and_log_err(cx);
690 }
691
692 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
693 let mut dispatch_context = KeyContext::new_with_defaults();
694 dispatch_context.add("KeymapEditor");
695 dispatch_context.add("menu");
696
697 dispatch_context
698 }
699
700 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
701 let index = usize::min(index, self.matches.len().saturating_sub(1));
702 self.table_interaction_state.update(cx, |this, _cx| {
703 this.scroll_handle.scroll_to_item(index, strategy);
704 });
705 }
706
707 fn focus_search(
708 &mut self,
709 _: &search::FocusSearch,
710 window: &mut Window,
711 cx: &mut Context<Self>,
712 ) {
713 if !self
714 .filter_editor
715 .focus_handle(cx)
716 .contains_focused(window, cx)
717 {
718 window.focus(&self.filter_editor.focus_handle(cx));
719 } else {
720 self.filter_editor.update(cx, |editor, cx| {
721 editor.select_all(&Default::default(), window, cx);
722 });
723 }
724 self.selected_index.take();
725 }
726
727 fn selected_keybind_idx(&self) -> Option<usize> {
728 self.selected_index
729 .and_then(|match_index| self.matches.get(match_index))
730 .map(|r#match| r#match.candidate_id)
731 }
732
733 fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
734 self.selected_keybind_idx()
735 .and_then(|keybind_index| self.keybindings.get(keybind_index))
736 }
737
738 fn select_index(&mut self, index: usize, cx: &mut Context<Self>) {
739 if self.selected_index != Some(index) {
740 self.selected_index = Some(index);
741 cx.notify();
742 }
743 }
744
745 fn create_context_menu(
746 &mut self,
747 position: Point<Pixels>,
748 window: &mut Window,
749 cx: &mut Context<Self>,
750 ) {
751 let weak = cx.weak_entity();
752 self.context_menu = self.selected_binding().map(|selected_binding| {
753 let key_strokes = selected_binding
754 .keystrokes()
755 .map(Vec::from)
756 .unwrap_or_default();
757 let selected_binding_has_no_context = selected_binding
758 .context
759 .as_ref()
760 .and_then(KeybindContextString::local)
761 .is_none();
762
763 let selected_binding_is_unbound = selected_binding.keystrokes().is_none();
764
765 let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
766 menu.action_disabled_when(
767 selected_binding_is_unbound,
768 "Edit",
769 Box::new(EditBinding),
770 )
771 .action("Create", Box::new(CreateBinding))
772 .action_disabled_when(
773 selected_binding_is_unbound,
774 "Delete",
775 Box::new(DeleteBinding),
776 )
777 .separator()
778 .action("Copy Action", Box::new(CopyAction))
779 .action_disabled_when(
780 selected_binding_has_no_context,
781 "Copy Context",
782 Box::new(CopyContext),
783 )
784 .entry("Show matching keybindings", None, {
785 let weak = weak.clone();
786 let key_strokes = key_strokes.clone();
787
788 move |_, cx| {
789 weak.update(cx, |this, cx| {
790 this.filter_state = FilterState::All;
791 this.search_mode = SearchMode::KeyStroke { exact_match: true };
792
793 this.keystroke_editor.update(cx, |editor, cx| {
794 editor.set_keystrokes(key_strokes.clone(), cx);
795 });
796 })
797 .ok();
798 }
799 })
800 });
801
802 let context_menu_handle = context_menu.focus_handle(cx);
803 window.defer(cx, move |window, _cx| window.focus(&context_menu_handle));
804 let subscription = cx.subscribe_in(
805 &context_menu,
806 window,
807 |this, _, _: &DismissEvent, window, cx| {
808 this.dismiss_context_menu(window, cx);
809 },
810 );
811 (context_menu, position, subscription)
812 });
813
814 cx.notify();
815 }
816
817 fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
818 self.context_menu.take();
819 window.focus(&self.focus_handle);
820 cx.notify();
821 }
822
823 fn context_menu_deployed(&self) -> bool {
824 self.context_menu.is_some()
825 }
826
827 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
828 if let Some(selected) = self.selected_index {
829 let selected = selected + 1;
830 if selected >= self.matches.len() {
831 self.select_last(&Default::default(), window, cx);
832 } else {
833 self.selected_index = Some(selected);
834 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
835 cx.notify();
836 }
837 } else {
838 self.select_first(&Default::default(), window, cx);
839 }
840 }
841
842 fn select_previous(
843 &mut self,
844 _: &menu::SelectPrevious,
845 window: &mut Window,
846 cx: &mut Context<Self>,
847 ) {
848 if let Some(selected) = self.selected_index {
849 if selected == 0 {
850 return;
851 }
852
853 let selected = selected - 1;
854
855 if selected >= self.matches.len() {
856 self.select_last(&Default::default(), window, cx);
857 } else {
858 self.selected_index = Some(selected);
859 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
860 cx.notify();
861 }
862 } else {
863 self.select_last(&Default::default(), window, cx);
864 }
865 }
866
867 fn select_first(
868 &mut self,
869 _: &menu::SelectFirst,
870 _window: &mut Window,
871 cx: &mut Context<Self>,
872 ) {
873 if self.matches.get(0).is_some() {
874 self.selected_index = Some(0);
875 self.scroll_to_item(0, ScrollStrategy::Center, cx);
876 cx.notify();
877 }
878 }
879
880 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
881 if self.matches.last().is_some() {
882 let index = self.matches.len() - 1;
883 self.selected_index = Some(index);
884 self.scroll_to_item(index, ScrollStrategy::Center, cx);
885 cx.notify();
886 }
887 }
888
889 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
890 self.open_edit_keybinding_modal(false, window, cx);
891 }
892
893 fn open_edit_keybinding_modal(
894 &mut self,
895 create: bool,
896 window: &mut Window,
897 cx: &mut Context<Self>,
898 ) {
899 let Some((keybind_idx, keybind)) = self
900 .selected_keybind_idx()
901 .zip(self.selected_binding().cloned())
902 else {
903 return;
904 };
905 let keymap_editor = cx.entity();
906 self.workspace
907 .update(cx, |workspace, cx| {
908 let fs = workspace.app_state().fs.clone();
909 let workspace_weak = cx.weak_entity();
910 workspace.toggle_modal(window, cx, |window, cx| {
911 let modal = KeybindingEditorModal::new(
912 create,
913 keybind,
914 keybind_idx,
915 keymap_editor,
916 workspace_weak,
917 fs,
918 window,
919 cx,
920 );
921 window.focus(&modal.focus_handle(cx));
922 modal
923 });
924 })
925 .log_err();
926 }
927
928 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
929 self.open_edit_keybinding_modal(false, window, cx);
930 }
931
932 fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
933 self.open_edit_keybinding_modal(true, window, cx);
934 }
935
936 fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
937 let Some(to_remove) = self.selected_binding().cloned() else {
938 return;
939 };
940
941 let Ok(fs) = self
942 .workspace
943 .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
944 else {
945 return;
946 };
947 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
948 self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
949 self.table_interaction_state
950 .read(cx)
951 .get_scrollbar_offset(Axis::Vertical),
952 ));
953 cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
954 .detach_and_notify_err(window, cx);
955 }
956
957 fn copy_context_to_clipboard(
958 &mut self,
959 _: &CopyContext,
960 _window: &mut Window,
961 cx: &mut Context<Self>,
962 ) {
963 let context = self
964 .selected_binding()
965 .and_then(|binding| binding.context.as_ref())
966 .and_then(KeybindContextString::local_str)
967 .map(|context| context.to_string());
968 let Some(context) = context else {
969 return;
970 };
971 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
972 }
973
974 fn copy_action_to_clipboard(
975 &mut self,
976 _: &CopyAction,
977 _window: &mut Window,
978 cx: &mut Context<Self>,
979 ) {
980 let action = self
981 .selected_binding()
982 .map(|binding| binding.action_name.to_string());
983 let Some(action) = action else {
984 return;
985 };
986 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
987 }
988
989 fn toggle_conflict_filter(
990 &mut self,
991 _: &ToggleConflictFilter,
992 _: &mut Window,
993 cx: &mut Context<Self>,
994 ) {
995 self.set_filter_state(self.filter_state.invert(), cx);
996 }
997
998 fn set_filter_state(&mut self, filter_state: FilterState, cx: &mut Context<Self>) {
999 if self.filter_state != filter_state {
1000 self.filter_state = filter_state;
1001 self.on_query_changed(cx);
1002 }
1003 }
1004
1005 fn toggle_keystroke_search(
1006 &mut self,
1007 _: &ToggleKeystrokeSearch,
1008 window: &mut Window,
1009 cx: &mut Context<Self>,
1010 ) {
1011 self.search_mode = self.search_mode.invert();
1012 self.on_query_changed(cx);
1013
1014 // Update the keystroke editor to turn the `search` bool on
1015 self.keystroke_editor.update(cx, |keystroke_editor, cx| {
1016 keystroke_editor
1017 .set_search_mode(matches!(self.search_mode, SearchMode::KeyStroke { .. }));
1018 cx.notify();
1019 });
1020
1021 match self.search_mode {
1022 SearchMode::KeyStroke { .. } => {
1023 window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
1024 }
1025 SearchMode::Normal => {}
1026 }
1027 }
1028
1029 fn toggle_exact_keystroke_matching(
1030 &mut self,
1031 _: &ToggleExactKeystrokeMatching,
1032 _: &mut Window,
1033 cx: &mut Context<Self>,
1034 ) {
1035 let SearchMode::KeyStroke { exact_match } = &mut self.search_mode else {
1036 return;
1037 };
1038
1039 *exact_match = !(*exact_match);
1040 self.on_query_changed(cx);
1041 }
1042}
1043
1044#[derive(Clone)]
1045struct ProcessedKeybinding {
1046 keystroke_text: SharedString,
1047 ui_key_binding: Option<ui::KeyBinding>,
1048 action_name: &'static str,
1049 action_arguments: Option<SyntaxHighlightedText>,
1050 action_docs: Option<&'static str>,
1051 action_schema: Option<schemars::Schema>,
1052 context: Option<KeybindContextString>,
1053 source: Option<(KeybindSource, SharedString)>,
1054}
1055
1056impl ProcessedKeybinding {
1057 fn get_action_mapping(&self) -> ActionMapping {
1058 ActionMapping {
1059 keystroke_text: self.keystroke_text.clone(),
1060 context: self
1061 .context
1062 .as_ref()
1063 .and_then(|context| context.local())
1064 .cloned(),
1065 }
1066 }
1067
1068 fn keystrokes(&self) -> Option<&[Keystroke]> {
1069 self.ui_key_binding
1070 .as_ref()
1071 .map(|binding| binding.keystrokes.as_slice())
1072 }
1073}
1074
1075#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
1076enum KeybindContextString {
1077 Global,
1078 Local(SharedString, Arc<Language>),
1079}
1080
1081impl KeybindContextString {
1082 const GLOBAL: SharedString = SharedString::new_static("<global>");
1083
1084 pub fn local(&self) -> Option<&SharedString> {
1085 match self {
1086 KeybindContextString::Global => None,
1087 KeybindContextString::Local(name, _) => Some(name),
1088 }
1089 }
1090
1091 pub fn local_str(&self) -> Option<&str> {
1092 match self {
1093 KeybindContextString::Global => None,
1094 KeybindContextString::Local(name, _) => Some(name),
1095 }
1096 }
1097}
1098
1099impl RenderOnce for KeybindContextString {
1100 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
1101 match self {
1102 KeybindContextString::Global => {
1103 muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
1104 }
1105 KeybindContextString::Local(name, language) => {
1106 SyntaxHighlightedText::new(name, language).into_any_element()
1107 }
1108 }
1109 }
1110}
1111
1112fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
1113 let len = text.len();
1114 StyledText::new(text).with_highlights([(
1115 0..len,
1116 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
1117 )])
1118}
1119
1120impl Item for KeymapEditor {
1121 type Event = ();
1122
1123 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
1124 "Keymap Editor".into()
1125 }
1126}
1127
1128impl Render for KeymapEditor {
1129 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
1130 let row_count = self.matches.len();
1131 let theme = cx.theme();
1132
1133 v_flex()
1134 .id("keymap-editor")
1135 .track_focus(&self.focus_handle)
1136 .key_context(self.dispatch_context(window, cx))
1137 .on_action(cx.listener(Self::select_next))
1138 .on_action(cx.listener(Self::select_previous))
1139 .on_action(cx.listener(Self::select_first))
1140 .on_action(cx.listener(Self::select_last))
1141 .on_action(cx.listener(Self::focus_search))
1142 .on_action(cx.listener(Self::confirm))
1143 .on_action(cx.listener(Self::edit_binding))
1144 .on_action(cx.listener(Self::create_binding))
1145 .on_action(cx.listener(Self::delete_binding))
1146 .on_action(cx.listener(Self::copy_action_to_clipboard))
1147 .on_action(cx.listener(Self::copy_context_to_clipboard))
1148 .on_action(cx.listener(Self::toggle_conflict_filter))
1149 .on_action(cx.listener(Self::toggle_keystroke_search))
1150 .on_action(cx.listener(Self::toggle_exact_keystroke_matching))
1151 .size_full()
1152 .p_2()
1153 .gap_1()
1154 .bg(theme.colors().editor_background)
1155 .child(
1156 v_flex()
1157 .p_2()
1158 .gap_2()
1159 .child(
1160 h_flex()
1161 .gap_2()
1162 .child(
1163 div()
1164 .key_context({
1165 let mut context = KeyContext::new_with_defaults();
1166 context.add("BufferSearchBar");
1167 context
1168 })
1169 .size_full()
1170 .h_8()
1171 .pl_2()
1172 .pr_1()
1173 .py_1()
1174 .border_1()
1175 .border_color(theme.colors().border)
1176 .rounded_lg()
1177 .child(self.filter_editor.clone()),
1178 )
1179 .child(
1180 IconButton::new(
1181 "KeymapEditorToggleFiltersIcon",
1182 IconName::Keyboard,
1183 )
1184 .shape(ui::IconButtonShape::Square)
1185 .tooltip(|window, cx| {
1186 Tooltip::for_action(
1187 "Search by Keystroke",
1188 &ToggleKeystrokeSearch,
1189 window,
1190 cx,
1191 )
1192 })
1193 .toggle_state(matches!(
1194 self.search_mode,
1195 SearchMode::KeyStroke { .. }
1196 ))
1197 .on_click(|_, window, cx| {
1198 window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
1199 }),
1200 )
1201 .when(self.keybinding_conflict_state.any_conflicts(), |this| {
1202 this.child(
1203 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
1204 .shape(ui::IconButtonShape::Square)
1205 .tooltip({
1206 let filter_state = self.filter_state;
1207
1208 move |window, cx| {
1209 Tooltip::for_action(
1210 match filter_state {
1211 FilterState::All => "Show Conflicts",
1212 FilterState::Conflicts => "Hide Conflicts",
1213 },
1214 &ToggleConflictFilter,
1215 window,
1216 cx,
1217 )
1218 }
1219 })
1220 .selected_icon_color(Color::Warning)
1221 .toggle_state(matches!(
1222 self.filter_state,
1223 FilterState::Conflicts
1224 ))
1225 .on_click(|_, window, cx| {
1226 window.dispatch_action(
1227 ToggleConflictFilter.boxed_clone(),
1228 cx,
1229 );
1230 }),
1231 )
1232 }),
1233 )
1234 .when_some(
1235 match self.search_mode {
1236 SearchMode::Normal => None,
1237 SearchMode::KeyStroke { exact_match } => Some(exact_match),
1238 },
1239 |this, exact_match| {
1240 this.child(
1241 h_flex()
1242 .map(|this| {
1243 if self.keybinding_conflict_state.any_conflicts() {
1244 this.pr(rems_from_px(54.))
1245 } else {
1246 this.pr_7()
1247 }
1248 })
1249 .child(self.keystroke_editor.clone())
1250 .child(
1251 div().p_1().child(
1252 IconButton::new(
1253 "keystrokes-exact-match",
1254 IconName::Equal,
1255 )
1256 .shape(IconButtonShape::Square)
1257 .toggle_state(exact_match)
1258 .on_click(
1259 cx.listener(|_, _, window, cx| {
1260 window.dispatch_action(
1261 ToggleExactKeystrokeMatching.boxed_clone(),
1262 cx,
1263 );
1264 }),
1265 ),
1266 ),
1267 ),
1268 )
1269 },
1270 ),
1271 )
1272 .child(
1273 Table::new()
1274 .interactable(&self.table_interaction_state)
1275 .striped()
1276 .column_widths([
1277 rems(2.5),
1278 rems(16.),
1279 rems(16.),
1280 rems(16.),
1281 rems(32.),
1282 rems(8.),
1283 ])
1284 .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"])
1285 .uniform_list(
1286 "keymap-editor-table",
1287 row_count,
1288 cx.processor(move |this, range: Range<usize>, _window, cx| {
1289 let context_menu_deployed = this.context_menu_deployed();
1290 range
1291 .filter_map(|index| {
1292 let candidate_id = this.matches.get(index)?.candidate_id;
1293 let binding = &this.keybindings[candidate_id];
1294 let action_name = binding.action_name;
1295
1296 let icon = (this.filter_state != FilterState::Conflicts
1297 && this.has_conflict(index))
1298 .then(|| {
1299 base_button_style(index, IconName::Warning)
1300 .icon_color(Color::Warning)
1301 .tooltip(|window, cx| {
1302 Tooltip::with_meta(
1303 "Edit Keybinding",
1304 None,
1305 "Use alt+click to show conflicts",
1306 window,
1307 cx,
1308 )
1309 })
1310 .on_click(cx.listener(
1311 move |this, click: &ClickEvent, window, cx| {
1312 if click.modifiers().alt {
1313 this.set_filter_state(
1314 FilterState::Conflicts,
1315 cx,
1316 );
1317 } else {
1318 this.select_index(index, cx);
1319 this.open_edit_keybinding_modal(
1320 false, window, cx,
1321 );
1322 cx.stop_propagation();
1323 }
1324 },
1325 ))
1326 })
1327 .unwrap_or_else(|| {
1328 base_button_style(index, IconName::Pencil)
1329 .visible_on_hover(row_group_id(index))
1330 .tooltip(Tooltip::text("Edit Keybinding"))
1331 .on_click(cx.listener(move |this, _, window, cx| {
1332 this.select_index(index, cx);
1333 this.open_edit_keybinding_modal(false, window, cx);
1334 cx.stop_propagation();
1335 }))
1336 })
1337 .into_any_element();
1338
1339 let action = div()
1340 .id(("keymap action", index))
1341 .child({
1342 if action_name != gpui::NoAction.name() {
1343 this.humanized_action_names
1344 .get(action_name)
1345 .cloned()
1346 .unwrap_or(action_name.into())
1347 .into_any_element()
1348 } else {
1349 const NULL: SharedString =
1350 SharedString::new_static("<null>");
1351 muted_styled_text(NULL.clone(), cx)
1352 .into_any_element()
1353 }
1354 })
1355 .when(!context_menu_deployed, |this| {
1356 this.tooltip({
1357 let action_name = binding.action_name;
1358 let action_docs = binding.action_docs;
1359 move |_, cx| {
1360 let action_tooltip = Tooltip::new(action_name);
1361 let action_tooltip = match action_docs {
1362 Some(docs) => action_tooltip.meta(docs),
1363 None => action_tooltip,
1364 };
1365 cx.new(|_| action_tooltip).into()
1366 }
1367 })
1368 })
1369 .into_any_element();
1370 let keystrokes = binding.ui_key_binding.clone().map_or(
1371 binding.keystroke_text.clone().into_any_element(),
1372 IntoElement::into_any_element,
1373 );
1374 let action_arguments = match binding.action_arguments.clone() {
1375 Some(arguments) => arguments.into_any_element(),
1376 None => {
1377 if binding.action_schema.is_some() {
1378 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
1379 .into_any_element()
1380 } else {
1381 gpui::Empty.into_any_element()
1382 }
1383 }
1384 };
1385 let context = binding.context.clone().map_or(
1386 gpui::Empty.into_any_element(),
1387 |context| {
1388 let is_local = context.local().is_some();
1389
1390 div()
1391 .id(("keymap context", index))
1392 .child(context.clone())
1393 .when(is_local && !context_menu_deployed, |this| {
1394 this.tooltip(Tooltip::element({
1395 move |_, _| {
1396 context.clone().into_any_element()
1397 }
1398 }))
1399 })
1400 .into_any_element()
1401 },
1402 );
1403 let source = binding
1404 .source
1405 .clone()
1406 .map(|(_source, name)| name)
1407 .unwrap_or_default()
1408 .into_any_element();
1409 Some([
1410 icon,
1411 action,
1412 action_arguments,
1413 keystrokes,
1414 context,
1415 source,
1416 ])
1417 })
1418 .collect()
1419 }),
1420 )
1421 .map_row(
1422 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
1423 let is_conflict = this.has_conflict(row_index);
1424 let is_selected = this.selected_index == Some(row_index);
1425
1426 let row_id = row_group_id(row_index);
1427
1428 let row = row
1429 .id(row_id.clone())
1430 .on_any_mouse_down(cx.listener(
1431 move |this,
1432 mouse_down_event: &gpui::MouseDownEvent,
1433 window,
1434 cx| {
1435 match mouse_down_event.button {
1436 MouseButton::Right => {
1437 this.select_index(row_index, cx);
1438 this.create_context_menu(
1439 mouse_down_event.position,
1440 window,
1441 cx,
1442 );
1443 }
1444 _ => {}
1445 }
1446 },
1447 ))
1448 .on_click(cx.listener(
1449 move |this, event: &ClickEvent, window, cx| {
1450 this.select_index(row_index, cx);
1451 if event.up.click_count == 2 {
1452 this.open_edit_keybinding_modal(false, window, cx);
1453 }
1454 },
1455 ))
1456 .group(row_id)
1457 .border_2()
1458 .when(is_conflict, |row| {
1459 row.bg(cx.theme().status().error_background)
1460 })
1461 .when(is_selected, |row| {
1462 row.border_color(cx.theme().colors().panel_focused_border)
1463 });
1464
1465 row.into_any_element()
1466 }),
1467 ),
1468 )
1469 .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
1470 // This ensures that the menu is not dismissed in cases where scroll events
1471 // with a delta of zero are emitted
1472 if !event.delta.pixel_delta(px(1.)).y.is_zero() {
1473 this.context_menu.take();
1474 cx.notify();
1475 }
1476 }))
1477 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1478 deferred(
1479 anchored()
1480 .position(*position)
1481 .anchor(gpui::Corner::TopLeft)
1482 .child(menu.clone()),
1483 )
1484 .with_priority(1)
1485 }))
1486 }
1487}
1488
1489fn row_group_id(row_index: usize) -> SharedString {
1490 SharedString::new(format!("keymap-table-row-{}", row_index))
1491}
1492
1493fn base_button_style(row_index: usize, icon: IconName) -> IconButton {
1494 IconButton::new(("keymap-icon", row_index), icon)
1495 .shape(IconButtonShape::Square)
1496 .size(ButtonSize::Compact)
1497}
1498
1499#[derive(Debug, Clone, IntoElement)]
1500struct SyntaxHighlightedText {
1501 text: SharedString,
1502 language: Arc<Language>,
1503}
1504
1505impl SyntaxHighlightedText {
1506 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1507 Self {
1508 text: text.into(),
1509 language,
1510 }
1511 }
1512}
1513
1514impl RenderOnce for SyntaxHighlightedText {
1515 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1516 let text_style = window.text_style();
1517 let syntax_theme = cx.theme().syntax();
1518
1519 let text = self.text.clone();
1520
1521 let highlights = self
1522 .language
1523 .highlight_text(&text.as_ref().into(), 0..text.len());
1524 let mut runs = Vec::with_capacity(highlights.len());
1525 let mut offset = 0;
1526
1527 for (highlight_range, highlight_id) in highlights {
1528 // Add un-highlighted text before the current highlight
1529 if highlight_range.start > offset {
1530 runs.push(text_style.to_run(highlight_range.start - offset));
1531 }
1532
1533 let mut run_style = text_style.clone();
1534 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1535 run_style = run_style.highlight(highlight_style);
1536 }
1537 // add the highlighted range
1538 runs.push(run_style.to_run(highlight_range.len()));
1539 offset = highlight_range.end;
1540 }
1541
1542 // Add any remaining un-highlighted text
1543 if offset < text.len() {
1544 runs.push(text_style.to_run(text.len() - offset));
1545 }
1546
1547 return StyledText::new(text).with_runs(runs);
1548 }
1549}
1550
1551#[derive(PartialEq)]
1552enum InputError {
1553 Warning(SharedString),
1554 Error(SharedString),
1555}
1556
1557impl InputError {
1558 fn warning(message: impl Into<SharedString>) -> Self {
1559 Self::Warning(message.into())
1560 }
1561
1562 fn error(message: impl Into<SharedString>) -> Self {
1563 Self::Error(message.into())
1564 }
1565
1566 fn content(&self) -> &SharedString {
1567 match self {
1568 InputError::Warning(content) | InputError::Error(content) => content,
1569 }
1570 }
1571
1572 fn is_warning(&self) -> bool {
1573 matches!(self, InputError::Warning(_))
1574 }
1575}
1576
1577struct KeybindingEditorModal {
1578 creating: bool,
1579 editing_keybind: ProcessedKeybinding,
1580 editing_keybind_idx: usize,
1581 keybind_editor: Entity<KeystrokeInput>,
1582 context_editor: Entity<SingleLineInput>,
1583 action_arguments_editor: Option<Entity<Editor>>,
1584 fs: Arc<dyn Fs>,
1585 error: Option<InputError>,
1586 keymap_editor: Entity<KeymapEditor>,
1587 workspace: WeakEntity<Workspace>,
1588 focus_state: KeybindingEditorModalFocusState,
1589}
1590
1591impl ModalView for KeybindingEditorModal {}
1592
1593impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
1594
1595impl Focusable for KeybindingEditorModal {
1596 fn focus_handle(&self, cx: &App) -> FocusHandle {
1597 self.keybind_editor.focus_handle(cx)
1598 }
1599}
1600
1601impl KeybindingEditorModal {
1602 pub fn new(
1603 create: bool,
1604 editing_keybind: ProcessedKeybinding,
1605 editing_keybind_idx: usize,
1606 keymap_editor: Entity<KeymapEditor>,
1607 workspace: WeakEntity<Workspace>,
1608 fs: Arc<dyn Fs>,
1609 window: &mut Window,
1610 cx: &mut App,
1611 ) -> Self {
1612 let keybind_editor = cx
1613 .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
1614
1615 let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
1616 let input = SingleLineInput::new(window, cx, "Keybinding Context")
1617 .label("Edit Context")
1618 .label_size(LabelSize::Default);
1619
1620 if let Some(context) = editing_keybind
1621 .context
1622 .as_ref()
1623 .and_then(KeybindContextString::local)
1624 {
1625 input.editor().update(cx, |editor, cx| {
1626 editor.set_text(context.clone(), window, cx);
1627 });
1628 }
1629
1630 let editor_entity = input.editor().clone();
1631 let workspace = workspace.clone();
1632 cx.spawn(async move |_input_handle, cx| {
1633 let contexts = cx
1634 .background_spawn(async { collect_contexts_from_assets() })
1635 .await;
1636
1637 let language = load_keybind_context_language(workspace, cx).await;
1638 editor_entity
1639 .update(cx, |editor, cx| {
1640 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1641 buffer.update(cx, |buffer, cx| {
1642 buffer.set_language(Some(language), cx);
1643 });
1644 }
1645 editor.set_completion_provider(Some(std::rc::Rc::new(
1646 KeyContextCompletionProvider { contexts },
1647 )));
1648 })
1649 .context("Failed to load completions for keybinding context")
1650 })
1651 .detach_and_log_err(cx);
1652
1653 input
1654 });
1655
1656 let action_arguments_editor = editing_keybind.action_schema.clone().map(|_schema| {
1657 cx.new(|cx| {
1658 let mut editor = Editor::auto_height_unbounded(1, window, cx);
1659 let workspace = workspace.clone();
1660
1661 if let Some(arguments) = editing_keybind.action_arguments.clone() {
1662 editor.set_text(arguments.text, window, cx);
1663 } else {
1664 // TODO: default value from schema?
1665 editor.set_placeholder_text("Action Arguments", cx);
1666 }
1667 cx.spawn(async |editor, cx| {
1668 let json_language = load_json_language(workspace, cx).await;
1669 editor
1670 .update(cx, |editor, cx| {
1671 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1672 buffer.update(cx, |buffer, cx| {
1673 buffer.set_language(Some(json_language), cx)
1674 });
1675 }
1676 })
1677 .context("Failed to load JSON language for editing keybinding action arguments input")
1678 })
1679 .detach_and_log_err(cx);
1680 editor
1681 })
1682 });
1683
1684 let focus_state = KeybindingEditorModalFocusState::new(
1685 keybind_editor.read_with(cx, |keybind_editor, cx| keybind_editor.focus_handle(cx)),
1686 action_arguments_editor.as_ref().map(|args_editor| {
1687 args_editor.read_with(cx, |args_editor, cx| args_editor.focus_handle(cx))
1688 }),
1689 context_editor.read_with(cx, |context_editor, cx| context_editor.focus_handle(cx)),
1690 );
1691
1692 Self {
1693 creating: create,
1694 editing_keybind,
1695 editing_keybind_idx,
1696 fs,
1697 keybind_editor,
1698 context_editor,
1699 action_arguments_editor,
1700 error: None,
1701 keymap_editor,
1702 workspace,
1703 focus_state,
1704 }
1705 }
1706
1707 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
1708 if self
1709 .error
1710 .as_ref()
1711 .is_some_and(|old_error| old_error.is_warning() && *old_error == error)
1712 {
1713 false
1714 } else {
1715 self.error = Some(error);
1716 cx.notify();
1717 true
1718 }
1719 }
1720
1721 fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> {
1722 let action_arguments = self
1723 .action_arguments_editor
1724 .as_ref()
1725 .map(|editor| editor.read(cx).text(cx));
1726
1727 let value = action_arguments
1728 .as_ref()
1729 .map(|args| {
1730 serde_json::from_str(args).context("Failed to parse action arguments as JSON")
1731 })
1732 .transpose()?;
1733
1734 cx.build_action(&self.editing_keybind.action_name, value)
1735 .context("Failed to validate action arguments")?;
1736 Ok(action_arguments)
1737 }
1738
1739 fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
1740 let new_keystrokes = self
1741 .keybind_editor
1742 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
1743 anyhow::ensure!(!new_keystrokes.is_empty(), "Keystrokes cannot be empty");
1744 Ok(new_keystrokes)
1745 }
1746
1747 fn validate_context(&self, cx: &App) -> anyhow::Result<Option<String>> {
1748 let new_context = self
1749 .context_editor
1750 .read_with(cx, |input, cx| input.editor().read(cx).text(cx));
1751 let Some(context) = new_context.is_empty().not().then_some(new_context) else {
1752 return Ok(None);
1753 };
1754 gpui::KeyBindingContextPredicate::parse(&context).context("Failed to parse key context")?;
1755
1756 Ok(Some(context))
1757 }
1758
1759 fn save(&mut self, cx: &mut Context<Self>) {
1760 let existing_keybind = self.editing_keybind.clone();
1761 let fs = self.fs.clone();
1762 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1763 let new_keystrokes = match self.validate_keystrokes(cx) {
1764 Err(err) => {
1765 self.set_error(InputError::error(err.to_string()), cx);
1766 return;
1767 }
1768 Ok(keystrokes) => keystrokes,
1769 };
1770
1771 let new_context = match self.validate_context(cx) {
1772 Err(err) => {
1773 self.set_error(InputError::error(err.to_string()), cx);
1774 return;
1775 }
1776 Ok(context) => context,
1777 };
1778
1779 let new_action_args = match self.validate_action_arguments(cx) {
1780 Err(input_err) => {
1781 self.set_error(InputError::error(input_err.to_string()), cx);
1782 return;
1783 }
1784 Ok(input) => input,
1785 };
1786
1787 let action_mapping = ActionMapping {
1788 keystroke_text: ui::text_for_keystrokes(&new_keystrokes, cx).into(),
1789 context: new_context.as_ref().map(Into::into),
1790 };
1791
1792 let conflicting_indices = if self.creating {
1793 self.keymap_editor
1794 .read(cx)
1795 .keybinding_conflict_state
1796 .will_conflict(action_mapping)
1797 } else {
1798 self.keymap_editor
1799 .read(cx)
1800 .keybinding_conflict_state
1801 .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
1802 };
1803 if let Some(conflicting_indices) = conflicting_indices {
1804 let first_conflicting_index = conflicting_indices[0];
1805 let conflicting_action_name = self
1806 .keymap_editor
1807 .read(cx)
1808 .keybindings
1809 .get(first_conflicting_index)
1810 .map(|keybind| keybind.action_name);
1811
1812 let warning_message = match conflicting_action_name {
1813 Some(name) => {
1814 let confliction_action_amount = conflicting_indices.len() - 1;
1815 if confliction_action_amount > 0 {
1816 format!(
1817 "Your keybind would conflict with the \"{}\" action and {} other bindings",
1818 name, confliction_action_amount
1819 )
1820 } else {
1821 format!("Your keybind would conflict with the \"{}\" action", name)
1822 }
1823 }
1824 None => {
1825 log::info!(
1826 "Could not find action in keybindings with index {}",
1827 first_conflicting_index
1828 );
1829 "Your keybind would conflict with other actions".to_string()
1830 }
1831 };
1832
1833 if self.set_error(InputError::warning(warning_message), cx) {
1834 return;
1835 }
1836 }
1837
1838 let create = self.creating;
1839
1840 let status_toast = StatusToast::new(
1841 format!(
1842 "Saved edits to the {} action.",
1843 command_palette::humanize_action_name(&self.editing_keybind.action_name)
1844 ),
1845 cx,
1846 move |this, _cx| {
1847 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
1848 .dismiss_button(true)
1849 // .action("Undo", f) todo: wire the undo functionality
1850 },
1851 );
1852
1853 self.workspace
1854 .update(cx, |workspace, cx| {
1855 workspace.toggle_status_toast(status_toast, cx);
1856 })
1857 .log_err();
1858
1859 cx.spawn(async move |this, cx| {
1860 let action_name = existing_keybind.action_name;
1861
1862 if let Err(err) = save_keybinding_update(
1863 create,
1864 existing_keybind,
1865 &new_keystrokes,
1866 new_context.as_deref(),
1867 new_action_args.as_deref(),
1868 &fs,
1869 tab_size,
1870 )
1871 .await
1872 {
1873 this.update(cx, |this, cx| {
1874 this.set_error(InputError::error(err.to_string()), cx);
1875 })
1876 .log_err();
1877 } else {
1878 this.update(cx, |this, cx| {
1879 let action_mapping = ActionMapping {
1880 keystroke_text: ui::text_for_keystrokes(new_keystrokes.as_slice(), cx)
1881 .into(),
1882 context: new_context.map(SharedString::from),
1883 };
1884
1885 this.keymap_editor.update(cx, |keymap, cx| {
1886 keymap.previous_edit = Some(PreviousEdit::Keybinding {
1887 action_mapping,
1888 action_name,
1889 fallback: keymap
1890 .table_interaction_state
1891 .read(cx)
1892 .get_scrollbar_offset(Axis::Vertical),
1893 })
1894 });
1895 cx.emit(DismissEvent);
1896 })
1897 .ok();
1898 }
1899 })
1900 .detach();
1901 }
1902
1903 fn key_context(&self) -> KeyContext {
1904 let mut key_context = KeyContext::new_with_defaults();
1905 key_context.add("KeybindEditorModal");
1906 key_context
1907 }
1908
1909 fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1910 self.focus_state.focus_next(window, cx);
1911 }
1912
1913 fn focus_prev(
1914 &mut self,
1915 _: &menu::SelectPrevious,
1916 window: &mut Window,
1917 cx: &mut Context<Self>,
1918 ) {
1919 self.focus_state.focus_previous(window, cx);
1920 }
1921
1922 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
1923 self.save(cx);
1924 }
1925
1926 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
1927 cx.emit(DismissEvent)
1928 }
1929}
1930
1931impl Render for KeybindingEditorModal {
1932 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1933 let theme = cx.theme().colors();
1934 let action_name =
1935 command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
1936
1937 v_flex()
1938 .w(rems(34.))
1939 .elevation_3(cx)
1940 .key_context(self.key_context())
1941 .on_action(cx.listener(Self::focus_next))
1942 .on_action(cx.listener(Self::focus_prev))
1943 .on_action(cx.listener(Self::confirm))
1944 .on_action(cx.listener(Self::cancel))
1945 .child(
1946 Modal::new("keybinding_editor_modal", None)
1947 .header(
1948 ModalHeader::new().child(
1949 v_flex()
1950 .pb_1p5()
1951 .mb_1()
1952 .gap_0p5()
1953 .border_b_1()
1954 .border_color(theme.border_variant)
1955 .child(Label::new(action_name))
1956 .when_some(self.editing_keybind.action_docs, |this, docs| {
1957 this.child(
1958 Label::new(docs).size(LabelSize::Small).color(Color::Muted),
1959 )
1960 }),
1961 ),
1962 )
1963 .section(
1964 Section::new().child(
1965 v_flex()
1966 .gap_2()
1967 .child(
1968 v_flex()
1969 .child(Label::new("Edit Keystroke"))
1970 .gap_1()
1971 .child(self.keybind_editor.clone()),
1972 )
1973 .when_some(self.action_arguments_editor.clone(), |this, editor| {
1974 this.child(
1975 v_flex()
1976 .mt_1p5()
1977 .gap_1()
1978 .child(Label::new("Edit Arguments"))
1979 .child(
1980 div()
1981 .w_full()
1982 .py_1()
1983 .px_1p5()
1984 .rounded_lg()
1985 .bg(theme.editor_background)
1986 .border_1()
1987 .border_color(theme.border_variant)
1988 .child(editor),
1989 ),
1990 )
1991 })
1992 .child(self.context_editor.clone())
1993 .when_some(self.error.as_ref(), |this, error| {
1994 this.child(
1995 Banner::new()
1996 .map(|banner| match error {
1997 InputError::Error(_) => {
1998 banner.severity(ui::Severity::Error)
1999 }
2000 InputError::Warning(_) => {
2001 banner.severity(ui::Severity::Warning)
2002 }
2003 })
2004 // For some reason, the div overflows its container to the
2005 //right. The padding accounts for that.
2006 .child(
2007 div()
2008 .size_full()
2009 .pr_2()
2010 .child(Label::new(error.content())),
2011 ),
2012 )
2013 }),
2014 ),
2015 )
2016 .footer(
2017 ModalFooter::new().end_slot(
2018 h_flex()
2019 .gap_1()
2020 .child(
2021 Button::new("cancel", "Cancel")
2022 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
2023 )
2024 .child(Button::new("save-btn", "Save").on_click(cx.listener(
2025 |this, _event, _window, cx| {
2026 this.save(cx);
2027 },
2028 ))),
2029 ),
2030 ),
2031 )
2032 }
2033}
2034
2035struct KeybindingEditorModalFocusState {
2036 handles: Vec<FocusHandle>,
2037}
2038
2039impl KeybindingEditorModalFocusState {
2040 fn new(
2041 keystrokes: FocusHandle,
2042 action_input: Option<FocusHandle>,
2043 context: FocusHandle,
2044 ) -> Self {
2045 Self {
2046 handles: Vec::from_iter(
2047 [Some(keystrokes), action_input, Some(context)]
2048 .into_iter()
2049 .flatten(),
2050 ),
2051 }
2052 }
2053
2054 fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> {
2055 self.handles
2056 .iter()
2057 .position(|handle| handle.contains_focused(window, cx))
2058 .map(|i| i as i32)
2059 }
2060
2061 fn focus_index(&self, mut index: i32, window: &mut Window) {
2062 if index < 0 {
2063 index = self.handles.len() as i32 - 1;
2064 }
2065 if index >= self.handles.len() as i32 {
2066 index = 0;
2067 }
2068 window.focus(&self.handles[index as usize]);
2069 }
2070
2071 fn focus_next(&self, window: &mut Window, cx: &App) {
2072 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2073 index + 1
2074 } else {
2075 0
2076 };
2077 self.focus_index(index_to_focus, window);
2078 }
2079
2080 fn focus_previous(&self, window: &mut Window, cx: &App) {
2081 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2082 index - 1
2083 } else {
2084 self.handles.len() as i32 - 1
2085 };
2086 self.focus_index(index_to_focus, window);
2087 }
2088}
2089
2090struct KeyContextCompletionProvider {
2091 contexts: Vec<SharedString>,
2092}
2093
2094impl CompletionProvider for KeyContextCompletionProvider {
2095 fn completions(
2096 &self,
2097 _excerpt_id: editor::ExcerptId,
2098 buffer: &Entity<language::Buffer>,
2099 buffer_position: language::Anchor,
2100 _trigger: editor::CompletionContext,
2101 _window: &mut Window,
2102 cx: &mut Context<Editor>,
2103 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
2104 let buffer = buffer.read(cx);
2105 let mut count_back = 0;
2106 for char in buffer.reversed_chars_at(buffer_position) {
2107 if char.is_ascii_alphanumeric() || char == '_' {
2108 count_back += 1;
2109 } else {
2110 break;
2111 }
2112 }
2113 let start_anchor = buffer.anchor_before(
2114 buffer_position
2115 .to_offset(&buffer)
2116 .saturating_sub(count_back),
2117 );
2118 let replace_range = start_anchor..buffer_position;
2119 gpui::Task::ready(Ok(vec![project::CompletionResponse {
2120 completions: self
2121 .contexts
2122 .iter()
2123 .map(|context| project::Completion {
2124 replace_range: replace_range.clone(),
2125 label: language::CodeLabel::plain(context.to_string(), None),
2126 new_text: context.to_string(),
2127 documentation: None,
2128 source: project::CompletionSource::Custom,
2129 icon_path: None,
2130 insert_text_mode: None,
2131 confirm: None,
2132 })
2133 .collect(),
2134 is_incomplete: false,
2135 }]))
2136 }
2137
2138 fn is_completion_trigger(
2139 &self,
2140 _buffer: &Entity<language::Buffer>,
2141 _position: language::Anchor,
2142 text: &str,
2143 _trigger_in_words: bool,
2144 _menu_is_open: bool,
2145 _cx: &mut Context<Editor>,
2146 ) -> bool {
2147 text.chars().last().map_or(false, |last_char| {
2148 last_char.is_ascii_alphanumeric() || last_char == '_'
2149 })
2150 }
2151}
2152
2153async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
2154 let json_language_task = workspace
2155 .read_with(cx, |workspace, cx| {
2156 workspace
2157 .project()
2158 .read(cx)
2159 .languages()
2160 .language_for_name("JSON")
2161 })
2162 .context("Failed to load JSON language")
2163 .log_err();
2164 let json_language = match json_language_task {
2165 Some(task) => task.await.context("Failed to load JSON language").log_err(),
2166 None => None,
2167 };
2168 return json_language.unwrap_or_else(|| {
2169 Arc::new(Language::new(
2170 LanguageConfig {
2171 name: "JSON".into(),
2172 ..Default::default()
2173 },
2174 Some(tree_sitter_json::LANGUAGE.into()),
2175 ))
2176 });
2177}
2178
2179async fn load_keybind_context_language(
2180 workspace: WeakEntity<Workspace>,
2181 cx: &mut AsyncApp,
2182) -> Arc<Language> {
2183 let language_task = workspace
2184 .read_with(cx, |workspace, cx| {
2185 workspace
2186 .project()
2187 .read(cx)
2188 .languages()
2189 .language_for_name("Zed Keybind Context")
2190 })
2191 .context("Failed to load Zed Keybind Context language")
2192 .log_err();
2193 let language = match language_task {
2194 Some(task) => task
2195 .await
2196 .context("Failed to load Zed Keybind Context language")
2197 .log_err(),
2198 None => None,
2199 };
2200 return language.unwrap_or_else(|| {
2201 Arc::new(Language::new(
2202 LanguageConfig {
2203 name: "Zed Keybind Context".into(),
2204 ..Default::default()
2205 },
2206 Some(tree_sitter_rust::LANGUAGE.into()),
2207 ))
2208 });
2209}
2210
2211async fn save_keybinding_update(
2212 create: bool,
2213 existing: ProcessedKeybinding,
2214 new_keystrokes: &[Keystroke],
2215 new_context: Option<&str>,
2216 new_args: Option<&str>,
2217 fs: &Arc<dyn Fs>,
2218 tab_size: usize,
2219) -> anyhow::Result<()> {
2220 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2221 .await
2222 .context("Failed to load keymap file")?;
2223
2224 let existing_keystrokes = existing.keystrokes().unwrap_or_default();
2225 let existing_context = existing
2226 .context
2227 .as_ref()
2228 .and_then(KeybindContextString::local_str);
2229 let existing_args = existing
2230 .action_arguments
2231 .as_ref()
2232 .map(|args| args.text.as_ref());
2233
2234 let target = settings::KeybindUpdateTarget {
2235 context: existing_context,
2236 keystrokes: existing_keystrokes,
2237 action_name: &existing.action_name,
2238 action_arguments: existing_args,
2239 };
2240
2241 let source = settings::KeybindUpdateTarget {
2242 context: new_context,
2243 keystrokes: new_keystrokes,
2244 action_name: &existing.action_name,
2245 action_arguments: new_args,
2246 };
2247
2248 let operation = if !create {
2249 settings::KeybindUpdateOperation::Replace {
2250 target,
2251 target_keybind_source: existing
2252 .source
2253 .as_ref()
2254 .map(|(source, _name)| *source)
2255 .unwrap_or(KeybindSource::User),
2256 source,
2257 }
2258 } else {
2259 settings::KeybindUpdateOperation::Add {
2260 source,
2261 from: Some(target),
2262 }
2263 };
2264 let updated_keymap_contents =
2265 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2266 .context("Failed to update keybinding")?;
2267 fs.write(
2268 paths::keymap_file().as_path(),
2269 updated_keymap_contents.as_bytes(),
2270 )
2271 .await
2272 .context("Failed to write keymap file")?;
2273 Ok(())
2274}
2275
2276async fn remove_keybinding(
2277 existing: ProcessedKeybinding,
2278 fs: &Arc<dyn Fs>,
2279 tab_size: usize,
2280) -> anyhow::Result<()> {
2281 let Some(keystrokes) = existing.keystrokes() else {
2282 anyhow::bail!("Cannot remove a keybinding that does not exist");
2283 };
2284 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2285 .await
2286 .context("Failed to load keymap file")?;
2287
2288 let operation = settings::KeybindUpdateOperation::Remove {
2289 target: settings::KeybindUpdateTarget {
2290 context: existing
2291 .context
2292 .as_ref()
2293 .and_then(KeybindContextString::local_str),
2294 keystrokes,
2295 action_name: &existing.action_name,
2296 action_arguments: existing
2297 .action_arguments
2298 .as_ref()
2299 .map(|arguments| arguments.text.as_ref()),
2300 },
2301 target_keybind_source: existing
2302 .source
2303 .as_ref()
2304 .map(|(source, _name)| *source)
2305 .unwrap_or(KeybindSource::User),
2306 };
2307
2308 let updated_keymap_contents =
2309 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2310 .context("Failed to update keybinding")?;
2311 fs.write(
2312 paths::keymap_file().as_path(),
2313 updated_keymap_contents.as_bytes(),
2314 )
2315 .await
2316 .context("Failed to write keymap file")?;
2317 Ok(())
2318}
2319
2320#[derive(PartialEq, Eq, Debug, Copy, Clone)]
2321enum CloseKeystrokeResult {
2322 Partial,
2323 Close,
2324 None,
2325}
2326
2327#[derive(PartialEq, Eq, Debug, Clone)]
2328enum KeyPress<'a> {
2329 Alt,
2330 Control,
2331 Function,
2332 Shift,
2333 Platform,
2334 Key(&'a String),
2335}
2336
2337struct KeystrokeInput {
2338 keystrokes: Vec<Keystroke>,
2339 placeholder_keystrokes: Option<Vec<Keystroke>>,
2340 highlight_on_focus: bool,
2341 outer_focus_handle: FocusHandle,
2342 inner_focus_handle: FocusHandle,
2343 intercept_subscription: Option<Subscription>,
2344 _focus_subscriptions: [Subscription; 2],
2345 search: bool,
2346 /// Handles tripe escape to stop recording
2347 close_keystrokes: Option<Vec<Keystroke>>,
2348 close_keystrokes_start: Option<usize>,
2349}
2350
2351impl KeystrokeInput {
2352 const KEYSTROKE_COUNT_MAX: usize = 3;
2353
2354 fn new(
2355 placeholder_keystrokes: Option<Vec<Keystroke>>,
2356 window: &mut Window,
2357 cx: &mut Context<Self>,
2358 ) -> Self {
2359 let outer_focus_handle = cx.focus_handle();
2360 let inner_focus_handle = cx.focus_handle();
2361 let _focus_subscriptions = [
2362 cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
2363 cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
2364 ];
2365 Self {
2366 keystrokes: Vec::new(),
2367 placeholder_keystrokes,
2368 highlight_on_focus: true,
2369 inner_focus_handle,
2370 outer_focus_handle,
2371 intercept_subscription: None,
2372 _focus_subscriptions,
2373 search: false,
2374 close_keystrokes: None,
2375 close_keystrokes_start: None,
2376 }
2377 }
2378
2379 fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
2380 self.keystrokes = keystrokes;
2381 self.keystrokes_changed(cx);
2382 }
2383
2384 fn dummy(modifiers: Modifiers) -> Keystroke {
2385 return Keystroke {
2386 modifiers,
2387 key: "".to_string(),
2388 key_char: None,
2389 };
2390 }
2391
2392 fn keystrokes_changed(&self, cx: &mut Context<Self>) {
2393 cx.emit(());
2394 cx.notify();
2395 }
2396
2397 fn key_context() -> KeyContext {
2398 let mut key_context = KeyContext::new_with_defaults();
2399 key_context.add("KeystrokeInput");
2400 key_context
2401 }
2402
2403 fn handle_possible_close_keystroke(
2404 &mut self,
2405 keystroke: &Keystroke,
2406 window: &mut Window,
2407 cx: &mut Context<Self>,
2408 ) -> CloseKeystrokeResult {
2409 let Some(keybind_for_close_action) = window
2410 .highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context())
2411 else {
2412 log::trace!("No keybinding to stop recording keystrokes in keystroke input");
2413 self.close_keystrokes.take();
2414 return CloseKeystrokeResult::None;
2415 };
2416 let action_keystrokes = keybind_for_close_action.keystrokes();
2417
2418 if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
2419 let mut index = 0;
2420
2421 while index < action_keystrokes.len() && index < close_keystrokes.len() {
2422 if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
2423 break;
2424 }
2425 index += 1;
2426 }
2427 if index == close_keystrokes.len() {
2428 if index >= action_keystrokes.len() {
2429 self.close_keystrokes_start.take();
2430 return CloseKeystrokeResult::None;
2431 }
2432 if keystroke.should_match(&action_keystrokes[index]) {
2433 if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 {
2434 self.stop_recording(&StopRecording, window, cx);
2435 return CloseKeystrokeResult::Close;
2436 } else {
2437 close_keystrokes.push(keystroke.clone());
2438 self.close_keystrokes = Some(close_keystrokes);
2439 return CloseKeystrokeResult::Partial;
2440 }
2441 } else {
2442 self.close_keystrokes_start.take();
2443 return CloseKeystrokeResult::None;
2444 }
2445 }
2446 } else if let Some(first_action_keystroke) = action_keystrokes.first()
2447 && keystroke.should_match(first_action_keystroke)
2448 {
2449 self.close_keystrokes = Some(vec![keystroke.clone()]);
2450 return CloseKeystrokeResult::Partial;
2451 }
2452 self.close_keystrokes_start.take();
2453 return CloseKeystrokeResult::None;
2454 }
2455
2456 fn on_modifiers_changed(
2457 &mut self,
2458 event: &ModifiersChangedEvent,
2459 _window: &mut Window,
2460 cx: &mut Context<Self>,
2461 ) {
2462 let keystrokes_len = self.keystrokes.len();
2463
2464 if let Some(last) = self.keystrokes.last_mut()
2465 && last.key.is_empty()
2466 && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
2467 {
2468 if self.search {
2469 last.modifiers = last.modifiers.xor(&event.modifiers);
2470 } else if !event.modifiers.modified() {
2471 self.keystrokes.pop();
2472 } else {
2473 last.modifiers = event.modifiers;
2474 }
2475
2476 self.keystrokes_changed(cx);
2477 } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
2478 self.keystrokes.push(Self::dummy(event.modifiers));
2479 self.keystrokes_changed(cx);
2480 }
2481 cx.stop_propagation();
2482 }
2483
2484 fn handle_keystroke(
2485 &mut self,
2486 keystroke: &Keystroke,
2487 window: &mut Window,
2488 cx: &mut Context<Self>,
2489 ) {
2490 let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
2491 if close_keystroke_result != CloseKeystrokeResult::Close {
2492 let key_len = self.keystrokes.len();
2493 if let Some(last) = self.keystrokes.last_mut()
2494 && last.key.is_empty()
2495 && key_len <= Self::KEYSTROKE_COUNT_MAX
2496 {
2497 if self.search {
2498 last.key = keystroke.key.clone();
2499 self.keystrokes_changed(cx);
2500 cx.stop_propagation();
2501 return;
2502 } else {
2503 self.keystrokes.pop();
2504 }
2505 }
2506 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
2507 if close_keystroke_result == CloseKeystrokeResult::Partial
2508 && self.close_keystrokes_start.is_none()
2509 {
2510 self.close_keystrokes_start = Some(self.keystrokes.len());
2511 }
2512 self.keystrokes.push(keystroke.clone());
2513 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
2514 self.keystrokes.push(Self::dummy(keystroke.modifiers));
2515 }
2516 }
2517 }
2518 self.keystrokes_changed(cx);
2519 cx.stop_propagation();
2520 }
2521
2522 fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
2523 if self.intercept_subscription.is_none() {
2524 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
2525 this.handle_keystroke(&event.keystroke, window, cx);
2526 });
2527 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
2528 }
2529 }
2530
2531 fn on_inner_focus_out(
2532 &mut self,
2533 _event: gpui::FocusOutEvent,
2534 _window: &mut Window,
2535 cx: &mut Context<Self>,
2536 ) {
2537 self.intercept_subscription.take();
2538 cx.notify();
2539 }
2540
2541 fn keystrokes(&self) -> &[Keystroke] {
2542 if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
2543 && self.keystrokes.is_empty()
2544 {
2545 return placeholders;
2546 }
2547 if !self.search
2548 && self
2549 .keystrokes
2550 .last()
2551 .map_or(false, |last| last.key.is_empty())
2552 {
2553 return &self.keystrokes[..self.keystrokes.len() - 1];
2554 }
2555 return &self.keystrokes;
2556 }
2557
2558 fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
2559 let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
2560 && self.keystrokes.is_empty()
2561 {
2562 if is_recording {
2563 &[]
2564 } else {
2565 placeholders.as_slice()
2566 }
2567 } else {
2568 &self.keystrokes
2569 };
2570 keystrokes.iter().map(move |keystroke| {
2571 h_flex().children(ui::render_keystroke(
2572 keystroke,
2573 Some(Color::Default),
2574 Some(rems(0.875).into()),
2575 ui::PlatformStyle::platform(),
2576 false,
2577 ))
2578 })
2579 }
2580
2581 fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
2582 self.inner_focus_handle.clone()
2583 }
2584
2585 fn set_search_mode(&mut self, search: bool) {
2586 self.search = search;
2587 }
2588
2589 fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
2590 if !self.outer_focus_handle.is_focused(window) {
2591 return;
2592 }
2593 self.clear_keystrokes(&ClearKeystrokes, window, cx);
2594 window.focus(&self.inner_focus_handle);
2595 cx.notify();
2596 }
2597
2598 fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
2599 if !self.inner_focus_handle.is_focused(window) {
2600 return;
2601 }
2602 window.focus(&self.outer_focus_handle);
2603 if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() {
2604 self.keystrokes.drain(close_keystrokes_start..);
2605 }
2606 self.close_keystrokes.take();
2607 cx.notify();
2608 }
2609
2610 fn clear_keystrokes(
2611 &mut self,
2612 _: &ClearKeystrokes,
2613 _window: &mut Window,
2614 cx: &mut Context<Self>,
2615 ) {
2616 self.keystrokes.clear();
2617 self.keystrokes_changed(cx);
2618 }
2619}
2620
2621impl EventEmitter<()> for KeystrokeInput {}
2622
2623impl Focusable for KeystrokeInput {
2624 fn focus_handle(&self, _cx: &App) -> FocusHandle {
2625 self.outer_focus_handle.clone()
2626 }
2627}
2628
2629impl Render for KeystrokeInput {
2630 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2631 let colors = cx.theme().colors();
2632 let is_focused = self.outer_focus_handle.contains_focused(window, cx);
2633 let is_recording = self.inner_focus_handle.is_focused(window);
2634
2635 let horizontal_padding = rems_from_px(64.);
2636
2637 let recording_bg_color = colors
2638 .editor_background
2639 .blend(colors.text_accent.opacity(0.1));
2640
2641 let recording_pulse = || {
2642 Icon::new(IconName::Circle)
2643 .size(IconSize::Small)
2644 .color(Color::Error)
2645 .with_animation(
2646 "recording-pulse",
2647 Animation::new(std::time::Duration::from_secs(2))
2648 .repeat()
2649 .with_easing(gpui::pulsating_between(0.4, 0.8)),
2650 {
2651 let color = Color::Error.color(cx);
2652 move |this, delta| this.color(Color::Custom(color.opacity(delta)))
2653 },
2654 )
2655 };
2656
2657 let recording_indicator = h_flex()
2658 .h_4()
2659 .pr_1()
2660 .gap_0p5()
2661 .border_1()
2662 .border_color(colors.border)
2663 .bg(colors
2664 .editor_background
2665 .blend(colors.text_accent.opacity(0.1)))
2666 .rounded_sm()
2667 .child(recording_pulse())
2668 .child(
2669 Label::new("REC")
2670 .size(LabelSize::XSmall)
2671 .weight(FontWeight::SEMIBOLD)
2672 .color(Color::Error),
2673 );
2674
2675 let search_indicator = h_flex()
2676 .h_4()
2677 .pr_1()
2678 .gap_0p5()
2679 .border_1()
2680 .border_color(colors.border)
2681 .bg(colors
2682 .editor_background
2683 .blend(colors.text_accent.opacity(0.1)))
2684 .rounded_sm()
2685 .child(recording_pulse())
2686 .child(
2687 Label::new("SEARCH")
2688 .size(LabelSize::XSmall)
2689 .weight(FontWeight::SEMIBOLD)
2690 .color(Color::Accent),
2691 );
2692
2693 let record_icon = if self.search {
2694 IconName::MagnifyingGlass
2695 } else {
2696 IconName::PlayFilled
2697 };
2698
2699 return h_flex()
2700 .id("keystroke-input")
2701 .track_focus(&self.outer_focus_handle)
2702 .py_2()
2703 .px_3()
2704 .gap_2()
2705 .min_h_10()
2706 .w_full()
2707 .flex_1()
2708 .justify_between()
2709 .rounded_lg()
2710 .overflow_hidden()
2711 .map(|this| {
2712 if is_recording {
2713 this.bg(recording_bg_color)
2714 } else {
2715 this.bg(colors.editor_background)
2716 }
2717 })
2718 .border_1()
2719 .border_color(colors.border_variant)
2720 .when(is_focused, |parent| {
2721 parent.border_color(colors.border_focused)
2722 })
2723 .key_context(Self::key_context())
2724 .on_action(cx.listener(Self::start_recording))
2725 .on_action(cx.listener(Self::stop_recording))
2726 .child(
2727 h_flex()
2728 .w(horizontal_padding)
2729 .gap_0p5()
2730 .justify_start()
2731 .flex_none()
2732 .when(is_recording, |this| {
2733 this.map(|this| {
2734 if self.search {
2735 this.child(search_indicator)
2736 } else {
2737 this.child(recording_indicator)
2738 }
2739 })
2740 }),
2741 )
2742 .child(
2743 h_flex()
2744 .id("keystroke-input-inner")
2745 .track_focus(&self.inner_focus_handle)
2746 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
2747 .size_full()
2748 .when(self.highlight_on_focus, |this| {
2749 this.focus(|mut style| {
2750 style.border_color = Some(colors.border_focused);
2751 style
2752 })
2753 })
2754 .w_full()
2755 .min_w_0()
2756 .justify_center()
2757 .flex_wrap()
2758 .gap(ui::DynamicSpacing::Base04.rems(cx))
2759 .children(self.render_keystrokes(is_recording)),
2760 )
2761 .child(
2762 h_flex()
2763 .w(horizontal_padding)
2764 .gap_0p5()
2765 .justify_end()
2766 .flex_none()
2767 .map(|this| {
2768 if is_recording {
2769 this.child(
2770 IconButton::new("stop-record-btn", IconName::StopFilled)
2771 .shape(ui::IconButtonShape::Square)
2772 .map(|this| {
2773 this.tooltip(Tooltip::for_action_title(
2774 if self.search {
2775 "Stop Searching"
2776 } else {
2777 "Stop Recording"
2778 },
2779 &StopRecording,
2780 ))
2781 })
2782 .icon_color(Color::Error)
2783 .on_click(cx.listener(|this, _event, window, cx| {
2784 this.stop_recording(&StopRecording, window, cx);
2785 })),
2786 )
2787 } else {
2788 this.child(
2789 IconButton::new("record-btn", record_icon)
2790 .shape(ui::IconButtonShape::Square)
2791 .map(|this| {
2792 this.tooltip(Tooltip::for_action_title(
2793 if self.search {
2794 "Start Searching"
2795 } else {
2796 "Start Recording"
2797 },
2798 &StartRecording,
2799 ))
2800 })
2801 .when(!is_focused, |this| this.icon_color(Color::Muted))
2802 .on_click(cx.listener(|this, _event, window, cx| {
2803 this.start_recording(&StartRecording, window, cx);
2804 })),
2805 )
2806 }
2807 })
2808 .child(
2809 IconButton::new("clear-btn", IconName::Delete)
2810 .shape(ui::IconButtonShape::Square)
2811 .tooltip(Tooltip::for_action_title(
2812 "Clear Keystrokes",
2813 &ClearKeystrokes,
2814 ))
2815 .when(!is_recording || !is_focused, |this| {
2816 this.icon_color(Color::Muted)
2817 })
2818 .on_click(cx.listener(|this, _event, window, cx| {
2819 this.clear_keystrokes(&ClearKeystrokes, window, cx);
2820 })),
2821 ),
2822 );
2823 }
2824}
2825
2826fn collect_contexts_from_assets() -> Vec<SharedString> {
2827 let mut keymap_assets = vec![
2828 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
2829 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
2830 ];
2831 keymap_assets.extend(
2832 BaseKeymap::OPTIONS
2833 .iter()
2834 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
2835 .map(util::asset_str::<SettingsAssets>),
2836 );
2837
2838 let mut contexts = HashSet::default();
2839
2840 for keymap_asset in keymap_assets {
2841 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
2842 continue;
2843 };
2844
2845 for section in keymap.sections() {
2846 let context_expr = §ion.context;
2847 let mut queue = Vec::new();
2848 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
2849 continue;
2850 };
2851
2852 queue.push(root_context);
2853 while let Some(context) = queue.pop() {
2854 match context {
2855 gpui::KeyBindingContextPredicate::Identifier(ident) => {
2856 contexts.insert(ident);
2857 }
2858 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
2859 contexts.insert(ident_a);
2860 contexts.insert(ident_b);
2861 }
2862 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
2863 contexts.insert(ident_a);
2864 contexts.insert(ident_b);
2865 }
2866 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
2867 queue.push(*ctx_a);
2868 queue.push(*ctx_b);
2869 }
2870 gpui::KeyBindingContextPredicate::Not(ctx) => {
2871 queue.push(*ctx);
2872 }
2873 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
2874 queue.push(*ctx_a);
2875 queue.push(*ctx_b);
2876 }
2877 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
2878 queue.push(*ctx_a);
2879 queue.push(*ctx_b);
2880 }
2881 }
2882 }
2883 }
2884 }
2885
2886 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
2887 contexts.sort();
2888
2889 return contexts;
2890}
2891
2892impl SerializableItem for KeymapEditor {
2893 fn serialized_item_kind() -> &'static str {
2894 "KeymapEditor"
2895 }
2896
2897 fn cleanup(
2898 workspace_id: workspace::WorkspaceId,
2899 alive_items: Vec<workspace::ItemId>,
2900 _window: &mut Window,
2901 cx: &mut App,
2902 ) -> gpui::Task<gpui::Result<()>> {
2903 workspace::delete_unloaded_items(
2904 alive_items,
2905 workspace_id,
2906 "keybinding_editors",
2907 &KEYBINDING_EDITORS,
2908 cx,
2909 )
2910 }
2911
2912 fn deserialize(
2913 _project: Entity<project::Project>,
2914 workspace: WeakEntity<Workspace>,
2915 workspace_id: workspace::WorkspaceId,
2916 item_id: workspace::ItemId,
2917 window: &mut Window,
2918 cx: &mut App,
2919 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
2920 window.spawn(cx, async move |cx| {
2921 if KEYBINDING_EDITORS
2922 .get_keybinding_editor(item_id, workspace_id)?
2923 .is_some()
2924 {
2925 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
2926 } else {
2927 Err(anyhow!("No keybinding editor to deserialize"))
2928 }
2929 })
2930 }
2931
2932 fn serialize(
2933 &mut self,
2934 workspace: &mut Workspace,
2935 item_id: workspace::ItemId,
2936 _closing: bool,
2937 _window: &mut Window,
2938 cx: &mut ui::Context<Self>,
2939 ) -> Option<gpui::Task<gpui::Result<()>>> {
2940 let workspace_id = workspace.database_id()?;
2941 Some(cx.background_spawn(async move {
2942 KEYBINDING_EDITORS
2943 .save_keybinding_editor(item_id, workspace_id)
2944 .await
2945 }))
2946 }
2947
2948 fn should_serialize(&self, _event: &Self::Event) -> bool {
2949 false
2950 }
2951}
2952
2953mod persistence {
2954 use db::{define_connection, query, sqlez_macros::sql};
2955 use workspace::WorkspaceDb;
2956
2957 define_connection! {
2958 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
2959 &[sql!(
2960 CREATE TABLE keybinding_editors (
2961 workspace_id INTEGER,
2962 item_id INTEGER UNIQUE,
2963
2964 PRIMARY KEY(workspace_id, item_id),
2965 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2966 ON DELETE CASCADE
2967 ) STRICT;
2968 )];
2969 }
2970
2971 impl KeybindingEditorDb {
2972 query! {
2973 pub async fn save_keybinding_editor(
2974 item_id: workspace::ItemId,
2975 workspace_id: workspace::WorkspaceId
2976 ) -> Result<()> {
2977 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
2978 VALUES (?, ?)
2979 }
2980 }
2981
2982 query! {
2983 pub fn get_keybinding_editor(
2984 item_id: workspace::ItemId,
2985 workspace_id: workspace::WorkspaceId
2986 ) -> Result<Option<workspace::ItemId>> {
2987 SELECT item_id
2988 FROM keybinding_editors
2989 WHERE item_id = ? AND workspace_id = ?
2990 }
2991 }
2992 }
2993}
2994
2995/// Iterator that yields KeyPress values from a slice of Keystrokes
2996struct KeyPressIterator<'a> {
2997 keystrokes: &'a [Keystroke],
2998 current_keystroke_index: usize,
2999 current_key_press_index: usize,
3000}
3001
3002impl<'a> KeyPressIterator<'a> {
3003 fn new(keystrokes: &'a [Keystroke]) -> Self {
3004 Self {
3005 keystrokes,
3006 current_keystroke_index: 0,
3007 current_key_press_index: 0,
3008 }
3009 }
3010}
3011
3012impl<'a> Iterator for KeyPressIterator<'a> {
3013 type Item = KeyPress<'a>;
3014
3015 fn next(&mut self) -> Option<Self::Item> {
3016 loop {
3017 let keystroke = self.keystrokes.get(self.current_keystroke_index)?;
3018
3019 match self.current_key_press_index {
3020 0 => {
3021 self.current_key_press_index = 1;
3022 if keystroke.modifiers.platform {
3023 return Some(KeyPress::Platform);
3024 }
3025 }
3026 1 => {
3027 self.current_key_press_index = 2;
3028 if keystroke.modifiers.alt {
3029 return Some(KeyPress::Alt);
3030 }
3031 }
3032 2 => {
3033 self.current_key_press_index = 3;
3034 if keystroke.modifiers.control {
3035 return Some(KeyPress::Control);
3036 }
3037 }
3038 3 => {
3039 self.current_key_press_index = 4;
3040 if keystroke.modifiers.shift {
3041 return Some(KeyPress::Shift);
3042 }
3043 }
3044 4 => {
3045 self.current_key_press_index = 5;
3046 if keystroke.modifiers.function {
3047 return Some(KeyPress::Function);
3048 }
3049 }
3050 _ => {
3051 self.current_keystroke_index += 1;
3052 self.current_key_press_index = 0;
3053
3054 if keystroke.key.is_empty() {
3055 continue;
3056 }
3057 return Some(KeyPress::Key(&keystroke.key));
3058 }
3059 }
3060 }
3061 }
3062}