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