1use std::{
2 ops::{Not as _, Range},
3 sync::Arc,
4 time::Duration,
5};
6
7use anyhow::{Context as _, anyhow};
8use collections::{HashMap, HashSet};
9use editor::{CompletionProvider, Editor, EditorEvent};
10use fs::Fs;
11use fuzzy::{StringMatch, StringMatchCandidate};
12use gpui::{
13 Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context,
14 DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero,
15 KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
16 ScrollWheelEvent, StyledText, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
17};
18use language::{Language, LanguageConfig, ToOffset as _};
19use notifications::status_toast::{StatusToast, ToastIcon};
20use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
21
22use util::ResultExt;
23
24use ui::{
25 ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Modal,
26 ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, Styled as _,
27 Tooltip, Window, prelude::*,
28};
29use ui_input::SingleLineInput;
30use workspace::{
31 Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
32 register_serializable_item,
33};
34
35use crate::{
36 keybindings::persistence::KEYBINDING_EDITORS,
37 ui_components::table::{Table, TableInteractionState},
38};
39
40const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
41
42actions!(
43 zed,
44 [
45 /// Opens the keymap editor.
46 OpenKeymapEditor
47 ]
48);
49
50actions!(
51 keymap_editor,
52 [
53 /// Edits the selected key binding.
54 EditBinding,
55 /// Creates a new key binding for the selected action.
56 CreateBinding,
57 /// Deletes the selected key binding.
58 DeleteBinding,
59 /// Copies the action name to clipboard.
60 CopyAction,
61 /// Copies the context predicate to clipboard.
62 CopyContext,
63 /// Toggles Conflict Filtering
64 ToggleConflictFilter,
65 /// Toggle Keystroke search
66 ToggleKeystrokeSearch,
67 /// Toggles exact matching for keystroke search
68 ToggleExactKeystrokeMatching,
69 ]
70);
71
72actions!(
73 keystroke_input,
74 [
75 /// Starts recording keystrokes
76 StartRecording,
77 /// Stops recording keystrokes
78 StopRecording,
79 /// Clears the recorded keystrokes
80 ClearKeystrokes,
81 ]
82);
83
84pub fn init(cx: &mut App) {
85 let keymap_event_channel = KeymapEventChannel::new();
86 cx.set_global(keymap_event_channel);
87
88 cx.on_action(|_: &OpenKeymapEditor, cx| {
89 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
90 workspace
91 .with_local_workspace(window, cx, |workspace, window, cx| {
92 let existing = workspace
93 .active_pane()
94 .read(cx)
95 .items()
96 .find_map(|item| item.downcast::<KeymapEditor>());
97
98 if let Some(existing) = existing {
99 workspace.activate_item(&existing, true, true, window, cx);
100 } else {
101 let keymap_editor =
102 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
103 workspace.add_item_to_active_pane(
104 Box::new(keymap_editor),
105 None,
106 true,
107 window,
108 cx,
109 );
110 }
111 })
112 .detach();
113 })
114 });
115
116 register_serializable_item::<KeymapEditor>(cx);
117}
118
119pub struct KeymapEventChannel {}
120
121impl Global for KeymapEventChannel {}
122
123impl KeymapEventChannel {
124 fn new() -> Self {
125 Self {}
126 }
127
128 pub fn trigger_keymap_changed(cx: &mut App) {
129 let Some(_event_channel) = cx.try_global::<Self>() else {
130 // don't panic if no global defined. This usually happens in tests
131 return;
132 };
133 cx.update_global(|_event_channel: &mut Self, _| {
134 /* triggers observers in KeymapEditors */
135 });
136 }
137}
138
139#[derive(Default, PartialEq)]
140enum SearchMode {
141 #[default]
142 Normal,
143 KeyStroke {
144 exact_match: bool,
145 },
146}
147
148impl SearchMode {
149 fn invert(&self) -> Self {
150 match self {
151 SearchMode::Normal => SearchMode::KeyStroke { exact_match: false },
152 SearchMode::KeyStroke { .. } => SearchMode::Normal,
153 }
154 }
155
156 fn exact_match(&self) -> bool {
157 match self {
158 SearchMode::Normal => false,
159 SearchMode::KeyStroke { exact_match } => *exact_match,
160 }
161 }
162}
163
164#[derive(Default, PartialEq, Copy, Clone)]
165enum FilterState {
166 #[default]
167 All,
168 Conflicts,
169}
170
171impl FilterState {
172 fn invert(&self) -> Self {
173 match self {
174 FilterState::All => FilterState::Conflicts,
175 FilterState::Conflicts => FilterState::All,
176 }
177 }
178}
179
180#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
181struct ActionMapping {
182 keystroke_text: SharedString,
183 context: Option<SharedString>,
184}
185
186#[derive(Default)]
187struct ConflictState {
188 conflicts: Vec<usize>,
189 action_keybind_mapping: HashMap<ActionMapping, Vec<usize>>,
190}
191
192impl ConflictState {
193 fn new(key_bindings: &[ProcessedKeybinding]) -> Self {
194 let mut action_keybind_mapping: HashMap<_, Vec<usize>> = HashMap::default();
195
196 key_bindings
197 .iter()
198 .enumerate()
199 .filter(|(_, binding)| {
200 !binding.keystroke_text.is_empty()
201 && binding
202 .source
203 .as_ref()
204 .is_some_and(|source| matches!(source.0, KeybindSource::User))
205 })
206 .for_each(|(index, binding)| {
207 action_keybind_mapping
208 .entry(binding.get_action_mapping())
209 .or_default()
210 .push(index);
211 });
212
213 Self {
214 conflicts: action_keybind_mapping
215 .values()
216 .filter(|indices| indices.len() > 1)
217 .flatten()
218 .copied()
219 .collect(),
220 action_keybind_mapping,
221 }
222 }
223
224 fn conflicting_indices_for_mapping(
225 &self,
226 action_mapping: ActionMapping,
227 keybind_idx: usize,
228 ) -> Option<Vec<usize>> {
229 self.action_keybind_mapping
230 .get(&action_mapping)
231 .and_then(|indices| {
232 let mut indices = indices.iter().filter(|&idx| *idx != keybind_idx).peekable();
233 indices.peek().is_some().then(|| indices.copied().collect())
234 })
235 }
236
237 fn will_conflict(&self, action_mapping: ActionMapping) -> Option<Vec<usize>> {
238 self.action_keybind_mapping
239 .get(&action_mapping)
240 .and_then(|indices| indices.is_empty().not().then_some(indices.clone()))
241 }
242
243 fn has_conflict(&self, candidate_idx: &usize) -> bool {
244 self.conflicts.contains(candidate_idx)
245 }
246
247 fn any_conflicts(&self) -> bool {
248 !self.conflicts.is_empty()
249 }
250}
251
252struct KeymapEditor {
253 workspace: WeakEntity<Workspace>,
254 focus_handle: FocusHandle,
255 _keymap_subscription: Subscription,
256 keybindings: Vec<ProcessedKeybinding>,
257 keybinding_conflict_state: ConflictState,
258 filter_state: FilterState,
259 search_mode: SearchMode,
260 search_query_debounce: Option<Task<()>>,
261 // corresponds 1 to 1 with keybindings
262 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
263 matches: Vec<StringMatch>,
264 table_interaction_state: Entity<TableInteractionState>,
265 filter_editor: Entity<Editor>,
266 keystroke_editor: Entity<KeystrokeInput>,
267 selected_index: Option<usize>,
268 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
269 previous_edit: Option<PreviousEdit>,
270 humanized_action_names: HashMap<&'static str, SharedString>,
271 show_hover_menus: bool,
272}
273
274enum PreviousEdit {
275 /// When deleting, we want to maintain the same scroll position
276 ScrollBarOffset(Point<Pixels>),
277 /// When editing or creating, because the new keybinding could be in a different position in the sort order
278 /// we store metadata about the new binding (either the modified version or newly created one)
279 /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches
280 /// this metadata, we set the selected index to it and scroll to it,
281 /// and if we don't find it, we scroll to 0 and don't set a selected index
282 Keybinding {
283 action_mapping: ActionMapping,
284 action_name: &'static str,
285 /// The scrollbar position to fallback to if we don't find the keybinding during a refresh
286 /// this can happen if there's a filter applied to the search and the keybinding modification
287 /// filters the binding from the search results
288 fallback: Point<Pixels>,
289 },
290}
291
292impl EventEmitter<()> for KeymapEditor {}
293
294impl Focusable for KeymapEditor {
295 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
296 return self.filter_editor.focus_handle(cx);
297 }
298}
299
300impl KeymapEditor {
301 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
302 let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(Self::on_keymap_changed);
303 let table_interaction_state = TableInteractionState::new(window, cx);
304
305 let keystroke_editor = cx.new(|cx| {
306 let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
307 keystroke_editor.search = true;
308 keystroke_editor
309 });
310
311 let filter_editor = cx.new(|cx| {
312 let mut editor = Editor::single_line(window, cx);
313 editor.set_placeholder_text("Filter action names…", cx);
314 editor
315 });
316
317 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
318 if !matches!(e, EditorEvent::BufferEdited) {
319 return;
320 }
321
322 this.on_query_changed(cx);
323 })
324 .detach();
325
326 cx.subscribe(&keystroke_editor, |this, _, _, cx| {
327 if matches!(this.search_mode, SearchMode::Normal) {
328 return;
329 }
330
331 this.on_query_changed(cx);
332 })
333 .detach();
334
335 let humanized_action_names =
336 HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| {
337 (
338 action_name,
339 command_palette::humanize_action_name(action_name).into(),
340 )
341 }));
342
343 let mut this = Self {
344 workspace,
345 keybindings: vec![],
346 keybinding_conflict_state: ConflictState::default(),
347 filter_state: FilterState::default(),
348 search_mode: SearchMode::default(),
349 string_match_candidates: Arc::new(vec![]),
350 matches: vec![],
351 focus_handle: cx.focus_handle(),
352 _keymap_subscription,
353 table_interaction_state,
354 filter_editor,
355 keystroke_editor,
356 selected_index: None,
357 context_menu: None,
358 previous_edit: None,
359 humanized_action_names,
360 search_query_debounce: None,
361 show_hover_menus: true,
362 };
363
364 this.on_keymap_changed(cx);
365
366 this
367 }
368
369 fn current_action_query(&self, cx: &App) -> String {
370 self.filter_editor.read(cx).text(cx)
371 }
372
373 fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
374 match self.search_mode {
375 SearchMode::KeyStroke { .. } => self
376 .keystroke_editor
377 .read(cx)
378 .keystrokes()
379 .iter()
380 .cloned()
381 .collect(),
382 SearchMode::Normal => Default::default(),
383 }
384 }
385
386 fn on_query_changed(&mut self, cx: &mut Context<Self>) {
387 let action_query = self.current_action_query(cx);
388 let keystroke_query = self.current_keystroke_query(cx);
389 let exact_match = self.search_mode.exact_match();
390
391 let timer = cx.background_executor().timer(Duration::from_secs(1));
392 self.search_query_debounce = Some(cx.background_spawn({
393 let action_query = action_query.clone();
394 let keystroke_query = keystroke_query.clone();
395 async move {
396 timer.await;
397
398 let keystroke_query = keystroke_query
399 .into_iter()
400 .map(|keystroke| keystroke.unparse())
401 .collect::<Vec<String>>()
402 .join(" ");
403
404 telemetry::event!(
405 "Keystroke Search Completed",
406 action_query = action_query,
407 keystroke_query = keystroke_query,
408 keystroke_exact_match = exact_match
409 )
410 }
411 }));
412 cx.spawn(async move |this, cx| {
413 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
414 this.update(cx, |this, cx| {
415 this.scroll_to_item(0, ScrollStrategy::Top, cx)
416 })
417 })
418 .detach();
419 }
420
421 async fn update_matches(
422 this: WeakEntity<Self>,
423 action_query: String,
424 keystroke_query: Vec<Keystroke>,
425 cx: &mut AsyncApp,
426 ) -> anyhow::Result<()> {
427 let action_query = command_palette::normalize_action_query(&action_query);
428 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
429 (this.string_match_candidates.clone(), this.keybindings.len())
430 })?;
431 let executor = cx.background_executor().clone();
432 let mut matches = fuzzy::match_strings(
433 &string_match_candidates,
434 &action_query,
435 true,
436 true,
437 keybind_count,
438 &Default::default(),
439 executor,
440 )
441 .await;
442 this.update(cx, |this, cx| {
443 match this.filter_state {
444 FilterState::Conflicts => {
445 matches.retain(|candidate| {
446 this.keybinding_conflict_state
447 .has_conflict(&candidate.candidate_id)
448 });
449 }
450 FilterState::All => {}
451 }
452
453 match this.search_mode {
454 SearchMode::KeyStroke { exact_match } => {
455 matches.retain(|item| {
456 this.keybindings[item.candidate_id]
457 .keystrokes()
458 .is_some_and(|keystrokes| {
459 if exact_match {
460 keystroke_query.len() == keystrokes.len()
461 && keystroke_query.iter().zip(keystrokes).all(
462 |(query, keystroke)| {
463 query.key == keystroke.key
464 && query.modifiers == keystroke.modifiers
465 },
466 )
467 } else {
468 let key_press_query =
469 KeyPressIterator::new(keystroke_query.as_slice());
470 let mut last_match_idx = 0;
471
472 key_press_query.into_iter().all(|key| {
473 let key_presses = KeyPressIterator::new(keystrokes);
474 key_presses.into_iter().enumerate().any(
475 |(index, keystroke)| {
476 if last_match_idx > index || keystroke != key {
477 return false;
478 }
479
480 last_match_idx = index;
481 true
482 },
483 )
484 })
485 }
486 })
487 });
488 }
489 SearchMode::Normal => {}
490 }
491
492 if action_query.is_empty() {
493 // apply default sort
494 // sorts by source precedence, and alphabetically by action name within each source
495 matches.sort_by_key(|match_item| {
496 let keybind = &this.keybindings[match_item.candidate_id];
497 let source = keybind.source.as_ref().map(|s| s.0);
498 use KeybindSource::*;
499 let source_precedence = match source {
500 Some(User) => 0,
501 Some(Vim) => 1,
502 Some(Base) => 2,
503 Some(Default) => 3,
504 None => 4,
505 };
506 return (source_precedence, keybind.action_name);
507 });
508 }
509 this.selected_index.take();
510 this.matches = matches;
511
512 cx.notify();
513 })
514 }
515
516 fn has_conflict(&self, row_index: usize) -> bool {
517 self.matches
518 .get(row_index)
519 .map(|candidate| candidate.candidate_id)
520 .is_some_and(|id| self.keybinding_conflict_state.has_conflict(&id))
521 }
522
523 fn process_bindings(
524 json_language: Arc<Language>,
525 zed_keybind_context_language: Arc<Language>,
526 cx: &mut App,
527 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
528 let key_bindings_ptr = cx.key_bindings();
529 let lock = key_bindings_ptr.borrow();
530 let key_bindings = lock.bindings();
531 let mut unmapped_action_names =
532 HashSet::from_iter(cx.all_action_names().into_iter().copied());
533 let action_documentation = cx.action_documentation();
534 let mut generator = KeymapFile::action_schema_generator();
535 let action_schema = HashMap::from_iter(
536 cx.action_schemas(&mut generator)
537 .into_iter()
538 .filter_map(|(name, schema)| schema.map(|schema| (name, schema))),
539 );
540
541 let mut processed_bindings = Vec::new();
542 let mut string_match_candidates = Vec::new();
543
544 for key_binding in key_bindings {
545 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
546
547 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
548 let ui_key_binding = Some(
549 ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
550 .vim_mode(source == Some(settings::KeybindSource::Vim)),
551 );
552
553 let context = key_binding
554 .predicate()
555 .map(|predicate| {
556 KeybindContextString::Local(
557 predicate.to_string().into(),
558 zed_keybind_context_language.clone(),
559 )
560 })
561 .unwrap_or(KeybindContextString::Global);
562
563 let source = source.map(|source| (source, source.name().into()));
564
565 let action_name = key_binding.action().name();
566 unmapped_action_names.remove(&action_name);
567 let action_arguments = key_binding
568 .action_input()
569 .map(|arguments| SyntaxHighlightedText::new(arguments, json_language.clone()));
570 let action_docs = action_documentation.get(action_name).copied();
571
572 let index = processed_bindings.len();
573 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
574 processed_bindings.push(ProcessedKeybinding {
575 keystroke_text: keystroke_text.into(),
576 ui_key_binding,
577 action_name,
578 action_arguments,
579 action_docs,
580 action_schema: action_schema.get(action_name).cloned(),
581 context: Some(context),
582 source,
583 });
584 string_match_candidates.push(string_match_candidate);
585 }
586
587 let empty = SharedString::new_static("");
588 for action_name in unmapped_action_names.into_iter() {
589 let index = processed_bindings.len();
590 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
591 processed_bindings.push(ProcessedKeybinding {
592 keystroke_text: empty.clone(),
593 ui_key_binding: None,
594 action_name,
595 action_arguments: None,
596 action_docs: action_documentation.get(action_name).copied(),
597 action_schema: action_schema.get(action_name).cloned(),
598 context: None,
599 source: None,
600 });
601 string_match_candidates.push(string_match_candidate);
602 }
603
604 (processed_bindings, string_match_candidates)
605 }
606
607 fn on_keymap_changed(&mut self, cx: &mut Context<KeymapEditor>) {
608 let workspace = self.workspace.clone();
609 cx.spawn(async move |this, cx| {
610 let json_language = load_json_language(workspace.clone(), cx).await;
611 let zed_keybind_context_language =
612 load_keybind_context_language(workspace.clone(), cx).await;
613
614 let (action_query, keystroke_query) = this.update(cx, |this, cx| {
615 let (key_bindings, string_match_candidates) =
616 Self::process_bindings(json_language, zed_keybind_context_language, cx);
617
618 this.keybinding_conflict_state = ConflictState::new(&key_bindings);
619
620 if !this.keybinding_conflict_state.any_conflicts() {
621 this.filter_state = FilterState::All;
622 }
623
624 this.keybindings = key_bindings;
625 this.string_match_candidates = Arc::new(string_match_candidates);
626 this.matches = this
627 .string_match_candidates
628 .iter()
629 .enumerate()
630 .map(|(ix, candidate)| StringMatch {
631 candidate_id: ix,
632 score: 0.0,
633 positions: vec![],
634 string: candidate.string.clone(),
635 })
636 .collect();
637 (
638 this.current_action_query(cx),
639 this.current_keystroke_query(cx),
640 )
641 })?;
642 // calls cx.notify
643 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
644 this.update(cx, |this, cx| {
645 if let Some(previous_edit) = this.previous_edit.take() {
646 match previous_edit {
647 // should remove scroll from process_query
648 PreviousEdit::ScrollBarOffset(offset) => {
649 this.table_interaction_state.update(cx, |table, _| {
650 table.set_scrollbar_offset(Axis::Vertical, offset)
651 })
652 // set selected index and scroll
653 }
654 PreviousEdit::Keybinding {
655 action_mapping,
656 action_name,
657 fallback,
658 } => {
659 let scroll_position =
660 this.matches.iter().enumerate().find_map(|(index, item)| {
661 let binding = &this.keybindings[item.candidate_id];
662 if binding.get_action_mapping() == action_mapping
663 && binding.action_name == action_name
664 {
665 Some(index)
666 } else {
667 None
668 }
669 });
670
671 if let Some(scroll_position) = scroll_position {
672 this.scroll_to_item(scroll_position, ScrollStrategy::Top, cx);
673 this.selected_index = Some(scroll_position);
674 } else {
675 this.table_interaction_state.update(cx, |table, _| {
676 table.set_scrollbar_offset(Axis::Vertical, fallback)
677 });
678 }
679 cx.notify();
680 }
681 }
682 }
683 })
684 })
685 .detach_and_log_err(cx);
686 }
687
688 fn key_context(&self) -> KeyContext {
689 let mut dispatch_context = KeyContext::new_with_defaults();
690 dispatch_context.add("KeymapEditor");
691 dispatch_context.add("menu");
692
693 dispatch_context
694 }
695
696 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
697 let index = usize::min(index, self.matches.len().saturating_sub(1));
698 self.table_interaction_state.update(cx, |this, _cx| {
699 this.scroll_handle.scroll_to_item(index, strategy);
700 });
701 }
702
703 fn focus_search(
704 &mut self,
705 _: &search::FocusSearch,
706 window: &mut Window,
707 cx: &mut Context<Self>,
708 ) {
709 if !self
710 .filter_editor
711 .focus_handle(cx)
712 .contains_focused(window, cx)
713 {
714 window.focus(&self.filter_editor.focus_handle(cx));
715 } else {
716 self.filter_editor.update(cx, |editor, cx| {
717 editor.select_all(&Default::default(), window, cx);
718 });
719 }
720 self.selected_index.take();
721 }
722
723 fn selected_keybind_index(&self) -> Option<usize> {
724 self.selected_index
725 .and_then(|match_index| self.matches.get(match_index))
726 .map(|r#match| r#match.candidate_id)
727 }
728
729 fn selected_keybind_and_index(&self) -> Option<(&ProcessedKeybinding, usize)> {
730 self.selected_keybind_index()
731 .map(|keybind_index| (&self.keybindings[keybind_index], keybind_index))
732 }
733
734 fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
735 self.selected_keybind_index()
736 .and_then(|keybind_index| self.keybindings.get(keybind_index))
737 }
738
739 fn select_index(&mut self, index: usize, cx: &mut Context<Self>) {
740 if self.selected_index != Some(index) {
741 self.selected_index = Some(index);
742 cx.notify();
743 }
744 }
745
746 fn create_context_menu(
747 &mut self,
748 position: Point<Pixels>,
749 window: &mut Window,
750 cx: &mut Context<Self>,
751 ) {
752 let weak = cx.weak_entity();
753 self.context_menu = self.selected_binding().map(|selected_binding| {
754 let key_strokes = selected_binding
755 .keystrokes()
756 .map(Vec::from)
757 .unwrap_or_default();
758 let selected_binding_has_no_context = selected_binding
759 .context
760 .as_ref()
761 .and_then(KeybindContextString::local)
762 .is_none();
763
764 let selected_binding_is_unbound = selected_binding.keystrokes().is_none();
765
766 let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
767 menu.context(self.focus_handle.clone())
768 .action_disabled_when(
769 selected_binding_is_unbound,
770 "Edit",
771 Box::new(EditBinding),
772 )
773 .action("Create", Box::new(CreateBinding))
774 .action_disabled_when(
775 selected_binding_is_unbound,
776 "Delete",
777 Box::new(DeleteBinding),
778 )
779 .separator()
780 .action("Copy Action", Box::new(CopyAction))
781 .action_disabled_when(
782 selected_binding_has_no_context,
783 "Copy Context",
784 Box::new(CopyContext),
785 )
786 .entry("Show matching keybindings", None, {
787 let weak = weak.clone();
788 let key_strokes = key_strokes.clone();
789
790 move |_, cx| {
791 weak.update(cx, |this, cx| {
792 this.filter_state = FilterState::All;
793 this.search_mode = SearchMode::KeyStroke { exact_match: true };
794
795 this.keystroke_editor.update(cx, |editor, cx| {
796 editor.set_keystrokes(key_strokes.clone(), cx);
797 });
798 })
799 .ok();
800 }
801 })
802 });
803
804 let context_menu_handle = context_menu.focus_handle(cx);
805 window.defer(cx, move |window, _cx| window.focus(&context_menu_handle));
806 let subscription = cx.subscribe_in(
807 &context_menu,
808 window,
809 |this, _, _: &DismissEvent, window, cx| {
810 this.dismiss_context_menu(window, cx);
811 },
812 );
813 (context_menu, position, subscription)
814 });
815
816 cx.notify();
817 }
818
819 fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
820 self.context_menu.take();
821 window.focus(&self.focus_handle);
822 cx.notify();
823 }
824
825 fn context_menu_deployed(&self) -> bool {
826 self.context_menu.is_some()
827 }
828
829 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
830 self.show_hover_menus = false;
831 if let Some(selected) = self.selected_index {
832 let selected = selected + 1;
833 if selected >= self.matches.len() {
834 self.select_last(&Default::default(), window, cx);
835 } else {
836 self.selected_index = Some(selected);
837 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
838 cx.notify();
839 }
840 } else {
841 self.select_first(&Default::default(), window, cx);
842 }
843 }
844
845 fn select_previous(
846 &mut self,
847 _: &menu::SelectPrevious,
848 window: &mut Window,
849 cx: &mut Context<Self>,
850 ) {
851 self.show_hover_menus = false;
852 if let Some(selected) = self.selected_index {
853 if selected == 0 {
854 return;
855 }
856
857 let selected = selected - 1;
858
859 if selected >= self.matches.len() {
860 self.select_last(&Default::default(), window, cx);
861 } else {
862 self.selected_index = Some(selected);
863 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
864 cx.notify();
865 }
866 } else {
867 self.select_last(&Default::default(), window, cx);
868 }
869 }
870
871 fn select_first(
872 &mut self,
873 _: &menu::SelectFirst,
874 _window: &mut Window,
875 cx: &mut Context<Self>,
876 ) {
877 self.show_hover_menus = false;
878 if self.matches.get(0).is_some() {
879 self.selected_index = Some(0);
880 self.scroll_to_item(0, ScrollStrategy::Center, cx);
881 cx.notify();
882 }
883 }
884
885 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
886 self.show_hover_menus = false;
887 if self.matches.last().is_some() {
888 let index = self.matches.len() - 1;
889 self.selected_index = Some(index);
890 self.scroll_to_item(index, ScrollStrategy::Center, cx);
891 cx.notify();
892 }
893 }
894
895 fn open_edit_keybinding_modal(
896 &mut self,
897 create: bool,
898 window: &mut Window,
899 cx: &mut Context<Self>,
900 ) {
901 self.show_hover_menus = false;
902 let Some((keybind, keybind_index)) = self.selected_keybind_and_index() else {
903 return;
904 };
905 let keybind = keybind.clone();
906 let keymap_editor = cx.entity();
907
908 let arguments = keybind
909 .action_arguments
910 .as_ref()
911 .map(|arguments| arguments.text.clone());
912 let context = keybind
913 .context
914 .as_ref()
915 .map(|context| context.local_str().unwrap_or("global"));
916 let source = keybind.source.as_ref().map(|source| source.1.clone());
917
918 telemetry::event!(
919 "Edit Keybinding Modal Opened",
920 keystroke = keybind.keystroke_text,
921 action = keybind.action_name,
922 source = source,
923 context = context,
924 arguments = arguments,
925 );
926
927 self.workspace
928 .update(cx, |workspace, cx| {
929 let fs = workspace.app_state().fs.clone();
930 let workspace_weak = cx.weak_entity();
931 workspace.toggle_modal(window, cx, |window, cx| {
932 let modal = KeybindingEditorModal::new(
933 create,
934 keybind,
935 keybind_index,
936 keymap_editor,
937 workspace_weak,
938 fs,
939 window,
940 cx,
941 );
942 window.focus(&modal.focus_handle(cx));
943 modal
944 });
945 })
946 .log_err();
947 }
948
949 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
950 self.open_edit_keybinding_modal(false, window, cx);
951 }
952
953 fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
954 self.open_edit_keybinding_modal(true, window, cx);
955 }
956
957 fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
958 let Some(to_remove) = self.selected_binding().cloned() else {
959 return;
960 };
961
962 let std::result::Result::Ok(fs) = self
963 .workspace
964 .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
965 else {
966 return;
967 };
968 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
969 self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
970 self.table_interaction_state
971 .read(cx)
972 .get_scrollbar_offset(Axis::Vertical),
973 ));
974 cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
975 .detach_and_notify_err(window, cx);
976 }
977
978 fn copy_context_to_clipboard(
979 &mut self,
980 _: &CopyContext,
981 _window: &mut Window,
982 cx: &mut Context<Self>,
983 ) {
984 let context = self
985 .selected_binding()
986 .and_then(|binding| binding.context.as_ref())
987 .and_then(KeybindContextString::local_str)
988 .map(|context| context.to_string());
989 let Some(context) = context else {
990 return;
991 };
992
993 telemetry::event!("Keybinding Context Copied", context = context.clone());
994 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
995 }
996
997 fn copy_action_to_clipboard(
998 &mut self,
999 _: &CopyAction,
1000 _window: &mut Window,
1001 cx: &mut Context<Self>,
1002 ) {
1003 let action = self
1004 .selected_binding()
1005 .map(|binding| binding.action_name.to_string());
1006 let Some(action) = action else {
1007 return;
1008 };
1009
1010 telemetry::event!("Keybinding Action Copied", action = action.clone());
1011 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
1012 }
1013
1014 fn toggle_conflict_filter(
1015 &mut self,
1016 _: &ToggleConflictFilter,
1017 _: &mut Window,
1018 cx: &mut Context<Self>,
1019 ) {
1020 self.set_filter_state(self.filter_state.invert(), cx);
1021 }
1022
1023 fn set_filter_state(&mut self, filter_state: FilterState, cx: &mut Context<Self>) {
1024 if self.filter_state != filter_state {
1025 self.filter_state = filter_state;
1026 self.on_query_changed(cx);
1027 }
1028 }
1029
1030 fn toggle_keystroke_search(
1031 &mut self,
1032 _: &ToggleKeystrokeSearch,
1033 window: &mut Window,
1034 cx: &mut Context<Self>,
1035 ) {
1036 self.search_mode = self.search_mode.invert();
1037 self.on_query_changed(cx);
1038
1039 match self.search_mode {
1040 SearchMode::KeyStroke { .. } => {
1041 window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
1042 }
1043 SearchMode::Normal => {
1044 self.keystroke_editor.update(cx, |editor, cx| {
1045 editor.clear_keystrokes(&ClearKeystrokes, window, cx)
1046 });
1047 window.focus(&self.filter_editor.focus_handle(cx));
1048 }
1049 }
1050 }
1051
1052 fn toggle_exact_keystroke_matching(
1053 &mut self,
1054 _: &ToggleExactKeystrokeMatching,
1055 _: &mut Window,
1056 cx: &mut Context<Self>,
1057 ) {
1058 let SearchMode::KeyStroke { exact_match } = &mut self.search_mode else {
1059 return;
1060 };
1061
1062 *exact_match = !(*exact_match);
1063 self.on_query_changed(cx);
1064 }
1065}
1066
1067#[derive(Clone)]
1068struct ProcessedKeybinding {
1069 keystroke_text: SharedString,
1070 ui_key_binding: Option<ui::KeyBinding>,
1071 action_name: &'static str,
1072 action_arguments: Option<SyntaxHighlightedText>,
1073 action_docs: Option<&'static str>,
1074 action_schema: Option<schemars::Schema>,
1075 context: Option<KeybindContextString>,
1076 source: Option<(KeybindSource, SharedString)>,
1077}
1078
1079impl ProcessedKeybinding {
1080 fn get_action_mapping(&self) -> ActionMapping {
1081 ActionMapping {
1082 keystroke_text: self.keystroke_text.clone(),
1083 context: self
1084 .context
1085 .as_ref()
1086 .and_then(|context| context.local())
1087 .cloned(),
1088 }
1089 }
1090
1091 fn keystrokes(&self) -> Option<&[Keystroke]> {
1092 self.ui_key_binding
1093 .as_ref()
1094 .map(|binding| binding.keystrokes.as_slice())
1095 }
1096}
1097
1098#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
1099enum KeybindContextString {
1100 Global,
1101 Local(SharedString, Arc<Language>),
1102}
1103
1104impl KeybindContextString {
1105 const GLOBAL: SharedString = SharedString::new_static("<global>");
1106
1107 pub fn local(&self) -> Option<&SharedString> {
1108 match self {
1109 KeybindContextString::Global => None,
1110 KeybindContextString::Local(name, _) => Some(name),
1111 }
1112 }
1113
1114 pub fn local_str(&self) -> Option<&str> {
1115 match self {
1116 KeybindContextString::Global => None,
1117 KeybindContextString::Local(name, _) => Some(name),
1118 }
1119 }
1120}
1121
1122impl RenderOnce for KeybindContextString {
1123 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
1124 match self {
1125 KeybindContextString::Global => {
1126 muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
1127 }
1128 KeybindContextString::Local(name, language) => {
1129 SyntaxHighlightedText::new(name, language).into_any_element()
1130 }
1131 }
1132 }
1133}
1134
1135fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
1136 let len = text.len();
1137 StyledText::new(text).with_highlights([(
1138 0..len,
1139 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
1140 )])
1141}
1142
1143impl Item for KeymapEditor {
1144 type Event = ();
1145
1146 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
1147 "Keymap Editor".into()
1148 }
1149}
1150
1151impl Render for KeymapEditor {
1152 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
1153 let row_count = self.matches.len();
1154 let theme = cx.theme();
1155
1156 v_flex()
1157 .id("keymap-editor")
1158 .track_focus(&self.focus_handle)
1159 .key_context(self.key_context())
1160 .on_action(cx.listener(Self::select_next))
1161 .on_action(cx.listener(Self::select_previous))
1162 .on_action(cx.listener(Self::select_first))
1163 .on_action(cx.listener(Self::select_last))
1164 .on_action(cx.listener(Self::focus_search))
1165 .on_action(cx.listener(Self::edit_binding))
1166 .on_action(cx.listener(Self::create_binding))
1167 .on_action(cx.listener(Self::delete_binding))
1168 .on_action(cx.listener(Self::copy_action_to_clipboard))
1169 .on_action(cx.listener(Self::copy_context_to_clipboard))
1170 .on_action(cx.listener(Self::toggle_conflict_filter))
1171 .on_action(cx.listener(Self::toggle_keystroke_search))
1172 .on_action(cx.listener(Self::toggle_exact_keystroke_matching))
1173 .size_full()
1174 .p_2()
1175 .gap_1()
1176 .bg(theme.colors().editor_background)
1177 .on_mouse_move(cx.listener(|this, _, _window, _cx| {
1178 this.show_hover_menus = true;
1179 }))
1180 .child(
1181 v_flex()
1182 .p_2()
1183 .gap_2()
1184 .child(
1185 h_flex()
1186 .gap_2()
1187 .child(
1188 div()
1189 .key_context({
1190 let mut context = KeyContext::new_with_defaults();
1191 context.add("BufferSearchBar");
1192 context
1193 })
1194 .size_full()
1195 .h_8()
1196 .pl_2()
1197 .pr_1()
1198 .py_1()
1199 .border_1()
1200 .border_color(theme.colors().border)
1201 .rounded_lg()
1202 .child(self.filter_editor.clone()),
1203 )
1204 .child(
1205 IconButton::new(
1206 "KeymapEditorToggleFiltersIcon",
1207 IconName::Keyboard,
1208 )
1209 .shape(ui::IconButtonShape::Square)
1210 .tooltip(|window, cx| {
1211 Tooltip::for_action(
1212 "Search by Keystroke",
1213 &ToggleKeystrokeSearch,
1214 window,
1215 cx,
1216 )
1217 })
1218 .toggle_state(matches!(
1219 self.search_mode,
1220 SearchMode::KeyStroke { .. }
1221 ))
1222 .on_click(|_, window, cx| {
1223 window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
1224 }),
1225 )
1226 .when(self.keybinding_conflict_state.any_conflicts(), |this| {
1227 this.child(
1228 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
1229 .shape(ui::IconButtonShape::Square)
1230 .tooltip({
1231 let filter_state = self.filter_state;
1232
1233 move |window, cx| {
1234 Tooltip::for_action(
1235 match filter_state {
1236 FilterState::All => "Show Conflicts",
1237 FilterState::Conflicts => "Hide Conflicts",
1238 },
1239 &ToggleConflictFilter,
1240 window,
1241 cx,
1242 )
1243 }
1244 })
1245 .selected_icon_color(Color::Warning)
1246 .toggle_state(matches!(
1247 self.filter_state,
1248 FilterState::Conflicts
1249 ))
1250 .on_click(|_, window, cx| {
1251 window.dispatch_action(
1252 ToggleConflictFilter.boxed_clone(),
1253 cx,
1254 );
1255 }),
1256 )
1257 }),
1258 )
1259 .when_some(
1260 match self.search_mode {
1261 SearchMode::Normal => None,
1262 SearchMode::KeyStroke { exact_match } => Some(exact_match),
1263 },
1264 |this, exact_match| {
1265 this.child(
1266 h_flex()
1267 .map(|this| {
1268 if self.keybinding_conflict_state.any_conflicts() {
1269 this.pr(rems_from_px(54.))
1270 } else {
1271 this.pr_7()
1272 }
1273 })
1274 .child(self.keystroke_editor.clone())
1275 .child(
1276 div().p_1().child(
1277 IconButton::new(
1278 "keystrokes-exact-match",
1279 IconName::Equal,
1280 )
1281 .tooltip(move |window, cx| {
1282 Tooltip::for_action(
1283 if exact_match {
1284 "Partial match mode"
1285 } else {
1286 "Exact match mode"
1287 },
1288 &ToggleExactKeystrokeMatching,
1289 window,
1290 cx,
1291 )
1292 })
1293 .shape(IconButtonShape::Square)
1294 .toggle_state(exact_match)
1295 .on_click(
1296 cx.listener(|_, _, window, cx| {
1297 window.dispatch_action(
1298 ToggleExactKeystrokeMatching.boxed_clone(),
1299 cx,
1300 );
1301 }),
1302 ),
1303 ),
1304 ),
1305 )
1306 },
1307 ),
1308 )
1309 .child(
1310 Table::new()
1311 .interactable(&self.table_interaction_state)
1312 .striped()
1313 .column_widths([
1314 rems(2.5),
1315 rems(16.),
1316 rems(16.),
1317 rems(16.),
1318 rems(32.),
1319 rems(8.),
1320 ])
1321 .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"])
1322 .uniform_list(
1323 "keymap-editor-table",
1324 row_count,
1325 cx.processor(move |this, range: Range<usize>, _window, cx| {
1326 let context_menu_deployed = this.context_menu_deployed();
1327 range
1328 .filter_map(|index| {
1329 let candidate_id = this.matches.get(index)?.candidate_id;
1330 let binding = &this.keybindings[candidate_id];
1331 let action_name = binding.action_name;
1332
1333 let icon = if this.filter_state != FilterState::Conflicts
1334 && this.has_conflict(index)
1335 {
1336 base_button_style(index, IconName::Warning)
1337 .icon_color(Color::Warning)
1338 .tooltip(|window, cx| {
1339 Tooltip::with_meta(
1340 "View conflicts",
1341 Some(&ToggleConflictFilter),
1342 "Use alt+click to show all conflicts",
1343 window,
1344 cx,
1345 )
1346 })
1347 .on_click(cx.listener(
1348 move |this, click: &ClickEvent, window, cx| {
1349 if click.modifiers().alt {
1350 this.set_filter_state(
1351 FilterState::Conflicts,
1352 cx,
1353 );
1354 } else {
1355 this.select_index(index, cx);
1356 this.open_edit_keybinding_modal(
1357 false, window, cx,
1358 );
1359 cx.stop_propagation();
1360 }
1361 },
1362 ))
1363 .into_any_element()
1364 } else {
1365 base_button_style(index, IconName::Pencil)
1366 .visible_on_hover(
1367 if this.selected_index == Some(index) {
1368 "".into()
1369 } else if this.show_hover_menus {
1370 row_group_id(index)
1371 } else {
1372 "never-show".into()
1373 },
1374 )
1375 .when(
1376 this.show_hover_menus && !context_menu_deployed,
1377 |this| {
1378 this.tooltip(Tooltip::for_action_title(
1379 "Edit Keybinding",
1380 &EditBinding,
1381 ))
1382 },
1383 )
1384 .on_click(cx.listener(move |this, _, window, cx| {
1385 this.select_index(index, cx);
1386 this.open_edit_keybinding_modal(false, window, cx);
1387 cx.stop_propagation();
1388 }))
1389 .into_any_element()
1390 };
1391
1392 let action = div()
1393 .id(("keymap action", index))
1394 .child({
1395 if action_name != gpui::NoAction.name() {
1396 this.humanized_action_names
1397 .get(action_name)
1398 .cloned()
1399 .unwrap_or(action_name.into())
1400 .into_any_element()
1401 } else {
1402 const NULL: SharedString =
1403 SharedString::new_static("<null>");
1404 muted_styled_text(NULL.clone(), cx)
1405 .into_any_element()
1406 }
1407 })
1408 .when(
1409 !context_menu_deployed && this.show_hover_menus,
1410 |this| {
1411 this.tooltip({
1412 let action_name = binding.action_name;
1413 let action_docs = binding.action_docs;
1414 move |_, cx| {
1415 let action_tooltip =
1416 Tooltip::new(action_name);
1417 let action_tooltip = match action_docs {
1418 Some(docs) => action_tooltip.meta(docs),
1419 None => action_tooltip,
1420 };
1421 cx.new(|_| action_tooltip).into()
1422 }
1423 })
1424 },
1425 )
1426 .into_any_element();
1427 let keystrokes = binding.ui_key_binding.clone().map_or(
1428 binding.keystroke_text.clone().into_any_element(),
1429 IntoElement::into_any_element,
1430 );
1431 let action_arguments = match binding.action_arguments.clone() {
1432 Some(arguments) => arguments.into_any_element(),
1433 None => {
1434 if binding.action_schema.is_some() {
1435 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
1436 .into_any_element()
1437 } else {
1438 gpui::Empty.into_any_element()
1439 }
1440 }
1441 };
1442 let context = binding.context.clone().map_or(
1443 gpui::Empty.into_any_element(),
1444 |context| {
1445 let is_local = context.local().is_some();
1446
1447 div()
1448 .id(("keymap context", index))
1449 .child(context.clone())
1450 .when(
1451 is_local
1452 && !context_menu_deployed
1453 && this.show_hover_menus,
1454 |this| {
1455 this.tooltip(Tooltip::element({
1456 move |_, _| {
1457 context.clone().into_any_element()
1458 }
1459 }))
1460 },
1461 )
1462 .into_any_element()
1463 },
1464 );
1465 let source = binding
1466 .source
1467 .clone()
1468 .map(|(_source, name)| name)
1469 .unwrap_or_default()
1470 .into_any_element();
1471 Some([
1472 icon,
1473 action,
1474 action_arguments,
1475 keystrokes,
1476 context,
1477 source,
1478 ])
1479 })
1480 .collect()
1481 }),
1482 )
1483 .map_row(
1484 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
1485 let is_conflict = this.has_conflict(row_index);
1486 let is_selected = this.selected_index == Some(row_index);
1487
1488 let row_id = row_group_id(row_index);
1489
1490 let row = row
1491 .id(row_id.clone())
1492 .on_any_mouse_down(cx.listener(
1493 move |this,
1494 mouse_down_event: &gpui::MouseDownEvent,
1495 window,
1496 cx| {
1497 match mouse_down_event.button {
1498 MouseButton::Right => {
1499 this.select_index(row_index, cx);
1500 this.create_context_menu(
1501 mouse_down_event.position,
1502 window,
1503 cx,
1504 );
1505 }
1506 _ => {}
1507 }
1508 },
1509 ))
1510 .on_click(cx.listener(
1511 move |this, event: &ClickEvent, window, cx| {
1512 this.select_index(row_index, cx);
1513 if event.up.click_count == 2 {
1514 this.open_edit_keybinding_modal(false, window, cx);
1515 }
1516 },
1517 ))
1518 .group(row_id)
1519 .border_2()
1520 .when(is_conflict, |row| {
1521 row.bg(cx.theme().status().error_background)
1522 })
1523 .when(is_selected, |row| {
1524 row.border_color(cx.theme().colors().panel_focused_border)
1525 });
1526
1527 row.into_any_element()
1528 }),
1529 ),
1530 )
1531 .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
1532 // This ensures that the menu is not dismissed in cases where scroll events
1533 // with a delta of zero are emitted
1534 if !event.delta.pixel_delta(px(1.)).y.is_zero() {
1535 this.context_menu.take();
1536 cx.notify();
1537 }
1538 }))
1539 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1540 deferred(
1541 anchored()
1542 .position(*position)
1543 .anchor(gpui::Corner::TopLeft)
1544 .child(menu.clone()),
1545 )
1546 .with_priority(1)
1547 }))
1548 }
1549}
1550
1551fn row_group_id(row_index: usize) -> SharedString {
1552 SharedString::new(format!("keymap-table-row-{}", row_index))
1553}
1554
1555fn base_button_style(row_index: usize, icon: IconName) -> IconButton {
1556 IconButton::new(("keymap-icon", row_index), icon)
1557 .shape(IconButtonShape::Square)
1558 .size(ButtonSize::Compact)
1559}
1560
1561#[derive(Debug, Clone, IntoElement)]
1562struct SyntaxHighlightedText {
1563 text: SharedString,
1564 language: Arc<Language>,
1565}
1566
1567impl SyntaxHighlightedText {
1568 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1569 Self {
1570 text: text.into(),
1571 language,
1572 }
1573 }
1574}
1575
1576impl RenderOnce for SyntaxHighlightedText {
1577 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1578 let text_style = window.text_style();
1579 let syntax_theme = cx.theme().syntax();
1580
1581 let text = self.text.clone();
1582
1583 let highlights = self
1584 .language
1585 .highlight_text(&text.as_ref().into(), 0..text.len());
1586 let mut runs = Vec::with_capacity(highlights.len());
1587 let mut offset = 0;
1588
1589 for (highlight_range, highlight_id) in highlights {
1590 // Add un-highlighted text before the current highlight
1591 if highlight_range.start > offset {
1592 runs.push(text_style.to_run(highlight_range.start - offset));
1593 }
1594
1595 let mut run_style = text_style.clone();
1596 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1597 run_style = run_style.highlight(highlight_style);
1598 }
1599 // add the highlighted range
1600 runs.push(run_style.to_run(highlight_range.len()));
1601 offset = highlight_range.end;
1602 }
1603
1604 // Add any remaining un-highlighted text
1605 if offset < text.len() {
1606 runs.push(text_style.to_run(text.len() - offset));
1607 }
1608
1609 return StyledText::new(text).with_runs(runs);
1610 }
1611}
1612
1613#[derive(PartialEq)]
1614enum InputError {
1615 Warning(SharedString),
1616 Error(SharedString),
1617}
1618
1619impl InputError {
1620 fn warning(message: impl Into<SharedString>) -> Self {
1621 Self::Warning(message.into())
1622 }
1623
1624 fn error(message: impl Into<SharedString>) -> Self {
1625 Self::Error(message.into())
1626 }
1627
1628 fn content(&self) -> &SharedString {
1629 match self {
1630 InputError::Warning(content) | InputError::Error(content) => content,
1631 }
1632 }
1633
1634 fn is_warning(&self) -> bool {
1635 matches!(self, InputError::Warning(_))
1636 }
1637}
1638
1639struct KeybindingEditorModal {
1640 creating: bool,
1641 editing_keybind: ProcessedKeybinding,
1642 editing_keybind_idx: usize,
1643 keybind_editor: Entity<KeystrokeInput>,
1644 context_editor: Entity<SingleLineInput>,
1645 action_arguments_editor: Option<Entity<Editor>>,
1646 fs: Arc<dyn Fs>,
1647 error: Option<InputError>,
1648 keymap_editor: Entity<KeymapEditor>,
1649 workspace: WeakEntity<Workspace>,
1650 focus_state: KeybindingEditorModalFocusState,
1651}
1652
1653impl ModalView for KeybindingEditorModal {}
1654
1655impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
1656
1657impl Focusable for KeybindingEditorModal {
1658 fn focus_handle(&self, cx: &App) -> FocusHandle {
1659 self.keybind_editor.focus_handle(cx)
1660 }
1661}
1662
1663impl KeybindingEditorModal {
1664 pub fn new(
1665 create: bool,
1666 editing_keybind: ProcessedKeybinding,
1667 editing_keybind_idx: usize,
1668 keymap_editor: Entity<KeymapEditor>,
1669 workspace: WeakEntity<Workspace>,
1670 fs: Arc<dyn Fs>,
1671 window: &mut Window,
1672 cx: &mut App,
1673 ) -> Self {
1674 let keybind_editor = cx
1675 .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
1676
1677 let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
1678 let input = SingleLineInput::new(window, cx, "Keybinding Context")
1679 .label("Edit Context")
1680 .label_size(LabelSize::Default);
1681
1682 if let Some(context) = editing_keybind
1683 .context
1684 .as_ref()
1685 .and_then(KeybindContextString::local)
1686 {
1687 input.editor().update(cx, |editor, cx| {
1688 editor.set_text(context.clone(), window, cx);
1689 });
1690 }
1691
1692 let editor_entity = input.editor().clone();
1693 let workspace = workspace.clone();
1694 cx.spawn(async move |_input_handle, cx| {
1695 let contexts = cx
1696 .background_spawn(async { collect_contexts_from_assets() })
1697 .await;
1698
1699 let language = load_keybind_context_language(workspace, cx).await;
1700 editor_entity
1701 .update(cx, |editor, cx| {
1702 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1703 buffer.update(cx, |buffer, cx| {
1704 buffer.set_language(Some(language), cx);
1705 });
1706 }
1707 editor.set_completion_provider(Some(std::rc::Rc::new(
1708 KeyContextCompletionProvider { contexts },
1709 )));
1710 })
1711 .context("Failed to load completions for keybinding context")
1712 })
1713 .detach_and_log_err(cx);
1714
1715 input
1716 });
1717
1718 let action_arguments_editor = editing_keybind.action_schema.clone().map(|_schema| {
1719 cx.new(|cx| {
1720 let mut editor = Editor::auto_height_unbounded(1, window, cx);
1721 let workspace = workspace.clone();
1722
1723 if let Some(arguments) = editing_keybind.action_arguments.clone() {
1724 editor.set_text(arguments.text, window, cx);
1725 } else {
1726 // TODO: default value from schema?
1727 editor.set_placeholder_text("Action Arguments", cx);
1728 }
1729 cx.spawn(async |editor, cx| {
1730 let json_language = load_json_language(workspace, cx).await;
1731 editor
1732 .update(cx, |editor, cx| {
1733 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1734 buffer.update(cx, |buffer, cx| {
1735 buffer.set_language(Some(json_language), cx)
1736 });
1737 }
1738 })
1739 .context("Failed to load JSON language for editing keybinding action arguments input")
1740 })
1741 .detach_and_log_err(cx);
1742 editor
1743 })
1744 });
1745
1746 let focus_state = KeybindingEditorModalFocusState::new(
1747 keybind_editor.read_with(cx, |keybind_editor, cx| keybind_editor.focus_handle(cx)),
1748 action_arguments_editor.as_ref().map(|args_editor| {
1749 args_editor.read_with(cx, |args_editor, cx| args_editor.focus_handle(cx))
1750 }),
1751 context_editor.read_with(cx, |context_editor, cx| context_editor.focus_handle(cx)),
1752 );
1753
1754 Self {
1755 creating: create,
1756 editing_keybind,
1757 editing_keybind_idx,
1758 fs,
1759 keybind_editor,
1760 context_editor,
1761 action_arguments_editor,
1762 error: None,
1763 keymap_editor,
1764 workspace,
1765 focus_state,
1766 }
1767 }
1768
1769 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
1770 if self
1771 .error
1772 .as_ref()
1773 .is_some_and(|old_error| old_error.is_warning() && *old_error == error)
1774 {
1775 false
1776 } else {
1777 self.error = Some(error);
1778 cx.notify();
1779 true
1780 }
1781 }
1782
1783 fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> {
1784 let action_arguments = self
1785 .action_arguments_editor
1786 .as_ref()
1787 .map(|editor| editor.read(cx).text(cx));
1788
1789 let value = action_arguments
1790 .as_ref()
1791 .map(|args| {
1792 serde_json::from_str(args).context("Failed to parse action arguments as JSON")
1793 })
1794 .transpose()?;
1795
1796 cx.build_action(&self.editing_keybind.action_name, value)
1797 .context("Failed to validate action arguments")?;
1798 Ok(action_arguments)
1799 }
1800
1801 fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
1802 let new_keystrokes = self
1803 .keybind_editor
1804 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
1805 anyhow::ensure!(!new_keystrokes.is_empty(), "Keystrokes cannot be empty");
1806 Ok(new_keystrokes)
1807 }
1808
1809 fn validate_context(&self, cx: &App) -> anyhow::Result<Option<String>> {
1810 let new_context = self
1811 .context_editor
1812 .read_with(cx, |input, cx| input.editor().read(cx).text(cx));
1813 let Some(context) = new_context.is_empty().not().then_some(new_context) else {
1814 return Ok(None);
1815 };
1816 gpui::KeyBindingContextPredicate::parse(&context).context("Failed to parse key context")?;
1817
1818 Ok(Some(context))
1819 }
1820
1821 fn save(&mut self, cx: &mut Context<Self>) {
1822 let existing_keybind = self.editing_keybind.clone();
1823 let fs = self.fs.clone();
1824 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1825 let new_keystrokes = match self.validate_keystrokes(cx) {
1826 Err(err) => {
1827 self.set_error(InputError::error(err.to_string()), cx);
1828 return;
1829 }
1830 Ok(keystrokes) => keystrokes,
1831 };
1832
1833 let new_context = match self.validate_context(cx) {
1834 Err(err) => {
1835 self.set_error(InputError::error(err.to_string()), cx);
1836 return;
1837 }
1838 Ok(context) => context,
1839 };
1840
1841 let new_action_args = match self.validate_action_arguments(cx) {
1842 Err(input_err) => {
1843 self.set_error(InputError::error(input_err.to_string()), cx);
1844 return;
1845 }
1846 Ok(input) => input,
1847 };
1848
1849 let action_mapping = ActionMapping {
1850 keystroke_text: ui::text_for_keystrokes(&new_keystrokes, cx).into(),
1851 context: new_context.as_ref().map(Into::into),
1852 };
1853
1854 let conflicting_indices = if self.creating {
1855 self.keymap_editor
1856 .read(cx)
1857 .keybinding_conflict_state
1858 .will_conflict(action_mapping)
1859 } else {
1860 self.keymap_editor
1861 .read(cx)
1862 .keybinding_conflict_state
1863 .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
1864 };
1865 if let Some(conflicting_indices) = conflicting_indices {
1866 let first_conflicting_index = conflicting_indices[0];
1867 let conflicting_action_name = self
1868 .keymap_editor
1869 .read(cx)
1870 .keybindings
1871 .get(first_conflicting_index)
1872 .map(|keybind| keybind.action_name);
1873
1874 let warning_message = match conflicting_action_name {
1875 Some(name) => {
1876 let confliction_action_amount = conflicting_indices.len() - 1;
1877 if confliction_action_amount > 0 {
1878 format!(
1879 "Your keybind would conflict with the \"{}\" action and {} other bindings",
1880 name, confliction_action_amount
1881 )
1882 } else {
1883 format!("Your keybind would conflict with the \"{}\" action", name)
1884 }
1885 }
1886 None => {
1887 log::info!(
1888 "Could not find action in keybindings with index {}",
1889 first_conflicting_index
1890 );
1891 "Your keybind would conflict with other actions".to_string()
1892 }
1893 };
1894
1895 if self.set_error(InputError::warning(warning_message), cx) {
1896 return;
1897 }
1898 }
1899
1900 let create = self.creating;
1901
1902 let status_toast = StatusToast::new(
1903 format!(
1904 "Saved edits to the {} action.",
1905 command_palette::humanize_action_name(&self.editing_keybind.action_name)
1906 ),
1907 cx,
1908 move |this, _cx| {
1909 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
1910 .dismiss_button(true)
1911 // .action("Undo", f) todo: wire the undo functionality
1912 },
1913 );
1914
1915 self.workspace
1916 .update(cx, |workspace, cx| {
1917 workspace.toggle_status_toast(status_toast, cx);
1918 })
1919 .log_err();
1920
1921 cx.spawn(async move |this, cx| {
1922 let action_name = existing_keybind.action_name;
1923
1924 if let Err(err) = save_keybinding_update(
1925 create,
1926 existing_keybind,
1927 &new_keystrokes,
1928 new_context.as_deref(),
1929 new_action_args.as_deref(),
1930 &fs,
1931 tab_size,
1932 )
1933 .await
1934 {
1935 this.update(cx, |this, cx| {
1936 this.set_error(InputError::error(err.to_string()), cx);
1937 })
1938 .log_err();
1939 } else {
1940 this.update(cx, |this, cx| {
1941 let action_mapping = ActionMapping {
1942 keystroke_text: ui::text_for_keystrokes(new_keystrokes.as_slice(), cx)
1943 .into(),
1944 context: new_context.map(SharedString::from),
1945 };
1946
1947 this.keymap_editor.update(cx, |keymap, cx| {
1948 keymap.previous_edit = Some(PreviousEdit::Keybinding {
1949 action_mapping,
1950 action_name,
1951 fallback: keymap
1952 .table_interaction_state
1953 .read(cx)
1954 .get_scrollbar_offset(Axis::Vertical),
1955 })
1956 });
1957 cx.emit(DismissEvent);
1958 })
1959 .ok();
1960 }
1961 })
1962 .detach();
1963 }
1964
1965 fn key_context(&self) -> KeyContext {
1966 let mut key_context = KeyContext::new_with_defaults();
1967 key_context.add("KeybindEditorModal");
1968 key_context
1969 }
1970
1971 fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1972 self.focus_state.focus_next(window, cx);
1973 }
1974
1975 fn focus_prev(
1976 &mut self,
1977 _: &menu::SelectPrevious,
1978 window: &mut Window,
1979 cx: &mut Context<Self>,
1980 ) {
1981 self.focus_state.focus_previous(window, cx);
1982 }
1983
1984 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
1985 self.save(cx);
1986 }
1987
1988 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
1989 cx.emit(DismissEvent)
1990 }
1991}
1992
1993impl Render for KeybindingEditorModal {
1994 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1995 let theme = cx.theme().colors();
1996 let action_name =
1997 command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
1998
1999 v_flex()
2000 .w(rems(34.))
2001 .elevation_3(cx)
2002 .key_context(self.key_context())
2003 .on_action(cx.listener(Self::focus_next))
2004 .on_action(cx.listener(Self::focus_prev))
2005 .on_action(cx.listener(Self::confirm))
2006 .on_action(cx.listener(Self::cancel))
2007 .child(
2008 Modal::new("keybinding_editor_modal", None)
2009 .header(
2010 ModalHeader::new().child(
2011 v_flex()
2012 .pb_1p5()
2013 .mb_1()
2014 .gap_0p5()
2015 .border_b_1()
2016 .border_color(theme.border_variant)
2017 .child(Label::new(action_name))
2018 .when_some(self.editing_keybind.action_docs, |this, docs| {
2019 this.child(
2020 Label::new(docs).size(LabelSize::Small).color(Color::Muted),
2021 )
2022 }),
2023 ),
2024 )
2025 .section(
2026 Section::new().child(
2027 v_flex()
2028 .gap_2()
2029 .child(
2030 v_flex()
2031 .child(Label::new("Edit Keystroke"))
2032 .gap_1()
2033 .child(self.keybind_editor.clone()),
2034 )
2035 .when_some(self.action_arguments_editor.clone(), |this, editor| {
2036 this.child(
2037 v_flex()
2038 .mt_1p5()
2039 .gap_1()
2040 .child(Label::new("Edit Arguments"))
2041 .child(
2042 div()
2043 .w_full()
2044 .py_1()
2045 .px_1p5()
2046 .rounded_lg()
2047 .bg(theme.editor_background)
2048 .border_1()
2049 .border_color(theme.border_variant)
2050 .child(editor),
2051 ),
2052 )
2053 })
2054 .child(self.context_editor.clone())
2055 .when_some(self.error.as_ref(), |this, error| {
2056 this.child(
2057 Banner::new()
2058 .map(|banner| match error {
2059 InputError::Error(_) => {
2060 banner.severity(ui::Severity::Error)
2061 }
2062 InputError::Warning(_) => {
2063 banner.severity(ui::Severity::Warning)
2064 }
2065 })
2066 // For some reason, the div overflows its container to the
2067 //right. The padding accounts for that.
2068 .child(
2069 div()
2070 .size_full()
2071 .pr_2()
2072 .child(Label::new(error.content())),
2073 ),
2074 )
2075 }),
2076 ),
2077 )
2078 .footer(
2079 ModalFooter::new().end_slot(
2080 h_flex()
2081 .gap_1()
2082 .child(
2083 Button::new("cancel", "Cancel")
2084 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
2085 )
2086 .child(Button::new("save-btn", "Save").on_click(cx.listener(
2087 |this, _event, _window, cx| {
2088 this.save(cx);
2089 },
2090 ))),
2091 ),
2092 ),
2093 )
2094 }
2095}
2096
2097struct KeybindingEditorModalFocusState {
2098 handles: Vec<FocusHandle>,
2099}
2100
2101impl KeybindingEditorModalFocusState {
2102 fn new(
2103 keystrokes: FocusHandle,
2104 action_input: Option<FocusHandle>,
2105 context: FocusHandle,
2106 ) -> Self {
2107 Self {
2108 handles: Vec::from_iter(
2109 [Some(keystrokes), action_input, Some(context)]
2110 .into_iter()
2111 .flatten(),
2112 ),
2113 }
2114 }
2115
2116 fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> {
2117 self.handles
2118 .iter()
2119 .position(|handle| handle.contains_focused(window, cx))
2120 .map(|i| i as i32)
2121 }
2122
2123 fn focus_index(&self, mut index: i32, window: &mut Window) {
2124 if index < 0 {
2125 index = self.handles.len() as i32 - 1;
2126 }
2127 if index >= self.handles.len() as i32 {
2128 index = 0;
2129 }
2130 window.focus(&self.handles[index as usize]);
2131 }
2132
2133 fn focus_next(&self, window: &mut Window, cx: &App) {
2134 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2135 index + 1
2136 } else {
2137 0
2138 };
2139 self.focus_index(index_to_focus, window);
2140 }
2141
2142 fn focus_previous(&self, window: &mut Window, cx: &App) {
2143 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2144 index - 1
2145 } else {
2146 self.handles.len() as i32 - 1
2147 };
2148 self.focus_index(index_to_focus, window);
2149 }
2150}
2151
2152struct KeyContextCompletionProvider {
2153 contexts: Vec<SharedString>,
2154}
2155
2156impl CompletionProvider for KeyContextCompletionProvider {
2157 fn completions(
2158 &self,
2159 _excerpt_id: editor::ExcerptId,
2160 buffer: &Entity<language::Buffer>,
2161 buffer_position: language::Anchor,
2162 _trigger: editor::CompletionContext,
2163 _window: &mut Window,
2164 cx: &mut Context<Editor>,
2165 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
2166 let buffer = buffer.read(cx);
2167 let mut count_back = 0;
2168 for char in buffer.reversed_chars_at(buffer_position) {
2169 if char.is_ascii_alphanumeric() || char == '_' {
2170 count_back += 1;
2171 } else {
2172 break;
2173 }
2174 }
2175 let start_anchor = buffer.anchor_before(
2176 buffer_position
2177 .to_offset(&buffer)
2178 .saturating_sub(count_back),
2179 );
2180 let replace_range = start_anchor..buffer_position;
2181 gpui::Task::ready(Ok(vec![project::CompletionResponse {
2182 completions: self
2183 .contexts
2184 .iter()
2185 .map(|context| project::Completion {
2186 replace_range: replace_range.clone(),
2187 label: language::CodeLabel::plain(context.to_string(), None),
2188 new_text: context.to_string(),
2189 documentation: None,
2190 source: project::CompletionSource::Custom,
2191 icon_path: None,
2192 insert_text_mode: None,
2193 confirm: None,
2194 })
2195 .collect(),
2196 is_incomplete: false,
2197 }]))
2198 }
2199
2200 fn is_completion_trigger(
2201 &self,
2202 _buffer: &Entity<language::Buffer>,
2203 _position: language::Anchor,
2204 text: &str,
2205 _trigger_in_words: bool,
2206 _menu_is_open: bool,
2207 _cx: &mut Context<Editor>,
2208 ) -> bool {
2209 text.chars().last().map_or(false, |last_char| {
2210 last_char.is_ascii_alphanumeric() || last_char == '_'
2211 })
2212 }
2213}
2214
2215async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
2216 let json_language_task = workspace
2217 .read_with(cx, |workspace, cx| {
2218 workspace
2219 .project()
2220 .read(cx)
2221 .languages()
2222 .language_for_name("JSON")
2223 })
2224 .context("Failed to load JSON language")
2225 .log_err();
2226 let json_language = match json_language_task {
2227 Some(task) => task.await.context("Failed to load JSON language").log_err(),
2228 None => None,
2229 };
2230 return json_language.unwrap_or_else(|| {
2231 Arc::new(Language::new(
2232 LanguageConfig {
2233 name: "JSON".into(),
2234 ..Default::default()
2235 },
2236 Some(tree_sitter_json::LANGUAGE.into()),
2237 ))
2238 });
2239}
2240
2241async fn load_keybind_context_language(
2242 workspace: WeakEntity<Workspace>,
2243 cx: &mut AsyncApp,
2244) -> Arc<Language> {
2245 let language_task = workspace
2246 .read_with(cx, |workspace, cx| {
2247 workspace
2248 .project()
2249 .read(cx)
2250 .languages()
2251 .language_for_name("Zed Keybind Context")
2252 })
2253 .context("Failed to load Zed Keybind Context language")
2254 .log_err();
2255 let language = match language_task {
2256 Some(task) => task
2257 .await
2258 .context("Failed to load Zed Keybind Context language")
2259 .log_err(),
2260 None => None,
2261 };
2262 return language.unwrap_or_else(|| {
2263 Arc::new(Language::new(
2264 LanguageConfig {
2265 name: "Zed Keybind Context".into(),
2266 ..Default::default()
2267 },
2268 Some(tree_sitter_rust::LANGUAGE.into()),
2269 ))
2270 });
2271}
2272
2273async fn save_keybinding_update(
2274 create: bool,
2275 existing: ProcessedKeybinding,
2276 new_keystrokes: &[Keystroke],
2277 new_context: Option<&str>,
2278 new_args: Option<&str>,
2279 fs: &Arc<dyn Fs>,
2280 tab_size: usize,
2281) -> anyhow::Result<()> {
2282 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2283 .await
2284 .context("Failed to load keymap file")?;
2285
2286 let existing_keystrokes = existing.keystrokes().unwrap_or_default();
2287 let existing_context = existing
2288 .context
2289 .as_ref()
2290 .and_then(KeybindContextString::local_str);
2291 let existing_args = existing
2292 .action_arguments
2293 .as_ref()
2294 .map(|args| args.text.as_ref());
2295
2296 let target = settings::KeybindUpdateTarget {
2297 context: existing_context,
2298 keystrokes: existing_keystrokes,
2299 action_name: &existing.action_name,
2300 action_arguments: existing_args,
2301 };
2302
2303 let source = settings::KeybindUpdateTarget {
2304 context: new_context,
2305 keystrokes: new_keystrokes,
2306 action_name: &existing.action_name,
2307 action_arguments: new_args,
2308 };
2309
2310 let operation = if !create {
2311 settings::KeybindUpdateOperation::Replace {
2312 target,
2313 target_keybind_source: existing
2314 .source
2315 .as_ref()
2316 .map(|(source, _name)| *source)
2317 .unwrap_or(KeybindSource::User),
2318 source,
2319 }
2320 } else {
2321 settings::KeybindUpdateOperation::Add {
2322 source,
2323 from: Some(target),
2324 }
2325 };
2326
2327 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
2328
2329 let updated_keymap_contents =
2330 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2331 .context("Failed to update keybinding")?;
2332 fs.write(
2333 paths::keymap_file().as_path(),
2334 updated_keymap_contents.as_bytes(),
2335 )
2336 .await
2337 .context("Failed to write keymap file")?;
2338
2339 telemetry::event!(
2340 "Keybinding Updated",
2341 new_keybinding = new_keybinding,
2342 removed_keybinding = removed_keybinding,
2343 source = source
2344 );
2345 Ok(())
2346}
2347
2348async fn remove_keybinding(
2349 existing: ProcessedKeybinding,
2350 fs: &Arc<dyn Fs>,
2351 tab_size: usize,
2352) -> anyhow::Result<()> {
2353 let Some(keystrokes) = existing.keystrokes() else {
2354 anyhow::bail!("Cannot remove a keybinding that does not exist");
2355 };
2356 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2357 .await
2358 .context("Failed to load keymap file")?;
2359
2360 let operation = settings::KeybindUpdateOperation::Remove {
2361 target: settings::KeybindUpdateTarget {
2362 context: existing
2363 .context
2364 .as_ref()
2365 .and_then(KeybindContextString::local_str),
2366 keystrokes,
2367 action_name: &existing.action_name,
2368 action_arguments: existing
2369 .action_arguments
2370 .as_ref()
2371 .map(|arguments| arguments.text.as_ref()),
2372 },
2373 target_keybind_source: existing
2374 .source
2375 .as_ref()
2376 .map(|(source, _name)| *source)
2377 .unwrap_or(KeybindSource::User),
2378 };
2379
2380 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
2381 let updated_keymap_contents =
2382 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2383 .context("Failed to update keybinding")?;
2384 fs.write(
2385 paths::keymap_file().as_path(),
2386 updated_keymap_contents.as_bytes(),
2387 )
2388 .await
2389 .context("Failed to write keymap file")?;
2390
2391 telemetry::event!(
2392 "Keybinding Removed",
2393 new_keybinding = new_keybinding,
2394 removed_keybinding = removed_keybinding,
2395 source = source
2396 );
2397 Ok(())
2398}
2399
2400#[derive(PartialEq, Eq, Debug, Copy, Clone)]
2401enum CloseKeystrokeResult {
2402 Partial,
2403 Close,
2404 None,
2405}
2406
2407#[derive(PartialEq, Eq, Debug, Clone)]
2408enum KeyPress<'a> {
2409 Alt,
2410 Control,
2411 Function,
2412 Shift,
2413 Platform,
2414 Key(&'a String),
2415}
2416
2417struct KeystrokeInput {
2418 keystrokes: Vec<Keystroke>,
2419 placeholder_keystrokes: Option<Vec<Keystroke>>,
2420 outer_focus_handle: FocusHandle,
2421 inner_focus_handle: FocusHandle,
2422 intercept_subscription: Option<Subscription>,
2423 _focus_subscriptions: [Subscription; 2],
2424 search: bool,
2425 /// Handles tripe escape to stop recording
2426 close_keystrokes: Option<Vec<Keystroke>>,
2427 close_keystrokes_start: Option<usize>,
2428}
2429
2430impl KeystrokeInput {
2431 const KEYSTROKE_COUNT_MAX: usize = 3;
2432
2433 fn new(
2434 placeholder_keystrokes: Option<Vec<Keystroke>>,
2435 window: &mut Window,
2436 cx: &mut Context<Self>,
2437 ) -> Self {
2438 let outer_focus_handle = cx.focus_handle();
2439 let inner_focus_handle = cx.focus_handle();
2440 let _focus_subscriptions = [
2441 cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
2442 cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
2443 ];
2444 Self {
2445 keystrokes: Vec::new(),
2446 placeholder_keystrokes,
2447 inner_focus_handle,
2448 outer_focus_handle,
2449 intercept_subscription: None,
2450 _focus_subscriptions,
2451 search: false,
2452 close_keystrokes: None,
2453 close_keystrokes_start: None,
2454 }
2455 }
2456
2457 fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
2458 self.keystrokes = keystrokes;
2459 self.keystrokes_changed(cx);
2460 }
2461
2462 fn dummy(modifiers: Modifiers) -> Keystroke {
2463 return Keystroke {
2464 modifiers,
2465 key: "".to_string(),
2466 key_char: None,
2467 };
2468 }
2469
2470 fn keystrokes_changed(&self, cx: &mut Context<Self>) {
2471 cx.emit(());
2472 cx.notify();
2473 }
2474
2475 fn key_context() -> KeyContext {
2476 let mut key_context = KeyContext::new_with_defaults();
2477 key_context.add("KeystrokeInput");
2478 key_context
2479 }
2480
2481 fn handle_possible_close_keystroke(
2482 &mut self,
2483 keystroke: &Keystroke,
2484 window: &mut Window,
2485 cx: &mut Context<Self>,
2486 ) -> CloseKeystrokeResult {
2487 let Some(keybind_for_close_action) = window
2488 .highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context())
2489 else {
2490 log::trace!("No keybinding to stop recording keystrokes in keystroke input");
2491 self.close_keystrokes.take();
2492 return CloseKeystrokeResult::None;
2493 };
2494 let action_keystrokes = keybind_for_close_action.keystrokes();
2495
2496 if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
2497 let mut index = 0;
2498
2499 while index < action_keystrokes.len() && index < close_keystrokes.len() {
2500 if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
2501 break;
2502 }
2503 index += 1;
2504 }
2505 if index == close_keystrokes.len() {
2506 if index >= action_keystrokes.len() {
2507 self.close_keystrokes_start.take();
2508 return CloseKeystrokeResult::None;
2509 }
2510 if keystroke.should_match(&action_keystrokes[index]) {
2511 if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 {
2512 self.stop_recording(&StopRecording, window, cx);
2513 return CloseKeystrokeResult::Close;
2514 } else {
2515 close_keystrokes.push(keystroke.clone());
2516 self.close_keystrokes = Some(close_keystrokes);
2517 return CloseKeystrokeResult::Partial;
2518 }
2519 } else {
2520 self.close_keystrokes_start.take();
2521 return CloseKeystrokeResult::None;
2522 }
2523 }
2524 } else if let Some(first_action_keystroke) = action_keystrokes.first()
2525 && keystroke.should_match(first_action_keystroke)
2526 {
2527 self.close_keystrokes = Some(vec![keystroke.clone()]);
2528 return CloseKeystrokeResult::Partial;
2529 }
2530 self.close_keystrokes_start.take();
2531 return CloseKeystrokeResult::None;
2532 }
2533
2534 fn on_modifiers_changed(
2535 &mut self,
2536 event: &ModifiersChangedEvent,
2537 _window: &mut Window,
2538 cx: &mut Context<Self>,
2539 ) {
2540 let keystrokes_len = self.keystrokes.len();
2541
2542 if let Some(last) = self.keystrokes.last_mut()
2543 && last.key.is_empty()
2544 && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
2545 {
2546 if self.search {
2547 last.modifiers = last.modifiers.xor(&event.modifiers);
2548 } else if !event.modifiers.modified() {
2549 self.keystrokes.pop();
2550 } else {
2551 last.modifiers = event.modifiers;
2552 }
2553
2554 self.keystrokes_changed(cx);
2555 } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
2556 self.keystrokes.push(Self::dummy(event.modifiers));
2557 self.keystrokes_changed(cx);
2558 }
2559 cx.stop_propagation();
2560 }
2561
2562 fn handle_keystroke(
2563 &mut self,
2564 keystroke: &Keystroke,
2565 window: &mut Window,
2566 cx: &mut Context<Self>,
2567 ) {
2568 let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
2569 if close_keystroke_result != CloseKeystrokeResult::Close {
2570 let key_len = self.keystrokes.len();
2571 if let Some(last) = self.keystrokes.last_mut()
2572 && last.key.is_empty()
2573 && key_len <= Self::KEYSTROKE_COUNT_MAX
2574 {
2575 if self.search {
2576 last.key = keystroke.key.clone();
2577 self.keystrokes_changed(cx);
2578 cx.stop_propagation();
2579 return;
2580 } else {
2581 self.keystrokes.pop();
2582 }
2583 }
2584 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
2585 if close_keystroke_result == CloseKeystrokeResult::Partial
2586 && self.close_keystrokes_start.is_none()
2587 {
2588 self.close_keystrokes_start = Some(self.keystrokes.len());
2589 }
2590 self.keystrokes.push(keystroke.clone());
2591 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
2592 self.keystrokes.push(Self::dummy(keystroke.modifiers));
2593 }
2594 } else if close_keystroke_result != CloseKeystrokeResult::Partial {
2595 self.clear_keystrokes(&ClearKeystrokes, window, cx);
2596 }
2597 }
2598 self.keystrokes_changed(cx);
2599 cx.stop_propagation();
2600 }
2601
2602 fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
2603 if self.intercept_subscription.is_none() {
2604 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
2605 this.handle_keystroke(&event.keystroke, window, cx);
2606 });
2607 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
2608 }
2609 }
2610
2611 fn on_inner_focus_out(
2612 &mut self,
2613 _event: gpui::FocusOutEvent,
2614 _window: &mut Window,
2615 cx: &mut Context<Self>,
2616 ) {
2617 self.intercept_subscription.take();
2618 cx.notify();
2619 }
2620
2621 fn keystrokes(&self) -> &[Keystroke] {
2622 if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
2623 && self.keystrokes.is_empty()
2624 {
2625 return placeholders;
2626 }
2627 if !self.search
2628 && self
2629 .keystrokes
2630 .last()
2631 .map_or(false, |last| last.key.is_empty())
2632 {
2633 return &self.keystrokes[..self.keystrokes.len() - 1];
2634 }
2635 return &self.keystrokes;
2636 }
2637
2638 fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
2639 let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
2640 && self.keystrokes.is_empty()
2641 {
2642 if is_recording {
2643 &[]
2644 } else {
2645 placeholders.as_slice()
2646 }
2647 } else {
2648 &self.keystrokes
2649 };
2650 keystrokes.iter().map(move |keystroke| {
2651 h_flex().children(ui::render_keystroke(
2652 keystroke,
2653 Some(Color::Default),
2654 Some(rems(0.875).into()),
2655 ui::PlatformStyle::platform(),
2656 false,
2657 ))
2658 })
2659 }
2660
2661 fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
2662 self.inner_focus_handle.clone()
2663 }
2664
2665 fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
2666 if !self.outer_focus_handle.is_focused(window) {
2667 return;
2668 }
2669 self.clear_keystrokes(&ClearKeystrokes, window, cx);
2670 window.focus(&self.inner_focus_handle);
2671 cx.notify();
2672 }
2673
2674 fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
2675 if !self.inner_focus_handle.is_focused(window) {
2676 return;
2677 }
2678 window.focus(&self.outer_focus_handle);
2679 if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() {
2680 self.keystrokes.drain(close_keystrokes_start..);
2681 }
2682 self.close_keystrokes.take();
2683 cx.notify();
2684 }
2685
2686 fn clear_keystrokes(
2687 &mut self,
2688 _: &ClearKeystrokes,
2689 _window: &mut Window,
2690 cx: &mut Context<Self>,
2691 ) {
2692 self.keystrokes.clear();
2693 self.keystrokes_changed(cx);
2694 }
2695}
2696
2697impl EventEmitter<()> for KeystrokeInput {}
2698
2699impl Focusable for KeystrokeInput {
2700 fn focus_handle(&self, _cx: &App) -> FocusHandle {
2701 self.outer_focus_handle.clone()
2702 }
2703}
2704
2705impl Render for KeystrokeInput {
2706 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2707 let colors = cx.theme().colors();
2708 let is_focused = self.outer_focus_handle.contains_focused(window, cx);
2709 let is_recording = self.inner_focus_handle.is_focused(window);
2710
2711 let horizontal_padding = rems_from_px(64.);
2712
2713 let recording_bg_color = colors
2714 .editor_background
2715 .blend(colors.text_accent.opacity(0.1));
2716
2717 let recording_pulse = || {
2718 Icon::new(IconName::Circle)
2719 .size(IconSize::Small)
2720 .color(Color::Error)
2721 .with_animation(
2722 "recording-pulse",
2723 Animation::new(std::time::Duration::from_secs(2))
2724 .repeat()
2725 .with_easing(gpui::pulsating_between(0.4, 0.8)),
2726 {
2727 let color = Color::Error.color(cx);
2728 move |this, delta| this.color(Color::Custom(color.opacity(delta)))
2729 },
2730 )
2731 };
2732
2733 let recording_indicator = h_flex()
2734 .h_4()
2735 .pr_1()
2736 .gap_0p5()
2737 .border_1()
2738 .border_color(colors.border)
2739 .bg(colors
2740 .editor_background
2741 .blend(colors.text_accent.opacity(0.1)))
2742 .rounded_sm()
2743 .child(recording_pulse())
2744 .child(
2745 Label::new("REC")
2746 .size(LabelSize::XSmall)
2747 .weight(FontWeight::SEMIBOLD)
2748 .color(Color::Error),
2749 );
2750
2751 let search_indicator = h_flex()
2752 .h_4()
2753 .pr_1()
2754 .gap_0p5()
2755 .border_1()
2756 .border_color(colors.border)
2757 .bg(colors
2758 .editor_background
2759 .blend(colors.text_accent.opacity(0.1)))
2760 .rounded_sm()
2761 .child(recording_pulse())
2762 .child(
2763 Label::new("SEARCH")
2764 .size(LabelSize::XSmall)
2765 .weight(FontWeight::SEMIBOLD)
2766 .color(Color::Accent),
2767 );
2768
2769 let record_icon = if self.search {
2770 IconName::MagnifyingGlass
2771 } else {
2772 IconName::PlayFilled
2773 };
2774
2775 return h_flex()
2776 .id("keystroke-input")
2777 .track_focus(&self.outer_focus_handle)
2778 .py_2()
2779 .px_3()
2780 .gap_2()
2781 .min_h_10()
2782 .w_full()
2783 .flex_1()
2784 .justify_between()
2785 .rounded_lg()
2786 .overflow_hidden()
2787 .map(|this| {
2788 if is_recording {
2789 this.bg(recording_bg_color)
2790 } else {
2791 this.bg(colors.editor_background)
2792 }
2793 })
2794 .border_1()
2795 .border_color(colors.border_variant)
2796 .when(is_focused, |parent| {
2797 parent.border_color(colors.border_focused)
2798 })
2799 .key_context(Self::key_context())
2800 .on_action(cx.listener(Self::start_recording))
2801 .on_action(cx.listener(Self::stop_recording))
2802 .child(
2803 h_flex()
2804 .w(horizontal_padding)
2805 .gap_0p5()
2806 .justify_start()
2807 .flex_none()
2808 .when(is_recording, |this| {
2809 this.map(|this| {
2810 if self.search {
2811 this.child(search_indicator)
2812 } else {
2813 this.child(recording_indicator)
2814 }
2815 })
2816 }),
2817 )
2818 .child(
2819 h_flex()
2820 .id("keystroke-input-inner")
2821 .track_focus(&self.inner_focus_handle)
2822 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
2823 .size_full()
2824 .when(!self.search, |this| {
2825 this.focus(|mut style| {
2826 style.border_color = Some(colors.border_focused);
2827 style
2828 })
2829 })
2830 .w_full()
2831 .min_w_0()
2832 .justify_center()
2833 .flex_wrap()
2834 .gap(ui::DynamicSpacing::Base04.rems(cx))
2835 .children(self.render_keystrokes(is_recording)),
2836 )
2837 .child(
2838 h_flex()
2839 .w(horizontal_padding)
2840 .gap_0p5()
2841 .justify_end()
2842 .flex_none()
2843 .map(|this| {
2844 if is_recording {
2845 this.child(
2846 IconButton::new("stop-record-btn", IconName::StopFilled)
2847 .shape(ui::IconButtonShape::Square)
2848 .map(|this| {
2849 this.tooltip(Tooltip::for_action_title(
2850 if self.search {
2851 "Stop Searching"
2852 } else {
2853 "Stop Recording"
2854 },
2855 &StopRecording,
2856 ))
2857 })
2858 .icon_color(Color::Error)
2859 .on_click(cx.listener(|this, _event, window, cx| {
2860 this.stop_recording(&StopRecording, window, cx);
2861 })),
2862 )
2863 } else {
2864 this.child(
2865 IconButton::new("record-btn", record_icon)
2866 .shape(ui::IconButtonShape::Square)
2867 .map(|this| {
2868 this.tooltip(Tooltip::for_action_title(
2869 if self.search {
2870 "Start Searching"
2871 } else {
2872 "Start Recording"
2873 },
2874 &StartRecording,
2875 ))
2876 })
2877 .when(!is_focused, |this| this.icon_color(Color::Muted))
2878 .on_click(cx.listener(|this, _event, window, cx| {
2879 this.start_recording(&StartRecording, window, cx);
2880 })),
2881 )
2882 }
2883 })
2884 .child(
2885 IconButton::new("clear-btn", IconName::Delete)
2886 .shape(ui::IconButtonShape::Square)
2887 .tooltip(Tooltip::for_action_title(
2888 "Clear Keystrokes",
2889 &ClearKeystrokes,
2890 ))
2891 .when(!is_recording || !is_focused, |this| {
2892 this.icon_color(Color::Muted)
2893 })
2894 .on_click(cx.listener(|this, _event, window, cx| {
2895 this.clear_keystrokes(&ClearKeystrokes, window, cx);
2896 })),
2897 ),
2898 );
2899 }
2900}
2901
2902fn collect_contexts_from_assets() -> Vec<SharedString> {
2903 let mut keymap_assets = vec![
2904 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
2905 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
2906 ];
2907 keymap_assets.extend(
2908 BaseKeymap::OPTIONS
2909 .iter()
2910 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
2911 .map(util::asset_str::<SettingsAssets>),
2912 );
2913
2914 let mut contexts = HashSet::default();
2915
2916 for keymap_asset in keymap_assets {
2917 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
2918 continue;
2919 };
2920
2921 for section in keymap.sections() {
2922 let context_expr = §ion.context;
2923 let mut queue = Vec::new();
2924 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
2925 continue;
2926 };
2927
2928 queue.push(root_context);
2929 while let Some(context) = queue.pop() {
2930 match context {
2931 gpui::KeyBindingContextPredicate::Identifier(ident) => {
2932 contexts.insert(ident);
2933 }
2934 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
2935 contexts.insert(ident_a);
2936 contexts.insert(ident_b);
2937 }
2938 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
2939 contexts.insert(ident_a);
2940 contexts.insert(ident_b);
2941 }
2942 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
2943 queue.push(*ctx_a);
2944 queue.push(*ctx_b);
2945 }
2946 gpui::KeyBindingContextPredicate::Not(ctx) => {
2947 queue.push(*ctx);
2948 }
2949 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
2950 queue.push(*ctx_a);
2951 queue.push(*ctx_b);
2952 }
2953 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
2954 queue.push(*ctx_a);
2955 queue.push(*ctx_b);
2956 }
2957 }
2958 }
2959 }
2960 }
2961
2962 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
2963 contexts.sort();
2964
2965 return contexts;
2966}
2967
2968impl SerializableItem for KeymapEditor {
2969 fn serialized_item_kind() -> &'static str {
2970 "KeymapEditor"
2971 }
2972
2973 fn cleanup(
2974 workspace_id: workspace::WorkspaceId,
2975 alive_items: Vec<workspace::ItemId>,
2976 _window: &mut Window,
2977 cx: &mut App,
2978 ) -> gpui::Task<gpui::Result<()>> {
2979 workspace::delete_unloaded_items(
2980 alive_items,
2981 workspace_id,
2982 "keybinding_editors",
2983 &KEYBINDING_EDITORS,
2984 cx,
2985 )
2986 }
2987
2988 fn deserialize(
2989 _project: Entity<project::Project>,
2990 workspace: WeakEntity<Workspace>,
2991 workspace_id: workspace::WorkspaceId,
2992 item_id: workspace::ItemId,
2993 window: &mut Window,
2994 cx: &mut App,
2995 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
2996 window.spawn(cx, async move |cx| {
2997 if KEYBINDING_EDITORS
2998 .get_keybinding_editor(item_id, workspace_id)?
2999 .is_some()
3000 {
3001 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
3002 } else {
3003 Err(anyhow!("No keybinding editor to deserialize"))
3004 }
3005 })
3006 }
3007
3008 fn serialize(
3009 &mut self,
3010 workspace: &mut Workspace,
3011 item_id: workspace::ItemId,
3012 _closing: bool,
3013 _window: &mut Window,
3014 cx: &mut ui::Context<Self>,
3015 ) -> Option<gpui::Task<gpui::Result<()>>> {
3016 let workspace_id = workspace.database_id()?;
3017 Some(cx.background_spawn(async move {
3018 KEYBINDING_EDITORS
3019 .save_keybinding_editor(item_id, workspace_id)
3020 .await
3021 }))
3022 }
3023
3024 fn should_serialize(&self, _event: &Self::Event) -> bool {
3025 false
3026 }
3027}
3028
3029mod persistence {
3030 use db::{define_connection, query, sqlez_macros::sql};
3031 use workspace::WorkspaceDb;
3032
3033 define_connection! {
3034 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
3035 &[sql!(
3036 CREATE TABLE keybinding_editors (
3037 workspace_id INTEGER,
3038 item_id INTEGER UNIQUE,
3039
3040 PRIMARY KEY(workspace_id, item_id),
3041 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
3042 ON DELETE CASCADE
3043 ) STRICT;
3044 )];
3045 }
3046
3047 impl KeybindingEditorDb {
3048 query! {
3049 pub async fn save_keybinding_editor(
3050 item_id: workspace::ItemId,
3051 workspace_id: workspace::WorkspaceId
3052 ) -> Result<()> {
3053 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
3054 VALUES (?, ?)
3055 }
3056 }
3057
3058 query! {
3059 pub fn get_keybinding_editor(
3060 item_id: workspace::ItemId,
3061 workspace_id: workspace::WorkspaceId
3062 ) -> Result<Option<workspace::ItemId>> {
3063 SELECT item_id
3064 FROM keybinding_editors
3065 WHERE item_id = ? AND workspace_id = ?
3066 }
3067 }
3068 }
3069}
3070
3071/// Iterator that yields KeyPress values from a slice of Keystrokes
3072struct KeyPressIterator<'a> {
3073 keystrokes: &'a [Keystroke],
3074 current_keystroke_index: usize,
3075 current_key_press_index: usize,
3076}
3077
3078impl<'a> KeyPressIterator<'a> {
3079 fn new(keystrokes: &'a [Keystroke]) -> Self {
3080 Self {
3081 keystrokes,
3082 current_keystroke_index: 0,
3083 current_key_press_index: 0,
3084 }
3085 }
3086}
3087
3088impl<'a> Iterator for KeyPressIterator<'a> {
3089 type Item = KeyPress<'a>;
3090
3091 fn next(&mut self) -> Option<Self::Item> {
3092 loop {
3093 let keystroke = self.keystrokes.get(self.current_keystroke_index)?;
3094
3095 match self.current_key_press_index {
3096 0 => {
3097 self.current_key_press_index = 1;
3098 if keystroke.modifiers.platform {
3099 return Some(KeyPress::Platform);
3100 }
3101 }
3102 1 => {
3103 self.current_key_press_index = 2;
3104 if keystroke.modifiers.alt {
3105 return Some(KeyPress::Alt);
3106 }
3107 }
3108 2 => {
3109 self.current_key_press_index = 3;
3110 if keystroke.modifiers.control {
3111 return Some(KeyPress::Control);
3112 }
3113 }
3114 3 => {
3115 self.current_key_press_index = 4;
3116 if keystroke.modifiers.shift {
3117 return Some(KeyPress::Shift);
3118 }
3119 }
3120 4 => {
3121 self.current_key_press_index = 5;
3122 if keystroke.modifiers.function {
3123 return Some(KeyPress::Function);
3124 }
3125 }
3126 _ => {
3127 self.current_keystroke_index += 1;
3128 self.current_key_press_index = 0;
3129
3130 if keystroke.key.is_empty() {
3131 continue;
3132 }
3133 return Some(KeyPress::Key(&keystroke.key));
3134 }
3135 }
3136 }
3137 }
3138}