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