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, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
14 Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription,
15 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(move |this, _event, _window, _cx| {
801 this.selected_index = Some(row_index);
802 }))
803 .border_2()
804 .when(is_conflict, |row| {
805 row.bg(cx.theme().status().error_background)
806 })
807 .when(is_selected, |row| {
808 row.border_color(cx.theme().colors().panel_focused_border)
809 });
810
811 right_click_menu(("keymap-table-row-menu", row_index))
812 .trigger({
813 let this = cx.weak_entity();
814 move |is_menu_open: bool, _window, cx| {
815 if is_menu_open {
816 this.update(cx, |this, cx| {
817 if this.selected_index != Some(row_index) {
818 this.selected_index = Some(row_index);
819 cx.notify();
820 }
821 })
822 .ok();
823 }
824 row
825 }
826 })
827 .menu({
828 let this = cx.weak_entity();
829 move |window, cx| build_keybind_context_menu(&this, window, cx)
830 })
831 .into_any_element()
832 }),
833 ),
834 )
835 }
836}
837
838#[derive(Debug, Clone, IntoElement)]
839struct SyntaxHighlightedText {
840 text: SharedString,
841 language: Arc<Language>,
842}
843
844impl SyntaxHighlightedText {
845 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
846 Self {
847 text: text.into(),
848 language,
849 }
850 }
851}
852
853impl RenderOnce for SyntaxHighlightedText {
854 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
855 let text_style = window.text_style();
856 let syntax_theme = cx.theme().syntax();
857
858 let text = self.text.clone();
859
860 let highlights = self
861 .language
862 .highlight_text(&text.as_ref().into(), 0..text.len());
863 let mut runs = Vec::with_capacity(highlights.len());
864 let mut offset = 0;
865
866 for (highlight_range, highlight_id) in highlights {
867 // Add un-highlighted text before the current highlight
868 if highlight_range.start > offset {
869 runs.push(text_style.to_run(highlight_range.start - offset));
870 }
871
872 let mut run_style = text_style.clone();
873 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
874 run_style = run_style.highlight(highlight_style);
875 }
876 // add the highlighted range
877 runs.push(run_style.to_run(highlight_range.len()));
878 offset = highlight_range.end;
879 }
880
881 // Add any remaining un-highlighted text
882 if offset < text.len() {
883 runs.push(text_style.to_run(text.len() - offset));
884 }
885
886 return StyledText::new(text).with_runs(runs);
887 }
888}
889
890struct KeybindingEditorModal {
891 creating: bool,
892 editing_keybind: ProcessedKeybinding,
893 keybind_editor: Entity<KeystrokeInput>,
894 context_editor: Entity<Editor>,
895 input_editor: Option<Entity<Editor>>,
896 fs: Arc<dyn Fs>,
897 error: Option<String>,
898}
899
900impl ModalView for KeybindingEditorModal {}
901
902impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
903
904impl Focusable for KeybindingEditorModal {
905 fn focus_handle(&self, cx: &App) -> FocusHandle {
906 self.keybind_editor.focus_handle(cx)
907 }
908}
909
910impl KeybindingEditorModal {
911 pub fn new(
912 create: bool,
913 editing_keybind: ProcessedKeybinding,
914 workspace: WeakEntity<Workspace>,
915 fs: Arc<dyn Fs>,
916 window: &mut Window,
917 cx: &mut App,
918 ) -> Self {
919 let keybind_editor = cx.new(KeystrokeInput::new);
920
921 let context_editor = cx.new(|cx| {
922 let mut editor = Editor::single_line(window, cx);
923
924 if let Some(context) = editing_keybind
925 .context
926 .as_ref()
927 .and_then(KeybindContextString::local)
928 {
929 editor.set_text(context.clone(), window, cx);
930 } else {
931 editor.set_placeholder_text("Keybinding context", cx);
932 }
933
934 cx.spawn(async |editor, cx| {
935 let contexts = cx
936 .background_spawn(async { collect_contexts_from_assets() })
937 .await;
938
939 editor
940 .update(cx, |editor, _cx| {
941 editor.set_completion_provider(Some(std::rc::Rc::new(
942 KeyContextCompletionProvider { contexts },
943 )));
944 })
945 .context("Failed to load completions for keybinding context")
946 })
947 .detach_and_log_err(cx);
948
949 editor
950 });
951
952 let input_editor = editing_keybind.action_schema.clone().map(|_schema| {
953 cx.new(|cx| {
954 let mut editor = Editor::auto_height_unbounded(1, window, cx);
955 if let Some(input) = editing_keybind.action_input.clone() {
956 editor.set_text(input.text, window, cx);
957 } else {
958 // TODO: default value from schema?
959 editor.set_placeholder_text("Action input", cx);
960 }
961 cx.spawn(async |editor, cx| {
962 let json_language = load_json_language(workspace, cx).await;
963 editor
964 .update(cx, |editor, cx| {
965 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
966 buffer.update(cx, |buffer, cx| {
967 buffer.set_language(Some(json_language), cx)
968 });
969 }
970 })
971 .context("Failed to load JSON language for editing keybinding action input")
972 })
973 .detach_and_log_err(cx);
974 editor
975 })
976 });
977
978 Self {
979 creating: create,
980 editing_keybind,
981 fs,
982 keybind_editor,
983 context_editor,
984 input_editor,
985 error: None,
986 }
987 }
988
989 fn save(&mut self, cx: &mut Context<Self>) {
990 let existing_keybind = self.editing_keybind.clone();
991 let fs = self.fs.clone();
992 let new_keystrokes = self
993 .keybind_editor
994 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
995 if new_keystrokes.is_empty() {
996 self.error = Some("Keystrokes cannot be empty".to_string());
997 cx.notify();
998 return;
999 }
1000 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1001 let new_context = self
1002 .context_editor
1003 .read_with(cx, |editor, cx| editor.text(cx));
1004 let new_context = new_context.is_empty().not().then_some(new_context);
1005 let new_context_err = new_context.as_deref().and_then(|context| {
1006 gpui::KeyBindingContextPredicate::parse(context)
1007 .context("Failed to parse key context")
1008 .err()
1009 });
1010 if let Some(err) = new_context_err {
1011 // TODO: store and display as separate error
1012 // TODO: also, should be validating on keystroke
1013 self.error = Some(err.to_string());
1014 cx.notify();
1015 return;
1016 }
1017
1018 let create = self.creating;
1019
1020 cx.spawn(async move |this, cx| {
1021 if let Err(err) = save_keybinding_update(
1022 create,
1023 existing_keybind,
1024 &new_keystrokes,
1025 new_context.as_deref(),
1026 &fs,
1027 tab_size,
1028 )
1029 .await
1030 {
1031 this.update(cx, |this, cx| {
1032 this.error = Some(err.to_string());
1033 cx.notify();
1034 })
1035 .log_err();
1036 } else {
1037 this.update(cx, |_this, cx| {
1038 cx.emit(DismissEvent);
1039 })
1040 .ok();
1041 }
1042 })
1043 .detach();
1044 }
1045}
1046
1047impl Render for KeybindingEditorModal {
1048 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1049 let theme = cx.theme().colors();
1050
1051 return v_flex()
1052 .w(rems(34.))
1053 .elevation_3(cx)
1054 .child(
1055 v_flex()
1056 .p_3()
1057 .gap_2()
1058 .child(
1059 v_flex().child(Label::new("Edit Keystroke")).child(
1060 Label::new("Input the desired keystroke for the selected action.")
1061 .color(Color::Muted),
1062 ),
1063 )
1064 .child(self.keybind_editor.clone()),
1065 )
1066 .when_some(self.input_editor.clone(), |this, editor| {
1067 this.child(
1068 v_flex()
1069 .p_3()
1070 .gap_3()
1071 .child(
1072 v_flex().child(Label::new("Edit Input")).child(
1073 Label::new("Input the desired input to the binding.")
1074 .color(Color::Muted),
1075 ),
1076 )
1077 .child(
1078 div()
1079 .w_full()
1080 .border_color(cx.theme().colors().border_variant)
1081 .border_1()
1082 .py_2()
1083 .px_3()
1084 .min_h_8()
1085 .rounded_md()
1086 .bg(theme.editor_background)
1087 .child(editor),
1088 ),
1089 )
1090 })
1091 .child(
1092 v_flex()
1093 .p_3()
1094 .gap_3()
1095 .child(
1096 v_flex().child(Label::new("Edit Context")).child(
1097 Label::new("Input the desired context for the binding.")
1098 .color(Color::Muted),
1099 ),
1100 )
1101 .child(
1102 div()
1103 .w_full()
1104 .border_color(cx.theme().colors().border_variant)
1105 .border_1()
1106 .py_2()
1107 .px_3()
1108 .min_h_8()
1109 .rounded_md()
1110 .bg(theme.editor_background)
1111 .child(self.context_editor.clone()),
1112 ),
1113 )
1114 .child(
1115 h_flex()
1116 .p_2()
1117 .w_full()
1118 .gap_1()
1119 .justify_end()
1120 .border_t_1()
1121 .border_color(cx.theme().colors().border_variant)
1122 .child(
1123 Button::new("cancel", "Cancel")
1124 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
1125 )
1126 .child(
1127 Button::new("save-btn", "Save").on_click(
1128 cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
1129 ),
1130 ),
1131 )
1132 .when_some(self.error.clone(), |this, error| {
1133 this.child(
1134 div()
1135 .bg(theme.background)
1136 .border_color(theme.border)
1137 .border_2()
1138 .rounded_md()
1139 .child(error),
1140 )
1141 });
1142 }
1143}
1144
1145struct KeyContextCompletionProvider {
1146 contexts: Vec<SharedString>,
1147}
1148
1149impl CompletionProvider for KeyContextCompletionProvider {
1150 fn completions(
1151 &self,
1152 _excerpt_id: editor::ExcerptId,
1153 buffer: &Entity<language::Buffer>,
1154 buffer_position: language::Anchor,
1155 _trigger: editor::CompletionContext,
1156 _window: &mut Window,
1157 cx: &mut Context<Editor>,
1158 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
1159 let buffer = buffer.read(cx);
1160 let mut count_back = 0;
1161 for char in buffer.reversed_chars_at(buffer_position) {
1162 if char.is_ascii_alphanumeric() || char == '_' {
1163 count_back += 1;
1164 } else {
1165 break;
1166 }
1167 }
1168 let start_anchor = buffer.anchor_before(
1169 buffer_position
1170 .to_offset(&buffer)
1171 .saturating_sub(count_back),
1172 );
1173 let replace_range = start_anchor..buffer_position;
1174 gpui::Task::ready(Ok(vec![project::CompletionResponse {
1175 completions: self
1176 .contexts
1177 .iter()
1178 .map(|context| project::Completion {
1179 replace_range: replace_range.clone(),
1180 label: language::CodeLabel::plain(context.to_string(), None),
1181 new_text: context.to_string(),
1182 documentation: None,
1183 source: project::CompletionSource::Custom,
1184 icon_path: None,
1185 insert_text_mode: None,
1186 confirm: None,
1187 })
1188 .collect(),
1189 is_incomplete: false,
1190 }]))
1191 }
1192
1193 fn is_completion_trigger(
1194 &self,
1195 _buffer: &Entity<language::Buffer>,
1196 _position: language::Anchor,
1197 text: &str,
1198 _trigger_in_words: bool,
1199 _menu_is_open: bool,
1200 _cx: &mut Context<Editor>,
1201 ) -> bool {
1202 text.chars().last().map_or(false, |last_char| {
1203 last_char.is_ascii_alphanumeric() || last_char == '_'
1204 })
1205 }
1206}
1207
1208async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1209 let json_language_task = workspace
1210 .read_with(cx, |workspace, cx| {
1211 workspace
1212 .project()
1213 .read(cx)
1214 .languages()
1215 .language_for_name("JSON")
1216 })
1217 .context("Failed to load JSON language")
1218 .log_err();
1219 let json_language = match json_language_task {
1220 Some(task) => task.await.context("Failed to load JSON language").log_err(),
1221 None => None,
1222 };
1223 return json_language.unwrap_or_else(|| {
1224 Arc::new(Language::new(
1225 LanguageConfig {
1226 name: "JSON".into(),
1227 ..Default::default()
1228 },
1229 Some(tree_sitter_json::LANGUAGE.into()),
1230 ))
1231 });
1232}
1233
1234async fn load_rust_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
1235 let rust_language_task = workspace
1236 .read_with(cx, |workspace, cx| {
1237 workspace
1238 .project()
1239 .read(cx)
1240 .languages()
1241 .language_for_name("Rust")
1242 })
1243 .context("Failed to load Rust language")
1244 .log_err();
1245 let rust_language = match rust_language_task {
1246 Some(task) => task.await.context("Failed to load Rust language").log_err(),
1247 None => None,
1248 };
1249 return rust_language.unwrap_or_else(|| {
1250 Arc::new(Language::new(
1251 LanguageConfig {
1252 name: "Rust".into(),
1253 ..Default::default()
1254 },
1255 Some(tree_sitter_rust::LANGUAGE.into()),
1256 ))
1257 });
1258}
1259
1260async fn save_keybinding_update(
1261 create: bool,
1262 existing: ProcessedKeybinding,
1263 new_keystrokes: &[Keystroke],
1264 new_context: Option<&str>,
1265 fs: &Arc<dyn Fs>,
1266 tab_size: usize,
1267) -> anyhow::Result<()> {
1268 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
1269 .await
1270 .context("Failed to load keymap file")?;
1271
1272 let existing_keystrokes = existing
1273 .ui_key_binding
1274 .as_ref()
1275 .map(|keybinding| keybinding.keystrokes.as_slice())
1276 .unwrap_or_default();
1277
1278 let existing_context = existing
1279 .context
1280 .as_ref()
1281 .and_then(KeybindContextString::local_str);
1282
1283 let input = existing
1284 .action_input
1285 .as_ref()
1286 .map(|input| input.text.as_ref());
1287
1288 let operation = if !create {
1289 settings::KeybindUpdateOperation::Replace {
1290 target: settings::KeybindUpdateTarget {
1291 context: existing_context,
1292 keystrokes: existing_keystrokes,
1293 action_name: &existing.action_name,
1294 use_key_equivalents: false,
1295 input,
1296 },
1297 target_keybind_source: existing
1298 .source
1299 .map(|(source, _name)| source)
1300 .unwrap_or(KeybindSource::User),
1301 source: settings::KeybindUpdateTarget {
1302 context: new_context,
1303 keystrokes: new_keystrokes,
1304 action_name: &existing.action_name,
1305 use_key_equivalents: false,
1306 input,
1307 },
1308 }
1309 } else {
1310 settings::KeybindUpdateOperation::Add(settings::KeybindUpdateTarget {
1311 context: new_context,
1312 keystrokes: new_keystrokes,
1313 action_name: &existing.action_name,
1314 use_key_equivalents: false,
1315 input,
1316 })
1317 };
1318 let updated_keymap_contents =
1319 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
1320 .context("Failed to update keybinding")?;
1321 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
1322 .await
1323 .context("Failed to write keymap file")?;
1324 Ok(())
1325}
1326
1327struct KeystrokeInput {
1328 keystrokes: Vec<Keystroke>,
1329 focus_handle: FocusHandle,
1330}
1331
1332impl KeystrokeInput {
1333 fn new(cx: &mut Context<Self>) -> Self {
1334 let focus_handle = cx.focus_handle();
1335 Self {
1336 keystrokes: Vec::new(),
1337 focus_handle,
1338 }
1339 }
1340
1341 fn on_modifiers_changed(
1342 &mut self,
1343 event: &ModifiersChangedEvent,
1344 _window: &mut Window,
1345 cx: &mut Context<Self>,
1346 ) {
1347 if let Some(last) = self.keystrokes.last_mut()
1348 && last.key.is_empty()
1349 {
1350 if !event.modifiers.modified() {
1351 self.keystrokes.pop();
1352 } else {
1353 last.modifiers = event.modifiers;
1354 }
1355 } else {
1356 self.keystrokes.push(Keystroke {
1357 modifiers: event.modifiers,
1358 key: "".to_string(),
1359 key_char: None,
1360 });
1361 }
1362 cx.stop_propagation();
1363 cx.notify();
1364 }
1365
1366 fn on_key_down(
1367 &mut self,
1368 event: &gpui::KeyDownEvent,
1369 _window: &mut Window,
1370 cx: &mut Context<Self>,
1371 ) {
1372 if event.is_held {
1373 return;
1374 }
1375 if let Some(last) = self.keystrokes.last_mut()
1376 && last.key.is_empty()
1377 {
1378 *last = event.keystroke.clone();
1379 } else {
1380 self.keystrokes.push(event.keystroke.clone());
1381 }
1382 cx.stop_propagation();
1383 cx.notify();
1384 }
1385
1386 fn on_key_up(
1387 &mut self,
1388 event: &gpui::KeyUpEvent,
1389 _window: &mut Window,
1390 cx: &mut Context<Self>,
1391 ) {
1392 if let Some(last) = self.keystrokes.last_mut()
1393 && !last.key.is_empty()
1394 && last.modifiers == event.keystroke.modifiers
1395 {
1396 self.keystrokes.push(Keystroke {
1397 modifiers: event.keystroke.modifiers,
1398 key: "".to_string(),
1399 key_char: None,
1400 });
1401 }
1402 cx.stop_propagation();
1403 cx.notify();
1404 }
1405
1406 fn keystrokes(&self) -> &[Keystroke] {
1407 if self
1408 .keystrokes
1409 .last()
1410 .map_or(false, |last| last.key.is_empty())
1411 {
1412 return &self.keystrokes[..self.keystrokes.len() - 1];
1413 }
1414 return &self.keystrokes;
1415 }
1416}
1417
1418impl Focusable for KeystrokeInput {
1419 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1420 self.focus_handle.clone()
1421 }
1422}
1423
1424impl Render for KeystrokeInput {
1425 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1426 let colors = cx.theme().colors();
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_down(cx.listener(Self::on_key_down))
1433 .on_key_up(cx.listener(Self::on_key_up))
1434 .focus(|mut style| {
1435 style.border_color = Some(colors.border_focused);
1436 style
1437 })
1438 .py_2()
1439 .px_3()
1440 .gap_2()
1441 .min_h_8()
1442 .w_full()
1443 .justify_between()
1444 .bg(colors.editor_background)
1445 .border_1()
1446 .rounded_md()
1447 .flex_1()
1448 .overflow_hidden()
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 .on_click(cx.listener(|this, _event, _window, cx| {
1474 this.keystrokes.pop();
1475 cx.notify();
1476 })),
1477 )
1478 .child(
1479 IconButton::new("clear-btn", IconName::Eraser)
1480 .tooltip(Tooltip::text("Clear Keystrokes"))
1481 .on_click(cx.listener(|this, _event, _window, cx| {
1482 this.keystrokes.clear();
1483 cx.notify();
1484 })),
1485 ),
1486 );
1487 }
1488}
1489
1490fn build_keybind_context_menu(
1491 this: &WeakEntity<KeymapEditor>,
1492 window: &mut Window,
1493 cx: &mut App,
1494) -> Entity<ContextMenu> {
1495 ContextMenu::build(window, cx, |menu, _window, cx| {
1496 let Some(this) = this.upgrade() else {
1497 return menu;
1498 };
1499 let selected_binding = this.read_with(cx, |this, _cx| this.selected_binding().cloned());
1500 let Some(selected_binding) = selected_binding else {
1501 return menu;
1502 };
1503
1504 let selected_binding_has_no_context = selected_binding
1505 .context
1506 .as_ref()
1507 .and_then(KeybindContextString::local)
1508 .is_none();
1509
1510 let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none();
1511
1512 menu.action_disabled_when(selected_binding_is_unbound, "Edit", Box::new(EditBinding))
1513 .action("Create", Box::new(CreateBinding))
1514 .action("Copy action", Box::new(CopyAction))
1515 .action_disabled_when(
1516 selected_binding_has_no_context,
1517 "Copy Context",
1518 Box::new(CopyContext),
1519 )
1520 })
1521}
1522
1523fn collect_contexts_from_assets() -> Vec<SharedString> {
1524 let mut keymap_assets = vec![
1525 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
1526 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
1527 ];
1528 keymap_assets.extend(
1529 BaseKeymap::OPTIONS
1530 .iter()
1531 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
1532 .map(util::asset_str::<SettingsAssets>),
1533 );
1534
1535 let mut contexts = HashSet::default();
1536
1537 for keymap_asset in keymap_assets {
1538 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
1539 continue;
1540 };
1541
1542 for section in keymap.sections() {
1543 let context_expr = §ion.context;
1544 let mut queue = Vec::new();
1545 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
1546 continue;
1547 };
1548
1549 queue.push(root_context);
1550 while let Some(context) = queue.pop() {
1551 match context {
1552 gpui::KeyBindingContextPredicate::Identifier(ident) => {
1553 contexts.insert(ident);
1554 }
1555 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
1556 contexts.insert(ident_a);
1557 contexts.insert(ident_b);
1558 }
1559 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
1560 contexts.insert(ident_a);
1561 contexts.insert(ident_b);
1562 }
1563 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
1564 queue.push(*ctx_a);
1565 queue.push(*ctx_b);
1566 }
1567 gpui::KeyBindingContextPredicate::Not(ctx) => {
1568 queue.push(*ctx);
1569 }
1570 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
1571 queue.push(*ctx_a);
1572 queue.push(*ctx_b);
1573 }
1574 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
1575 queue.push(*ctx_a);
1576 queue.push(*ctx_b);
1577 }
1578 }
1579 }
1580 }
1581 }
1582
1583 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
1584 contexts.sort();
1585
1586 return contexts;
1587}
1588
1589impl SerializableItem for KeymapEditor {
1590 fn serialized_item_kind() -> &'static str {
1591 "KeymapEditor"
1592 }
1593
1594 fn cleanup(
1595 workspace_id: workspace::WorkspaceId,
1596 alive_items: Vec<workspace::ItemId>,
1597 _window: &mut Window,
1598 cx: &mut App,
1599 ) -> gpui::Task<gpui::Result<()>> {
1600 workspace::delete_unloaded_items(
1601 alive_items,
1602 workspace_id,
1603 "keybinding_editors",
1604 &KEYBINDING_EDITORS,
1605 cx,
1606 )
1607 }
1608
1609 fn deserialize(
1610 _project: Entity<project::Project>,
1611 workspace: WeakEntity<Workspace>,
1612 workspace_id: workspace::WorkspaceId,
1613 item_id: workspace::ItemId,
1614 window: &mut Window,
1615 cx: &mut App,
1616 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
1617 window.spawn(cx, async move |cx| {
1618 if KEYBINDING_EDITORS
1619 .get_keybinding_editor(item_id, workspace_id)?
1620 .is_some()
1621 {
1622 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
1623 } else {
1624 Err(anyhow!("No keybinding editor to deserialize"))
1625 }
1626 })
1627 }
1628
1629 fn serialize(
1630 &mut self,
1631 workspace: &mut Workspace,
1632 item_id: workspace::ItemId,
1633 _closing: bool,
1634 _window: &mut Window,
1635 cx: &mut ui::Context<Self>,
1636 ) -> Option<gpui::Task<gpui::Result<()>>> {
1637 let workspace_id = workspace.database_id()?;
1638 Some(cx.background_spawn(async move {
1639 KEYBINDING_EDITORS
1640 .save_keybinding_editor(item_id, workspace_id)
1641 .await
1642 }))
1643 }
1644
1645 fn should_serialize(&self, _event: &Self::Event) -> bool {
1646 false
1647 }
1648}
1649
1650mod persistence {
1651 use db::{define_connection, query, sqlez_macros::sql};
1652 use workspace::WorkspaceDb;
1653
1654 define_connection! {
1655 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
1656 &[sql!(
1657 CREATE TABLE keybinding_editors (
1658 workspace_id INTEGER,
1659 item_id INTEGER UNIQUE,
1660
1661 PRIMARY KEY(workspace_id, item_id),
1662 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1663 ON DELETE CASCADE
1664 ) STRICT;
1665 )];
1666 }
1667
1668 impl KeybindingEditorDb {
1669 query! {
1670 pub async fn save_keybinding_editor(
1671 item_id: workspace::ItemId,
1672 workspace_id: workspace::WorkspaceId
1673 ) -> Result<()> {
1674 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
1675 VALUES (?, ?)
1676 }
1677 }
1678
1679 query! {
1680 pub fn get_keybinding_editor(
1681 item_id: workspace::ItemId,
1682 workspace_id: workspace::WorkspaceId
1683 ) -> Result<Option<workspace::ItemId>> {
1684 SELECT item_id
1685 FROM keybinding_editors
1686 WHERE item_id = ? AND workspace_id = ?
1687 }
1688 }
1689 }
1690}