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