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.highlight_on_focus = false;
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 // Update the keystroke editor to turn the `search` bool on
1040 self.keystroke_editor.update(cx, |keystroke_editor, cx| {
1041 keystroke_editor
1042 .set_search_mode(matches!(self.search_mode, SearchMode::KeyStroke { .. }));
1043 cx.notify();
1044 });
1045
1046 match self.search_mode {
1047 SearchMode::KeyStroke { .. } => {
1048 window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
1049 }
1050 SearchMode::Normal => {}
1051 }
1052 }
1053
1054 fn toggle_exact_keystroke_matching(
1055 &mut self,
1056 _: &ToggleExactKeystrokeMatching,
1057 _: &mut Window,
1058 cx: &mut Context<Self>,
1059 ) {
1060 let SearchMode::KeyStroke { exact_match } = &mut self.search_mode else {
1061 return;
1062 };
1063
1064 *exact_match = !(*exact_match);
1065 self.on_query_changed(cx);
1066 }
1067}
1068
1069#[derive(Clone)]
1070struct ProcessedKeybinding {
1071 keystroke_text: SharedString,
1072 ui_key_binding: Option<ui::KeyBinding>,
1073 action_name: &'static str,
1074 action_arguments: Option<SyntaxHighlightedText>,
1075 action_docs: Option<&'static str>,
1076 action_schema: Option<schemars::Schema>,
1077 context: Option<KeybindContextString>,
1078 source: Option<(KeybindSource, SharedString)>,
1079}
1080
1081impl ProcessedKeybinding {
1082 fn get_action_mapping(&self) -> ActionMapping {
1083 ActionMapping {
1084 keystroke_text: self.keystroke_text.clone(),
1085 context: self
1086 .context
1087 .as_ref()
1088 .and_then(|context| context.local())
1089 .cloned(),
1090 }
1091 }
1092
1093 fn keystrokes(&self) -> Option<&[Keystroke]> {
1094 self.ui_key_binding
1095 .as_ref()
1096 .map(|binding| binding.keystrokes.as_slice())
1097 }
1098}
1099
1100#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
1101enum KeybindContextString {
1102 Global,
1103 Local(SharedString, Arc<Language>),
1104}
1105
1106impl KeybindContextString {
1107 const GLOBAL: SharedString = SharedString::new_static("<global>");
1108
1109 pub fn local(&self) -> Option<&SharedString> {
1110 match self {
1111 KeybindContextString::Global => None,
1112 KeybindContextString::Local(name, _) => Some(name),
1113 }
1114 }
1115
1116 pub fn local_str(&self) -> Option<&str> {
1117 match self {
1118 KeybindContextString::Global => None,
1119 KeybindContextString::Local(name, _) => Some(name),
1120 }
1121 }
1122}
1123
1124impl RenderOnce for KeybindContextString {
1125 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
1126 match self {
1127 KeybindContextString::Global => {
1128 muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
1129 }
1130 KeybindContextString::Local(name, language) => {
1131 SyntaxHighlightedText::new(name, language).into_any_element()
1132 }
1133 }
1134 }
1135}
1136
1137fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
1138 let len = text.len();
1139 StyledText::new(text).with_highlights([(
1140 0..len,
1141 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
1142 )])
1143}
1144
1145impl Item for KeymapEditor {
1146 type Event = ();
1147
1148 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
1149 "Keymap Editor".into()
1150 }
1151}
1152
1153impl Render for KeymapEditor {
1154 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
1155 let row_count = self.matches.len();
1156 let theme = cx.theme();
1157
1158 v_flex()
1159 .id("keymap-editor")
1160 .track_focus(&self.focus_handle)
1161 .key_context(self.key_context())
1162 .on_action(cx.listener(Self::select_next))
1163 .on_action(cx.listener(Self::select_previous))
1164 .on_action(cx.listener(Self::select_first))
1165 .on_action(cx.listener(Self::select_last))
1166 .on_action(cx.listener(Self::focus_search))
1167 .on_action(cx.listener(Self::edit_binding))
1168 .on_action(cx.listener(Self::create_binding))
1169 .on_action(cx.listener(Self::delete_binding))
1170 .on_action(cx.listener(Self::copy_action_to_clipboard))
1171 .on_action(cx.listener(Self::copy_context_to_clipboard))
1172 .on_action(cx.listener(Self::toggle_conflict_filter))
1173 .on_action(cx.listener(Self::toggle_keystroke_search))
1174 .on_action(cx.listener(Self::toggle_exact_keystroke_matching))
1175 .size_full()
1176 .p_2()
1177 .gap_1()
1178 .bg(theme.colors().editor_background)
1179 .on_mouse_move(cx.listener(|this, _, _window, _cx| {
1180 this.show_hover_menus = true;
1181 }))
1182 .child(
1183 v_flex()
1184 .p_2()
1185 .gap_2()
1186 .child(
1187 h_flex()
1188 .gap_2()
1189 .child(
1190 div()
1191 .key_context({
1192 let mut context = KeyContext::new_with_defaults();
1193 context.add("BufferSearchBar");
1194 context
1195 })
1196 .size_full()
1197 .h_8()
1198 .pl_2()
1199 .pr_1()
1200 .py_1()
1201 .border_1()
1202 .border_color(theme.colors().border)
1203 .rounded_lg()
1204 .child(self.filter_editor.clone()),
1205 )
1206 .child(
1207 IconButton::new(
1208 "KeymapEditorToggleFiltersIcon",
1209 IconName::Keyboard,
1210 )
1211 .shape(ui::IconButtonShape::Square)
1212 .tooltip(|window, cx| {
1213 Tooltip::for_action(
1214 "Search by Keystroke",
1215 &ToggleKeystrokeSearch,
1216 window,
1217 cx,
1218 )
1219 })
1220 .toggle_state(matches!(
1221 self.search_mode,
1222 SearchMode::KeyStroke { .. }
1223 ))
1224 .on_click(|_, window, cx| {
1225 window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
1226 }),
1227 )
1228 .when(self.keybinding_conflict_state.any_conflicts(), |this| {
1229 this.child(
1230 IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
1231 .shape(ui::IconButtonShape::Square)
1232 .tooltip({
1233 let filter_state = self.filter_state;
1234
1235 move |window, cx| {
1236 Tooltip::for_action(
1237 match filter_state {
1238 FilterState::All => "Show Conflicts",
1239 FilterState::Conflicts => "Hide Conflicts",
1240 },
1241 &ToggleConflictFilter,
1242 window,
1243 cx,
1244 )
1245 }
1246 })
1247 .selected_icon_color(Color::Warning)
1248 .toggle_state(matches!(
1249 self.filter_state,
1250 FilterState::Conflicts
1251 ))
1252 .on_click(|_, window, cx| {
1253 window.dispatch_action(
1254 ToggleConflictFilter.boxed_clone(),
1255 cx,
1256 );
1257 }),
1258 )
1259 }),
1260 )
1261 .when_some(
1262 match self.search_mode {
1263 SearchMode::Normal => None,
1264 SearchMode::KeyStroke { exact_match } => Some(exact_match),
1265 },
1266 |this, exact_match| {
1267 this.child(
1268 h_flex()
1269 .map(|this| {
1270 if self.keybinding_conflict_state.any_conflicts() {
1271 this.pr(rems_from_px(54.))
1272 } else {
1273 this.pr_7()
1274 }
1275 })
1276 .child(self.keystroke_editor.clone())
1277 .child(
1278 div().p_1().child(
1279 IconButton::new(
1280 "keystrokes-exact-match",
1281 IconName::Equal,
1282 )
1283 .tooltip(move |window, cx| {
1284 Tooltip::for_action(
1285 if exact_match {
1286 "Partial match mode"
1287 } else {
1288 "Exact match mode"
1289 },
1290 &ToggleExactKeystrokeMatching,
1291 window,
1292 cx,
1293 )
1294 })
1295 .shape(IconButtonShape::Square)
1296 .toggle_state(exact_match)
1297 .on_click(
1298 cx.listener(|_, _, window, cx| {
1299 window.dispatch_action(
1300 ToggleExactKeystrokeMatching.boxed_clone(),
1301 cx,
1302 );
1303 }),
1304 ),
1305 ),
1306 ),
1307 )
1308 },
1309 ),
1310 )
1311 .child(
1312 Table::new()
1313 .interactable(&self.table_interaction_state)
1314 .striped()
1315 .column_widths([
1316 rems(2.5),
1317 rems(16.),
1318 rems(16.),
1319 rems(16.),
1320 rems(32.),
1321 rems(8.),
1322 ])
1323 .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"])
1324 .uniform_list(
1325 "keymap-editor-table",
1326 row_count,
1327 cx.processor(move |this, range: Range<usize>, _window, cx| {
1328 let context_menu_deployed = this.context_menu_deployed();
1329 range
1330 .filter_map(|index| {
1331 let candidate_id = this.matches.get(index)?.candidate_id;
1332 let binding = &this.keybindings[candidate_id];
1333 let action_name = binding.action_name;
1334
1335 let icon = if this.filter_state != FilterState::Conflicts
1336 && this.has_conflict(index)
1337 {
1338 base_button_style(index, IconName::Warning)
1339 .icon_color(Color::Warning)
1340 .tooltip(|window, cx| {
1341 Tooltip::with_meta(
1342 "View conflicts",
1343 Some(&ToggleConflictFilter),
1344 "Use alt+click to show all conflicts",
1345 window,
1346 cx,
1347 )
1348 })
1349 .on_click(cx.listener(
1350 move |this, click: &ClickEvent, window, cx| {
1351 if click.modifiers().alt {
1352 this.set_filter_state(
1353 FilterState::Conflicts,
1354 cx,
1355 );
1356 } else {
1357 this.select_index(index, cx);
1358 this.open_edit_keybinding_modal(
1359 false, window, cx,
1360 );
1361 cx.stop_propagation();
1362 }
1363 },
1364 ))
1365 .into_any_element()
1366 } else {
1367 base_button_style(index, IconName::Pencil)
1368 .visible_on_hover(
1369 if this.selected_index == Some(index) {
1370 "".into()
1371 } else if this.show_hover_menus {
1372 row_group_id(index)
1373 } else {
1374 "never-show".into()
1375 },
1376 )
1377 .when(
1378 this.show_hover_menus && !context_menu_deployed,
1379 |this| {
1380 this.tooltip(Tooltip::for_action_title(
1381 "Edit Keybinding",
1382 &EditBinding,
1383 ))
1384 },
1385 )
1386 .on_click(cx.listener(move |this, _, window, cx| {
1387 this.select_index(index, cx);
1388 this.open_edit_keybinding_modal(false, window, cx);
1389 cx.stop_propagation();
1390 }))
1391 .into_any_element()
1392 };
1393
1394 let action = div()
1395 .id(("keymap action", index))
1396 .child({
1397 if action_name != gpui::NoAction.name() {
1398 this.humanized_action_names
1399 .get(action_name)
1400 .cloned()
1401 .unwrap_or(action_name.into())
1402 .into_any_element()
1403 } else {
1404 const NULL: SharedString =
1405 SharedString::new_static("<null>");
1406 muted_styled_text(NULL.clone(), cx)
1407 .into_any_element()
1408 }
1409 })
1410 .when(
1411 !context_menu_deployed && this.show_hover_menus,
1412 |this| {
1413 this.tooltip({
1414 let action_name = binding.action_name;
1415 let action_docs = binding.action_docs;
1416 move |_, cx| {
1417 let action_tooltip =
1418 Tooltip::new(action_name);
1419 let action_tooltip = match action_docs {
1420 Some(docs) => action_tooltip.meta(docs),
1421 None => action_tooltip,
1422 };
1423 cx.new(|_| action_tooltip).into()
1424 }
1425 })
1426 },
1427 )
1428 .into_any_element();
1429 let keystrokes = binding.ui_key_binding.clone().map_or(
1430 binding.keystroke_text.clone().into_any_element(),
1431 IntoElement::into_any_element,
1432 );
1433 let action_arguments = match binding.action_arguments.clone() {
1434 Some(arguments) => arguments.into_any_element(),
1435 None => {
1436 if binding.action_schema.is_some() {
1437 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
1438 .into_any_element()
1439 } else {
1440 gpui::Empty.into_any_element()
1441 }
1442 }
1443 };
1444 let context = binding.context.clone().map_or(
1445 gpui::Empty.into_any_element(),
1446 |context| {
1447 let is_local = context.local().is_some();
1448
1449 div()
1450 .id(("keymap context", index))
1451 .child(context.clone())
1452 .when(
1453 is_local
1454 && !context_menu_deployed
1455 && this.show_hover_menus,
1456 |this| {
1457 this.tooltip(Tooltip::element({
1458 move |_, _| {
1459 context.clone().into_any_element()
1460 }
1461 }))
1462 },
1463 )
1464 .into_any_element()
1465 },
1466 );
1467 let source = binding
1468 .source
1469 .clone()
1470 .map(|(_source, name)| name)
1471 .unwrap_or_default()
1472 .into_any_element();
1473 Some([
1474 icon,
1475 action,
1476 action_arguments,
1477 keystrokes,
1478 context,
1479 source,
1480 ])
1481 })
1482 .collect()
1483 }),
1484 )
1485 .map_row(
1486 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
1487 let is_conflict = this.has_conflict(row_index);
1488 let is_selected = this.selected_index == Some(row_index);
1489
1490 let row_id = row_group_id(row_index);
1491
1492 let row = row
1493 .id(row_id.clone())
1494 .on_any_mouse_down(cx.listener(
1495 move |this,
1496 mouse_down_event: &gpui::MouseDownEvent,
1497 window,
1498 cx| {
1499 match mouse_down_event.button {
1500 MouseButton::Right => {
1501 this.select_index(row_index, cx);
1502 this.create_context_menu(
1503 mouse_down_event.position,
1504 window,
1505 cx,
1506 );
1507 }
1508 _ => {}
1509 }
1510 },
1511 ))
1512 .on_click(cx.listener(
1513 move |this, event: &ClickEvent, window, cx| {
1514 this.select_index(row_index, cx);
1515 if event.up.click_count == 2 {
1516 this.open_edit_keybinding_modal(false, window, cx);
1517 }
1518 },
1519 ))
1520 .group(row_id)
1521 .border_2()
1522 .when(is_conflict, |row| {
1523 row.bg(cx.theme().status().error_background)
1524 })
1525 .when(is_selected, |row| {
1526 row.border_color(cx.theme().colors().panel_focused_border)
1527 });
1528
1529 row.into_any_element()
1530 }),
1531 ),
1532 )
1533 .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
1534 // This ensures that the menu is not dismissed in cases where scroll events
1535 // with a delta of zero are emitted
1536 if !event.delta.pixel_delta(px(1.)).y.is_zero() {
1537 this.context_menu.take();
1538 cx.notify();
1539 }
1540 }))
1541 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1542 deferred(
1543 anchored()
1544 .position(*position)
1545 .anchor(gpui::Corner::TopLeft)
1546 .child(menu.clone()),
1547 )
1548 .with_priority(1)
1549 }))
1550 }
1551}
1552
1553fn row_group_id(row_index: usize) -> SharedString {
1554 SharedString::new(format!("keymap-table-row-{}", row_index))
1555}
1556
1557fn base_button_style(row_index: usize, icon: IconName) -> IconButton {
1558 IconButton::new(("keymap-icon", row_index), icon)
1559 .shape(IconButtonShape::Square)
1560 .size(ButtonSize::Compact)
1561}
1562
1563#[derive(Debug, Clone, IntoElement)]
1564struct SyntaxHighlightedText {
1565 text: SharedString,
1566 language: Arc<Language>,
1567}
1568
1569impl SyntaxHighlightedText {
1570 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
1571 Self {
1572 text: text.into(),
1573 language,
1574 }
1575 }
1576}
1577
1578impl RenderOnce for SyntaxHighlightedText {
1579 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1580 let text_style = window.text_style();
1581 let syntax_theme = cx.theme().syntax();
1582
1583 let text = self.text.clone();
1584
1585 let highlights = self
1586 .language
1587 .highlight_text(&text.as_ref().into(), 0..text.len());
1588 let mut runs = Vec::with_capacity(highlights.len());
1589 let mut offset = 0;
1590
1591 for (highlight_range, highlight_id) in highlights {
1592 // Add un-highlighted text before the current highlight
1593 if highlight_range.start > offset {
1594 runs.push(text_style.to_run(highlight_range.start - offset));
1595 }
1596
1597 let mut run_style = text_style.clone();
1598 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
1599 run_style = run_style.highlight(highlight_style);
1600 }
1601 // add the highlighted range
1602 runs.push(run_style.to_run(highlight_range.len()));
1603 offset = highlight_range.end;
1604 }
1605
1606 // Add any remaining un-highlighted text
1607 if offset < text.len() {
1608 runs.push(text_style.to_run(text.len() - offset));
1609 }
1610
1611 return StyledText::new(text).with_runs(runs);
1612 }
1613}
1614
1615#[derive(PartialEq)]
1616enum InputError {
1617 Warning(SharedString),
1618 Error(SharedString),
1619}
1620
1621impl InputError {
1622 fn warning(message: impl Into<SharedString>) -> Self {
1623 Self::Warning(message.into())
1624 }
1625
1626 fn error(message: impl Into<SharedString>) -> Self {
1627 Self::Error(message.into())
1628 }
1629
1630 fn content(&self) -> &SharedString {
1631 match self {
1632 InputError::Warning(content) | InputError::Error(content) => content,
1633 }
1634 }
1635
1636 fn is_warning(&self) -> bool {
1637 matches!(self, InputError::Warning(_))
1638 }
1639}
1640
1641struct KeybindingEditorModal {
1642 creating: bool,
1643 editing_keybind: ProcessedKeybinding,
1644 editing_keybind_idx: usize,
1645 keybind_editor: Entity<KeystrokeInput>,
1646 context_editor: Entity<SingleLineInput>,
1647 action_arguments_editor: Option<Entity<Editor>>,
1648 fs: Arc<dyn Fs>,
1649 error: Option<InputError>,
1650 keymap_editor: Entity<KeymapEditor>,
1651 workspace: WeakEntity<Workspace>,
1652 focus_state: KeybindingEditorModalFocusState,
1653}
1654
1655impl ModalView for KeybindingEditorModal {}
1656
1657impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
1658
1659impl Focusable for KeybindingEditorModal {
1660 fn focus_handle(&self, cx: &App) -> FocusHandle {
1661 self.keybind_editor.focus_handle(cx)
1662 }
1663}
1664
1665impl KeybindingEditorModal {
1666 pub fn new(
1667 create: bool,
1668 editing_keybind: ProcessedKeybinding,
1669 editing_keybind_idx: usize,
1670 keymap_editor: Entity<KeymapEditor>,
1671 workspace: WeakEntity<Workspace>,
1672 fs: Arc<dyn Fs>,
1673 window: &mut Window,
1674 cx: &mut App,
1675 ) -> Self {
1676 let keybind_editor = cx
1677 .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
1678
1679 let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
1680 let input = SingleLineInput::new(window, cx, "Keybinding Context")
1681 .label("Edit Context")
1682 .label_size(LabelSize::Default);
1683
1684 if let Some(context) = editing_keybind
1685 .context
1686 .as_ref()
1687 .and_then(KeybindContextString::local)
1688 {
1689 input.editor().update(cx, |editor, cx| {
1690 editor.set_text(context.clone(), window, cx);
1691 });
1692 }
1693
1694 let editor_entity = input.editor().clone();
1695 let workspace = workspace.clone();
1696 cx.spawn(async move |_input_handle, cx| {
1697 let contexts = cx
1698 .background_spawn(async { collect_contexts_from_assets() })
1699 .await;
1700
1701 let language = load_keybind_context_language(workspace, cx).await;
1702 editor_entity
1703 .update(cx, |editor, cx| {
1704 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1705 buffer.update(cx, |buffer, cx| {
1706 buffer.set_language(Some(language), cx);
1707 });
1708 }
1709 editor.set_completion_provider(Some(std::rc::Rc::new(
1710 KeyContextCompletionProvider { contexts },
1711 )));
1712 })
1713 .context("Failed to load completions for keybinding context")
1714 })
1715 .detach_and_log_err(cx);
1716
1717 input
1718 });
1719
1720 let action_arguments_editor = editing_keybind.action_schema.clone().map(|_schema| {
1721 cx.new(|cx| {
1722 let mut editor = Editor::auto_height_unbounded(1, window, cx);
1723 let workspace = workspace.clone();
1724
1725 if let Some(arguments) = editing_keybind.action_arguments.clone() {
1726 editor.set_text(arguments.text, window, cx);
1727 } else {
1728 // TODO: default value from schema?
1729 editor.set_placeholder_text("Action Arguments", cx);
1730 }
1731 cx.spawn(async |editor, cx| {
1732 let json_language = load_json_language(workspace, cx).await;
1733 editor
1734 .update(cx, |editor, cx| {
1735 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1736 buffer.update(cx, |buffer, cx| {
1737 buffer.set_language(Some(json_language), cx)
1738 });
1739 }
1740 })
1741 .context("Failed to load JSON language for editing keybinding action arguments input")
1742 })
1743 .detach_and_log_err(cx);
1744 editor
1745 })
1746 });
1747
1748 let focus_state = KeybindingEditorModalFocusState::new(
1749 keybind_editor.read_with(cx, |keybind_editor, cx| keybind_editor.focus_handle(cx)),
1750 action_arguments_editor.as_ref().map(|args_editor| {
1751 args_editor.read_with(cx, |args_editor, cx| args_editor.focus_handle(cx))
1752 }),
1753 context_editor.read_with(cx, |context_editor, cx| context_editor.focus_handle(cx)),
1754 );
1755
1756 Self {
1757 creating: create,
1758 editing_keybind,
1759 editing_keybind_idx,
1760 fs,
1761 keybind_editor,
1762 context_editor,
1763 action_arguments_editor,
1764 error: None,
1765 keymap_editor,
1766 workspace,
1767 focus_state,
1768 }
1769 }
1770
1771 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
1772 if self
1773 .error
1774 .as_ref()
1775 .is_some_and(|old_error| old_error.is_warning() && *old_error == error)
1776 {
1777 false
1778 } else {
1779 self.error = Some(error);
1780 cx.notify();
1781 true
1782 }
1783 }
1784
1785 fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> {
1786 let action_arguments = self
1787 .action_arguments_editor
1788 .as_ref()
1789 .map(|editor| editor.read(cx).text(cx));
1790
1791 let value = action_arguments
1792 .as_ref()
1793 .map(|args| {
1794 serde_json::from_str(args).context("Failed to parse action arguments as JSON")
1795 })
1796 .transpose()?;
1797
1798 cx.build_action(&self.editing_keybind.action_name, value)
1799 .context("Failed to validate action arguments")?;
1800 Ok(action_arguments)
1801 }
1802
1803 fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
1804 let new_keystrokes = self
1805 .keybind_editor
1806 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
1807 anyhow::ensure!(!new_keystrokes.is_empty(), "Keystrokes cannot be empty");
1808 Ok(new_keystrokes)
1809 }
1810
1811 fn validate_context(&self, cx: &App) -> anyhow::Result<Option<String>> {
1812 let new_context = self
1813 .context_editor
1814 .read_with(cx, |input, cx| input.editor().read(cx).text(cx));
1815 let Some(context) = new_context.is_empty().not().then_some(new_context) else {
1816 return Ok(None);
1817 };
1818 gpui::KeyBindingContextPredicate::parse(&context).context("Failed to parse key context")?;
1819
1820 Ok(Some(context))
1821 }
1822
1823 fn save(&mut self, cx: &mut Context<Self>) {
1824 let existing_keybind = self.editing_keybind.clone();
1825 let fs = self.fs.clone();
1826 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
1827 let new_keystrokes = match self.validate_keystrokes(cx) {
1828 Err(err) => {
1829 self.set_error(InputError::error(err.to_string()), cx);
1830 return;
1831 }
1832 Ok(keystrokes) => keystrokes,
1833 };
1834
1835 let new_context = match self.validate_context(cx) {
1836 Err(err) => {
1837 self.set_error(InputError::error(err.to_string()), cx);
1838 return;
1839 }
1840 Ok(context) => context,
1841 };
1842
1843 let new_action_args = match self.validate_action_arguments(cx) {
1844 Err(input_err) => {
1845 self.set_error(InputError::error(input_err.to_string()), cx);
1846 return;
1847 }
1848 Ok(input) => input,
1849 };
1850
1851 let action_mapping = ActionMapping {
1852 keystroke_text: ui::text_for_keystrokes(&new_keystrokes, cx).into(),
1853 context: new_context.as_ref().map(Into::into),
1854 };
1855
1856 let conflicting_indices = if self.creating {
1857 self.keymap_editor
1858 .read(cx)
1859 .keybinding_conflict_state
1860 .will_conflict(action_mapping)
1861 } else {
1862 self.keymap_editor
1863 .read(cx)
1864 .keybinding_conflict_state
1865 .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx)
1866 };
1867 if let Some(conflicting_indices) = conflicting_indices {
1868 let first_conflicting_index = conflicting_indices[0];
1869 let conflicting_action_name = self
1870 .keymap_editor
1871 .read(cx)
1872 .keybindings
1873 .get(first_conflicting_index)
1874 .map(|keybind| keybind.action_name);
1875
1876 let warning_message = match conflicting_action_name {
1877 Some(name) => {
1878 let confliction_action_amount = conflicting_indices.len() - 1;
1879 if confliction_action_amount > 0 {
1880 format!(
1881 "Your keybind would conflict with the \"{}\" action and {} other bindings",
1882 name, confliction_action_amount
1883 )
1884 } else {
1885 format!("Your keybind would conflict with the \"{}\" action", name)
1886 }
1887 }
1888 None => {
1889 log::info!(
1890 "Could not find action in keybindings with index {}",
1891 first_conflicting_index
1892 );
1893 "Your keybind would conflict with other actions".to_string()
1894 }
1895 };
1896
1897 if self.set_error(InputError::warning(warning_message), cx) {
1898 return;
1899 }
1900 }
1901
1902 let create = self.creating;
1903
1904 let status_toast = StatusToast::new(
1905 format!(
1906 "Saved edits to the {} action.",
1907 command_palette::humanize_action_name(&self.editing_keybind.action_name)
1908 ),
1909 cx,
1910 move |this, _cx| {
1911 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
1912 .dismiss_button(true)
1913 // .action("Undo", f) todo: wire the undo functionality
1914 },
1915 );
1916
1917 self.workspace
1918 .update(cx, |workspace, cx| {
1919 workspace.toggle_status_toast(status_toast, cx);
1920 })
1921 .log_err();
1922
1923 cx.spawn(async move |this, cx| {
1924 let action_name = existing_keybind.action_name;
1925
1926 if let Err(err) = save_keybinding_update(
1927 create,
1928 existing_keybind,
1929 &new_keystrokes,
1930 new_context.as_deref(),
1931 new_action_args.as_deref(),
1932 &fs,
1933 tab_size,
1934 )
1935 .await
1936 {
1937 this.update(cx, |this, cx| {
1938 this.set_error(InputError::error(err.to_string()), cx);
1939 })
1940 .log_err();
1941 } else {
1942 this.update(cx, |this, cx| {
1943 let action_mapping = ActionMapping {
1944 keystroke_text: ui::text_for_keystrokes(new_keystrokes.as_slice(), cx)
1945 .into(),
1946 context: new_context.map(SharedString::from),
1947 };
1948
1949 this.keymap_editor.update(cx, |keymap, cx| {
1950 keymap.previous_edit = Some(PreviousEdit::Keybinding {
1951 action_mapping,
1952 action_name,
1953 fallback: keymap
1954 .table_interaction_state
1955 .read(cx)
1956 .get_scrollbar_offset(Axis::Vertical),
1957 })
1958 });
1959 cx.emit(DismissEvent);
1960 })
1961 .ok();
1962 }
1963 })
1964 .detach();
1965 }
1966
1967 fn key_context(&self) -> KeyContext {
1968 let mut key_context = KeyContext::new_with_defaults();
1969 key_context.add("KeybindEditorModal");
1970 key_context
1971 }
1972
1973 fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1974 self.focus_state.focus_next(window, cx);
1975 }
1976
1977 fn focus_prev(
1978 &mut self,
1979 _: &menu::SelectPrevious,
1980 window: &mut Window,
1981 cx: &mut Context<Self>,
1982 ) {
1983 self.focus_state.focus_previous(window, cx);
1984 }
1985
1986 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
1987 self.save(cx);
1988 }
1989
1990 fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
1991 cx.emit(DismissEvent)
1992 }
1993}
1994
1995impl Render for KeybindingEditorModal {
1996 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1997 let theme = cx.theme().colors();
1998 let action_name =
1999 command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
2000
2001 v_flex()
2002 .w(rems(34.))
2003 .elevation_3(cx)
2004 .key_context(self.key_context())
2005 .on_action(cx.listener(Self::focus_next))
2006 .on_action(cx.listener(Self::focus_prev))
2007 .on_action(cx.listener(Self::confirm))
2008 .on_action(cx.listener(Self::cancel))
2009 .child(
2010 Modal::new("keybinding_editor_modal", None)
2011 .header(
2012 ModalHeader::new().child(
2013 v_flex()
2014 .pb_1p5()
2015 .mb_1()
2016 .gap_0p5()
2017 .border_b_1()
2018 .border_color(theme.border_variant)
2019 .child(Label::new(action_name))
2020 .when_some(self.editing_keybind.action_docs, |this, docs| {
2021 this.child(
2022 Label::new(docs).size(LabelSize::Small).color(Color::Muted),
2023 )
2024 }),
2025 ),
2026 )
2027 .section(
2028 Section::new().child(
2029 v_flex()
2030 .gap_2()
2031 .child(
2032 v_flex()
2033 .child(Label::new("Edit Keystroke"))
2034 .gap_1()
2035 .child(self.keybind_editor.clone()),
2036 )
2037 .when_some(self.action_arguments_editor.clone(), |this, editor| {
2038 this.child(
2039 v_flex()
2040 .mt_1p5()
2041 .gap_1()
2042 .child(Label::new("Edit Arguments"))
2043 .child(
2044 div()
2045 .w_full()
2046 .py_1()
2047 .px_1p5()
2048 .rounded_lg()
2049 .bg(theme.editor_background)
2050 .border_1()
2051 .border_color(theme.border_variant)
2052 .child(editor),
2053 ),
2054 )
2055 })
2056 .child(self.context_editor.clone())
2057 .when_some(self.error.as_ref(), |this, error| {
2058 this.child(
2059 Banner::new()
2060 .map(|banner| match error {
2061 InputError::Error(_) => {
2062 banner.severity(ui::Severity::Error)
2063 }
2064 InputError::Warning(_) => {
2065 banner.severity(ui::Severity::Warning)
2066 }
2067 })
2068 // For some reason, the div overflows its container to the
2069 //right. The padding accounts for that.
2070 .child(
2071 div()
2072 .size_full()
2073 .pr_2()
2074 .child(Label::new(error.content())),
2075 ),
2076 )
2077 }),
2078 ),
2079 )
2080 .footer(
2081 ModalFooter::new().end_slot(
2082 h_flex()
2083 .gap_1()
2084 .child(
2085 Button::new("cancel", "Cancel")
2086 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
2087 )
2088 .child(Button::new("save-btn", "Save").on_click(cx.listener(
2089 |this, _event, _window, cx| {
2090 this.save(cx);
2091 },
2092 ))),
2093 ),
2094 ),
2095 )
2096 }
2097}
2098
2099struct KeybindingEditorModalFocusState {
2100 handles: Vec<FocusHandle>,
2101}
2102
2103impl KeybindingEditorModalFocusState {
2104 fn new(
2105 keystrokes: FocusHandle,
2106 action_input: Option<FocusHandle>,
2107 context: FocusHandle,
2108 ) -> Self {
2109 Self {
2110 handles: Vec::from_iter(
2111 [Some(keystrokes), action_input, Some(context)]
2112 .into_iter()
2113 .flatten(),
2114 ),
2115 }
2116 }
2117
2118 fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> {
2119 self.handles
2120 .iter()
2121 .position(|handle| handle.contains_focused(window, cx))
2122 .map(|i| i as i32)
2123 }
2124
2125 fn focus_index(&self, mut index: i32, window: &mut Window) {
2126 if index < 0 {
2127 index = self.handles.len() as i32 - 1;
2128 }
2129 if index >= self.handles.len() as i32 {
2130 index = 0;
2131 }
2132 window.focus(&self.handles[index as usize]);
2133 }
2134
2135 fn focus_next(&self, window: &mut Window, cx: &App) {
2136 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2137 index + 1
2138 } else {
2139 0
2140 };
2141 self.focus_index(index_to_focus, window);
2142 }
2143
2144 fn focus_previous(&self, window: &mut Window, cx: &App) {
2145 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
2146 index - 1
2147 } else {
2148 self.handles.len() as i32 - 1
2149 };
2150 self.focus_index(index_to_focus, window);
2151 }
2152}
2153
2154struct KeyContextCompletionProvider {
2155 contexts: Vec<SharedString>,
2156}
2157
2158impl CompletionProvider for KeyContextCompletionProvider {
2159 fn completions(
2160 &self,
2161 _excerpt_id: editor::ExcerptId,
2162 buffer: &Entity<language::Buffer>,
2163 buffer_position: language::Anchor,
2164 _trigger: editor::CompletionContext,
2165 _window: &mut Window,
2166 cx: &mut Context<Editor>,
2167 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
2168 let buffer = buffer.read(cx);
2169 let mut count_back = 0;
2170 for char in buffer.reversed_chars_at(buffer_position) {
2171 if char.is_ascii_alphanumeric() || char == '_' {
2172 count_back += 1;
2173 } else {
2174 break;
2175 }
2176 }
2177 let start_anchor = buffer.anchor_before(
2178 buffer_position
2179 .to_offset(&buffer)
2180 .saturating_sub(count_back),
2181 );
2182 let replace_range = start_anchor..buffer_position;
2183 gpui::Task::ready(Ok(vec![project::CompletionResponse {
2184 completions: self
2185 .contexts
2186 .iter()
2187 .map(|context| project::Completion {
2188 replace_range: replace_range.clone(),
2189 label: language::CodeLabel::plain(context.to_string(), None),
2190 new_text: context.to_string(),
2191 documentation: None,
2192 source: project::CompletionSource::Custom,
2193 icon_path: None,
2194 insert_text_mode: None,
2195 confirm: None,
2196 })
2197 .collect(),
2198 is_incomplete: false,
2199 }]))
2200 }
2201
2202 fn is_completion_trigger(
2203 &self,
2204 _buffer: &Entity<language::Buffer>,
2205 _position: language::Anchor,
2206 text: &str,
2207 _trigger_in_words: bool,
2208 _menu_is_open: bool,
2209 _cx: &mut Context<Editor>,
2210 ) -> bool {
2211 text.chars().last().map_or(false, |last_char| {
2212 last_char.is_ascii_alphanumeric() || last_char == '_'
2213 })
2214 }
2215}
2216
2217async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
2218 let json_language_task = workspace
2219 .read_with(cx, |workspace, cx| {
2220 workspace
2221 .project()
2222 .read(cx)
2223 .languages()
2224 .language_for_name("JSON")
2225 })
2226 .context("Failed to load JSON language")
2227 .log_err();
2228 let json_language = match json_language_task {
2229 Some(task) => task.await.context("Failed to load JSON language").log_err(),
2230 None => None,
2231 };
2232 return json_language.unwrap_or_else(|| {
2233 Arc::new(Language::new(
2234 LanguageConfig {
2235 name: "JSON".into(),
2236 ..Default::default()
2237 },
2238 Some(tree_sitter_json::LANGUAGE.into()),
2239 ))
2240 });
2241}
2242
2243async fn load_keybind_context_language(
2244 workspace: WeakEntity<Workspace>,
2245 cx: &mut AsyncApp,
2246) -> Arc<Language> {
2247 let language_task = workspace
2248 .read_with(cx, |workspace, cx| {
2249 workspace
2250 .project()
2251 .read(cx)
2252 .languages()
2253 .language_for_name("Zed Keybind Context")
2254 })
2255 .context("Failed to load Zed Keybind Context language")
2256 .log_err();
2257 let language = match language_task {
2258 Some(task) => task
2259 .await
2260 .context("Failed to load Zed Keybind Context language")
2261 .log_err(),
2262 None => None,
2263 };
2264 return language.unwrap_or_else(|| {
2265 Arc::new(Language::new(
2266 LanguageConfig {
2267 name: "Zed Keybind Context".into(),
2268 ..Default::default()
2269 },
2270 Some(tree_sitter_rust::LANGUAGE.into()),
2271 ))
2272 });
2273}
2274
2275async fn save_keybinding_update(
2276 create: bool,
2277 existing: ProcessedKeybinding,
2278 new_keystrokes: &[Keystroke],
2279 new_context: Option<&str>,
2280 new_args: Option<&str>,
2281 fs: &Arc<dyn Fs>,
2282 tab_size: usize,
2283) -> anyhow::Result<()> {
2284 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2285 .await
2286 .context("Failed to load keymap file")?;
2287
2288 let existing_keystrokes = existing.keystrokes().unwrap_or_default();
2289 let existing_context = existing
2290 .context
2291 .as_ref()
2292 .and_then(KeybindContextString::local_str);
2293 let existing_args = existing
2294 .action_arguments
2295 .as_ref()
2296 .map(|args| args.text.as_ref());
2297
2298 let target = settings::KeybindUpdateTarget {
2299 context: existing_context,
2300 keystrokes: existing_keystrokes,
2301 action_name: &existing.action_name,
2302 action_arguments: existing_args,
2303 };
2304
2305 let source = settings::KeybindUpdateTarget {
2306 context: new_context,
2307 keystrokes: new_keystrokes,
2308 action_name: &existing.action_name,
2309 action_arguments: new_args,
2310 };
2311
2312 let operation = if !create {
2313 settings::KeybindUpdateOperation::Replace {
2314 target,
2315 target_keybind_source: existing
2316 .source
2317 .as_ref()
2318 .map(|(source, _name)| *source)
2319 .unwrap_or(KeybindSource::User),
2320 source,
2321 }
2322 } else {
2323 settings::KeybindUpdateOperation::Add {
2324 source,
2325 from: Some(target),
2326 }
2327 };
2328
2329 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
2330
2331 let updated_keymap_contents =
2332 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2333 .context("Failed to update keybinding")?;
2334 fs.write(
2335 paths::keymap_file().as_path(),
2336 updated_keymap_contents.as_bytes(),
2337 )
2338 .await
2339 .context("Failed to write keymap file")?;
2340
2341 telemetry::event!(
2342 "Keybinding Updated",
2343 new_keybinding = new_keybinding,
2344 removed_keybinding = removed_keybinding,
2345 source = source
2346 );
2347 Ok(())
2348}
2349
2350async fn remove_keybinding(
2351 existing: ProcessedKeybinding,
2352 fs: &Arc<dyn Fs>,
2353 tab_size: usize,
2354) -> anyhow::Result<()> {
2355 let Some(keystrokes) = existing.keystrokes() else {
2356 anyhow::bail!("Cannot remove a keybinding that does not exist");
2357 };
2358 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
2359 .await
2360 .context("Failed to load keymap file")?;
2361
2362 let operation = settings::KeybindUpdateOperation::Remove {
2363 target: settings::KeybindUpdateTarget {
2364 context: existing
2365 .context
2366 .as_ref()
2367 .and_then(KeybindContextString::local_str),
2368 keystrokes,
2369 action_name: &existing.action_name,
2370 action_arguments: existing
2371 .action_arguments
2372 .as_ref()
2373 .map(|arguments| arguments.text.as_ref()),
2374 },
2375 target_keybind_source: existing
2376 .source
2377 .as_ref()
2378 .map(|(source, _name)| *source)
2379 .unwrap_or(KeybindSource::User),
2380 };
2381
2382 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
2383 let updated_keymap_contents =
2384 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
2385 .context("Failed to update keybinding")?;
2386 fs.write(
2387 paths::keymap_file().as_path(),
2388 updated_keymap_contents.as_bytes(),
2389 )
2390 .await
2391 .context("Failed to write keymap file")?;
2392
2393 telemetry::event!(
2394 "Keybinding Removed",
2395 new_keybinding = new_keybinding,
2396 removed_keybinding = removed_keybinding,
2397 source = source
2398 );
2399 Ok(())
2400}
2401
2402#[derive(PartialEq, Eq, Debug, Copy, Clone)]
2403enum CloseKeystrokeResult {
2404 Partial,
2405 Close,
2406 None,
2407}
2408
2409#[derive(PartialEq, Eq, Debug, Clone)]
2410enum KeyPress<'a> {
2411 Alt,
2412 Control,
2413 Function,
2414 Shift,
2415 Platform,
2416 Key(&'a String),
2417}
2418
2419struct KeystrokeInput {
2420 keystrokes: Vec<Keystroke>,
2421 placeholder_keystrokes: Option<Vec<Keystroke>>,
2422 highlight_on_focus: bool,
2423 outer_focus_handle: FocusHandle,
2424 inner_focus_handle: FocusHandle,
2425 intercept_subscription: Option<Subscription>,
2426 _focus_subscriptions: [Subscription; 2],
2427 search: bool,
2428 /// Handles tripe escape to stop recording
2429 close_keystrokes: Option<Vec<Keystroke>>,
2430 close_keystrokes_start: Option<usize>,
2431}
2432
2433impl KeystrokeInput {
2434 const KEYSTROKE_COUNT_MAX: usize = 3;
2435
2436 fn new(
2437 placeholder_keystrokes: Option<Vec<Keystroke>>,
2438 window: &mut Window,
2439 cx: &mut Context<Self>,
2440 ) -> Self {
2441 let outer_focus_handle = cx.focus_handle();
2442 let inner_focus_handle = cx.focus_handle();
2443 let _focus_subscriptions = [
2444 cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
2445 cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
2446 ];
2447 Self {
2448 keystrokes: Vec::new(),
2449 placeholder_keystrokes,
2450 highlight_on_focus: true,
2451 inner_focus_handle,
2452 outer_focus_handle,
2453 intercept_subscription: None,
2454 _focus_subscriptions,
2455 search: false,
2456 close_keystrokes: None,
2457 close_keystrokes_start: None,
2458 }
2459 }
2460
2461 fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
2462 self.keystrokes = keystrokes;
2463 self.keystrokes_changed(cx);
2464 }
2465
2466 fn dummy(modifiers: Modifiers) -> Keystroke {
2467 return Keystroke {
2468 modifiers,
2469 key: "".to_string(),
2470 key_char: None,
2471 };
2472 }
2473
2474 fn keystrokes_changed(&self, cx: &mut Context<Self>) {
2475 cx.emit(());
2476 cx.notify();
2477 }
2478
2479 fn key_context() -> KeyContext {
2480 let mut key_context = KeyContext::new_with_defaults();
2481 key_context.add("KeystrokeInput");
2482 key_context
2483 }
2484
2485 fn handle_possible_close_keystroke(
2486 &mut self,
2487 keystroke: &Keystroke,
2488 window: &mut Window,
2489 cx: &mut Context<Self>,
2490 ) -> CloseKeystrokeResult {
2491 let Some(keybind_for_close_action) = window
2492 .highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context())
2493 else {
2494 log::trace!("No keybinding to stop recording keystrokes in keystroke input");
2495 self.close_keystrokes.take();
2496 return CloseKeystrokeResult::None;
2497 };
2498 let action_keystrokes = keybind_for_close_action.keystrokes();
2499
2500 if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
2501 let mut index = 0;
2502
2503 while index < action_keystrokes.len() && index < close_keystrokes.len() {
2504 if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
2505 break;
2506 }
2507 index += 1;
2508 }
2509 if index == close_keystrokes.len() {
2510 if index >= action_keystrokes.len() {
2511 self.close_keystrokes_start.take();
2512 return CloseKeystrokeResult::None;
2513 }
2514 if keystroke.should_match(&action_keystrokes[index]) {
2515 if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 {
2516 self.stop_recording(&StopRecording, window, cx);
2517 return CloseKeystrokeResult::Close;
2518 } else {
2519 close_keystrokes.push(keystroke.clone());
2520 self.close_keystrokes = Some(close_keystrokes);
2521 return CloseKeystrokeResult::Partial;
2522 }
2523 } else {
2524 self.close_keystrokes_start.take();
2525 return CloseKeystrokeResult::None;
2526 }
2527 }
2528 } else if let Some(first_action_keystroke) = action_keystrokes.first()
2529 && keystroke.should_match(first_action_keystroke)
2530 {
2531 self.close_keystrokes = Some(vec![keystroke.clone()]);
2532 return CloseKeystrokeResult::Partial;
2533 }
2534 self.close_keystrokes_start.take();
2535 return CloseKeystrokeResult::None;
2536 }
2537
2538 fn on_modifiers_changed(
2539 &mut self,
2540 event: &ModifiersChangedEvent,
2541 _window: &mut Window,
2542 cx: &mut Context<Self>,
2543 ) {
2544 let keystrokes_len = self.keystrokes.len();
2545
2546 if let Some(last) = self.keystrokes.last_mut()
2547 && last.key.is_empty()
2548 && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
2549 {
2550 if self.search {
2551 last.modifiers = last.modifiers.xor(&event.modifiers);
2552 } else if !event.modifiers.modified() {
2553 self.keystrokes.pop();
2554 } else {
2555 last.modifiers = event.modifiers;
2556 }
2557
2558 self.keystrokes_changed(cx);
2559 } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
2560 self.keystrokes.push(Self::dummy(event.modifiers));
2561 self.keystrokes_changed(cx);
2562 }
2563 cx.stop_propagation();
2564 }
2565
2566 fn handle_keystroke(
2567 &mut self,
2568 keystroke: &Keystroke,
2569 window: &mut Window,
2570 cx: &mut Context<Self>,
2571 ) {
2572 let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
2573 if close_keystroke_result != CloseKeystrokeResult::Close {
2574 let key_len = self.keystrokes.len();
2575 if let Some(last) = self.keystrokes.last_mut()
2576 && last.key.is_empty()
2577 && key_len <= Self::KEYSTROKE_COUNT_MAX
2578 {
2579 if self.search {
2580 last.key = keystroke.key.clone();
2581 self.keystrokes_changed(cx);
2582 cx.stop_propagation();
2583 return;
2584 } else {
2585 self.keystrokes.pop();
2586 }
2587 }
2588 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
2589 if close_keystroke_result == CloseKeystrokeResult::Partial
2590 && self.close_keystrokes_start.is_none()
2591 {
2592 self.close_keystrokes_start = Some(self.keystrokes.len());
2593 }
2594 self.keystrokes.push(keystroke.clone());
2595 if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
2596 self.keystrokes.push(Self::dummy(keystroke.modifiers));
2597 }
2598 } else if close_keystroke_result != CloseKeystrokeResult::Partial {
2599 self.clear_keystrokes(&ClearKeystrokes, window, cx);
2600 }
2601 }
2602 self.keystrokes_changed(cx);
2603 cx.stop_propagation();
2604 }
2605
2606 fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
2607 if self.intercept_subscription.is_none() {
2608 let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
2609 this.handle_keystroke(&event.keystroke, window, cx);
2610 });
2611 self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
2612 }
2613 }
2614
2615 fn on_inner_focus_out(
2616 &mut self,
2617 _event: gpui::FocusOutEvent,
2618 _window: &mut Window,
2619 cx: &mut Context<Self>,
2620 ) {
2621 self.intercept_subscription.take();
2622 cx.notify();
2623 }
2624
2625 fn keystrokes(&self) -> &[Keystroke] {
2626 if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
2627 && self.keystrokes.is_empty()
2628 {
2629 return placeholders;
2630 }
2631 if !self.search
2632 && self
2633 .keystrokes
2634 .last()
2635 .map_or(false, |last| last.key.is_empty())
2636 {
2637 return &self.keystrokes[..self.keystrokes.len() - 1];
2638 }
2639 return &self.keystrokes;
2640 }
2641
2642 fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
2643 let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
2644 && self.keystrokes.is_empty()
2645 {
2646 if is_recording {
2647 &[]
2648 } else {
2649 placeholders.as_slice()
2650 }
2651 } else {
2652 &self.keystrokes
2653 };
2654 keystrokes.iter().map(move |keystroke| {
2655 h_flex().children(ui::render_keystroke(
2656 keystroke,
2657 Some(Color::Default),
2658 Some(rems(0.875).into()),
2659 ui::PlatformStyle::platform(),
2660 false,
2661 ))
2662 })
2663 }
2664
2665 fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
2666 self.inner_focus_handle.clone()
2667 }
2668
2669 fn set_search_mode(&mut self, search: bool) {
2670 self.search = search;
2671 }
2672
2673 fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
2674 if !self.outer_focus_handle.is_focused(window) {
2675 return;
2676 }
2677 self.clear_keystrokes(&ClearKeystrokes, window, cx);
2678 window.focus(&self.inner_focus_handle);
2679 cx.notify();
2680 }
2681
2682 fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
2683 if !self.inner_focus_handle.is_focused(window) {
2684 return;
2685 }
2686 window.focus(&self.outer_focus_handle);
2687 if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() {
2688 self.keystrokes.drain(close_keystrokes_start..);
2689 }
2690 self.close_keystrokes.take();
2691 cx.notify();
2692 }
2693
2694 fn clear_keystrokes(
2695 &mut self,
2696 _: &ClearKeystrokes,
2697 _window: &mut Window,
2698 cx: &mut Context<Self>,
2699 ) {
2700 self.keystrokes.clear();
2701 self.keystrokes_changed(cx);
2702 }
2703}
2704
2705impl EventEmitter<()> for KeystrokeInput {}
2706
2707impl Focusable for KeystrokeInput {
2708 fn focus_handle(&self, _cx: &App) -> FocusHandle {
2709 self.outer_focus_handle.clone()
2710 }
2711}
2712
2713impl Render for KeystrokeInput {
2714 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2715 let colors = cx.theme().colors();
2716 let is_focused = self.outer_focus_handle.contains_focused(window, cx);
2717 let is_recording = self.inner_focus_handle.is_focused(window);
2718
2719 let horizontal_padding = rems_from_px(64.);
2720
2721 let recording_bg_color = colors
2722 .editor_background
2723 .blend(colors.text_accent.opacity(0.1));
2724
2725 let recording_pulse = || {
2726 Icon::new(IconName::Circle)
2727 .size(IconSize::Small)
2728 .color(Color::Error)
2729 .with_animation(
2730 "recording-pulse",
2731 Animation::new(std::time::Duration::from_secs(2))
2732 .repeat()
2733 .with_easing(gpui::pulsating_between(0.4, 0.8)),
2734 {
2735 let color = Color::Error.color(cx);
2736 move |this, delta| this.color(Color::Custom(color.opacity(delta)))
2737 },
2738 )
2739 };
2740
2741 let recording_indicator = h_flex()
2742 .h_4()
2743 .pr_1()
2744 .gap_0p5()
2745 .border_1()
2746 .border_color(colors.border)
2747 .bg(colors
2748 .editor_background
2749 .blend(colors.text_accent.opacity(0.1)))
2750 .rounded_sm()
2751 .child(recording_pulse())
2752 .child(
2753 Label::new("REC")
2754 .size(LabelSize::XSmall)
2755 .weight(FontWeight::SEMIBOLD)
2756 .color(Color::Error),
2757 );
2758
2759 let search_indicator = h_flex()
2760 .h_4()
2761 .pr_1()
2762 .gap_0p5()
2763 .border_1()
2764 .border_color(colors.border)
2765 .bg(colors
2766 .editor_background
2767 .blend(colors.text_accent.opacity(0.1)))
2768 .rounded_sm()
2769 .child(recording_pulse())
2770 .child(
2771 Label::new("SEARCH")
2772 .size(LabelSize::XSmall)
2773 .weight(FontWeight::SEMIBOLD)
2774 .color(Color::Accent),
2775 );
2776
2777 let record_icon = if self.search {
2778 IconName::MagnifyingGlass
2779 } else {
2780 IconName::PlayFilled
2781 };
2782
2783 return h_flex()
2784 .id("keystroke-input")
2785 .track_focus(&self.outer_focus_handle)
2786 .py_2()
2787 .px_3()
2788 .gap_2()
2789 .min_h_10()
2790 .w_full()
2791 .flex_1()
2792 .justify_between()
2793 .rounded_lg()
2794 .overflow_hidden()
2795 .map(|this| {
2796 if is_recording {
2797 this.bg(recording_bg_color)
2798 } else {
2799 this.bg(colors.editor_background)
2800 }
2801 })
2802 .border_1()
2803 .border_color(colors.border_variant)
2804 .when(is_focused, |parent| {
2805 parent.border_color(colors.border_focused)
2806 })
2807 .key_context(Self::key_context())
2808 .on_action(cx.listener(Self::start_recording))
2809 .on_action(cx.listener(Self::stop_recording))
2810 .child(
2811 h_flex()
2812 .w(horizontal_padding)
2813 .gap_0p5()
2814 .justify_start()
2815 .flex_none()
2816 .when(is_recording, |this| {
2817 this.map(|this| {
2818 if self.search {
2819 this.child(search_indicator)
2820 } else {
2821 this.child(recording_indicator)
2822 }
2823 })
2824 }),
2825 )
2826 .child(
2827 h_flex()
2828 .id("keystroke-input-inner")
2829 .track_focus(&self.inner_focus_handle)
2830 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
2831 .size_full()
2832 .when(self.highlight_on_focus, |this| {
2833 this.focus(|mut style| {
2834 style.border_color = Some(colors.border_focused);
2835 style
2836 })
2837 })
2838 .w_full()
2839 .min_w_0()
2840 .justify_center()
2841 .flex_wrap()
2842 .gap(ui::DynamicSpacing::Base04.rems(cx))
2843 .children(self.render_keystrokes(is_recording)),
2844 )
2845 .child(
2846 h_flex()
2847 .w(horizontal_padding)
2848 .gap_0p5()
2849 .justify_end()
2850 .flex_none()
2851 .map(|this| {
2852 if is_recording {
2853 this.child(
2854 IconButton::new("stop-record-btn", IconName::StopFilled)
2855 .shape(ui::IconButtonShape::Square)
2856 .map(|this| {
2857 this.tooltip(Tooltip::for_action_title(
2858 if self.search {
2859 "Stop Searching"
2860 } else {
2861 "Stop Recording"
2862 },
2863 &StopRecording,
2864 ))
2865 })
2866 .icon_color(Color::Error)
2867 .on_click(cx.listener(|this, _event, window, cx| {
2868 this.stop_recording(&StopRecording, window, cx);
2869 })),
2870 )
2871 } else {
2872 this.child(
2873 IconButton::new("record-btn", record_icon)
2874 .shape(ui::IconButtonShape::Square)
2875 .map(|this| {
2876 this.tooltip(Tooltip::for_action_title(
2877 if self.search {
2878 "Start Searching"
2879 } else {
2880 "Start Recording"
2881 },
2882 &StartRecording,
2883 ))
2884 })
2885 .when(!is_focused, |this| this.icon_color(Color::Muted))
2886 .on_click(cx.listener(|this, _event, window, cx| {
2887 this.start_recording(&StartRecording, window, cx);
2888 })),
2889 )
2890 }
2891 })
2892 .child(
2893 IconButton::new("clear-btn", IconName::Delete)
2894 .shape(ui::IconButtonShape::Square)
2895 .tooltip(Tooltip::for_action_title(
2896 "Clear Keystrokes",
2897 &ClearKeystrokes,
2898 ))
2899 .when(!is_recording || !is_focused, |this| {
2900 this.icon_color(Color::Muted)
2901 })
2902 .on_click(cx.listener(|this, _event, window, cx| {
2903 this.clear_keystrokes(&ClearKeystrokes, window, cx);
2904 })),
2905 ),
2906 );
2907 }
2908}
2909
2910fn collect_contexts_from_assets() -> Vec<SharedString> {
2911 let mut keymap_assets = vec![
2912 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
2913 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
2914 ];
2915 keymap_assets.extend(
2916 BaseKeymap::OPTIONS
2917 .iter()
2918 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
2919 .map(util::asset_str::<SettingsAssets>),
2920 );
2921
2922 let mut contexts = HashSet::default();
2923
2924 for keymap_asset in keymap_assets {
2925 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
2926 continue;
2927 };
2928
2929 for section in keymap.sections() {
2930 let context_expr = §ion.context;
2931 let mut queue = Vec::new();
2932 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
2933 continue;
2934 };
2935
2936 queue.push(root_context);
2937 while let Some(context) = queue.pop() {
2938 match context {
2939 gpui::KeyBindingContextPredicate::Identifier(ident) => {
2940 contexts.insert(ident);
2941 }
2942 gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => {
2943 contexts.insert(ident_a);
2944 contexts.insert(ident_b);
2945 }
2946 gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => {
2947 contexts.insert(ident_a);
2948 contexts.insert(ident_b);
2949 }
2950 gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => {
2951 queue.push(*ctx_a);
2952 queue.push(*ctx_b);
2953 }
2954 gpui::KeyBindingContextPredicate::Not(ctx) => {
2955 queue.push(*ctx);
2956 }
2957 gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => {
2958 queue.push(*ctx_a);
2959 queue.push(*ctx_b);
2960 }
2961 gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => {
2962 queue.push(*ctx_a);
2963 queue.push(*ctx_b);
2964 }
2965 }
2966 }
2967 }
2968 }
2969
2970 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
2971 contexts.sort();
2972
2973 return contexts;
2974}
2975
2976impl SerializableItem for KeymapEditor {
2977 fn serialized_item_kind() -> &'static str {
2978 "KeymapEditor"
2979 }
2980
2981 fn cleanup(
2982 workspace_id: workspace::WorkspaceId,
2983 alive_items: Vec<workspace::ItemId>,
2984 _window: &mut Window,
2985 cx: &mut App,
2986 ) -> gpui::Task<gpui::Result<()>> {
2987 workspace::delete_unloaded_items(
2988 alive_items,
2989 workspace_id,
2990 "keybinding_editors",
2991 &KEYBINDING_EDITORS,
2992 cx,
2993 )
2994 }
2995
2996 fn deserialize(
2997 _project: Entity<project::Project>,
2998 workspace: WeakEntity<Workspace>,
2999 workspace_id: workspace::WorkspaceId,
3000 item_id: workspace::ItemId,
3001 window: &mut Window,
3002 cx: &mut App,
3003 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
3004 window.spawn(cx, async move |cx| {
3005 if KEYBINDING_EDITORS
3006 .get_keybinding_editor(item_id, workspace_id)?
3007 .is_some()
3008 {
3009 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
3010 } else {
3011 Err(anyhow!("No keybinding editor to deserialize"))
3012 }
3013 })
3014 }
3015
3016 fn serialize(
3017 &mut self,
3018 workspace: &mut Workspace,
3019 item_id: workspace::ItemId,
3020 _closing: bool,
3021 _window: &mut Window,
3022 cx: &mut ui::Context<Self>,
3023 ) -> Option<gpui::Task<gpui::Result<()>>> {
3024 let workspace_id = workspace.database_id()?;
3025 Some(cx.background_spawn(async move {
3026 KEYBINDING_EDITORS
3027 .save_keybinding_editor(item_id, workspace_id)
3028 .await
3029 }))
3030 }
3031
3032 fn should_serialize(&self, _event: &Self::Event) -> bool {
3033 false
3034 }
3035}
3036
3037mod persistence {
3038 use db::{define_connection, query, sqlez_macros::sql};
3039 use workspace::WorkspaceDb;
3040
3041 define_connection! {
3042 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
3043 &[sql!(
3044 CREATE TABLE keybinding_editors (
3045 workspace_id INTEGER,
3046 item_id INTEGER UNIQUE,
3047
3048 PRIMARY KEY(workspace_id, item_id),
3049 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
3050 ON DELETE CASCADE
3051 ) STRICT;
3052 )];
3053 }
3054
3055 impl KeybindingEditorDb {
3056 query! {
3057 pub async fn save_keybinding_editor(
3058 item_id: workspace::ItemId,
3059 workspace_id: workspace::WorkspaceId
3060 ) -> Result<()> {
3061 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
3062 VALUES (?, ?)
3063 }
3064 }
3065
3066 query! {
3067 pub fn get_keybinding_editor(
3068 item_id: workspace::ItemId,
3069 workspace_id: workspace::WorkspaceId
3070 ) -> Result<Option<workspace::ItemId>> {
3071 SELECT item_id
3072 FROM keybinding_editors
3073 WHERE item_id = ? AND workspace_id = ?
3074 }
3075 }
3076 }
3077}
3078
3079/// Iterator that yields KeyPress values from a slice of Keystrokes
3080struct KeyPressIterator<'a> {
3081 keystrokes: &'a [Keystroke],
3082 current_keystroke_index: usize,
3083 current_key_press_index: usize,
3084}
3085
3086impl<'a> KeyPressIterator<'a> {
3087 fn new(keystrokes: &'a [Keystroke]) -> Self {
3088 Self {
3089 keystrokes,
3090 current_keystroke_index: 0,
3091 current_key_press_index: 0,
3092 }
3093 }
3094}
3095
3096impl<'a> Iterator for KeyPressIterator<'a> {
3097 type Item = KeyPress<'a>;
3098
3099 fn next(&mut self) -> Option<Self::Item> {
3100 loop {
3101 let keystroke = self.keystrokes.get(self.current_keystroke_index)?;
3102
3103 match self.current_key_press_index {
3104 0 => {
3105 self.current_key_press_index = 1;
3106 if keystroke.modifiers.platform {
3107 return Some(KeyPress::Platform);
3108 }
3109 }
3110 1 => {
3111 self.current_key_press_index = 2;
3112 if keystroke.modifiers.alt {
3113 return Some(KeyPress::Alt);
3114 }
3115 }
3116 2 => {
3117 self.current_key_press_index = 3;
3118 if keystroke.modifiers.control {
3119 return Some(KeyPress::Control);
3120 }
3121 }
3122 3 => {
3123 self.current_key_press_index = 4;
3124 if keystroke.modifiers.shift {
3125 return Some(KeyPress::Shift);
3126 }
3127 }
3128 4 => {
3129 self.current_key_press_index = 5;
3130 if keystroke.modifiers.function {
3131 return Some(KeyPress::Function);
3132 }
3133 }
3134 _ => {
3135 self.current_keystroke_index += 1;
3136 self.current_key_press_index = 0;
3137
3138 if keystroke.key.is_empty() {
3139 continue;
3140 }
3141 return Some(KeyPress::Key(&keystroke.key));
3142 }
3143 }
3144 }
3145 }
3146}