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