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