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 AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
14 FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy,
15 StyledText, Subscription, WeakEntity, actions, div,
16};
17use language::{Language, LanguageConfig, ToOffset as _};
18use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
19
20use util::ResultExt;
21
22use ui::{
23 ActiveTheme as _, App, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString,
24 Styled as _, Tooltip, Window, prelude::*, right_click_menu,
25};
26use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
27
28use crate::{
29 SettingsUiFeatureFlag,
30 keybindings::persistence::KEYBINDING_EDITORS,
31 ui_components::table::{Table, TableInteractionState},
32};
33
34const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
35
36actions!(
37 zed,
38 [
39 /// Opens the keymap editor.
40 OpenKeymapEditor
41 ]
42);
43
44const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
45actions!(
46 keymap_editor,
47 [
48 /// Edits the selected key binding.
49 EditBinding,
50 /// Creates a new key binding for the selected action.
51 CreateBinding,
52 /// Copies the action name to clipboard.
53 CopyAction,
54 /// Copies the context predicate to clipboard.
55 CopyContext
56 ]
57);
58
59pub fn init(cx: &mut App) {
60 let keymap_event_channel = KeymapEventChannel::new();
61 cx.set_global(keymap_event_channel);
62
63 cx.on_action(|_: &OpenKeymapEditor, cx| {
64 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
65 let existing = workspace
66 .active_pane()
67 .read(cx)
68 .items()
69 .find_map(|item| item.downcast::<KeymapEditor>());
70
71 if let Some(existing) = existing {
72 workspace.activate_item(&existing, true, true, window, cx);
73 } else {
74 let keymap_editor =
75 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
76 workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
77 }
78 });
79 });
80
81 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
82 let Some(window) = window else { return };
83
84 let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
85
86 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
87 filter.hide_action_types(&keymap_ui_actions);
88 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
89 });
90
91 cx.observe_flag::<SettingsUiFeatureFlag, _>(
92 window,
93 move |is_enabled, _workspace, _, cx| {
94 if is_enabled {
95 command_palette_hooks::CommandPaletteFilter::update_global(
96 cx,
97 |filter, _cx| {
98 filter.show_action_types(keymap_ui_actions.iter());
99 filter.show_namespace(KEYMAP_EDITOR_NAMESPACE);
100 },
101 );
102 } else {
103 command_palette_hooks::CommandPaletteFilter::update_global(
104 cx,
105 |filter, _cx| {
106 filter.hide_action_types(&keymap_ui_actions);
107 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
108 },
109 );
110 }
111 },
112 )
113 .detach();
114 })
115 .detach();
116
117 register_serializable_item::<KeymapEditor>(cx);
118}
119
120pub struct KeymapEventChannel {}
121
122impl Global for KeymapEventChannel {}
123
124impl KeymapEventChannel {
125 fn new() -> Self {
126 Self {}
127 }
128
129 pub fn trigger_keymap_changed(cx: &mut App) {
130 let Some(_event_channel) = cx.try_global::<Self>() else {
131 // don't panic if no global defined. This usually happens in tests
132 return;
133 };
134 cx.update_global(|_event_channel: &mut Self, _| {
135 /* triggers observers in KeymapEditors */
136 });
137 }
138}
139
140#[derive(Default, PartialEq)]
141enum FilterState {
142 #[default]
143 All,
144 Conflicts,
145}
146
147impl FilterState {
148 fn invert(&self) -> Self {
149 match self {
150 FilterState::All => FilterState::Conflicts,
151 FilterState::Conflicts => FilterState::All,
152 }
153 }
154}
155
156struct KeymapEditor {
157 workspace: WeakEntity<Workspace>,
158 focus_handle: FocusHandle,
159 _keymap_subscription: Subscription,
160 keybindings: Vec<ProcessedKeybinding>,
161 conflicts: Vec<usize>,
162 filter_state: FilterState,
163 // corresponds 1 to 1 with keybindings
164 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
165 matches: Vec<StringMatch>,
166 table_interaction_state: Entity<TableInteractionState>,
167 filter_editor: Entity<Editor>,
168 selected_index: Option<usize>,
169}
170
171impl EventEmitter<()> for KeymapEditor {}
172
173impl Focusable for KeymapEditor {
174 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
175 return self.filter_editor.focus_handle(cx);
176 }
177}
178
179impl KeymapEditor {
180 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
181 let focus_handle = cx.focus_handle();
182
183 let _keymap_subscription =
184 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
185 let table_interaction_state = TableInteractionState::new(window, cx);
186
187 let filter_editor = cx.new(|cx| {
188 let mut editor = Editor::single_line(window, cx);
189 editor.set_placeholder_text("Filter action names…", cx);
190 editor
191 });
192
193 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
194 if !matches!(e, EditorEvent::BufferEdited) {
195 return;
196 }
197
198 this.update_matches(cx);
199 })
200 .detach();
201
202 let mut this = Self {
203 workspace,
204 keybindings: vec![],
205 conflicts: Vec::default(),
206 filter_state: FilterState::default(),
207 string_match_candidates: Arc::new(vec![]),
208 matches: vec![],
209 focus_handle: focus_handle.clone(),
210 _keymap_subscription,
211 table_interaction_state,
212 filter_editor,
213 selected_index: None,
214 };
215
216 this.update_keybindings(cx);
217
218 this
219 }
220
221 fn current_query(&self, cx: &mut Context<Self>) -> String {
222 self.filter_editor.read(cx).text(cx)
223 }
224
225 fn update_matches(&self, cx: &mut Context<Self>) {
226 let query = self.current_query(cx);
227
228 cx.spawn(async move |this, cx| Self::process_query(this, query, cx).await)
229 .detach();
230 }
231
232 async fn process_query(
233 this: WeakEntity<Self>,
234 query: String,
235 cx: &mut AsyncApp,
236 ) -> anyhow::Result<()> {
237 let query = command_palette::normalize_action_query(&query);
238 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
239 (this.string_match_candidates.clone(), this.keybindings.len())
240 })?;
241 let executor = cx.background_executor().clone();
242 let mut matches = fuzzy::match_strings(
243 &string_match_candidates,
244 &query,
245 true,
246 true,
247 keybind_count,
248 &Default::default(),
249 executor,
250 )
251 .await;
252 this.update(cx, |this, cx| {
253 match this.filter_state {
254 FilterState::Conflicts => {
255 matches.retain(|candidate| this.conflicts.contains(&candidate.candidate_id));
256 }
257 FilterState::All => {}
258 }
259
260 if query.is_empty() {
261 // apply default sort
262 // sorts by source precedence, and alphabetically by action name within each source
263 matches.sort_by_key(|match_item| {
264 let keybind = &this.keybindings[match_item.candidate_id];
265 let source = keybind.source.as_ref().map(|s| s.0);
266 use KeybindSource::*;
267 let source_precedence = match source {
268 Some(User) => 0,
269 Some(Vim) => 1,
270 Some(Base) => 2,
271 Some(Default) => 3,
272 None => 4,
273 };
274 return (source_precedence, keybind.action_name.as_ref());
275 });
276 }
277 this.selected_index.take();
278 this.scroll_to_item(0, ScrollStrategy::Top, cx);
279 this.matches = matches;
280 cx.notify();
281 })
282 }
283
284 fn process_bindings(
285 json_language: Arc<Language>,
286 rust_language: Arc<Language>,
287 cx: &mut App,
288 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
289 let key_bindings_ptr = cx.key_bindings();
290 let lock = key_bindings_ptr.borrow();
291 let key_bindings = lock.bindings();
292 let mut unmapped_action_names =
293 HashSet::from_iter(cx.all_action_names().into_iter().copied());
294 let action_documentation = cx.action_documentation();
295 let mut generator = KeymapFile::action_schema_generator();
296 let action_schema = HashMap::from_iter(
297 cx.action_schemas(&mut generator)
298 .into_iter()
299 .filter_map(|(name, schema)| schema.map(|schema| (name, schema))),
300 );
301
302 let mut processed_bindings = Vec::new();
303 let mut string_match_candidates = Vec::new();
304
305 for key_binding in key_bindings {
306 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
307
308 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
309 let ui_key_binding = Some(
310 ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
311 .vim_mode(source == Some(settings::KeybindSource::Vim)),
312 );
313
314 let context = key_binding
315 .predicate()
316 .map(|predicate| {
317 KeybindContextString::Local(predicate.to_string().into(), rust_language.clone())
318 })
319 .unwrap_or(KeybindContextString::Global);
320
321 let source = source.map(|source| (source, source.name().into()));
322
323 let action_name = key_binding.action().name();
324 unmapped_action_names.remove(&action_name);
325 let action_input = key_binding
326 .action_input()
327 .map(|input| SyntaxHighlightedText::new(input, json_language.clone()));
328 let action_docs = action_documentation.get(action_name).copied();
329
330 let index = processed_bindings.len();
331 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
332 processed_bindings.push(ProcessedKeybinding {
333 keystroke_text: keystroke_text.into(),
334 ui_key_binding,
335 action_name: action_name.into(),
336 action_input,
337 action_docs,
338 action_schema: action_schema.get(action_name).cloned(),
339 context: Some(context),
340 source,
341 });
342 string_match_candidates.push(string_match_candidate);
343 }
344
345 let empty = SharedString::new_static("");
346 for action_name in unmapped_action_names.into_iter() {
347 let index = processed_bindings.len();
348 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
349 processed_bindings.push(ProcessedKeybinding {
350 keystroke_text: empty.clone(),
351 ui_key_binding: None,
352 action_name: action_name.into(),
353 action_input: None,
354 action_docs: action_documentation.get(action_name).copied(),
355 action_schema: action_schema.get(action_name).cloned(),
356 context: None,
357 source: None,
358 });
359 string_match_candidates.push(string_match_candidate);
360 }
361
362 (processed_bindings, string_match_candidates)
363 }
364
365 fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
366 let workspace = self.workspace.clone();
367 cx.spawn(async move |this, cx| {
368 let json_language = load_json_language(workspace.clone(), cx).await;
369 let rust_language = load_rust_language(workspace.clone(), cx).await;
370
371 let query = this.update(cx, |this, cx| {
372 let (key_bindings, string_match_candidates) =
373 Self::process_bindings(json_language, rust_language, cx);
374
375 let mut conflicts: HashMap<_, Vec<usize>> = HashMap::default();
376
377 key_bindings
378 .iter()
379 .enumerate()
380 .filter(|(_, binding)| !binding.keystroke_text.is_empty())
381 .for_each(|(index, binding)| {
382 conflicts
383 .entry(binding.get_action_mapping())
384 .or_default()
385 .push(index);
386 });
387
388 this.conflicts = conflicts
389 .into_values()
390 .filter(|indices| indices.len() > 1)
391 .flatten()
392 .collect();
393
394 if this.conflicts.is_empty() {
395 this.filter_state = FilterState::All;
396 }
397
398 this.keybindings = key_bindings;
399 this.string_match_candidates = Arc::new(string_match_candidates);
400 this.matches = this
401 .string_match_candidates
402 .iter()
403 .enumerate()
404 .map(|(ix, candidate)| StringMatch {
405 candidate_id: ix,
406 score: 0.0,
407 positions: vec![],
408 string: candidate.string.clone(),
409 })
410 .collect();
411 this.current_query(cx)
412 })?;
413 // calls cx.notify
414 Self::process_query(this, query, cx).await
415 })
416 .detach_and_log_err(cx);
417 }
418
419 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
420 let mut dispatch_context = KeyContext::new_with_defaults();
421 dispatch_context.add("KeymapEditor");
422 dispatch_context.add("menu");
423
424 dispatch_context
425 }
426
427 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
428 let index = usize::min(index, self.matches.len().saturating_sub(1));
429 self.table_interaction_state.update(cx, |this, _cx| {
430 this.scroll_handle.scroll_to_item(index, strategy);
431 });
432 }
433
434 fn focus_search(
435 &mut self,
436 _: &search::FocusSearch,
437 window: &mut Window,
438 cx: &mut Context<Self>,
439 ) {
440 if !self
441 .filter_editor
442 .focus_handle(cx)
443 .contains_focused(window, cx)
444 {
445 window.focus(&self.filter_editor.focus_handle(cx));
446 } else {
447 self.filter_editor.update(cx, |editor, cx| {
448 editor.select_all(&Default::default(), window, cx);
449 });
450 }
451 self.selected_index.take();
452 }
453
454 fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
455 self.selected_index
456 .and_then(|match_index| self.matches.get(match_index))
457 .map(|r#match| r#match.candidate_id)
458 .and_then(|keybind_index| self.keybindings.get(keybind_index))
459 }
460
461 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
462 if let Some(selected) = self.selected_index {
463 let selected = selected + 1;
464 if selected >= self.matches.len() {
465 self.select_last(&Default::default(), window, cx);
466 } else {
467 self.selected_index = Some(selected);
468 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
469 cx.notify();
470 }
471 } else {
472 self.select_first(&Default::default(), window, cx);
473 }
474 }
475
476 fn select_previous(
477 &mut self,
478 _: &menu::SelectPrevious,
479 window: &mut Window,
480 cx: &mut Context<Self>,
481 ) {
482 if let Some(selected) = self.selected_index {
483 if selected == 0 {
484 return;
485 }
486
487 let selected = selected - 1;
488
489 if selected >= self.matches.len() {
490 self.select_last(&Default::default(), window, cx);
491 } else {
492 self.selected_index = Some(selected);
493 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
494 cx.notify();
495 }
496 } else {
497 self.select_last(&Default::default(), window, cx);
498 }
499 }
500
501 fn select_first(
502 &mut self,
503 _: &menu::SelectFirst,
504 _window: &mut Window,
505 cx: &mut Context<Self>,
506 ) {
507 if self.matches.get(0).is_some() {
508 self.selected_index = Some(0);
509 self.scroll_to_item(0, ScrollStrategy::Center, cx);
510 cx.notify();
511 }
512 }
513
514 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
515 if self.matches.last().is_some() {
516 let index = self.matches.len() - 1;
517 self.selected_index = Some(index);
518 self.scroll_to_item(index, ScrollStrategy::Center, cx);
519 cx.notify();
520 }
521 }
522
523 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
524 self.open_edit_keybinding_modal(false, window, cx);
525 }
526
527 fn open_edit_keybinding_modal(
528 &mut self,
529 create: bool,
530 window: &mut Window,
531 cx: &mut Context<Self>,
532 ) {
533 let Some(keybind) = self.selected_binding().cloned() else {
534 return;
535 };
536 self.workspace
537 .update(cx, |workspace, cx| {
538 let fs = workspace.app_state().fs.clone();
539 let workspace_weak = cx.weak_entity();
540 workspace.toggle_modal(window, cx, |window, cx| {
541 let modal =
542 KeybindingEditorModal::new(create, keybind, workspace_weak, fs, window, cx);
543 window.focus(&modal.focus_handle(cx));
544 modal
545 });
546 })
547 .log_err();
548 }
549
550 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
551 self.open_edit_keybinding_modal(false, window, cx);
552 }
553
554 fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
555 self.open_edit_keybinding_modal(true, window, cx);
556 }
557
558 fn copy_context_to_clipboard(
559 &mut self,
560 _: &CopyContext,
561 _window: &mut Window,
562 cx: &mut Context<Self>,
563 ) {
564 let context = self
565 .selected_binding()
566 .and_then(|binding| binding.context.as_ref())
567 .and_then(KeybindContextString::local_str)
568 .map(|context| context.to_string());
569 let Some(context) = context else {
570 return;
571 };
572 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
573 }
574
575 fn copy_action_to_clipboard(
576 &mut self,
577 _: &CopyAction,
578 _window: &mut Window,
579 cx: &mut Context<Self>,
580 ) {
581 let action = self
582 .selected_binding()
583 .map(|binding| binding.action_name.to_string());
584 let Some(action) = action else {
585 return;
586 };
587 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
588 }
589}
590
591#[derive(Clone)]
592struct ProcessedKeybinding {
593 keystroke_text: SharedString,
594 ui_key_binding: Option<ui::KeyBinding>,
595 action_name: SharedString,
596 action_input: Option<SyntaxHighlightedText>,
597 action_docs: Option<&'static str>,
598 action_schema: Option<schemars::Schema>,
599 context: Option<KeybindContextString>,
600 source: Option<(KeybindSource, SharedString)>,
601}
602
603impl ProcessedKeybinding {
604 fn get_action_mapping(&self) -> (SharedString, Option<SharedString>) {
605 (
606 self.keystroke_text.clone(),
607 self.context
608 .as_ref()
609 .and_then(|context| context.local())
610 .cloned(),
611 )
612 }
613}
614
615#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
616enum KeybindContextString {
617 Global,
618 Local(SharedString, Arc<Language>),
619}
620
621impl KeybindContextString {
622 const GLOBAL: SharedString = SharedString::new_static("<global>");
623
624 pub fn local(&self) -> Option<&SharedString> {
625 match self {
626 KeybindContextString::Global => None,
627 KeybindContextString::Local(name, _) => Some(name),
628 }
629 }
630
631 pub fn local_str(&self) -> Option<&str> {
632 match self {
633 KeybindContextString::Global => None,
634 KeybindContextString::Local(name, _) => Some(name),
635 }
636 }
637}
638
639impl RenderOnce for KeybindContextString {
640 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
641 match self {
642 KeybindContextString::Global => {
643 muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
644 }
645 KeybindContextString::Local(name, language) => {
646 SyntaxHighlightedText::new(name, language).into_any_element()
647 }
648 }
649 }
650}
651
652fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
653 let len = text.len();
654 StyledText::new(text).with_highlights([(
655 0..len,
656 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
657 )])
658}
659
660impl Item for KeymapEditor {
661 type Event = ();
662
663 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
664 "Keymap Editor".into()
665 }
666}
667
668impl Render for KeymapEditor {
669 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
670 let row_count = self.matches.len();
671 let theme = cx.theme();
672
673 v_flex()
674 .id("keymap-editor")
675 .track_focus(&self.focus_handle)
676 .key_context(self.dispatch_context(window, cx))
677 .on_action(cx.listener(Self::select_next))
678 .on_action(cx.listener(Self::select_previous))
679 .on_action(cx.listener(Self::select_first))
680 .on_action(cx.listener(Self::select_last))
681 .on_action(cx.listener(Self::focus_search))
682 .on_action(cx.listener(Self::confirm))
683 .on_action(cx.listener(Self::edit_binding))
684 .on_action(cx.listener(Self::create_binding))
685 .on_action(cx.listener(Self::copy_action_to_clipboard))
686 .on_action(cx.listener(Self::copy_context_to_clipboard))
687 .size_full()
688 .p_2()
689 .gap_1()
690 .bg(theme.colors().editor_background)
691 .child(
692 h_flex()
693 .key_context({
694 let mut context = KeyContext::new_with_defaults();
695 context.add("BufferSearchBar");
696 context
697 })
698 .h_8()
699 .pl_2()
700 .pr_1()
701 .py_1()
702 .border_1()
703 .border_color(theme.colors().border)
704 .rounded_lg()
705 .child(self.filter_editor.clone())
706 .when(!self.conflicts.is_empty(), |this| {
707 this.child(
708 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
709 .tooltip(Tooltip::text(match self.filter_state {
710 FilterState::All => "Show conflicts",
711 FilterState::Conflicts => "Hide conflicts",
712 }))
713 .selected_icon_color(Color::Error)
714 .toggle_state(matches!(self.filter_state, FilterState::Conflicts))
715 .on_click(cx.listener(|this, _, _, cx| {
716 this.filter_state = this.filter_state.invert();
717 this.update_matches(cx);
718 })),
719 )
720 }),
721 )
722 .child(
723 Table::new()
724 .interactable(&self.table_interaction_state)
725 .striped()
726 .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
727 .header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
728 .uniform_list(
729 "keymap-editor-table",
730 row_count,
731 cx.processor(move |this, range: Range<usize>, _window, cx| {
732 range
733 .filter_map(|index| {
734 let candidate_id = this.matches.get(index)?.candidate_id;
735 let binding = &this.keybindings[candidate_id];
736
737 let action = div()
738 .child(binding.action_name.clone())
739 .id(("keymap action", index))
740 .tooltip({
741 let action_name = binding.action_name.clone();
742 let action_docs = binding.action_docs;
743 move |_, cx| {
744 let action_tooltip = Tooltip::new(
745 command_palette::humanize_action_name(
746 &action_name,
747 ),
748 );
749 let action_tooltip = match action_docs {
750 Some(docs) => action_tooltip.meta(docs),
751 None => action_tooltip,
752 };
753 cx.new(|_| action_tooltip).into()
754 }
755 })
756 .into_any_element();
757 let keystrokes = binding.ui_key_binding.clone().map_or(
758 binding.keystroke_text.clone().into_any_element(),
759 IntoElement::into_any_element,
760 );
761 let action_input = match binding.action_input.clone() {
762 Some(input) => input.into_any_element(),
763 None => {
764 if binding.action_schema.is_some() {
765 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
766 .into_any_element()
767 } else {
768 gpui::Empty.into_any_element()
769 }
770 }
771 };
772 let context = binding
773 .context
774 .clone()
775 .map_or(gpui::Empty.into_any_element(), |context| {
776 context.into_any_element()
777 });
778 let source = binding
779 .source
780 .clone()
781 .map(|(_source, name)| name)
782 .unwrap_or_default()
783 .into_any_element();
784 Some([action, action_input, keystrokes, context, source])
785 })
786 .collect()
787 }),
788 )
789 .map_row(
790 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
791 let is_conflict = this
792 .matches
793 .get(row_index)
794 .map(|candidate| candidate.candidate_id)
795 .is_some_and(|id| this.conflicts.contains(&id));
796 let is_selected = this.selected_index == Some(row_index);
797
798 let row = row
799 .id(("keymap-table-row", row_index))
800 .on_click(cx.listener(
801 move |this, event: &ClickEvent, window, cx| {
802 this.selected_index = Some(row_index);
803 if event.up.click_count == 2 {
804 this.open_edit_keybinding_modal(false, window, cx);
805 }
806 },
807 ))
808 .border_2()
809 .when(is_conflict, |row| {
810 row.bg(cx.theme().status().error_background)
811 })
812 .when(is_selected, |row| {
813 row.border_color(cx.theme().colors().panel_focused_border)
814 });
815
816 right_click_menu(("keymap-table-row-menu", row_index))
817 .trigger(move |_, _, _| row)
818 .menu({
819 let this = cx.weak_entity();
820 move |window, cx| {
821 build_keybind_context_menu(&this, row_index, window, cx)
822 }
823 })
824 .into_any_element()
825 }),
826 ),
827 )
828 }
829}
830
831#[derive(Debug, Clone, IntoElement)]
832struct SyntaxHighlightedText {
833 text: SharedString,
834 language: Arc<Language>,
835}
836
837impl SyntaxHighlightedText {
838 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
839 Self {
840 text: text.into(),
841 language,
842 }
843 }
844}
845
846impl RenderOnce for SyntaxHighlightedText {
847 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
848 let text_style = window.text_style();
849 let syntax_theme = cx.theme().syntax();
850
851 let text = self.text.clone();
852
853 let highlights = self
854 .language
855 .highlight_text(&text.as_ref().into(), 0..text.len());
856 let mut runs = Vec::with_capacity(highlights.len());
857 let mut offset = 0;
858
859 for (highlight_range, highlight_id) in highlights {
860 // Add un-highlighted text before the current highlight
861 if highlight_range.start > offset {
862 runs.push(text_style.to_run(highlight_range.start - offset));
863 }
864
865 let mut run_style = text_style.clone();
866 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
867 run_style = run_style.highlight(highlight_style);
868 }
869 // add the highlighted range
870 runs.push(run_style.to_run(highlight_range.len()));
871 offset = highlight_range.end;
872 }
873
874 // Add any remaining un-highlighted text
875 if offset < text.len() {
876 runs.push(text_style.to_run(text.len() - offset));
877 }
878
879 return StyledText::new(text).with_runs(runs);
880 }
881}
882
883struct KeybindingEditorModal {
884 creating: bool,
885 editing_keybind: ProcessedKeybinding,
886 keybind_editor: Entity<KeystrokeInput>,
887 context_editor: Entity<Editor>,
888 input_editor: Option<Entity<Editor>>,
889 fs: Arc<dyn Fs>,
890 error: Option<String>,
891}
892
893impl ModalView for KeybindingEditorModal {}
894
895impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
896
897impl Focusable for KeybindingEditorModal {
898 fn focus_handle(&self, cx: &App) -> FocusHandle {
899 self.keybind_editor.focus_handle(cx)
900 }
901}
902
903impl KeybindingEditorModal {
904 pub fn new(
905 create: bool,
906 editing_keybind: ProcessedKeybinding,
907 workspace: WeakEntity<Workspace>,
908 fs: Arc<dyn Fs>,
909 window: &mut Window,
910 cx: &mut App,
911 ) -> Self {
912 let keybind_editor = cx.new(|cx| KeystrokeInput::new(window, cx));
913
914 let context_editor = cx.new(|cx| {
915 let mut editor = Editor::single_line(window, cx);
916
917 if let Some(context) = editing_keybind
918 .context
919 .as_ref()
920 .and_then(KeybindContextString::local)
921 {
922 editor.set_text(context.clone(), window, cx);
923 } else {
924 editor.set_placeholder_text("Keybinding context", cx);
925 }
926
927 cx.spawn(async |editor, cx| {
928 let contexts = cx
929 .background_spawn(async { collect_contexts_from_assets() })
930 .await;
931
932 editor
933 .update(cx, |editor, _cx| {
934 editor.set_completion_provider(Some(std::rc::Rc::new(
935 KeyContextCompletionProvider { contexts },
936 )));
937 })
938 .context("Failed to load completions for keybinding context")
939 })
940 .detach_and_log_err(cx);
941
942 editor
943 });
944
945 let input_editor = editing_keybind.action_schema.clone().map(|_schema| {
946 cx.new(|cx| {
947 let mut editor = Editor::auto_height_unbounded(1, window, cx);
948 if let Some(input) = editing_keybind.action_input.clone() {
949 editor.set_text(input.text, window, cx);
950 } else {
951 // TODO: default value from schema?
952 editor.set_placeholder_text("Action input", cx);
953 }
954 cx.spawn(async |editor, cx| {
955 let json_language = load_json_language(workspace, cx).await;
956 editor
957 .update(cx, |editor, cx| {
958 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
959 buffer.update(cx, |buffer, cx| {
960 buffer.set_language(Some(json_language), cx)
961 });
962 }
963 })
964 .context("Failed to load JSON language for editing keybinding action input")
965 })
966 .detach_and_log_err(cx);
967 editor
968 })
969 });
970
971 Self {
972 creating: create,
973 editing_keybind,
974 fs,
975 keybind_editor,
976 context_editor,
977 input_editor,
978 error: None,
979 }
980 }
981
982 fn save(&mut self, cx: &mut Context<Self>) {
983 let existing_keybind = self.editing_keybind.clone();
984 let fs = self.fs.clone();
985 let new_keystrokes = self
986 .keybind_editor
987 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
988 if new_keystrokes.is_empty() {
989 self.error = Some("Keystrokes cannot be empty".to_string());
990 cx.notify();
991 return;
992 }
993 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
994 let new_context = self
995 .context_editor
996 .read_with(cx, |editor, cx| editor.text(cx));
997 let new_context = new_context.is_empty().not().then_some(new_context);
998 let new_context_err = new_context.as_deref().and_then(|context| {
999 gpui::KeyBindingContextPredicate::parse(context)
1000 .context("Failed to parse key context")
1001 .err()
1002 });
1003 if let Some(err) = new_context_err {
1004 // TODO: store and display as separate error
1005 // TODO: also, should be validating on keystroke
1006 self.error = Some(err.to_string());
1007 cx.notify();
1008 return;
1009 }
1010
1011 let create = self.creating;
1012
1013 cx.spawn(async move |this, cx| {
1014 if let Err(err) = save_keybinding_update(
1015 create,
1016 existing_keybind,
1017 &new_keystrokes,
1018 new_context.as_deref(),
1019 &fs,
1020 tab_size,
1021 )
1022 .await
1023 {
1024 this.update(cx, |this, cx| {
1025 this.error = Some(err.to_string());
1026 cx.notify();
1027 })
1028 .log_err();
1029 } else {
1030 this.update(cx, |_this, cx| {
1031 cx.emit(DismissEvent);
1032 })
1033 .ok();
1034 }
1035 })
1036 .detach();
1037 }
1038}
1039
1040impl Render for KeybindingEditorModal {
1041 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1042 let theme = cx.theme().colors();
1043 let input_base = || {
1044 div()
1045 .w_full()
1046 .py_2()
1047 .px_3()
1048 .min_h_8()
1049 .rounded_md()
1050 .bg(theme.editor_background)
1051 .border_1()
1052 .border_color(theme.border_variant)
1053 };
1054
1055 return v_flex()
1056 .w(rems(34.))
1057 .elevation_3(cx)
1058 .child(
1059 v_flex()
1060 .p_3()
1061 .child(Label::new("Edit Keystroke"))
1062 .child(
1063 Label::new("Input the desired keystroke for the selected action.")
1064 .color(Color::Muted)
1065 .mb_2(),
1066 )
1067 .child(self.keybind_editor.clone()),
1068 )
1069 .when_some(self.input_editor.clone(), |this, editor| {
1070 this.child(
1071 v_flex()
1072 .p_3()
1073 .pt_0()
1074 .child(Label::new("Edit Input"))
1075 .child(
1076 Label::new("Input the desired input to the binding.")
1077 .color(Color::Muted)
1078 .mb_2(),
1079 )
1080 .child(input_base().child(editor)),
1081 )
1082 })
1083 .child(
1084 v_flex()
1085 .p_3()
1086 .pt_0()
1087 .child(Label::new("Edit Context"))
1088 .child(
1089 Label::new("Input the desired context for the binding.")
1090 .color(Color::Muted)
1091 .mb_2(),
1092 )
1093 .child(input_base().child(self.context_editor.clone())),
1094 )
1095 .child(
1096 h_flex()
1097 .p_2()
1098 .w_full()
1099 .gap_1()
1100 .justify_end()
1101 .border_t_1()
1102 .border_color(theme.border_variant)
1103 .child(
1104 Button::new("cancel", "Cancel")
1105 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
1106 )
1107 .child(
1108 Button::new("save-btn", "Save").on_click(
1109 cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
1110 ),
1111 ),
1112 )
1113 .when_some(self.error.clone(), |this, error| {
1114 this.child(
1115 div()
1116 .bg(theme.background)
1117 .border_color(theme.border)
1118 .border_2()
1119 .rounded_md()
1120 .child(error),
1121 )
1122 });
1123 }
1124}
1125
1126struct KeyContextCompletionProvider {
1127 contexts: Vec<SharedString>,
1128}
1129
1130impl CompletionProvider for KeyContextCompletionProvider {
1131 fn completions(
1132 &self,
1133 _excerpt_id: editor::ExcerptId,
1134 buffer: &Entity<language::Buffer>,
1135 buffer_position: language::Anchor,
1136 _trigger: editor::CompletionContext,
1137 _window: &mut Window,
1138 cx: &mut Context<Editor>,
1139 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
1140 let buffer = buffer.read(cx);
1141 let mut count_back = 0;
1142 for char in buffer.reversed_chars_at(buffer_position) {
1143 if char.is_ascii_alphanumeric() || char == '_' {
1144 count_back += 1;
1145 } else {
1146 break;
1147 }
1148 }
1149 let start_anchor = buffer.anchor_before(
1150 buffer_position
1151 .to_offset(&buffer)
1152 .saturating_sub(count_back),
1153 );
1154 let replace_range = start_anchor..buffer_position;
1155 gpui::Task::ready(Ok(vec![project::CompletionResponse {
1156 completions: self
1157 .contexts
1158 .iter()
1159 .map(|context| project::Completion {
1160 replace_range: replace_range.clone(),
1161 label: language::CodeLabel::plain(context.to_string(), None),
1162 new_text: context.to_string(),
1163 documentation: None,
1164 source: project::CompletionSource::Custom,
1165 icon_path: None,
1166 insert_text_mode: None,
1167 confirm: None,
1168 })
1169 .collect(),
1170 is_incomplete: false,
1171 }]))
1172 }
1173
1174 fn is_completion_trigger(
1175 &self,
1176 _buffer: &Entity<language::Buffer>,
1177 _position: language::Anchor,
1178 text: &str,
1179 _trigger_in_words: bool,
1180 _menu_is_open: bool,
1181 _cx: &mut Context<Editor>,
1182 ) -> bool {
1183 text.chars().last().map_or(false, |last_char| {
1184 last_char.is_ascii_alphanumeric() || last_char == '_'
1185 })
1186 }
1187}
1188
1189async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1190 let json_language_task = workspace
1191 .read_with(cx, |workspace, cx| {
1192 workspace
1193 .project()
1194 .read(cx)
1195 .languages()
1196 .language_for_name("JSON")
1197 })
1198 .context("Failed to load JSON language")
1199 .log_err();
1200 let json_language = match json_language_task {
1201 Some(task) => task.await.context("Failed to load JSON language").log_err(),
1202 None => None,
1203 };
1204 return json_language.unwrap_or_else(|| {
1205 Arc::new(Language::new(
1206 LanguageConfig {
1207 name: "JSON".into(),
1208 ..Default::default()
1209 },
1210 Some(tree_sitter_json::LANGUAGE.into()),
1211 ))
1212 });
1213}
1214
1215async fn load_rust_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1216 let rust_language_task = workspace
1217 .read_with(cx, |workspace, cx| {
1218 workspace
1219 .project()
1220 .read(cx)
1221 .languages()
1222 .language_for_name("Rust")
1223 })
1224 .context("Failed to load Rust language")
1225 .log_err();
1226 let rust_language = match rust_language_task {
1227 Some(task) => task.await.context("Failed to load Rust language").log_err(),
1228 None => None,
1229 };
1230 return rust_language.unwrap_or_else(|| {
1231 Arc::new(Language::new(
1232 LanguageConfig {
1233 name: "Rust".into(),
1234 ..Default::default()
1235 },
1236 Some(tree_sitter_rust::LANGUAGE.into()),
1237 ))
1238 });
1239}
1240
1241async fn save_keybinding_update(
1242 create: bool,
1243 existing: ProcessedKeybinding,
1244 new_keystrokes: &[Keystroke],
1245 new_context: Option<&str>,
1246 fs: &Arc<dyn Fs>,
1247 tab_size: usize,
1248) -> anyhow::Result<()> {
1249 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1250 .await
1251 .context("Failed to load keymap file")?;
1252
1253 let existing_keystrokes = existing
1254 .ui_key_binding
1255 .as_ref()
1256 .map(|keybinding| keybinding.keystrokes.as_slice())
1257 .unwrap_or_default();
1258
1259 let existing_context = existing
1260 .context
1261 .as_ref()
1262 .and_then(KeybindContextString::local_str);
1263
1264 let input = existing
1265 .action_input
1266 .as_ref()
1267 .map(|input| input.text.as_ref());
1268
1269 let operation = if !create {
1270 settings::KeybindUpdateOperation::Replace {
1271 target: settings::KeybindUpdateTarget {
1272 context: existing_context,
1273 keystrokes: existing_keystrokes,
1274 action_name: &existing.action_name,
1275 use_key_equivalents: false,
1276 input,
1277 },
1278 target_keybind_source: existing
1279 .source
1280 .map(|(source, _name)| source)
1281 .unwrap_or(KeybindSource::User),
1282 source: settings::KeybindUpdateTarget {
1283 context: new_context,
1284 keystrokes: new_keystrokes,
1285 action_name: &existing.action_name,
1286 use_key_equivalents: false,
1287 input,
1288 },
1289 }
1290 } else {
1291 settings::KeybindUpdateOperation::Add(settings::KeybindUpdateTarget {
1292 context: new_context,
1293 keystrokes: new_keystrokes,
1294 action_name: &existing.action_name,
1295 use_key_equivalents: false,
1296 input,
1297 })
1298 };
1299 let updated_keymap_contents =
1300 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1301 .context("Failed to update keybinding")?;
1302 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1303 .await
1304 .context("Failed to write keymap file")?;
1305 Ok(())
1306}
1307
1308struct KeystrokeInput {
1309 keystrokes: Vec<Keystroke>,
1310 focus_handle: FocusHandle,
1311 intercept_subscription: Option<Subscription>,
1312 _focus_subscriptions: [Subscription; 2],
1313}
1314
1315impl KeystrokeInput {
1316 fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
1317 let focus_handle = cx.focus_handle();
1318 let _focus_subscriptions = [
1319 cx.on_focus_in(&focus_handle, window, Self::on_focus_in),
1320 cx.on_focus_out(&focus_handle, window, Self::on_focus_out),
1321 ];
1322 Self {
1323 keystrokes: Vec::new(),
1324 focus_handle,
1325 intercept_subscription: None,
1326 _focus_subscriptions,
1327 }
1328 }
1329
1330 fn on_modifiers_changed(
1331 &mut self,
1332 event: &ModifiersChangedEvent,
1333 _window: &mut Window,
1334 cx: &mut Context<Self>,
1335 ) {
1336 if let Some(last) = self.keystrokes.last_mut()
1337 && last.key.is_empty()
1338 {
1339 if !event.modifiers.modified() {
1340 self.keystrokes.pop();
1341 } else {
1342 last.modifiers = event.modifiers;
1343 }
1344 } else {
1345 self.keystrokes.push(Keystroke {
1346 modifiers: event.modifiers,
1347 key: "".to_string(),
1348 key_char: None,
1349 });
1350 }
1351 cx.stop_propagation();
1352 cx.notify();
1353 }
1354
1355 fn handle_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) {
1356 if let Some(last) = self.keystrokes.last_mut()
1357 && last.key.is_empty()
1358 {
1359 *last = keystroke.clone();
1360 } else if Some(keystroke) != self.keystrokes.last() {
1361 self.keystrokes.push(keystroke.clone());
1362 }
1363 cx.stop_propagation();
1364 cx.notify();
1365 }
1366
1367 fn on_key_up(
1368 &mut self,
1369 event: &gpui::KeyUpEvent,
1370 _window: &mut Window,
1371 cx: &mut Context<Self>,
1372 ) {
1373 if let Some(last) = self.keystrokes.last_mut()
1374 && !last.key.is_empty()
1375 && last.modifiers == event.keystroke.modifiers
1376 {
1377 self.keystrokes.push(Keystroke {
1378 modifiers: event.keystroke.modifiers,
1379 key: "".to_string(),
1380 key_char: None,
1381 });
1382 }
1383 cx.stop_propagation();
1384 cx.notify();
1385 }
1386
1387 fn on_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1388 if self.intercept_subscription.is_none() {
1389 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| {
1390 this.handle_keystroke(&event.keystroke, cx);
1391 });
1392 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
1393 }
1394 }
1395
1396 fn on_focus_out(
1397 &mut self,
1398 _event: gpui::FocusOutEvent,
1399 _window: &mut Window,
1400 _cx: &mut Context<Self>,
1401 ) {
1402 self.intercept_subscription.take();
1403 }
1404
1405 fn keystrokes(&self) -> &[Keystroke] {
1406 if self
1407 .keystrokes
1408 .last()
1409 .map_or(false, |last| last.key.is_empty())
1410 {
1411 return &self.keystrokes[..self.keystrokes.len() - 1];
1412 }
1413 return &self.keystrokes;
1414 }
1415}
1416
1417impl Focusable for KeystrokeInput {
1418 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1419 self.focus_handle.clone()
1420 }
1421}
1422
1423impl Render for KeystrokeInput {
1424 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1425 let colors = cx.theme().colors();
1426 let is_focused = self.focus_handle.is_focused(window);
1427
1428 return h_flex()
1429 .id("keybinding_input")
1430 .track_focus(&self.focus_handle)
1431 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
1432 .on_key_up(cx.listener(Self::on_key_up))
1433 .focus(|mut style| {
1434 style.border_color = Some(colors.border_focused);
1435 style
1436 })
1437 .py_2()
1438 .px_3()
1439 .gap_2()
1440 .min_h_8()
1441 .w_full()
1442 .flex_1()
1443 .justify_between()
1444 .rounded_md()
1445 .overflow_hidden()
1446 .bg(colors.editor_background)
1447 .border_1()
1448 .border_color(colors.border_variant)
1449 .child(
1450 h_flex()
1451 .w_full()
1452 .min_w_0()
1453 .justify_center()
1454 .flex_wrap()
1455 .gap(ui::DynamicSpacing::Base04.rems(cx))
1456 .children(self.keystrokes.iter().map(|keystroke| {
1457 h_flex().children(ui::render_keystroke(
1458 keystroke,
1459 None,
1460 Some(rems(0.875).into()),
1461 ui::PlatformStyle::platform(),
1462 false,
1463 ))
1464 })),
1465 )
1466 .child(
1467 h_flex()
1468 .gap_0p5()
1469 .flex_none()
1470 .child(
1471 IconButton::new("backspace-btn", IconName::Delete)
1472 .tooltip(Tooltip::text("Delete Keystroke"))
1473 .when(!is_focused, |this| this.icon_color(Color::Muted))
1474 .on_click(cx.listener(|this, _event, _window, cx| {
1475 this.keystrokes.pop();
1476 cx.notify();
1477 })),
1478 )
1479 .child(
1480 IconButton::new("clear-btn", IconName::Eraser)
1481 .tooltip(Tooltip::text("Clear Keystrokes"))
1482 .when(!is_focused, |this| this.icon_color(Color::Muted))
1483 .on_click(cx.listener(|this, _event, _window, cx| {
1484 this.keystrokes.clear();
1485 cx.notify();
1486 })),
1487 ),
1488 );
1489 }
1490}
1491
1492fn build_keybind_context_menu(
1493 this: &WeakEntity<KeymapEditor>,
1494 item_idx: usize,
1495 window: &mut Window,
1496 cx: &mut App,
1497) -> Entity<ContextMenu> {
1498 ContextMenu::build(window, cx, |menu, _window, cx| {
1499 let selected_binding = this
1500 .update(cx, |this, _cx| {
1501 this.selected_index = Some(item_idx);
1502 this.selected_binding().cloned()
1503 })
1504 .ok()
1505 .flatten();
1506
1507 let Some(selected_binding) = selected_binding else {
1508 return menu;
1509 };
1510
1511 let selected_binding_has_no_context = selected_binding
1512 .context
1513 .as_ref()
1514 .and_then(KeybindContextString::local)
1515 .is_none();
1516
1517 let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none();
1518
1519 menu.action_disabled_when(selected_binding_is_unbound, "Edit", Box::new(EditBinding))
1520 .action("Create", Box::new(CreateBinding))
1521 .action("Copy action", Box::new(CopyAction))
1522 .action_disabled_when(
1523 selected_binding_has_no_context,
1524 "Copy Context",
1525 Box::new(CopyContext),
1526 )
1527 })
1528}
1529
1530fn collect_contexts_from_assets() -> Vec<SharedString> {
1531 let mut keymap_assets = vec![
1532 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
1533 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
1534 ];
1535 keymap_assets.extend(
1536 BaseKeymap::OPTIONS
1537 .iter()
1538 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
1539 .map(util::asset_str::<SettingsAssets>),
1540 );
1541
1542 let mut contexts = HashSet::default();
1543
1544 for keymap_asset in keymap_assets {
1545 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
1546 continue;
1547 };
1548
1549 for section in keymap.sections() {
1550 let context_expr = §ion.context;
1551 let mut queue = Vec::new();
1552 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
1553 continue;
1554 };
1555
1556 queue.push(root_context);
1557 while let Some(context) = queue.pop() {
1558 match context {
1559 gpui::KeyBindingContextPredicate::Identifier(ident) => {
1560 contexts.insert(ident);
1561 }
1562 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
1563 contexts.insert(ident_a);
1564 contexts.insert(ident_b);
1565 }
1566 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
1567 contexts.insert(ident_a);
1568 contexts.insert(ident_b);
1569 }
1570 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
1571 queue.push(*ctx_a);
1572 queue.push(*ctx_b);
1573 }
1574 gpui::KeyBindingContextPredicate::Not(ctx) => {
1575 queue.push(*ctx);
1576 }
1577 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
1578 queue.push(*ctx_a);
1579 queue.push(*ctx_b);
1580 }
1581 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
1582 queue.push(*ctx_a);
1583 queue.push(*ctx_b);
1584 }
1585 }
1586 }
1587 }
1588 }
1589
1590 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
1591 contexts.sort();
1592
1593 return contexts;
1594}
1595
1596impl SerializableItem for KeymapEditor {
1597 fn serialized_item_kind() -> &'static str {
1598 "KeymapEditor"
1599 }
1600
1601 fn cleanup(
1602 workspace_id: workspace::WorkspaceId,
1603 alive_items: Vec<workspace::ItemId>,
1604 _window: &mut Window,
1605 cx: &mut App,
1606 ) -> gpui::Task<gpui::Result<()>> {
1607 workspace::delete_unloaded_items(
1608 alive_items,
1609 workspace_id,
1610 "keybinding_editors",
1611 &KEYBINDING_EDITORS,
1612 cx,
1613 )
1614 }
1615
1616 fn deserialize(
1617 _project: Entity<project::Project>,
1618 workspace: WeakEntity<Workspace>,
1619 workspace_id: workspace::WorkspaceId,
1620 item_id: workspace::ItemId,
1621 window: &mut Window,
1622 cx: &mut App,
1623 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
1624 window.spawn(cx, async move |cx| {
1625 if KEYBINDING_EDITORS
1626 .get_keybinding_editor(item_id, workspace_id)?
1627 .is_some()
1628 {
1629 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
1630 } else {
1631 Err(anyhow!("No keybinding editor to deserialize"))
1632 }
1633 })
1634 }
1635
1636 fn serialize(
1637 &mut self,
1638 workspace: &mut Workspace,
1639 item_id: workspace::ItemId,
1640 _closing: bool,
1641 _window: &mut Window,
1642 cx: &mut ui::Context<Self>,
1643 ) -> Option<gpui::Task<gpui::Result<()>>> {
1644 let workspace_id = workspace.database_id()?;
1645 Some(cx.background_spawn(async move {
1646 KEYBINDING_EDITORS
1647 .save_keybinding_editor(item_id, workspace_id)
1648 .await
1649 }))
1650 }
1651
1652 fn should_serialize(&self, _event: &Self::Event) -> bool {
1653 false
1654 }
1655}
1656
1657mod persistence {
1658 use db::{define_connection, query, sqlez_macros::sql};
1659 use workspace::WorkspaceDb;
1660
1661 define_connection! {
1662 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
1663 &[sql!(
1664 CREATE TABLE keybinding_editors (
1665 workspace_id INTEGER,
1666 item_id INTEGER UNIQUE,
1667
1668 PRIMARY KEY(workspace_id, item_id),
1669 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1670 ON DELETE CASCADE
1671 ) STRICT;
1672 )];
1673 }
1674
1675 impl KeybindingEditorDb {
1676 query! {
1677 pub async fn save_keybinding_editor(
1678 item_id: workspace::ItemId,
1679 workspace_id: workspace::WorkspaceId
1680 ) -> Result<()> {
1681 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
1682 VALUES (?, ?)
1683 }
1684 }
1685
1686 query! {
1687 pub fn get_keybinding_editor(
1688 item_id: workspace::ItemId,
1689 workspace_id: workspace::WorkspaceId
1690 ) -> Result<Option<workspace::ItemId>> {
1691 SELECT item_id
1692 FROM keybinding_editors
1693 WHERE item_id = ? AND workspace_id = ?
1694 }
1695 }
1696 }
1697}