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