1use std::{
2 cell::RefCell,
3 cmp::{self},
4 ops::{Not as _, Range},
5 rc::Rc,
6 sync::Arc,
7 time::{Duration, Instant},
8};
9
10mod action_completion_provider;
11mod ui_components;
12
13use anyhow::{Context as _, anyhow};
14use collections::{HashMap, HashSet};
15use editor::{CompletionProvider, Editor, EditorEvent, EditorMode, SizingBehavior};
16use fs::Fs;
17use fuzzy::{StringMatch, StringMatchCandidate};
18use gpui::{
19 Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
20 FocusHandle, Focusable, Global, IsZero,
21 KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
22 KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy,
23 ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,
24 actions, anchored, deferred, div,
25};
26use language::{Language, LanguageConfig, ToOffset as _};
27use notifications::status_toast::{StatusToast, ToastIcon};
28use project::{CompletionDisplayOptions, Project};
29use settings::{
30 BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size,
31};
32use ui::{
33 ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, IconPosition,
34 Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section,
35 SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState,
36 TableResizeBehavior, Tooltip, Window, prelude::*,
37};
38use ui_input::InputField;
39use util::ResultExt;
40use workspace::{
41 Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
42 register_serializable_item, with_active_or_new_workspace,
43};
44
45pub use ui_components::*;
46use zed_actions::{ChangeKeybinding, OpenKeymap};
47
48use crate::{
49 action_completion_provider::ActionCompletionProvider,
50 persistence::KeybindingEditorDb,
51 ui_components::keystroke_input::{
52 ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording,
53 },
54};
55
56const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
57const COLS: usize = 6;
58
59actions!(
60 keymap_editor,
61 [
62 /// Edits the selected key binding.
63 EditBinding,
64 /// Creates a new key binding for the selected action.
65 CreateBinding,
66 /// Creates a new key binding from scratch, prompting for the action.
67 OpenCreateKeybindingModal,
68 /// Deletes the selected key binding.
69 DeleteBinding,
70 /// Copies the action name to clipboard.
71 CopyAction,
72 /// Copies the context predicate to clipboard.
73 CopyContext,
74 /// Toggles Conflict Filtering
75 ToggleConflictFilter,
76 /// Toggles whether NoAction bindings are shown
77 ToggleNoActionBindings,
78 /// Toggle Keystroke search
79 ToggleKeystrokeSearch,
80 /// Toggles exact matching for keystroke search
81 ToggleExactKeystrokeMatching,
82 /// Shows matching keystrokes for the currently selected binding
83 ShowMatchingKeybinds
84 ]
85);
86
87pub fn init(cx: &mut App) {
88 let keymap_event_channel = KeymapEventChannel::new();
89 cx.set_global(keymap_event_channel);
90
91 fn open_keymap_editor(
92 filter: Option<String>,
93 workspace: &mut Workspace,
94 window: &mut Window,
95 cx: &mut Context<Workspace>,
96 ) {
97 let existing = workspace
98 .active_pane()
99 .read(cx)
100 .items()
101 .find_map(|item| item.downcast::<KeymapEditor>());
102
103 let keymap_editor = if let Some(existing) = existing {
104 workspace.activate_item(&existing, true, true, window, cx);
105 existing
106 } else {
107 let keymap_editor = cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
108 workspace.add_item_to_active_pane(
109 Box::new(keymap_editor.clone()),
110 None,
111 true,
112 window,
113 cx,
114 );
115 keymap_editor
116 };
117
118 if let Some(filter) = filter {
119 keymap_editor.update(cx, |editor, cx| {
120 editor.filter_editor.update(cx, |editor, cx| {
121 editor.clear(window, cx);
122 editor.insert(&filter, window, cx);
123 });
124 if !editor.has_binding_for(&filter) {
125 open_binding_modal_after_loading(cx)
126 }
127 })
128 }
129 }
130
131 cx.on_action(|_: &OpenKeymap, cx| {
132 with_active_or_new_workspace(cx, |workspace, window, cx| {
133 open_keymap_editor(None, workspace, window, cx);
134 });
135 });
136
137 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
138 workspace.register_action(|workspace, action: &ChangeKeybinding, window, cx| {
139 open_keymap_editor(Some(action.action.clone()), workspace, window, cx);
140 });
141 })
142 .detach();
143
144 register_serializable_item::<KeymapEditor>(cx);
145}
146
147fn open_binding_modal_after_loading(cx: &mut Context<KeymapEditor>) {
148 let started_at = Instant::now();
149 let observer = Rc::new(RefCell::new(None));
150 let handle = {
151 let observer = Rc::clone(&observer);
152 cx.observe(&cx.entity(), move |editor, _, cx| {
153 let subscription = observer.borrow_mut().take();
154
155 if started_at.elapsed().as_secs() > 10 {
156 return;
157 }
158 if !editor.matches.is_empty() {
159 editor.selected_index = Some(0);
160 cx.dispatch_action(&CreateBinding);
161 return;
162 }
163
164 *observer.borrow_mut() = subscription;
165 })
166 };
167 *observer.borrow_mut() = Some(handle);
168}
169
170pub struct KeymapEventChannel {}
171
172impl Global for KeymapEventChannel {}
173
174impl KeymapEventChannel {
175 fn new() -> Self {
176 Self {}
177 }
178
179 pub fn trigger_keymap_changed(cx: &mut App) {
180 let Some(_event_channel) = cx.try_global::<Self>() else {
181 // don't panic if no global defined. This usually happens in tests
182 return;
183 };
184 cx.update_global(|_event_channel: &mut Self, _| {
185 /* triggers observers in KeymapEditors */
186 });
187 }
188}
189
190#[derive(Default, PartialEq, Copy, Clone)]
191enum SearchMode {
192 #[default]
193 Normal,
194 KeyStroke {
195 exact_match: bool,
196 },
197}
198
199impl SearchMode {
200 fn invert(&self) -> Self {
201 match self {
202 SearchMode::Normal => SearchMode::KeyStroke { exact_match: true },
203 SearchMode::KeyStroke { .. } => SearchMode::Normal,
204 }
205 }
206
207 fn exact_match(&self) -> bool {
208 match self {
209 SearchMode::Normal => false,
210 SearchMode::KeyStroke { exact_match } => *exact_match,
211 }
212 }
213}
214
215#[derive(Default, PartialEq, Copy, Clone)]
216enum FilterState {
217 #[default]
218 All,
219 Conflicts,
220}
221
222impl FilterState {
223 fn invert(&self) -> Self {
224 match self {
225 FilterState::All => FilterState::Conflicts,
226 FilterState::Conflicts => FilterState::All,
227 }
228 }
229}
230
231#[derive(Default, PartialEq, Eq, Copy, Clone)]
232struct SourceFilters {
233 user: bool,
234 zed_defaults: bool,
235 vim_defaults: bool,
236}
237
238impl SourceFilters {
239 fn allows(&self, source: Option<KeybindSource>) -> bool {
240 match source {
241 Some(KeybindSource::User) => self.user,
242 Some(KeybindSource::Vim) => self.vim_defaults,
243 Some(KeybindSource::Base | KeybindSource::Default | KeybindSource::Unknown) | None => {
244 self.zed_defaults
245 }
246 }
247 }
248}
249
250#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
251struct ActionMapping {
252 keystrokes: Rc<[KeybindingKeystroke]>,
253 context: Option<SharedString>,
254}
255
256#[derive(Debug)]
257struct KeybindConflict {
258 first_conflict_index: usize,
259 remaining_conflict_amount: usize,
260}
261
262#[derive(Clone, Copy, PartialEq)]
263struct ConflictOrigin {
264 override_source: KeybindSource,
265 overridden_source: Option<KeybindSource>,
266 index: usize,
267}
268
269impl ConflictOrigin {
270 fn new(source: KeybindSource, index: usize) -> Self {
271 Self {
272 override_source: source,
273 index,
274 overridden_source: None,
275 }
276 }
277
278 fn with_overridden_source(self, source: KeybindSource) -> Self {
279 Self {
280 overridden_source: Some(source),
281 ..self
282 }
283 }
284
285 fn get_conflict_with(&self, other: &Self) -> Option<Self> {
286 if self.override_source == KeybindSource::User
287 && other.override_source == KeybindSource::User
288 {
289 Some(
290 Self::new(KeybindSource::User, other.index)
291 .with_overridden_source(self.override_source),
292 )
293 } else if self.override_source > other.override_source {
294 Some(other.with_overridden_source(self.override_source))
295 } else {
296 None
297 }
298 }
299
300 fn is_user_keybind_conflict(&self) -> bool {
301 self.override_source == KeybindSource::User
302 && self.overridden_source == Some(KeybindSource::User)
303 }
304}
305
306#[derive(Default)]
307struct ConflictState {
308 conflicts: Vec<Option<ConflictOrigin>>,
309 keybind_mapping: ConflictKeybindMapping,
310 has_user_conflicts: bool,
311}
312
313type ConflictKeybindMapping = HashMap<
314 Rc<[KeybindingKeystroke]>,
315 Vec<(
316 Option<gpui::KeyBindingContextPredicate>,
317 Vec<ConflictOrigin>,
318 )>,
319>;
320
321impl ConflictState {
322 fn new(key_bindings: &[ProcessedBinding]) -> Self {
323 let mut action_keybind_mapping = ConflictKeybindMapping::default();
324
325 let mut largest_index = 0;
326 for (index, binding) in key_bindings
327 .iter()
328 .enumerate()
329 .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information()))
330 {
331 let mapping = binding.get_action_mapping();
332 let predicate = mapping
333 .context
334 .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());
335 let entry = action_keybind_mapping
336 .entry(mapping.keystrokes.clone())
337 .or_default();
338 let origin = ConflictOrigin::new(binding.source, index);
339 if let Some((_, origins)) =
340 entry
341 .iter_mut()
342 .find(|(other_predicate, _)| match (&predicate, other_predicate) {
343 (None, None) => true,
344 (Some(a), Some(b)) => normalized_ctx_eq(a, b),
345 _ => false,
346 })
347 {
348 origins.push(origin);
349 } else {
350 entry.push((predicate, vec![origin]));
351 }
352 largest_index = index;
353 }
354
355 let mut conflicts = vec![None; largest_index + 1];
356 let mut has_user_conflicts = false;
357
358 for entries in action_keybind_mapping.values_mut() {
359 for (_, indices) in entries.iter_mut() {
360 indices.sort_unstable_by_key(|origin| origin.override_source);
361 let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else {
362 continue;
363 };
364
365 for origin in indices.iter() {
366 conflicts[origin.index] =
367 origin.get_conflict_with(if origin == fst { snd } else { fst })
368 }
369
370 has_user_conflicts |= fst.override_source == KeybindSource::User
371 && snd.override_source == KeybindSource::User;
372 }
373 }
374
375 Self {
376 conflicts,
377 keybind_mapping: action_keybind_mapping,
378 has_user_conflicts,
379 }
380 }
381
382 fn conflicting_indices_for_mapping(
383 &self,
384 action_mapping: &ActionMapping,
385 keybind_idx: Option<usize>,
386 ) -> Option<KeybindConflict> {
387 let ActionMapping {
388 keystrokes,
389 context,
390 } = action_mapping;
391 let predicate = context
392 .as_deref()
393 .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());
394 self.keybind_mapping.get(keystrokes).and_then(|entries| {
395 entries
396 .iter()
397 .find_map(|(other_predicate, indices)| {
398 match (&predicate, other_predicate) {
399 (None, None) => true,
400 (Some(pred), Some(other)) => normalized_ctx_eq(pred, other),
401 _ => false,
402 }
403 .then_some(indices)
404 })
405 .and_then(|indices| {
406 let mut indices = indices
407 .iter()
408 .filter(|&conflict| Some(conflict.index) != keybind_idx);
409 indices.next().map(|origin| KeybindConflict {
410 first_conflict_index: origin.index,
411 remaining_conflict_amount: indices.count(),
412 })
413 })
414 })
415 }
416
417 fn conflict_for_idx(&self, idx: usize) -> Option<ConflictOrigin> {
418 self.conflicts.get(idx).copied().flatten()
419 }
420
421 fn has_user_conflict(&self, candidate_idx: usize) -> bool {
422 self.conflict_for_idx(candidate_idx)
423 .is_some_and(|conflict| conflict.is_user_keybind_conflict())
424 }
425
426 fn any_user_binding_conflicts(&self) -> bool {
427 self.has_user_conflicts
428 }
429}
430
431struct KeymapEditor {
432 workspace: WeakEntity<Workspace>,
433 focus_handle: FocusHandle,
434 _keymap_subscription: Subscription,
435 keybindings: Vec<ProcessedBinding>,
436 keybinding_conflict_state: ConflictState,
437 filter_state: FilterState,
438 source_filters: SourceFilters,
439 show_no_action_bindings: bool,
440 search_mode: SearchMode,
441 search_query_debounce: Option<Task<()>>,
442 // corresponds 1 to 1 with keybindings
443 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
444 matches: Vec<StringMatch>,
445 table_interaction_state: Entity<TableInteractionState>,
446 filter_editor: Entity<Editor>,
447 keystroke_editor: Entity<KeystrokeInput>,
448 selected_index: Option<usize>,
449 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
450 previous_edit: Option<PreviousEdit>,
451 humanized_action_names: HumanizedActionNameCache,
452 current_widths: Entity<TableColumnWidths>,
453 show_hover_menus: bool,
454 actions_with_schemas: HashSet<&'static str>,
455 /// In order for the JSON LSP to run in the actions arguments editor, we
456 /// require a backing file In order to avoid issues (primarily log spam)
457 /// with drop order between the buffer, file, worktree, etc, we create a
458 /// temporary directory for these backing files in the keymap editor struct
459 /// instead of here. This has the added benefit of only having to create a
460 /// worktree and directory once, although the perf improvement is negligible.
461 action_args_temp_dir_worktree: Option<Entity<project::Worktree>>,
462 action_args_temp_dir: Option<tempfile::TempDir>,
463}
464
465enum PreviousEdit {
466 /// When deleting, we want to maintain the same scroll position
467 ScrollBarOffset(Point<Pixels>),
468 /// When editing or creating, because the new keybinding could be in a different position in the sort order
469 /// we store metadata about the new binding (either the modified version or newly created one)
470 /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches
471 /// this metadata, we set the selected index to it and scroll to it,
472 /// and if we don't find it, we scroll to 0 and don't set a selected index
473 Keybinding {
474 action_mapping: ActionMapping,
475 action_name: &'static str,
476 /// The scrollbar position to fallback to if we don't find the keybinding during a refresh
477 /// this can happen if there's a filter applied to the search and the keybinding modification
478 /// filters the binding from the search results
479 fallback: Point<Pixels>,
480 },
481}
482
483impl EventEmitter<()> for KeymapEditor {}
484
485impl Focusable for KeymapEditor {
486 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
487 if self.selected_index.is_some() {
488 self.focus_handle.clone()
489 } else {
490 self.filter_editor.focus_handle(cx)
491 }
492 }
493}
494/// Helper function to check if two keystroke sequences match exactly
495fn keystrokes_match_exactly(
496 keystrokes1: &[KeybindingKeystroke],
497 keystrokes2: &[KeybindingKeystroke],
498) -> bool {
499 keystrokes1.len() == keystrokes2.len()
500 && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
501 k1.inner().key == k2.inner().key && k1.inner().modifiers == k2.inner().modifiers
502 })
503}
504
505impl KeymapEditor {
506 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
507 let _keymap_subscription =
508 cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed);
509 let table_interaction_state = cx.new(|cx| {
510 TableInteractionState::new(cx)
511 .with_custom_scrollbar(ui::Scrollbars::for_settings::<editor::EditorSettings>())
512 });
513
514 let keystroke_editor = cx.new(|cx| {
515 let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
516 keystroke_editor.set_search(true);
517 keystroke_editor
518 });
519
520 let filter_editor = cx.new(|cx| {
521 let mut editor = Editor::single_line(window, cx);
522 editor.set_placeholder_text("Filter action names…", window, cx);
523 editor
524 });
525
526 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
527 if !matches!(e, EditorEvent::BufferEdited) {
528 return;
529 }
530
531 this.on_query_changed(cx);
532 })
533 .detach();
534
535 cx.subscribe(&keystroke_editor, |this, _, _, cx| {
536 if matches!(this.search_mode, SearchMode::Normal) {
537 return;
538 }
539
540 this.on_query_changed(cx);
541 })
542 .detach();
543
544 cx.spawn({
545 let workspace = workspace.clone();
546 async move |this, cx| {
547 let temp_dir = tempfile::tempdir_in(paths::temp_dir())?;
548 let worktree = workspace
549 .update(cx, |ws, cx| {
550 ws.project()
551 .update(cx, |p, cx| p.create_worktree(temp_dir.path(), false, cx))
552 })?
553 .await?;
554 this.update(cx, |this, _| {
555 this.action_args_temp_dir = Some(temp_dir);
556 this.action_args_temp_dir_worktree = Some(worktree);
557 })
558 }
559 })
560 .detach();
561
562 let mut this = Self {
563 workspace,
564 keybindings: vec![],
565 keybinding_conflict_state: ConflictState::default(),
566 filter_state: FilterState::default(),
567 source_filters: SourceFilters {
568 user: true,
569 zed_defaults: true,
570 vim_defaults: true,
571 },
572 show_no_action_bindings: true,
573 search_mode: SearchMode::default(),
574 string_match_candidates: Arc::new(vec![]),
575 matches: vec![],
576 focus_handle: cx.focus_handle(),
577 _keymap_subscription,
578 table_interaction_state,
579 filter_editor,
580 keystroke_editor,
581 selected_index: None,
582 context_menu: None,
583 previous_edit: None,
584 search_query_debounce: None,
585 humanized_action_names: HumanizedActionNameCache::new(cx),
586 show_hover_menus: true,
587 actions_with_schemas: HashSet::default(),
588 action_args_temp_dir: None,
589 action_args_temp_dir_worktree: None,
590 current_widths: cx.new(|cx| TableColumnWidths::new(COLS, cx)),
591 };
592
593 this.on_keymap_changed(window, cx);
594
595 this
596 }
597
598 fn current_action_query(&self, cx: &App) -> String {
599 self.filter_editor.read(cx).text(cx)
600 }
601
602 fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> {
603 match self.search_mode {
604 SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
605 SearchMode::Normal => Default::default(),
606 }
607 }
608
609 fn clear_action_query(&self, window: &mut Window, cx: &mut Context<Self>) {
610 self.filter_editor
611 .update(cx, |editor, cx| editor.clear(window, cx))
612 }
613
614 fn on_query_changed(&mut self, cx: &mut Context<Self>) {
615 let action_query = self.current_action_query(cx);
616 let keystroke_query = self.current_keystroke_query(cx);
617 let exact_match = self.search_mode.exact_match();
618
619 let timer = cx.background_executor().timer(Duration::from_secs(1));
620 self.search_query_debounce = Some(cx.background_spawn({
621 let action_query = action_query.clone();
622 let keystroke_query = keystroke_query.clone();
623 async move {
624 timer.await;
625
626 let keystroke_query = keystroke_query
627 .into_iter()
628 .map(|keystroke| keystroke.inner().unparse())
629 .collect::<Vec<String>>()
630 .join(" ");
631
632 telemetry::event!(
633 "Keystroke Search Completed",
634 action_query = action_query,
635 keystroke_query = keystroke_query,
636 keystroke_exact_match = exact_match
637 )
638 }
639 }));
640 cx.spawn(async move |this, cx| {
641 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
642 this.update(cx, |this, cx| {
643 this.scroll_to_item(0, ScrollStrategy::Top, cx)
644 })
645 })
646 .detach();
647 }
648
649 async fn update_matches(
650 this: WeakEntity<Self>,
651 action_query: String,
652 keystroke_query: Vec<KeybindingKeystroke>,
653 cx: &mut AsyncApp,
654 ) -> anyhow::Result<()> {
655 let action_query = command_palette::normalize_action_query(&action_query);
656 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
657 (this.string_match_candidates.clone(), this.keybindings.len())
658 })?;
659 let executor = cx.background_executor().clone();
660 let mut matches = fuzzy::match_strings(
661 &string_match_candidates,
662 &action_query,
663 true,
664 true,
665 keybind_count,
666 &Default::default(),
667 executor,
668 )
669 .await;
670 this.update(cx, |this, cx| {
671 matches.retain(|candidate| {
672 this.source_filters
673 .allows(this.keybindings[candidate.candidate_id].keybind_source())
674 });
675
676 match this.filter_state {
677 FilterState::Conflicts => {
678 matches.retain(|candidate| {
679 this.keybinding_conflict_state
680 .has_user_conflict(candidate.candidate_id)
681 });
682 }
683 FilterState::All => {}
684 }
685
686 match this.search_mode {
687 SearchMode::KeyStroke { exact_match } => {
688 matches.retain(|item| {
689 this.keybindings[item.candidate_id]
690 .keystrokes()
691 .is_some_and(|keystrokes| {
692 if exact_match {
693 keystrokes_match_exactly(&keystroke_query, keystrokes)
694 } else if keystroke_query.len() > keystrokes.len() {
695 false
696 } else {
697 for keystroke_offset in 0..keystrokes.len() {
698 let mut found_count = 0;
699 let mut query_cursor = 0;
700 let mut keystroke_cursor = keystroke_offset;
701 while query_cursor < keystroke_query.len()
702 && keystroke_cursor < keystrokes.len()
703 {
704 let query = &keystroke_query[query_cursor];
705 let keystroke = &keystrokes[keystroke_cursor];
706 let matches = query
707 .inner()
708 .modifiers
709 .is_subset_of(&keystroke.inner().modifiers)
710 && ((query.inner().key.is_empty()
711 || query.inner().key == keystroke.inner().key)
712 && query.inner().key_char.as_ref().is_none_or(
713 |q_kc| q_kc == &keystroke.inner().key,
714 ));
715 if matches {
716 found_count += 1;
717 query_cursor += 1;
718 }
719 keystroke_cursor += 1;
720 }
721
722 if found_count == keystroke_query.len() {
723 return true;
724 }
725 }
726 false
727 }
728 })
729 });
730 }
731 SearchMode::Normal => {}
732 }
733
734 // Filter out NoAction suppression bindings by default. These are internal
735 // markers created when a user deletes a default binding (to suppress the
736 // default at the GPUI level), not real bindings the user should usually see.
737 if !this.show_no_action_bindings {
738 matches.retain(|item| {
739 this.keybindings[item.candidate_id].action().name != gpui::NoAction.name()
740 });
741 }
742
743 if action_query.is_empty() {
744 matches.sort_by(|item1, item2| {
745 let binding1 = &this.keybindings[item1.candidate_id];
746 let binding2 = &this.keybindings[item2.candidate_id];
747
748 binding1.cmp(binding2)
749 });
750 }
751 this.selected_index.take();
752 this.matches = matches;
753
754 cx.notify();
755 })
756 }
757
758 fn get_conflict(&self, row_index: usize) -> Option<ConflictOrigin> {
759 self.matches.get(row_index).and_then(|candidate| {
760 self.keybinding_conflict_state
761 .conflict_for_idx(candidate.candidate_id)
762 })
763 }
764
765 fn process_bindings(
766 json_language: Arc<Language>,
767 zed_keybind_context_language: Arc<Language>,
768 humanized_action_names: &HumanizedActionNameCache,
769 cx: &mut App,
770 ) -> (
771 Vec<ProcessedBinding>,
772 Vec<StringMatchCandidate>,
773 HashSet<&'static str>,
774 ) {
775 let key_bindings_ptr = cx.key_bindings();
776 let lock = key_bindings_ptr.borrow();
777 let key_bindings = lock.bindings();
778 let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names().iter().copied());
779 let action_documentation = cx.action_documentation();
780 let mut generator = KeymapFile::action_schema_generator();
781 let actions_with_schemas = HashSet::from_iter(
782 cx.action_schemas(&mut generator)
783 .into_iter()
784 .filter_map(|(name, schema)| schema.is_some().then_some(name)),
785 );
786
787 let mut processed_bindings = Vec::new();
788 let mut string_match_candidates = Vec::new();
789
790 for key_binding in key_bindings {
791 let source = key_binding
792 .meta()
793 .map(KeybindSource::from_meta)
794 .unwrap_or(KeybindSource::Unknown);
795
796 let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
797 let binding = KeyBinding::new(key_binding, source);
798
799 let context = key_binding
800 .predicate()
801 .map(|predicate| {
802 KeybindContextString::Local(
803 predicate.to_string().into(),
804 zed_keybind_context_language.clone(),
805 )
806 })
807 .unwrap_or(KeybindContextString::Global);
808
809 let action_name = key_binding.action().name();
810 unmapped_action_names.remove(&action_name);
811
812 let action_arguments = key_binding
813 .action_input()
814 .map(|arguments| SyntaxHighlightedText::new(arguments, json_language.clone()));
815 let action_information = ActionInformation::new(
816 action_name,
817 action_arguments,
818 &actions_with_schemas,
819 action_documentation,
820 humanized_action_names,
821 );
822
823 let index = processed_bindings.len();
824 let string_match_candidate =
825 StringMatchCandidate::new(index, &action_information.humanized_name);
826 processed_bindings.push(ProcessedBinding::new_mapped(
827 keystroke_text,
828 binding,
829 context,
830 source,
831 action_information,
832 ));
833 string_match_candidates.push(string_match_candidate);
834 }
835
836 for action_name in unmapped_action_names.into_iter() {
837 let index = processed_bindings.len();
838 let action_information = ActionInformation::new(
839 action_name,
840 None,
841 &actions_with_schemas,
842 action_documentation,
843 humanized_action_names,
844 );
845 let string_match_candidate =
846 StringMatchCandidate::new(index, &action_information.humanized_name);
847
848 processed_bindings.push(ProcessedBinding::Unmapped(action_information));
849 string_match_candidates.push(string_match_candidate);
850 }
851 (
852 processed_bindings,
853 string_match_candidates,
854 actions_with_schemas,
855 )
856 }
857
858 fn on_keymap_changed(&mut self, window: &mut Window, cx: &mut Context<KeymapEditor>) {
859 let workspace = self.workspace.clone();
860 cx.spawn_in(window, async move |this, cx| {
861 let json_language = load_json_language(workspace.clone(), cx).await;
862 let zed_keybind_context_language =
863 load_keybind_context_language(workspace.clone(), cx).await;
864
865 let (action_query, keystroke_query) = this.update(cx, |this, cx| {
866 let (key_bindings, string_match_candidates, actions_with_schemas) =
867 Self::process_bindings(
868 json_language,
869 zed_keybind_context_language,
870 &this.humanized_action_names,
871 cx,
872 );
873
874 this.keybinding_conflict_state = ConflictState::new(&key_bindings);
875
876 this.keybindings = key_bindings;
877 this.actions_with_schemas = actions_with_schemas;
878 this.string_match_candidates = Arc::new(string_match_candidates);
879 this.matches = this
880 .string_match_candidates
881 .iter()
882 .enumerate()
883 .map(|(ix, candidate)| StringMatch {
884 candidate_id: ix,
885 score: 0.0,
886 positions: vec![],
887 string: candidate.string.clone(),
888 })
889 .collect();
890 (
891 this.current_action_query(cx),
892 this.current_keystroke_query(cx),
893 )
894 })?;
895 // calls cx.notify
896 Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?;
897 this.update_in(cx, |this, window, cx| {
898 if let Some(previous_edit) = this.previous_edit.take() {
899 match previous_edit {
900 // should remove scroll from process_query
901 PreviousEdit::ScrollBarOffset(offset) => {
902 this.table_interaction_state
903 .update(cx, |table, _| table.set_scroll_offset(offset))
904 // set selected index and scroll
905 }
906 PreviousEdit::Keybinding {
907 action_mapping,
908 action_name,
909 fallback,
910 } => {
911 let scroll_position =
912 this.matches.iter().enumerate().find_map(|(index, item)| {
913 let binding = &this.keybindings[item.candidate_id];
914 if binding.get_action_mapping().is_some_and(|binding_mapping| {
915 binding_mapping == action_mapping
916 }) && binding.action().name == action_name
917 {
918 Some(index)
919 } else {
920 None
921 }
922 });
923
924 if let Some(scroll_position) = scroll_position {
925 this.select_index(
926 scroll_position,
927 Some(ScrollStrategy::Top),
928 window,
929 cx,
930 );
931 } else {
932 this.table_interaction_state
933 .update(cx, |table, _| table.set_scroll_offset(fallback));
934 }
935 cx.notify();
936 }
937 }
938 }
939 })
940 })
941 .detach_and_log_err(cx);
942 }
943
944 fn key_context(&self) -> KeyContext {
945 let mut dispatch_context = KeyContext::new_with_defaults();
946 dispatch_context.add("KeymapEditor");
947 dispatch_context.add("menu");
948
949 dispatch_context
950 }
951
952 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
953 let index = usize::min(index, self.matches.len().saturating_sub(1));
954 self.table_interaction_state.update(cx, |this, _cx| {
955 this.scroll_handle.scroll_to_item(index, strategy);
956 });
957 }
958
959 fn focus_search(
960 &mut self,
961 _: &search::FocusSearch,
962 window: &mut Window,
963 cx: &mut Context<Self>,
964 ) {
965 if !self
966 .filter_editor
967 .focus_handle(cx)
968 .contains_focused(window, cx)
969 {
970 window.focus(&self.filter_editor.focus_handle(cx), cx);
971 } else {
972 self.filter_editor.update(cx, |editor, cx| {
973 editor.select_all(&Default::default(), window, cx);
974 });
975 }
976 self.selected_index.take();
977 }
978
979 fn selected_keybind_index(&self) -> Option<usize> {
980 self.selected_index
981 .and_then(|match_index| self.matches.get(match_index))
982 .map(|r#match| r#match.candidate_id)
983 }
984
985 fn selected_keybind_and_index(&self) -> Option<(&ProcessedBinding, usize)> {
986 self.selected_keybind_index()
987 .map(|keybind_index| (&self.keybindings[keybind_index], keybind_index))
988 }
989
990 fn selected_binding(&self) -> Option<&ProcessedBinding> {
991 self.selected_keybind_index()
992 .and_then(|keybind_index| self.keybindings.get(keybind_index))
993 }
994
995 fn select_index(
996 &mut self,
997 index: usize,
998 scroll: Option<ScrollStrategy>,
999 window: &mut Window,
1000 cx: &mut Context<Self>,
1001 ) {
1002 if self.selected_index != Some(index) {
1003 self.selected_index = Some(index);
1004 if let Some(scroll_strategy) = scroll {
1005 self.scroll_to_item(index, scroll_strategy, cx);
1006 }
1007 window.focus(&self.focus_handle, cx);
1008 cx.notify();
1009 }
1010 }
1011
1012 fn create_context_menu(
1013 &mut self,
1014 position: Point<Pixels>,
1015 window: &mut Window,
1016 cx: &mut Context<Self>,
1017 ) {
1018 self.context_menu = self.selected_binding().map(|selected_binding| {
1019 let selected_binding_has_no_context = selected_binding
1020 .context()
1021 .and_then(KeybindContextString::local)
1022 .is_none();
1023
1024 let selected_binding_is_unbound = selected_binding.is_unbound();
1025
1026 let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
1027 menu.context(self.focus_handle.clone())
1028 .when(selected_binding_is_unbound, |this| {
1029 this.action("Create", Box::new(CreateBinding))
1030 })
1031 .action_disabled_when(
1032 selected_binding_is_unbound,
1033 "Edit",
1034 Box::new(EditBinding),
1035 )
1036 .action_disabled_when(
1037 selected_binding_is_unbound,
1038 "Delete",
1039 Box::new(DeleteBinding),
1040 )
1041 .separator()
1042 .action("Copy Action", Box::new(CopyAction))
1043 .action_disabled_when(
1044 selected_binding_has_no_context,
1045 "Copy Context",
1046 Box::new(CopyContext),
1047 )
1048 .separator()
1049 .action_disabled_when(
1050 selected_binding_has_no_context,
1051 "Show Matching Keybindings",
1052 Box::new(ShowMatchingKeybinds),
1053 )
1054 });
1055
1056 let context_menu_handle = context_menu.focus_handle(cx);
1057 window.defer(cx, move |window, cx| window.focus(&context_menu_handle, cx));
1058 let subscription = cx.subscribe_in(
1059 &context_menu,
1060 window,
1061 |this, _, _: &DismissEvent, window, cx| {
1062 this.dismiss_context_menu(window, cx);
1063 },
1064 );
1065 (context_menu, position, subscription)
1066 });
1067
1068 cx.notify();
1069 }
1070
1071 fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1072 self.context_menu.take();
1073 window.focus(&self.focus_handle, cx);
1074 cx.notify();
1075 }
1076
1077 fn context_menu_deployed(&self) -> bool {
1078 self.context_menu.is_some()
1079 }
1080
1081 fn create_row_button(
1082 &self,
1083 index: usize,
1084 conflict: Option<ConflictOrigin>,
1085 cx: &mut Context<Self>,
1086 ) -> IconButton {
1087 if self.filter_state != FilterState::Conflicts
1088 && let Some(conflict) = conflict
1089 {
1090 if conflict.is_user_keybind_conflict() {
1091 base_button_style(index, IconName::Warning)
1092 .icon_color(Color::Warning)
1093 .tooltip(|_window, cx| {
1094 Tooltip::with_meta(
1095 "View conflicts",
1096 Some(&ToggleConflictFilter),
1097 "Use alt+click to show all conflicts",
1098 cx,
1099 )
1100 })
1101 .on_click(cx.listener(move |this, click: &ClickEvent, window, cx| {
1102 if click.modifiers().alt {
1103 this.set_filter_state(FilterState::Conflicts, cx);
1104 } else {
1105 this.select_index(index, None, window, cx);
1106 this.open_edit_keybinding_modal(false, window, cx);
1107 cx.stop_propagation();
1108 }
1109 }))
1110 } else if self.search_mode.exact_match() {
1111 base_button_style(index, IconName::Info)
1112 .tooltip(|_window, cx| {
1113 Tooltip::with_meta(
1114 "Edit this binding",
1115 Some(&ShowMatchingKeybinds),
1116 "This binding is overridden by other bindings.",
1117 cx,
1118 )
1119 })
1120 .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
1121 this.select_index(index, None, window, cx);
1122 this.open_edit_keybinding_modal(false, window, cx);
1123 cx.stop_propagation();
1124 }))
1125 } else {
1126 base_button_style(index, IconName::Info)
1127 .tooltip(|_window, cx| {
1128 Tooltip::with_meta(
1129 "Show matching keybinds",
1130 Some(&ShowMatchingKeybinds),
1131 "This binding is overridden by other bindings.\nUse alt+click to edit this binding",
1132 cx,
1133 )
1134 })
1135 .on_click(cx.listener(move |this, click: &ClickEvent, window, cx| {
1136 if click.modifiers().alt {
1137 this.select_index(index, None, window, cx);
1138 this.open_edit_keybinding_modal(false, window, cx);
1139 cx.stop_propagation();
1140 } else {
1141 this.show_matching_keystrokes(&Default::default(), window, cx);
1142 }
1143 }))
1144 }
1145 } else {
1146 base_button_style(index, IconName::Pencil)
1147 .visible_on_hover(if self.selected_index == Some(index) {
1148 "".into()
1149 } else if self.show_hover_menus {
1150 row_group_id(index)
1151 } else {
1152 "never-show".into()
1153 })
1154 .when(
1155 self.show_hover_menus && !self.context_menu_deployed(),
1156 |this| this.tooltip(Tooltip::for_action_title("Edit Keybinding", &EditBinding)),
1157 )
1158 .on_click(cx.listener(move |this, _, window, cx| {
1159 this.select_index(index, None, window, cx);
1160 this.open_edit_keybinding_modal(false, window, cx);
1161 cx.stop_propagation();
1162 }))
1163 }
1164 }
1165
1166 fn render_no_matches_hint(&self, _window: &mut Window, _cx: &App) -> AnyElement {
1167 let hint = match (self.filter_state, &self.search_mode) {
1168 (FilterState::Conflicts, _) => {
1169 if self.keybinding_conflict_state.any_user_binding_conflicts() {
1170 "No conflicting keybinds found that match the provided query"
1171 } else {
1172 "No conflicting keybinds found"
1173 }
1174 }
1175 (FilterState::All, SearchMode::KeyStroke { .. }) => {
1176 "No keybinds found matching the entered keystrokes"
1177 }
1178 (FilterState::All, SearchMode::Normal) => "No matches found for the provided query",
1179 };
1180
1181 Label::new(hint).color(Color::Muted).into_any_element()
1182 }
1183
1184 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1185 self.show_hover_menus = false;
1186 if let Some(selected) = self.selected_index {
1187 let selected = selected + 1;
1188 if selected >= self.matches.len() {
1189 self.select_last(&Default::default(), window, cx);
1190 } else {
1191 self.select_index(selected, Some(ScrollStrategy::Center), window, cx);
1192 }
1193 } else {
1194 self.select_first(&Default::default(), window, cx);
1195 }
1196 }
1197
1198 fn select_previous(
1199 &mut self,
1200 _: &menu::SelectPrevious,
1201 window: &mut Window,
1202 cx: &mut Context<Self>,
1203 ) {
1204 self.show_hover_menus = false;
1205 if let Some(selected) = self.selected_index {
1206 if selected == 0 {
1207 return;
1208 }
1209
1210 let selected = selected - 1;
1211
1212 if selected >= self.matches.len() {
1213 self.select_last(&Default::default(), window, cx);
1214 } else {
1215 self.select_index(selected, Some(ScrollStrategy::Center), window, cx);
1216 }
1217 } else {
1218 self.select_last(&Default::default(), window, cx);
1219 }
1220 }
1221
1222 fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1223 self.show_hover_menus = false;
1224 if self.matches.get(0).is_some() {
1225 self.select_index(0, Some(ScrollStrategy::Center), window, cx);
1226 }
1227 }
1228
1229 fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
1230 self.show_hover_menus = false;
1231 if self.matches.last().is_some() {
1232 let index = self.matches.len() - 1;
1233 self.select_index(index, Some(ScrollStrategy::Center), window, cx);
1234 }
1235 }
1236
1237 fn open_edit_keybinding_modal(
1238 &mut self,
1239 create: bool,
1240 window: &mut Window,
1241 cx: &mut Context<Self>,
1242 ) {
1243 self.show_hover_menus = false;
1244 let Some((keybind, keybind_index)) = self.selected_keybind_and_index() else {
1245 return;
1246 };
1247 let keybind = keybind.clone();
1248 let keymap_editor = cx.entity();
1249
1250 let keystroke = keybind.keystroke_text().cloned().unwrap_or_default();
1251 let arguments = keybind
1252 .action()
1253 .arguments
1254 .as_ref()
1255 .map(|arguments| arguments.text.clone());
1256 let context = keybind
1257 .context()
1258 .map(|context| context.local_str().unwrap_or("global"));
1259 let action = keybind.action().name;
1260 let source = keybind.keybind_source().map(|source| source.name());
1261
1262 telemetry::event!(
1263 "Edit Keybinding Modal Opened",
1264 keystroke = keystroke,
1265 action = action,
1266 source = source,
1267 context = context,
1268 arguments = arguments,
1269 );
1270
1271 let temp_dir = self.action_args_temp_dir.as_ref().map(|dir| dir.path());
1272
1273 self.workspace
1274 .update(cx, |workspace, cx| {
1275 let fs = workspace.app_state().fs.clone();
1276 let workspace_weak = cx.weak_entity();
1277 workspace.toggle_modal(window, cx, |window, cx| {
1278 let modal = KeybindingEditorModal::new(
1279 create,
1280 keybind,
1281 keybind_index,
1282 keymap_editor,
1283 temp_dir,
1284 workspace_weak,
1285 fs,
1286 window,
1287 cx,
1288 );
1289 window.focus(&modal.focus_handle(cx), cx);
1290 modal
1291 });
1292 })
1293 .log_err();
1294 }
1295
1296 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
1297 self.open_edit_keybinding_modal(false, window, cx);
1298 }
1299
1300 fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
1301 self.open_edit_keybinding_modal(true, window, cx);
1302 }
1303
1304 fn open_create_keybinding_modal(
1305 &mut self,
1306 _: &OpenCreateKeybindingModal,
1307 window: &mut Window,
1308 cx: &mut Context<Self>,
1309 ) {
1310 let keymap_editor = cx.entity();
1311
1312 let action_information = ActionInformation::new(
1313 gpui::NoAction.name(),
1314 None,
1315 &HashSet::default(),
1316 cx.action_documentation(),
1317 &self.humanized_action_names,
1318 );
1319
1320 let dummy_binding = ProcessedBinding::Unmapped(action_information);
1321 let dummy_index = self.keybindings.len();
1322
1323 let temp_dir = self.action_args_temp_dir.as_ref().map(|dir| dir.path());
1324
1325 self.workspace
1326 .update(cx, |workspace, cx| {
1327 let fs = workspace.app_state().fs.clone();
1328 let workspace_weak = cx.weak_entity();
1329 workspace.toggle_modal(window, cx, |window, cx| {
1330 let modal = KeybindingEditorModal::new(
1331 true,
1332 dummy_binding,
1333 dummy_index,
1334 keymap_editor,
1335 temp_dir,
1336 workspace_weak,
1337 fs,
1338 window,
1339 cx,
1340 );
1341
1342 window.focus(&modal.focus_handle(cx), cx);
1343 modal
1344 });
1345 })
1346 .log_err();
1347 }
1348
1349 fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
1350 let Some(to_remove) = self.selected_binding().cloned() else {
1351 return;
1352 };
1353
1354 let std::result::Result::Ok(fs) = self
1355 .workspace
1356 .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
1357 else {
1358 return;
1359 };
1360 self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
1361 self.table_interaction_state.read(cx).scroll_offset(),
1362 ));
1363 let keyboard_mapper = cx.keyboard_mapper().clone();
1364 cx.spawn(async move |_, _| {
1365 remove_keybinding(to_remove, &fs, keyboard_mapper.as_ref()).await
1366 })
1367 .detach_and_notify_err(self.workspace.clone(), window, cx);
1368 }
1369
1370 fn copy_context_to_clipboard(
1371 &mut self,
1372 _: &CopyContext,
1373 _window: &mut Window,
1374 cx: &mut Context<Self>,
1375 ) {
1376 let context = self
1377 .selected_binding()
1378 .and_then(|binding| binding.context())
1379 .and_then(KeybindContextString::local_str)
1380 .map(|context| context.to_string());
1381 let Some(context) = context else {
1382 return;
1383 };
1384
1385 telemetry::event!("Keybinding Context Copied", context = context);
1386 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context));
1387 }
1388
1389 fn copy_action_to_clipboard(
1390 &mut self,
1391 _: &CopyAction,
1392 _window: &mut Window,
1393 cx: &mut Context<Self>,
1394 ) {
1395 let action = self
1396 .selected_binding()
1397 .map(|binding| binding.action().name.to_string());
1398 let Some(action) = action else {
1399 return;
1400 };
1401
1402 telemetry::event!("Keybinding Action Copied", action = action);
1403 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action));
1404 }
1405
1406 fn toggle_conflict_filter(
1407 &mut self,
1408 _: &ToggleConflictFilter,
1409 _: &mut Window,
1410 cx: &mut Context<Self>,
1411 ) {
1412 self.set_filter_state(self.filter_state.invert(), cx);
1413 }
1414
1415 fn toggle_no_action_bindings(
1416 &mut self,
1417 _: &ToggleNoActionBindings,
1418 _: &mut Window,
1419 cx: &mut Context<Self>,
1420 ) {
1421 self.show_no_action_bindings = !self.show_no_action_bindings;
1422 self.on_query_changed(cx);
1423 }
1424
1425 fn toggle_user_bindings_filter(&mut self, cx: &mut Context<Self>) {
1426 self.source_filters.user = !self.source_filters.user;
1427 self.on_query_changed(cx);
1428 }
1429
1430 fn toggle_zed_defaults_filter(&mut self, cx: &mut Context<Self>) {
1431 self.source_filters.zed_defaults = !self.source_filters.zed_defaults;
1432 self.on_query_changed(cx);
1433 }
1434
1435 fn toggle_vim_defaults_filter(&mut self, cx: &mut Context<Self>) {
1436 self.source_filters.vim_defaults = !self.source_filters.vim_defaults;
1437 self.on_query_changed(cx);
1438 }
1439
1440 fn set_filter_state(&mut self, filter_state: FilterState, cx: &mut Context<Self>) {
1441 if self.filter_state != filter_state {
1442 self.filter_state = filter_state;
1443 self.on_query_changed(cx);
1444 }
1445 }
1446
1447 fn toggle_keystroke_search(
1448 &mut self,
1449 _: &ToggleKeystrokeSearch,
1450 window: &mut Window,
1451 cx: &mut Context<Self>,
1452 ) {
1453 self.search_mode = self.search_mode.invert();
1454 self.on_query_changed(cx);
1455
1456 match self.search_mode {
1457 SearchMode::KeyStroke { .. } => {
1458 self.keystroke_editor.update(cx, |editor, cx| {
1459 editor.start_recording(&StartRecording, window, cx);
1460 });
1461 }
1462 SearchMode::Normal => {
1463 self.keystroke_editor.update(cx, |editor, cx| {
1464 editor.stop_recording(&StopRecording, window, cx);
1465 editor.clear_keystrokes(&ClearKeystrokes, window, cx);
1466 });
1467 window.focus(&self.filter_editor.focus_handle(cx), cx);
1468 }
1469 }
1470 }
1471
1472 fn toggle_exact_keystroke_matching(
1473 &mut self,
1474 _: &ToggleExactKeystrokeMatching,
1475 _: &mut Window,
1476 cx: &mut Context<Self>,
1477 ) {
1478 let SearchMode::KeyStroke { exact_match } = &mut self.search_mode else {
1479 return;
1480 };
1481
1482 *exact_match = !(*exact_match);
1483 self.on_query_changed(cx);
1484 }
1485
1486 fn show_matching_keystrokes(
1487 &mut self,
1488 _: &ShowMatchingKeybinds,
1489 _: &mut Window,
1490 cx: &mut Context<Self>,
1491 ) {
1492 let Some(selected_binding) = self.selected_binding() else {
1493 return;
1494 };
1495
1496 let keystrokes = selected_binding
1497 .keystrokes()
1498 .map(Vec::from)
1499 .unwrap_or_default();
1500
1501 self.filter_state = FilterState::All;
1502 self.search_mode = SearchMode::KeyStroke { exact_match: true };
1503
1504 self.keystroke_editor.update(cx, |editor, cx| {
1505 editor.set_keystrokes(keystrokes, cx);
1506 });
1507 }
1508
1509 fn has_binding_for(&self, action_name: &str) -> bool {
1510 self.keybindings
1511 .iter()
1512 .filter(|kb| kb.keystrokes().is_some())
1513 .any(|kb| kb.action().name == action_name)
1514 }
1515
1516 fn render_filter_dropdown(
1517 &self,
1518 focus_handle: &FocusHandle,
1519 cx: &mut Context<KeymapEditor>,
1520 ) -> impl IntoElement {
1521 let focus_handle = focus_handle.clone();
1522 let keymap_editor = cx.entity();
1523 return PopoverMenu::new("keymap-editor-filter-menu")
1524 .menu(move |window, cx| {
1525 Some(ContextMenu::build_persistent(window, cx, {
1526 let focus_handle = focus_handle.clone();
1527 let keymap_editor = keymap_editor.clone();
1528 move |mut menu, _window, cx| {
1529 let (filter_state, source_filters, show_no_action_bindings) = keymap_editor
1530 .read_with(cx, |editor, _| {
1531 (
1532 editor.filter_state,
1533 editor.source_filters,
1534 editor.show_no_action_bindings,
1535 )
1536 });
1537
1538 menu = menu
1539 .context(focus_handle.clone())
1540 .header("Filters")
1541 .map(add_filter(
1542 "Conflicts",
1543 matches!(filter_state, FilterState::Conflicts),
1544 Some(ToggleConflictFilter.boxed_clone()),
1545 &focus_handle,
1546 &keymap_editor,
1547 None,
1548 ))
1549 .map(add_filter(
1550 "No Action",
1551 show_no_action_bindings,
1552 Some(ToggleNoActionBindings.boxed_clone()),
1553 &focus_handle,
1554 &keymap_editor,
1555 None,
1556 ))
1557 .separator()
1558 .header("Categories")
1559 .map(add_filter(
1560 "User",
1561 source_filters.user,
1562 None,
1563 &focus_handle,
1564 &keymap_editor,
1565 Some(|editor, cx| {
1566 editor.toggle_user_bindings_filter(cx);
1567 }),
1568 ))
1569 .map(add_filter(
1570 "Default",
1571 source_filters.zed_defaults,
1572 None,
1573 &focus_handle,
1574 &keymap_editor,
1575 Some(|editor, cx| {
1576 editor.toggle_zed_defaults_filter(cx);
1577 }),
1578 ))
1579 .map(add_filter(
1580 "Vim",
1581 source_filters.vim_defaults,
1582 None,
1583 &focus_handle,
1584 &keymap_editor,
1585 Some(|editor, cx| {
1586 editor.toggle_vim_defaults_filter(cx);
1587 }),
1588 ));
1589 menu
1590 }
1591 }))
1592 })
1593 .anchor(gpui::Corner::TopRight)
1594 .offset(gpui::Point {
1595 x: px(0.0),
1596 y: px(2.0),
1597 })
1598 .trigger_with_tooltip(
1599 IconButton::new("KeymapEditorFilterMenuButton", IconName::Sliders)
1600 .icon_size(IconSize::Small)
1601 .when(
1602 self.keybinding_conflict_state.any_user_binding_conflicts(),
1603 |this| this.indicator(Indicator::dot().color(Color::Warning)),
1604 ),
1605 Tooltip::text("Filters"),
1606 );
1607
1608 fn add_filter(
1609 name: &'static str,
1610 toggled: bool,
1611 action: Option<Box<dyn Action>>,
1612 focus_handle: &FocusHandle,
1613 keymap_editor: &Entity<KeymapEditor>,
1614 cb: Option<fn(&mut KeymapEditor, &mut Context<KeymapEditor>)>,
1615 ) -> impl FnOnce(ContextMenu) -> ContextMenu {
1616 let focus_handle = focus_handle.clone();
1617 let keymap_editor = keymap_editor.clone();
1618 return move |menu: ContextMenu| {
1619 menu.toggleable_entry(
1620 name,
1621 toggled,
1622 IconPosition::End,
1623 action.as_ref().map(|a| a.boxed_clone()),
1624 move |window, cx| {
1625 window.focus(&focus_handle, cx);
1626 if let Some(action) = &action {
1627 window.dispatch_action(action.boxed_clone(), cx);
1628 } else if let Some(cb) = cb {
1629 keymap_editor.update(cx, cb);
1630 }
1631 },
1632 )
1633 };
1634 }
1635 }
1636}
1637
1638struct HumanizedActionNameCache {
1639 cache: HashMap<&'static str, SharedString>,
1640}
1641
1642impl HumanizedActionNameCache {
1643 fn new(cx: &App) -> Self {
1644 let cache = HashMap::from_iter(cx.all_action_names().iter().map(|&action_name| {
1645 (
1646 action_name,
1647 command_palette::humanize_action_name(action_name).into(),
1648 )
1649 }));
1650 Self { cache }
1651 }
1652
1653 fn get(&self, action_name: &'static str) -> SharedString {
1654 match self.cache.get(action_name) {
1655 Some(name) => name.clone(),
1656 None => action_name.into(),
1657 }
1658 }
1659}
1660
1661#[derive(Clone)]
1662struct KeyBinding {
1663 keystrokes: Rc<[KeybindingKeystroke]>,
1664 source: KeybindSource,
1665}
1666
1667impl KeyBinding {
1668 fn new(binding: &gpui::KeyBinding, source: KeybindSource) -> Self {
1669 Self {
1670 keystrokes: Rc::from(binding.keystrokes()),
1671 source,
1672 }
1673 }
1674}
1675
1676#[derive(Clone)]
1677struct KeybindInformation {
1678 keystroke_text: SharedString,
1679 binding: KeyBinding,
1680 context: KeybindContextString,
1681 source: KeybindSource,
1682}
1683
1684impl KeybindInformation {
1685 fn get_action_mapping(&self) -> ActionMapping {
1686 ActionMapping {
1687 keystrokes: self.binding.keystrokes.clone(),
1688 context: self.context.local().cloned(),
1689 }
1690 }
1691}
1692
1693#[derive(Clone)]
1694struct ActionInformation {
1695 name: &'static str,
1696 humanized_name: SharedString,
1697 arguments: Option<SyntaxHighlightedText>,
1698 documentation: Option<&'static str>,
1699 has_schema: bool,
1700}
1701
1702impl ActionInformation {
1703 fn new(
1704 action_name: &'static str,
1705 action_arguments: Option<SyntaxHighlightedText>,
1706 actions_with_schemas: &HashSet<&'static str>,
1707 action_documentation: &HashMap<&'static str, &'static str>,
1708 action_name_cache: &HumanizedActionNameCache,
1709 ) -> Self {
1710 Self {
1711 humanized_name: action_name_cache.get(action_name),
1712 has_schema: actions_with_schemas.contains(action_name),
1713 arguments: action_arguments,
1714 documentation: action_documentation.get(action_name).copied(),
1715 name: action_name,
1716 }
1717 }
1718}
1719
1720#[derive(Clone)]
1721enum ProcessedBinding {
1722 Mapped(KeybindInformation, ActionInformation),
1723 Unmapped(ActionInformation),
1724}
1725
1726impl ProcessedBinding {
1727 fn new_mapped(
1728 keystroke_text: impl Into<SharedString>,
1729 binding: KeyBinding,
1730 context: KeybindContextString,
1731 source: KeybindSource,
1732 action_information: ActionInformation,
1733 ) -> Self {
1734 Self::Mapped(
1735 KeybindInformation {
1736 keystroke_text: keystroke_text.into(),
1737 binding,
1738 context,
1739 source,
1740 },
1741 action_information,
1742 )
1743 }
1744
1745 fn is_unbound(&self) -> bool {
1746 matches!(self, Self::Unmapped(_))
1747 }
1748
1749 fn get_action_mapping(&self) -> Option<ActionMapping> {
1750 self.keybind_information()
1751 .map(|keybind| keybind.get_action_mapping())
1752 }
1753
1754 fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
1755 self.key_binding()
1756 .map(|binding| binding.keystrokes.as_ref())
1757 }
1758
1759 fn keybind_information(&self) -> Option<&KeybindInformation> {
1760 match self {
1761 Self::Mapped(keybind_information, _) => Some(keybind_information),
1762 Self::Unmapped(_) => None,
1763 }
1764 }
1765
1766 fn keybind_source(&self) -> Option<KeybindSource> {
1767 self.keybind_information().map(|keybind| keybind.source)
1768 }
1769
1770 fn context(&self) -> Option<&KeybindContextString> {
1771 self.keybind_information().map(|keybind| &keybind.context)
1772 }
1773
1774 fn key_binding(&self) -> Option<&KeyBinding> {
1775 self.keybind_information().map(|keybind| &keybind.binding)
1776 }
1777
1778 fn keystroke_text(&self) -> Option<&SharedString> {
1779 self.keybind_information()
1780 .map(|binding| &binding.keystroke_text)
1781 }
1782
1783 fn action(&self) -> &ActionInformation {
1784 match self {
1785 Self::Mapped(_, action) | Self::Unmapped(action) => action,
1786 }
1787 }
1788
1789 fn cmp(&self, other: &Self) -> cmp::Ordering {
1790 match (self, other) {
1791 (Self::Mapped(keybind1, action1), Self::Mapped(keybind2, action2)) => {
1792 match keybind1.source.cmp(&keybind2.source) {
1793 cmp::Ordering::Equal => action1.humanized_name.cmp(&action2.humanized_name),
1794 ordering => ordering,
1795 }
1796 }
1797 (Self::Mapped(_, _), Self::Unmapped(_)) => cmp::Ordering::Less,
1798 (Self::Unmapped(_), Self::Mapped(_, _)) => cmp::Ordering::Greater,
1799 (Self::Unmapped(action1), Self::Unmapped(action2)) => {
1800 action1.humanized_name.cmp(&action2.humanized_name)
1801 }
1802 }
1803 }
1804}
1805
1806#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
1807enum KeybindContextString {
1808 Global,
1809 Local(SharedString, Arc<Language>),
1810}
1811
1812impl KeybindContextString {
1813 const GLOBAL: SharedString = SharedString::new_static("<global>");
1814
1815 pub fn local(&self) -> Option<&SharedString> {
1816 match self {
1817 KeybindContextString::Global => None,
1818 KeybindContextString::Local(name, _) => Some(name),
1819 }
1820 }
1821
1822 pub fn local_str(&self) -> Option<&str> {
1823 match self {
1824 KeybindContextString::Global => None,
1825 KeybindContextString::Local(name, _) => Some(name),
1826 }
1827 }
1828}
1829
1830impl RenderOnce for KeybindContextString {
1831 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
1832 match self {
1833 KeybindContextString::Global => {
1834 muted_styled_text(KeybindContextString::GLOBAL, cx).into_any_element()
1835 }
1836 KeybindContextString::Local(name, language) => {
1837 SyntaxHighlightedText::new(name, language).into_any_element()
1838 }
1839 }
1840 }
1841}
1842
1843fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
1844 let len = text.len();
1845 StyledText::new(text).with_highlights([(
1846 0..len,
1847 gpui::HighlightStyle::color(cx.theme().colors().text_muted),
1848 )])
1849}
1850
1851impl Item for KeymapEditor {
1852 type Event = ();
1853
1854 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
1855 "Keymap Editor".into()
1856 }
1857}
1858
1859impl Render for KeymapEditor {
1860 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
1861 if let SearchMode::KeyStroke { exact_match } = self.search_mode {
1862 let button = IconButton::new("keystrokes-exact-match", IconName::CaseSensitive)
1863 .tooltip(move |_window, cx| {
1864 Tooltip::for_action(
1865 "Toggle Exact Match Mode",
1866 &ToggleExactKeystrokeMatching,
1867 cx,
1868 )
1869 })
1870 .shape(IconButtonShape::Square)
1871 .toggle_state(exact_match)
1872 .on_click(cx.listener(|_, _, window, cx| {
1873 window.dispatch_action(ToggleExactKeystrokeMatching.boxed_clone(), cx);
1874 }));
1875
1876 self.keystroke_editor.update(cx, |editor, _| {
1877 editor.actions_slot = Some(button.into_any_element());
1878 });
1879 } else {
1880 self.keystroke_editor.update(cx, |editor, _| {
1881 editor.actions_slot = None;
1882 });
1883 }
1884
1885 let row_count = self.matches.len();
1886 let focus_handle = &self.focus_handle;
1887 let theme = cx.theme();
1888 let search_mode = self.search_mode;
1889
1890 v_flex()
1891 .id("keymap-editor")
1892 .track_focus(focus_handle)
1893 .key_context(self.key_context())
1894 .on_action(cx.listener(Self::select_next))
1895 .on_action(cx.listener(Self::select_previous))
1896 .on_action(cx.listener(Self::select_first))
1897 .on_action(cx.listener(Self::select_last))
1898 .on_action(cx.listener(Self::focus_search))
1899 .on_action(cx.listener(Self::edit_binding))
1900 .on_action(cx.listener(Self::create_binding))
1901 .on_action(cx.listener(Self::open_create_keybinding_modal))
1902 .on_action(cx.listener(Self::delete_binding))
1903 .on_action(cx.listener(Self::copy_action_to_clipboard))
1904 .on_action(cx.listener(Self::copy_context_to_clipboard))
1905 .on_action(cx.listener(Self::toggle_conflict_filter))
1906 .on_action(cx.listener(Self::toggle_no_action_bindings))
1907 .on_action(cx.listener(Self::toggle_keystroke_search))
1908 .on_action(cx.listener(Self::toggle_exact_keystroke_matching))
1909 .on_action(cx.listener(Self::show_matching_keystrokes))
1910 .on_mouse_move(cx.listener(|this, _, _window, _cx| {
1911 this.show_hover_menus = true;
1912 }))
1913 .size_full()
1914 .p_2()
1915 .gap_1()
1916 .bg(theme.colors().editor_background)
1917 .child(
1918 v_flex()
1919 .gap_2()
1920 .child(
1921 h_flex()
1922 .gap_2()
1923 .items_center()
1924 .child(
1925 h_flex()
1926 .key_context({
1927 let mut context = KeyContext::new_with_defaults();
1928 context.add("BufferSearchBar");
1929 context
1930 })
1931 .size_full()
1932 .h_8()
1933 .pl_2()
1934 .pr_1()
1935 .py_1()
1936 .border_1()
1937 .border_color(theme.colors().border)
1938 .rounded_md()
1939 .child(self.filter_editor.clone()),
1940 )
1941 .child(
1942 h_flex()
1943 .gap_1()
1944 .min_w_96()
1945 .items_center()
1946 .child(
1947 IconButton::new(
1948 "KeymapEditorKeystrokeSearchButton",
1949 IconName::Keyboard,
1950 )
1951 .icon_size(IconSize::Small)
1952 .toggle_state(matches!(
1953 search_mode,
1954 SearchMode::KeyStroke { .. }
1955 ))
1956 .tooltip({
1957 let focus_handle = focus_handle.clone();
1958 move |_window, cx| {
1959 Tooltip::for_action_in(
1960 "Search by Keystrokes",
1961 &ToggleKeystrokeSearch,
1962 &focus_handle,
1963 cx,
1964 )
1965 }
1966 })
1967 .on_click(cx.listener(|_, _, window, cx| {
1968 window.dispatch_action(
1969 ToggleKeystrokeSearch.boxed_clone(),
1970 cx,
1971 );
1972 })),
1973 )
1974 .child(
1975 self.render_filter_dropdown(focus_handle, cx)
1976 )
1977 .child(
1978 Button::new("edit-in-json", "Edit in JSON")
1979 .style(ButtonStyle::Subtle)
1980 .key_binding(
1981 ui::KeyBinding::for_action_in(&zed_actions::OpenKeymapFile, &focus_handle, cx)
1982 .map(|kb| kb.size(rems_from_px(10.))),
1983 )
1984 .on_click(|_, window, cx| {
1985 window.dispatch_action(
1986 zed_actions::OpenKeymapFile.boxed_clone(),
1987 cx,
1988 );
1989 })
1990 )
1991 .child(
1992 Button::new("create", "Create Keybinding")
1993 .style(ButtonStyle::Outlined)
1994 .key_binding(
1995 ui::KeyBinding::for_action_in(&OpenCreateKeybindingModal, &focus_handle, cx)
1996 .map(|kb| kb.size(rems_from_px(10.))),
1997 )
1998 .on_click(|_, window, cx| {
1999 window.dispatch_action(
2000 OpenCreateKeybindingModal.boxed_clone(),
2001 cx,
2002 );
2003 })
2004 )
2005 ),
2006 )
2007 .when(
2008 matches!(self.search_mode, SearchMode::KeyStroke { .. }),
2009 |this| {
2010 this.child(
2011 h_flex()
2012 .gap_2()
2013 .child(self.keystroke_editor.clone())
2014 .child(div().min_w_96()), // Spacer div to align with the search input
2015 )
2016 },
2017 ),
2018 )
2019 .child(
2020 Table::new(COLS)
2021 .interactable(&self.table_interaction_state)
2022 .striped()
2023 .empty_table_callback({
2024 let this = cx.entity();
2025 move |window, cx| this.read(cx).render_no_matches_hint(window, cx)
2026 })
2027 .column_widths(vec![
2028 DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))),
2029 DefiniteLength::Fraction(0.25),
2030 DefiniteLength::Fraction(0.20),
2031 DefiniteLength::Fraction(0.14),
2032 DefiniteLength::Fraction(0.45),
2033 DefiniteLength::Fraction(0.08),
2034 ])
2035 .resizable_columns(
2036 vec![
2037 TableResizeBehavior::None,
2038 TableResizeBehavior::Resizable,
2039 TableResizeBehavior::Resizable,
2040 TableResizeBehavior::Resizable,
2041 TableResizeBehavior::Resizable,
2042 TableResizeBehavior::Resizable, // this column doesn't matter
2043 ],
2044 &self.current_widths,
2045 cx,
2046 )
2047 .header(vec!["", "Action", "Arguments", "Keystrokes", "Context", "Source"])
2048 .uniform_list(
2049 "keymap-editor-table",
2050 row_count,
2051 cx.processor(move |this, range: Range<usize>, _window, cx| {
2052 let context_menu_deployed = this.context_menu_deployed();
2053 range
2054 .filter_map(|index| {
2055 let candidate_id = this.matches.get(index)?.candidate_id;
2056 let binding = &this.keybindings[candidate_id];
2057 let action_name = binding.action().name;
2058 let conflict = this.get_conflict(index);
2059 let is_overridden = conflict.is_some_and(|conflict| {
2060 !conflict.is_user_keybind_conflict()
2061 });
2062
2063 let icon = this.create_row_button(index, conflict, cx);
2064
2065 let action = div()
2066 .id(("keymap action", index))
2067 .child({
2068 if action_name != gpui::NoAction.name() {
2069 binding
2070 .action()
2071 .humanized_name
2072 .clone()
2073 .into_any_element()
2074 } else {
2075 const NULL: SharedString =
2076 SharedString::new_static("<null>");
2077 muted_styled_text(NULL, cx)
2078 .into_any_element()
2079 }
2080 })
2081 .when(
2082 !context_menu_deployed
2083 && this.show_hover_menus
2084 && !is_overridden,
2085 |this| {
2086 this.tooltip({
2087 let action_name = binding.action().name;
2088 let action_docs =
2089 binding.action().documentation;
2090 move |_, cx| {
2091 let action_tooltip =
2092 Tooltip::new(action_name);
2093 let action_tooltip = match action_docs {
2094 Some(docs) => action_tooltip.meta(docs),
2095 None => action_tooltip,
2096 };
2097 cx.new(|_| action_tooltip).into()
2098 }
2099 })
2100 },
2101 )
2102 .into_any_element();
2103
2104 let keystrokes = binding.key_binding().map_or(
2105 binding
2106 .keystroke_text()
2107 .cloned()
2108 .unwrap_or_default()
2109 .into_any_element(),
2110 |binding| ui::KeyBinding::from_keystrokes(binding.keystrokes.clone(), binding.source).into_any_element()
2111 );
2112
2113 let action_arguments = match binding.action().arguments.clone()
2114 {
2115 Some(arguments) => arguments.into_any_element(),
2116 None => {
2117 if binding.action().has_schema {
2118 muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
2119 .into_any_element()
2120 } else {
2121 gpui::Empty.into_any_element()
2122 }
2123 }
2124 };
2125
2126 let context = binding.context().cloned().map_or(
2127 gpui::Empty.into_any_element(),
2128 |context| {
2129 let is_local = context.local().is_some();
2130
2131 div()
2132 .id(("keymap context", index))
2133 .child(context.clone())
2134 .when(
2135 is_local
2136 && !context_menu_deployed
2137 && !is_overridden
2138 && this.show_hover_menus,
2139 |this| {
2140 this.tooltip(Tooltip::element({
2141 move |_, _| {
2142 context.clone().into_any_element()
2143 }
2144 }))
2145 },
2146 )
2147 .into_any_element()
2148 },
2149 );
2150
2151 let source = binding
2152 .keybind_source()
2153 .map(|source| source.name())
2154 .unwrap_or_default()
2155 .into_any_element();
2156
2157 Some(vec![
2158 icon.into_any_element(),
2159 action,
2160 action_arguments,
2161 keystrokes,
2162 context,
2163 source,
2164 ])
2165 })
2166 .collect()
2167 }),
2168 )
2169 .map_row(cx.processor(
2170 |this, (row_index, row): (usize, Stateful<Div>), _window, cx| {
2171 let conflict = this.get_conflict(row_index);
2172 let is_selected = this.selected_index == Some(row_index);
2173
2174 let row_id = row_group_id(row_index);
2175
2176 div()
2177 .id(("keymap-row-wrapper", row_index))
2178 .child(
2179 row.id(row_id.clone())
2180 .on_any_mouse_down(cx.listener(
2181 move |this,
2182 mouse_down_event: &gpui::MouseDownEvent,
2183 window,
2184 cx| {
2185 if mouse_down_event.button == MouseButton::Right {
2186 this.select_index(
2187 row_index, None, window, cx,
2188 );
2189 this.create_context_menu(
2190 mouse_down_event.position,
2191 window,
2192 cx,
2193 );
2194 }
2195 },
2196 ))
2197 .on_click(cx.listener(
2198 move |this, event: &ClickEvent, window, cx| {
2199 this.select_index(row_index, None, window, cx);
2200 if event.click_count() == 2 {
2201 this.open_edit_keybinding_modal(
2202 false, window, cx,
2203 );
2204 }
2205 },
2206 ))
2207 .group(row_id)
2208 .when(
2209 conflict.is_some_and(|conflict| {
2210 !conflict.is_user_keybind_conflict()
2211 }),
2212 |row| {
2213 const OVERRIDDEN_OPACITY: f32 = 0.5;
2214 row.opacity(OVERRIDDEN_OPACITY)
2215 },
2216 )
2217 .when_some(
2218 conflict.filter(|conflict| {
2219 !this.context_menu_deployed() &&
2220 !conflict.is_user_keybind_conflict()
2221 }),
2222 |row, conflict| {
2223 let overriding_binding = this.keybindings.get(conflict.index);
2224 let context = overriding_binding.and_then(|binding| {
2225 match conflict.override_source {
2226 KeybindSource::User => Some("your keymap"),
2227 KeybindSource::Vim => Some("the vim keymap"),
2228 KeybindSource::Base => Some("your base keymap"),
2229 _ => {
2230 log::error!("Unexpected override from the {} keymap", conflict.override_source.name());
2231 None
2232 }
2233 }.map(|source| format!("This keybinding is overridden by the '{}' binding from {}.", binding.action().humanized_name, source))
2234 }).unwrap_or_else(|| "This binding is overridden.".to_string());
2235
2236 row.tooltip(Tooltip::text(context))},
2237 ),
2238 )
2239 .border_2()
2240 .when(
2241 conflict.is_some_and(|conflict| {
2242 conflict.is_user_keybind_conflict()
2243 }),
2244 |row| row.bg(cx.theme().status().error_background),
2245 )
2246 .when(is_selected, |row| {
2247 row.border_color(cx.theme().colors().panel_focused_border)
2248 })
2249 .into_any_element()
2250 }),
2251 ),
2252 )
2253 .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| {
2254 // This ensures that the menu is not dismissed in cases where scroll events
2255 // with a delta of zero are emitted
2256 if !event.delta.pixel_delta(px(1.)).y.is_zero() {
2257 this.context_menu.take();
2258 cx.notify();
2259 }
2260 }))
2261 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2262 deferred(
2263 anchored()
2264 .position(*position)
2265 .anchor(gpui::Corner::TopLeft)
2266 .child(menu.clone()),
2267 )
2268 .with_priority(1)
2269 }))
2270 }
2271}
2272
2273fn row_group_id(row_index: usize) -> SharedString {
2274 SharedString::new(format!("keymap-table-row-{}", row_index))
2275}
2276
2277fn base_button_style(row_index: usize, icon: IconName) -> IconButton {
2278 IconButton::new(("keymap-icon", row_index), icon)
2279 .shape(IconButtonShape::Square)
2280 .size(ButtonSize::Compact)
2281}
2282
2283#[derive(Debug, Clone, IntoElement)]
2284struct SyntaxHighlightedText {
2285 text: SharedString,
2286 language: Arc<Language>,
2287}
2288
2289impl SyntaxHighlightedText {
2290 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
2291 Self {
2292 text: text.into(),
2293 language,
2294 }
2295 }
2296}
2297
2298impl RenderOnce for SyntaxHighlightedText {
2299 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
2300 let text_style = window.text_style();
2301 let syntax_theme = cx.theme().syntax();
2302
2303 let text = self.text.clone();
2304
2305 let highlights = self
2306 .language
2307 .highlight_text(&text.as_ref().into(), 0..text.len());
2308 let mut runs = Vec::with_capacity(highlights.len());
2309 let mut offset = 0;
2310
2311 for (highlight_range, highlight_id) in highlights {
2312 // Add un-highlighted text before the current highlight
2313 if highlight_range.start > offset {
2314 runs.push(text_style.to_run(highlight_range.start - offset));
2315 }
2316
2317 let mut run_style = text_style.clone();
2318 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
2319 run_style = run_style.highlight(highlight_style);
2320 }
2321 // add the highlighted range
2322 runs.push(run_style.to_run(highlight_range.len()));
2323 offset = highlight_range.end;
2324 }
2325
2326 // Add any remaining un-highlighted text
2327 if offset < text.len() {
2328 runs.push(text_style.to_run(text.len() - offset));
2329 }
2330
2331 StyledText::new(text).with_runs(runs)
2332 }
2333}
2334
2335#[derive(PartialEq)]
2336struct InputError {
2337 severity: Severity,
2338 content: SharedString,
2339}
2340
2341impl InputError {
2342 fn warning(message: impl Into<SharedString>) -> Self {
2343 Self {
2344 severity: Severity::Warning,
2345 content: message.into(),
2346 }
2347 }
2348
2349 fn error(message: anyhow::Error) -> Self {
2350 Self {
2351 severity: Severity::Error,
2352 content: message.to_string().into(),
2353 }
2354 }
2355}
2356
2357struct KeybindingEditorModal {
2358 creating: bool,
2359 editing_keybind: ProcessedBinding,
2360 editing_keybind_idx: usize,
2361 keybind_editor: Entity<KeystrokeInput>,
2362 context_editor: Entity<InputField>,
2363 action_editor: Option<Entity<InputField>>,
2364 action_arguments_editor: Option<Entity<ActionArgumentsEditor>>,
2365 action_name_to_static: HashMap<String, &'static str>,
2366 selected_action_name: Option<&'static str>,
2367 fs: Arc<dyn Fs>,
2368 error: Option<InputError>,
2369 keymap_editor: Entity<KeymapEditor>,
2370 workspace: WeakEntity<Workspace>,
2371 focus_state: KeybindingEditorModalFocusState,
2372}
2373
2374impl ModalView for KeybindingEditorModal {}
2375
2376impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
2377
2378impl Focusable for KeybindingEditorModal {
2379 fn focus_handle(&self, cx: &App) -> FocusHandle {
2380 if let Some(action_editor) = &self.action_editor {
2381 return action_editor.focus_handle(cx);
2382 }
2383 self.keybind_editor.focus_handle(cx)
2384 }
2385}
2386
2387impl KeybindingEditorModal {
2388 pub fn new(
2389 create: bool,
2390 editing_keybind: ProcessedBinding,
2391 editing_keybind_idx: usize,
2392 keymap_editor: Entity<KeymapEditor>,
2393 action_args_temp_dir: Option<&std::path::Path>,
2394 workspace: WeakEntity<Workspace>,
2395 fs: Arc<dyn Fs>,
2396 window: &mut Window,
2397 cx: &mut App,
2398 ) -> Self {
2399 let keybind_editor = cx
2400 .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
2401
2402 let context_editor: Entity<InputField> = cx.new(|cx| {
2403 let input = InputField::new(window, cx, "Keybinding Context")
2404 .label("Edit Context")
2405 .label_size(LabelSize::Default);
2406
2407 if let Some(context) = editing_keybind
2408 .context()
2409 .and_then(KeybindContextString::local)
2410 {
2411 input.set_text(&context, window, cx);
2412 }
2413
2414 let editor_entity = input.editor();
2415 let editor_entity = editor_entity
2416 .as_any()
2417 .downcast_ref::<Entity<Editor>>()
2418 .unwrap()
2419 .clone();
2420 let workspace = workspace.clone();
2421 cx.spawn(async move |_input_handle, cx| {
2422 let contexts = cx
2423 .background_spawn(async { collect_contexts_from_assets() })
2424 .await;
2425
2426 let language = load_keybind_context_language(workspace, cx).await;
2427 editor_entity.update(cx, |editor, cx| {
2428 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
2429 buffer.update(cx, |buffer, cx| {
2430 buffer.set_language(Some(language), cx);
2431 });
2432 }
2433 editor.set_completion_provider(Some(std::rc::Rc::new(
2434 KeyContextCompletionProvider { contexts },
2435 )));
2436 });
2437 })
2438 .detach();
2439
2440 input
2441 });
2442
2443 let has_action_editor = create && editing_keybind.action().name == gpui::NoAction.name();
2444
2445 let (action_editor, action_name_to_static) = if has_action_editor {
2446 let actions: Vec<&'static str> = cx.all_action_names().to_vec();
2447
2448 let humanized_names: HashMap<&'static str, SharedString> = actions
2449 .iter()
2450 .map(|&name| (name, command_palette::humanize_action_name(name).into()))
2451 .collect();
2452
2453 let action_name_to_static: HashMap<String, &'static str> = actions
2454 .iter()
2455 .map(|&name| (name.to_string(), name))
2456 .collect();
2457
2458 let editor = cx.new(|cx| {
2459 let input = InputField::new(window, cx, "Type an action name")
2460 .label("Action")
2461 .label_size(LabelSize::Default);
2462
2463 let editor_entity = input.editor();
2464 let editor_entity = editor_entity
2465 .as_any()
2466 .downcast_ref::<Entity<Editor>>()
2467 .unwrap();
2468 editor_entity.update(cx, |editor, _cx| {
2469 editor.set_completion_provider(Some(std::rc::Rc::new(
2470 ActionCompletionProvider::new(actions, humanized_names),
2471 )));
2472 });
2473
2474 input
2475 });
2476
2477 (Some(editor), action_name_to_static)
2478 } else {
2479 (None, HashMap::default())
2480 };
2481
2482 let action_has_schema = editing_keybind.action().has_schema;
2483 let action_name_for_args = editing_keybind.action().name;
2484 let action_args = editing_keybind
2485 .action()
2486 .arguments
2487 .as_ref()
2488 .map(|args| args.text.clone());
2489
2490 let action_arguments_editor = action_has_schema.then(|| {
2491 cx.new(|cx| {
2492 ActionArgumentsEditor::new(
2493 action_name_for_args,
2494 action_args.clone(),
2495 action_args_temp_dir,
2496 workspace.clone(),
2497 window,
2498 cx,
2499 )
2500 })
2501 });
2502
2503 let focus_state = KeybindingEditorModalFocusState::new(
2504 action_editor.as_ref().map(|e| e.focus_handle(cx)),
2505 keybind_editor.focus_handle(cx),
2506 action_arguments_editor
2507 .as_ref()
2508 .map(|args_editor| args_editor.focus_handle(cx)),
2509 context_editor.focus_handle(cx),
2510 );
2511
2512 Self {
2513 creating: create,
2514 editing_keybind,
2515 editing_keybind_idx,
2516 fs,
2517 keybind_editor,
2518 context_editor,
2519 action_editor,
2520 action_arguments_editor,
2521 action_name_to_static,
2522 selected_action_name: None,
2523 error: None,
2524 keymap_editor,
2525 workspace,
2526 focus_state,
2527 }
2528 }
2529
2530 fn add_action_arguments_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2531 let Some(action_editor) = &self.action_editor else {
2532 return;
2533 };
2534
2535 let action_name_str = action_editor.read(cx).text(cx);
2536 let current_action = self.action_name_to_static.get(&action_name_str).copied();
2537
2538 if current_action == self.selected_action_name {
2539 return;
2540 }
2541
2542 self.selected_action_name = current_action;
2543
2544 let Some(action_name) = current_action else {
2545 if self.action_arguments_editor.is_some() {
2546 self.action_arguments_editor = None;
2547 self.rebuild_focus_state(cx);
2548 cx.notify();
2549 }
2550 return;
2551 };
2552
2553 let (action_has_schema, temp_dir) = {
2554 let keymap_editor = self.keymap_editor.read(cx);
2555 let has_schema = keymap_editor.actions_with_schemas.contains(action_name);
2556 let temp_dir = keymap_editor
2557 .action_args_temp_dir
2558 .as_ref()
2559 .map(|dir| dir.path().to_path_buf());
2560 (has_schema, temp_dir)
2561 };
2562
2563 let currently_has_editor = self.action_arguments_editor.is_some();
2564
2565 if action_has_schema && !currently_has_editor {
2566 let workspace = self.workspace.clone();
2567
2568 let new_editor = cx.new(|cx| {
2569 ActionArgumentsEditor::new(
2570 action_name,
2571 None,
2572 temp_dir.as_deref(),
2573 workspace,
2574 window,
2575 cx,
2576 )
2577 });
2578
2579 self.action_arguments_editor = Some(new_editor);
2580 self.rebuild_focus_state(cx);
2581 cx.notify();
2582 } else if !action_has_schema && currently_has_editor {
2583 self.action_arguments_editor = None;
2584 self.rebuild_focus_state(cx);
2585 cx.notify();
2586 }
2587 }
2588
2589 fn rebuild_focus_state(&mut self, cx: &App) {
2590 self.focus_state = KeybindingEditorModalFocusState::new(
2591 self.action_editor.as_ref().map(|e| e.focus_handle(cx)),
2592 self.keybind_editor.focus_handle(cx),
2593 self.action_arguments_editor
2594 .as_ref()
2595 .map(|args_editor| args_editor.focus_handle(cx)),
2596 self.context_editor.focus_handle(cx),
2597 );
2598 }
2599
2600 fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
2601 if self
2602 .error
2603 .as_ref()
2604 .is_some_and(|old_error| old_error.severity == Severity::Warning && *old_error == error)
2605 {
2606 false
2607 } else {
2608 self.error = Some(error);
2609 cx.notify();
2610 true
2611 }
2612 }
2613
2614 fn get_selected_action_name(&self, cx: &App) -> anyhow::Result<&'static str> {
2615 if let Some(selector) = self.action_editor.as_ref() {
2616 let action_name_str = selector.read(cx).text(cx);
2617
2618 if action_name_str.is_empty() {
2619 anyhow::bail!("Action name is required");
2620 }
2621
2622 self.action_name_to_static
2623 .get(&action_name_str)
2624 .copied()
2625 .ok_or_else(|| anyhow::anyhow!("Action '{}' not found", action_name_str))
2626 } else {
2627 Ok(self.editing_keybind.action().name)
2628 }
2629 }
2630
2631 fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> {
2632 let action_name = self.get_selected_action_name(cx)?;
2633 let action_arguments = self
2634 .action_arguments_editor
2635 .as_ref()
2636 .map(|arguments_editor| arguments_editor.read(cx).editor.read(cx).text(cx))
2637 .filter(|args| !args.is_empty());
2638
2639 let value = action_arguments
2640 .as_ref()
2641 .map(|args| {
2642 serde_json::from_str(args).context("Failed to parse action arguments as JSON")
2643 })
2644 .transpose()?;
2645
2646 cx.build_action(action_name, value)
2647 .context("Failed to validate action arguments")?;
2648 Ok(action_arguments)
2649 }
2650
2651 fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> {
2652 let new_keystrokes = self
2653 .keybind_editor
2654 .read_with(cx, |editor, _| editor.keystrokes().to_vec());
2655 anyhow::ensure!(!new_keystrokes.is_empty(), "Keystrokes cannot be empty");
2656 Ok(new_keystrokes)
2657 }
2658
2659 fn validate_context(&self, cx: &App) -> anyhow::Result<Option<String>> {
2660 let new_context = self
2661 .context_editor
2662 .read_with(cx, |input, cx| input.text(cx));
2663 let Some(context) = new_context.is_empty().not().then_some(new_context) else {
2664 return Ok(None);
2665 };
2666 gpui::KeyBindingContextPredicate::parse(&context).context("Failed to parse key context")?;
2667
2668 Ok(Some(context))
2669 }
2670
2671 fn save_or_display_error(&mut self, cx: &mut Context<Self>) {
2672 self.save(cx).map_err(|err| self.set_error(err, cx)).ok();
2673 }
2674
2675 fn save(&mut self, cx: &mut Context<Self>) -> Result<(), InputError> {
2676 let existing_keybind = self.editing_keybind.clone();
2677 let fs = self.fs.clone();
2678
2679 let mut new_keystrokes = self.validate_keystrokes(cx).map_err(InputError::error)?;
2680 new_keystrokes
2681 .iter_mut()
2682 .for_each(|ks| ks.remove_key_char());
2683
2684 let new_context = self.validate_context(cx).map_err(InputError::error)?;
2685 let new_action_args = self
2686 .validate_action_arguments(cx)
2687 .map_err(InputError::error)?;
2688
2689 let action_mapping = ActionMapping {
2690 keystrokes: Rc::from(new_keystrokes.as_slice()),
2691 context: new_context.map(SharedString::from),
2692 };
2693
2694 let conflicting_indices = self
2695 .keymap_editor
2696 .read(cx)
2697 .keybinding_conflict_state
2698 .conflicting_indices_for_mapping(
2699 &action_mapping,
2700 self.creating.not().then_some(self.editing_keybind_idx),
2701 );
2702
2703 conflicting_indices.map(|KeybindConflict {
2704 first_conflict_index,
2705 remaining_conflict_amount,
2706 }|
2707 {
2708 let conflicting_action_name = self
2709 .keymap_editor
2710 .read(cx)
2711 .keybindings
2712 .get(first_conflict_index)
2713 .map(|keybind| keybind.action().name);
2714
2715 let warning_message = match conflicting_action_name {
2716 Some(name) => {
2717 if remaining_conflict_amount > 0 {
2718 format!(
2719 "Your keybind would conflict with the \"{}\" action and {} other bindings",
2720 name, remaining_conflict_amount
2721 )
2722 } else {
2723 format!("Your keybind would conflict with the \"{}\" action", name)
2724 }
2725 }
2726 None => {
2727 log::info!(
2728 "Could not find action in keybindings with index {}",
2729 first_conflict_index
2730 );
2731 "Your keybind would conflict with other actions".to_string()
2732 }
2733 };
2734
2735 let warning = InputError::warning(warning_message);
2736 if self.error.as_ref().is_some_and(|old_error| *old_error == warning) {
2737 Ok(())
2738 } else {
2739 Err(warning)
2740 }
2741 }).unwrap_or(Ok(()))?;
2742
2743 let create = self.creating;
2744 let keyboard_mapper = cx.keyboard_mapper().clone();
2745
2746 let action_name = self
2747 .get_selected_action_name(cx)
2748 .map_err(InputError::error)?;
2749
2750 let humanized_action_name: SharedString =
2751 command_palette::humanize_action_name(action_name).into();
2752
2753 let action_information = ActionInformation::new(
2754 action_name,
2755 None,
2756 &HashSet::default(),
2757 cx.action_documentation(),
2758 &self.keymap_editor.read(cx).humanized_action_names,
2759 );
2760
2761 let keybind_for_save = if create {
2762 ProcessedBinding::Unmapped(action_information)
2763 } else {
2764 existing_keybind
2765 };
2766
2767 cx.spawn(async move |this, cx| {
2768 match save_keybinding_update(
2769 create,
2770 keybind_for_save,
2771 &action_mapping,
2772 new_action_args.as_deref(),
2773 &fs,
2774 keyboard_mapper.as_ref(),
2775 )
2776 .await
2777 {
2778 Ok(_) => {
2779 this.update(cx, |this, cx| {
2780 this.keymap_editor.update(cx, |keymap, cx| {
2781 keymap.previous_edit = Some(PreviousEdit::Keybinding {
2782 action_mapping,
2783 action_name,
2784 fallback: keymap.table_interaction_state.read(cx).scroll_offset(),
2785 });
2786 let status_toast = StatusToast::new(
2787 format!("Saved edits to the {} action.", humanized_action_name),
2788 cx,
2789 move |this, _cx| {
2790 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
2791 .dismiss_button(true)
2792 // .action("Undo", f) todo: wire the undo functionality
2793 },
2794 );
2795
2796 this.workspace
2797 .update(cx, |workspace, cx| {
2798 workspace.toggle_status_toast(status_toast, cx);
2799 })
2800 .log_err();
2801 });
2802 cx.emit(DismissEvent);
2803 })
2804 .ok();
2805 }
2806 Err(err) => {
2807 this.update(cx, |this, cx| {
2808 this.set_error(InputError::error(err), cx);
2809 })
2810 .log_err();
2811 }
2812 }
2813 })
2814 .detach();
2815
2816 Ok(())
2817 }
2818
2819 fn is_any_editor_showing_completions(&self, window: &Window, cx: &App) -> bool {
2820 let is_editor_showing_completions =
2821 |focus_handle: &FocusHandle, editor_entity: &Entity<Editor>| -> bool {
2822 focus_handle.contains_focused(window, cx)
2823 && editor_entity.read_with(cx, |editor, _cx| {
2824 editor
2825 .context_menu()
2826 .borrow()
2827 .as_ref()
2828 .is_some_and(|menu| menu.visible())
2829 })
2830 };
2831
2832 self.action_editor.as_ref().is_some_and(|action_editor| {
2833 let focus_handle = action_editor.read(cx).focus_handle(cx);
2834 let editor_entity = action_editor.read(cx).editor();
2835 let editor_entity = editor_entity
2836 .as_any()
2837 .downcast_ref::<Entity<Editor>>()
2838 .unwrap();
2839 is_editor_showing_completions(&focus_handle, editor_entity)
2840 }) || {
2841 let focus_handle = self.context_editor.read(cx).focus_handle(cx);
2842 let editor_entity = self.context_editor.read(cx).editor();
2843 let editor_entity = editor_entity
2844 .as_any()
2845 .downcast_ref::<Entity<Editor>>()
2846 .unwrap();
2847 is_editor_showing_completions(&focus_handle, editor_entity)
2848 } || self
2849 .action_arguments_editor
2850 .as_ref()
2851 .is_some_and(|args_editor| {
2852 let focus_handle = args_editor.read(cx).focus_handle(cx);
2853 let editor_entity = &args_editor.read(cx).editor;
2854 is_editor_showing_completions(&focus_handle, editor_entity)
2855 })
2856 }
2857
2858 fn key_context(&self) -> KeyContext {
2859 let mut key_context = KeyContext::new_with_defaults();
2860 key_context.add("KeybindEditorModal");
2861 key_context
2862 }
2863
2864 fn key_context_internal(&self, window: &Window, cx: &App) -> KeyContext {
2865 let mut key_context = self.key_context();
2866
2867 if self.is_any_editor_showing_completions(window, cx) {
2868 key_context.add("showing_completions");
2869 }
2870
2871 key_context
2872 }
2873
2874 fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
2875 if self.is_any_editor_showing_completions(window, cx) {
2876 return;
2877 }
2878 self.focus_state.focus_next(window, cx);
2879 }
2880
2881 fn focus_prev(
2882 &mut self,
2883 _: &menu::SelectPrevious,
2884 window: &mut Window,
2885 cx: &mut Context<Self>,
2886 ) {
2887 if self.is_any_editor_showing_completions(window, cx) {
2888 return;
2889 }
2890 self.focus_state.focus_previous(window, cx);
2891 }
2892
2893 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
2894 self.save_or_display_error(cx);
2895 }
2896
2897 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
2898 cx.emit(DismissEvent);
2899 }
2900
2901 fn get_matching_bindings_count(&self, cx: &Context<Self>) -> usize {
2902 let current_keystrokes = self.keybind_editor.read(cx).keystrokes();
2903
2904 if current_keystrokes.is_empty() {
2905 return 0;
2906 }
2907
2908 self.keymap_editor
2909 .read(cx)
2910 .keybindings
2911 .iter()
2912 .enumerate()
2913 .filter(|(idx, binding)| {
2914 // Don't count the binding we're currently editing
2915 if !self.creating && *idx == self.editing_keybind_idx {
2916 return false;
2917 }
2918
2919 binding.keystrokes().is_some_and(|keystrokes| {
2920 keystrokes_match_exactly(keystrokes, current_keystrokes)
2921 })
2922 })
2923 .count()
2924 }
2925
2926 fn show_matching_bindings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2927 let keystrokes = self.keybind_editor.read(cx).keystrokes().to_vec();
2928
2929 self.keymap_editor.update(cx, |keymap_editor, cx| {
2930 keymap_editor.clear_action_query(window, cx)
2931 });
2932
2933 // Dismiss the modal
2934 cx.emit(DismissEvent);
2935
2936 // Update the keymap editor to show matching keystrokes
2937 self.keymap_editor.update(cx, |editor, cx| {
2938 editor.filter_state = FilterState::All;
2939 editor.search_mode = SearchMode::KeyStroke { exact_match: true };
2940 editor.keystroke_editor.update(cx, |keystroke_editor, cx| {
2941 keystroke_editor.set_keystrokes(keystrokes, cx);
2942 });
2943 });
2944 }
2945}
2946
2947impl Render for KeybindingEditorModal {
2948 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2949 self.add_action_arguments_input(window, cx);
2950
2951 let theme = cx.theme().colors();
2952 let matching_bindings_count = self.get_matching_bindings_count(cx);
2953 let key_context = self.key_context_internal(window, cx);
2954 let showing_completions = key_context.contains("showing_completions");
2955
2956 v_flex()
2957 .w(rems(34.))
2958 .elevation_3(cx)
2959 .key_context(key_context)
2960 .on_action(cx.listener(Self::confirm))
2961 .on_action(cx.listener(Self::cancel))
2962 .when(!showing_completions, |this| {
2963 this.on_action(cx.listener(Self::focus_next))
2964 .on_action(cx.listener(Self::focus_prev))
2965 })
2966 .child(
2967 Modal::new("keybinding_editor_modal", None)
2968 .header(
2969 ModalHeader::new().child(
2970 v_flex()
2971 .w_full()
2972 .pb_1p5()
2973 .mb_1()
2974 .gap_0p5()
2975 .border_b_1()
2976 .border_color(theme.border_variant)
2977 .when(!self.creating, |this| {
2978 this.child(Label::new(
2979 self.editing_keybind.action().humanized_name.clone(),
2980 ))
2981 .when_some(
2982 self.editing_keybind.action().documentation,
2983 |this, docs| {
2984 this.child(
2985 Label::new(docs)
2986 .size(LabelSize::Small)
2987 .color(Color::Muted),
2988 )
2989 },
2990 )
2991 })
2992 .when(self.creating, |this| {
2993 this.child(Label::new("Create Keybinding"))
2994 }),
2995 ),
2996 )
2997 .section(
2998 Section::new().child(
2999 v_flex()
3000 .gap_2p5()
3001 .when_some(
3002 self.creating
3003 .then_some(())
3004 .and_then(|_| self.action_editor.as_ref()),
3005 |this, selector| this.child(selector.clone()),
3006 )
3007 .child(
3008 v_flex()
3009 .gap_1()
3010 .child(Label::new("Edit Keystroke"))
3011 .child(self.keybind_editor.clone())
3012 .child(h_flex().gap_px().when(
3013 matching_bindings_count > 0,
3014 |this| {
3015 let label = format!(
3016 "There {} {} {} with the same keystrokes.",
3017 if matching_bindings_count == 1 {
3018 "is"
3019 } else {
3020 "are"
3021 },
3022 matching_bindings_count,
3023 if matching_bindings_count == 1 {
3024 "binding"
3025 } else {
3026 "bindings"
3027 }
3028 );
3029
3030 this.child(
3031 Label::new(label)
3032 .size(LabelSize::Small)
3033 .color(Color::Muted),
3034 )
3035 .child(
3036 Button::new("show_matching", "View")
3037 .label_size(LabelSize::Small)
3038 .end_icon(
3039 Icon::new(IconName::ArrowUpRight)
3040 .size(IconSize::Small)
3041 .color(Color::Muted),
3042 )
3043 .on_click(cx.listener(
3044 |this, _, window, cx| {
3045 this.show_matching_bindings(
3046 window, cx,
3047 );
3048 },
3049 )),
3050 )
3051 },
3052 )),
3053 )
3054 .when_some(self.action_arguments_editor.clone(), |this, editor| {
3055 this.child(
3056 v_flex()
3057 .gap_1()
3058 .child(Label::new("Edit Arguments"))
3059 .child(editor),
3060 )
3061 })
3062 .child(self.context_editor.clone())
3063 .when_some(self.error.as_ref(), |this, error| {
3064 this.child(
3065 Banner::new()
3066 .severity(error.severity)
3067 .child(Label::new(error.content.clone())),
3068 )
3069 }),
3070 ),
3071 )
3072 .footer(
3073 ModalFooter::new().end_slot(
3074 h_flex()
3075 .gap_1()
3076 .child(
3077 Button::new("cancel", "Cancel")
3078 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
3079 )
3080 .child(Button::new("save-btn", "Save").on_click(cx.listener(
3081 |this, _event, _window, cx| {
3082 this.save_or_display_error(cx);
3083 },
3084 ))),
3085 ),
3086 ),
3087 )
3088 }
3089}
3090
3091struct KeybindingEditorModalFocusState {
3092 handles: Vec<FocusHandle>,
3093}
3094
3095impl KeybindingEditorModalFocusState {
3096 fn new(
3097 action_editor: Option<FocusHandle>,
3098 keystrokes: FocusHandle,
3099 action_arguments: Option<FocusHandle>,
3100 context: FocusHandle,
3101 ) -> Self {
3102 Self {
3103 handles: Vec::from_iter(
3104 [
3105 action_editor,
3106 Some(keystrokes),
3107 action_arguments,
3108 Some(context),
3109 ]
3110 .into_iter()
3111 .flatten(),
3112 ),
3113 }
3114 }
3115
3116 fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> {
3117 self.handles
3118 .iter()
3119 .position(|handle| handle.contains_focused(window, cx))
3120 .map(|i| i as i32)
3121 }
3122
3123 fn focus_index(&self, mut index: i32, window: &mut Window, cx: &mut App) {
3124 if index < 0 {
3125 index = self.handles.len() as i32 - 1;
3126 }
3127 if index >= self.handles.len() as i32 {
3128 index = 0;
3129 }
3130 window.focus(&self.handles[index as usize], cx);
3131 }
3132
3133 fn focus_next(&self, window: &mut Window, cx: &mut App) {
3134 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
3135 index + 1
3136 } else {
3137 0
3138 };
3139 self.focus_index(index_to_focus, window, cx);
3140 }
3141
3142 fn focus_previous(&self, window: &mut Window, cx: &mut App) {
3143 let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
3144 index - 1
3145 } else {
3146 self.handles.len() as i32 - 1
3147 };
3148 self.focus_index(index_to_focus, window, cx);
3149 }
3150}
3151
3152struct ActionArgumentsEditor {
3153 editor: Entity<Editor>,
3154 focus_handle: FocusHandle,
3155 is_loading: bool,
3156 /// See documentation in `KeymapEditor` for why a temp dir is needed.
3157 /// This field exists because the keymap editor temp dir creation may fail,
3158 /// and rather than implement a complicated retry mechanism, we simply
3159 /// fallback to trying to create a temporary directory in this editor on
3160 /// demand. Of note is that the TempDir struct will remove the directory
3161 /// when dropped.
3162 backup_temp_dir: Option<tempfile::TempDir>,
3163}
3164
3165impl Focusable for ActionArgumentsEditor {
3166 fn focus_handle(&self, _cx: &App) -> FocusHandle {
3167 self.focus_handle.clone()
3168 }
3169}
3170
3171impl ActionArgumentsEditor {
3172 fn new(
3173 action_name: &'static str,
3174 arguments: Option<SharedString>,
3175 temp_dir: Option<&std::path::Path>,
3176 workspace: WeakEntity<Workspace>,
3177 window: &mut Window,
3178 cx: &mut Context<Self>,
3179 ) -> Self {
3180 let focus_handle = cx.focus_handle();
3181 cx.on_focus_in(&focus_handle, window, |this, window, cx| {
3182 this.editor.focus_handle(cx).focus(window, cx);
3183 })
3184 .detach();
3185 let editor = cx.new(|cx| {
3186 let mut editor = Editor::auto_height_unbounded(1, window, cx);
3187 Self::set_editor_text(&mut editor, arguments.clone(), window, cx);
3188 editor.set_read_only(true);
3189 editor
3190 });
3191
3192 let temp_dir = temp_dir.map(|path| path.to_owned());
3193 cx.spawn_in(window, async move |this, cx| {
3194 let result = async {
3195 let (project, fs) = workspace.read_with(cx, |workspace, _cx| {
3196 (
3197 workspace.project().downgrade(),
3198 workspace.app_state().fs.clone(),
3199 )
3200 })?;
3201
3202 let file_name = json_schema_store::normalized_action_file_name(action_name);
3203
3204 let (buffer, backup_temp_dir) =
3205 Self::create_temp_buffer(temp_dir, file_name.clone(), project.clone(), fs, cx)
3206 .await
3207 .context(concat!(
3208 "Failed to create temporary buffer for action arguments. ",
3209 "Auto-complete will not work"
3210 ))?;
3211
3212 let editor = cx.new_window_entity(|window, cx| {
3213 let multi_buffer = cx.new(|cx| editor::MultiBuffer::singleton(buffer, cx));
3214 let mut editor = Editor::new(
3215 EditorMode::Full {
3216 scale_ui_elements_with_buffer_font_size: true,
3217 show_active_line_background: false,
3218 sizing_behavior: SizingBehavior::SizeByContent,
3219 },
3220 multi_buffer,
3221 project.upgrade(),
3222 window,
3223 cx,
3224 );
3225 editor.set_searchable(false);
3226 editor.disable_scrollbars_and_minimap(window, cx);
3227 editor.set_show_edit_predictions(Some(false), window, cx);
3228 editor.set_show_gutter(false, cx);
3229 Self::set_editor_text(&mut editor, arguments, window, cx);
3230 editor
3231 })?;
3232
3233 this.update_in(cx, |this, window, cx| {
3234 if this.editor.focus_handle(cx).is_focused(window) {
3235 editor.focus_handle(cx).focus(window, cx);
3236 }
3237 this.editor = editor;
3238 this.backup_temp_dir = backup_temp_dir;
3239 this.is_loading = false;
3240 })?;
3241
3242 anyhow::Ok(())
3243 }
3244 .await;
3245 if result.is_err() {
3246 let json_language = load_json_language(workspace.clone(), cx).await;
3247 this.update(cx, |this, cx| {
3248 this.editor.update(cx, |editor, cx| {
3249 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
3250 buffer.update(cx, |buffer, cx| {
3251 buffer.set_language(Some(json_language.clone()), cx)
3252 });
3253 }
3254 })
3255 // .context("Failed to load JSON language for editing keybinding action arguments input")
3256 })
3257 .ok();
3258 this.update(cx, |this, _cx| {
3259 this.is_loading = false;
3260 })
3261 .ok();
3262 }
3263 result
3264 })
3265 .detach_and_log_err(cx);
3266 Self {
3267 editor,
3268 focus_handle,
3269 is_loading: true,
3270 backup_temp_dir: None,
3271 }
3272 }
3273
3274 fn set_editor_text(
3275 editor: &mut Editor,
3276 arguments: Option<SharedString>,
3277 window: &mut Window,
3278 cx: &mut Context<Editor>,
3279 ) {
3280 if let Some(arguments) = arguments {
3281 editor.set_text(arguments, window, cx);
3282 } else {
3283 // TODO: default value from schema?
3284 editor.set_placeholder_text("Action Arguments", window, cx);
3285 }
3286 }
3287
3288 async fn create_temp_buffer(
3289 temp_dir: Option<std::path::PathBuf>,
3290 file_name: String,
3291 project: WeakEntity<Project>,
3292 fs: Arc<dyn Fs>,
3293 cx: &mut AsyncApp,
3294 ) -> anyhow::Result<(Entity<language::Buffer>, Option<tempfile::TempDir>)> {
3295 let (temp_file_path, temp_dir) = {
3296 let file_name = file_name.clone();
3297 async move {
3298 let temp_dir_backup = match temp_dir.as_ref() {
3299 Some(_) => None,
3300 None => {
3301 let temp_dir = paths::temp_dir();
3302 let sub_temp_dir = tempfile::Builder::new()
3303 .tempdir_in(temp_dir)
3304 .context("Failed to create temporary directory")?;
3305 Some(sub_temp_dir)
3306 }
3307 };
3308 let dir_path = temp_dir.as_deref().unwrap_or_else(|| {
3309 temp_dir_backup
3310 .as_ref()
3311 .expect("created backup tempdir")
3312 .path()
3313 });
3314 let path = dir_path.join(file_name);
3315 fs.create_file(
3316 &path,
3317 fs::CreateOptions {
3318 ignore_if_exists: true,
3319 overwrite: true,
3320 },
3321 )
3322 .await
3323 .context("Failed to create temporary file")?;
3324 anyhow::Ok((path, temp_dir_backup))
3325 }
3326 }
3327 .await
3328 .context("Failed to create backing file")?;
3329
3330 project
3331 .update(cx, |project, cx| {
3332 project.open_local_buffer(temp_file_path, cx)
3333 })?
3334 .await
3335 .context("Failed to create buffer")
3336 .map(|buffer| (buffer, temp_dir))
3337 }
3338}
3339
3340impl Render for ActionArgumentsEditor {
3341 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3342 let settings = theme::ThemeSettings::get_global(cx);
3343 let colors = cx.theme().colors();
3344
3345 let border_color = if self.is_loading {
3346 colors.border_disabled
3347 } else if self.focus_handle.contains_focused(window, cx) {
3348 colors.border_focused
3349 } else {
3350 colors.border_variant
3351 };
3352
3353 let text_style = {
3354 TextStyleRefinement {
3355 font_size: Some(rems(0.875).into()),
3356 font_weight: Some(settings.buffer_font.weight),
3357 line_height: Some(relative(1.2)),
3358 color: self.is_loading.then_some(colors.text_disabled),
3359 ..Default::default()
3360 }
3361 };
3362
3363 self.editor
3364 .update(cx, |editor, _| editor.set_text_style_refinement(text_style));
3365
3366 h_flex()
3367 .min_h_8()
3368 .min_w_48()
3369 .px_2()
3370 .flex_grow()
3371 .rounded_md()
3372 .bg(cx.theme().colors().editor_background)
3373 .border_1()
3374 .border_color(border_color)
3375 .track_focus(&self.focus_handle)
3376 .child(self.editor.clone())
3377 }
3378}
3379
3380struct KeyContextCompletionProvider {
3381 contexts: Vec<SharedString>,
3382}
3383
3384impl CompletionProvider for KeyContextCompletionProvider {
3385 fn completions(
3386 &self,
3387 _excerpt_id: editor::ExcerptId,
3388 buffer: &Entity<language::Buffer>,
3389 buffer_position: language::Anchor,
3390 _trigger: editor::CompletionContext,
3391 _window: &mut Window,
3392 cx: &mut Context<Editor>,
3393 ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
3394 let buffer = buffer.read(cx);
3395 let mut count_back = 0;
3396 for char in buffer.reversed_chars_at(buffer_position) {
3397 if char.is_ascii_alphanumeric() || char == '_' {
3398 count_back += 1;
3399 } else {
3400 break;
3401 }
3402 }
3403 let start_anchor =
3404 buffer.anchor_before(buffer_position.to_offset(buffer).saturating_sub(count_back));
3405 let replace_range = start_anchor..buffer_position;
3406 gpui::Task::ready(Ok(vec![project::CompletionResponse {
3407 completions: self
3408 .contexts
3409 .iter()
3410 .map(|context| project::Completion {
3411 replace_range: replace_range.clone(),
3412 label: language::CodeLabel::plain(context.to_string(), None),
3413 new_text: context.to_string(),
3414 documentation: None,
3415 source: project::CompletionSource::Custom,
3416 icon_path: None,
3417 match_start: None,
3418 snippet_deduplication_key: None,
3419 insert_text_mode: None,
3420 confirm: None,
3421 })
3422 .collect(),
3423 display_options: CompletionDisplayOptions::default(),
3424 is_incomplete: false,
3425 }]))
3426 }
3427
3428 fn is_completion_trigger(
3429 &self,
3430 _buffer: &Entity<language::Buffer>,
3431 _position: language::Anchor,
3432 text: &str,
3433 _trigger_in_words: bool,
3434 _cx: &mut Context<Editor>,
3435 ) -> bool {
3436 text.chars()
3437 .last()
3438 .is_some_and(|last_char| last_char.is_ascii_alphanumeric() || last_char == '_')
3439 }
3440}
3441
3442async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> {
3443 let json_language_task = workspace
3444 .read_with(cx, |workspace, cx| {
3445 workspace
3446 .project()
3447 .read(cx)
3448 .languages()
3449 .language_for_name("JSON")
3450 })
3451 .context("Failed to load JSON language")
3452 .log_err();
3453 let json_language = match json_language_task {
3454 Some(task) => task.await.context("Failed to load JSON language").log_err(),
3455 None => None,
3456 };
3457 json_language.unwrap_or_else(|| {
3458 Arc::new(Language::new(
3459 LanguageConfig {
3460 name: "JSON".into(),
3461 ..Default::default()
3462 },
3463 Some(tree_sitter_json::LANGUAGE.into()),
3464 ))
3465 })
3466}
3467
3468async fn load_keybind_context_language(
3469 workspace: WeakEntity<Workspace>,
3470 cx: &mut AsyncApp,
3471) -> Arc<Language> {
3472 let language_task = workspace
3473 .read_with(cx, |workspace, cx| {
3474 workspace
3475 .project()
3476 .read(cx)
3477 .languages()
3478 .language_for_name("Zed Keybind Context")
3479 })
3480 .context("Failed to load Zed Keybind Context language")
3481 .log_err();
3482 let language = match language_task {
3483 Some(task) => task
3484 .await
3485 .context("Failed to load Zed Keybind Context language")
3486 .log_err(),
3487 None => None,
3488 };
3489 language.unwrap_or_else(|| {
3490 Arc::new(Language::new(
3491 LanguageConfig {
3492 name: "Zed Keybind Context".into(),
3493 ..Default::default()
3494 },
3495 Some(tree_sitter_rust::LANGUAGE.into()),
3496 ))
3497 })
3498}
3499
3500async fn save_keybinding_update(
3501 create: bool,
3502 existing: ProcessedBinding,
3503 action_mapping: &ActionMapping,
3504 new_args: Option<&str>,
3505 fs: &Arc<dyn Fs>,
3506 keyboard_mapper: &dyn PlatformKeyboardMapper,
3507) -> anyhow::Result<()> {
3508 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
3509 .await
3510 .context("Failed to load keymap file")?;
3511
3512 let tab_size = infer_json_indent_size(&keymap_contents);
3513
3514 let existing_keystrokes = existing.keystrokes().unwrap_or_default();
3515 let existing_context = existing.context().and_then(KeybindContextString::local_str);
3516 let existing_args = existing
3517 .action()
3518 .arguments
3519 .as_ref()
3520 .map(|args| args.text.as_ref());
3521
3522 let target = settings::KeybindUpdateTarget {
3523 context: existing_context,
3524 keystrokes: existing_keystrokes,
3525 action_name: existing.action().name,
3526 action_arguments: existing_args,
3527 };
3528
3529 let source = settings::KeybindUpdateTarget {
3530 context: action_mapping.context.as_ref().map(|a| &***a),
3531 keystrokes: &action_mapping.keystrokes,
3532 action_name: existing.action().name,
3533 action_arguments: new_args,
3534 };
3535
3536 let operation = if !create {
3537 settings::KeybindUpdateOperation::Replace {
3538 target,
3539 target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User),
3540 source,
3541 }
3542 } else {
3543 settings::KeybindUpdateOperation::Add {
3544 source,
3545 from: Some(target),
3546 }
3547 };
3548
3549 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
3550
3551 let updated_keymap_contents = settings::KeymapFile::update_keybinding(
3552 operation,
3553 keymap_contents,
3554 tab_size,
3555 keyboard_mapper,
3556 )
3557 .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
3558 fs.write(
3559 paths::keymap_file().as_path(),
3560 updated_keymap_contents.as_bytes(),
3561 )
3562 .await
3563 .context("Failed to write keymap file")?;
3564
3565 telemetry::event!(
3566 "Keybinding Updated",
3567 new_keybinding = new_keybinding,
3568 removed_keybinding = removed_keybinding,
3569 source = source
3570 );
3571 Ok(())
3572}
3573
3574async fn remove_keybinding(
3575 existing: ProcessedBinding,
3576 fs: &Arc<dyn Fs>,
3577 keyboard_mapper: &dyn PlatformKeyboardMapper,
3578) -> anyhow::Result<()> {
3579 let Some(keystrokes) = existing.keystrokes() else {
3580 anyhow::bail!("Cannot remove a keybinding that does not exist");
3581 };
3582 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
3583 .await
3584 .context("Failed to load keymap file")?;
3585 let tab_size = infer_json_indent_size(&keymap_contents);
3586
3587 let operation = settings::KeybindUpdateOperation::Remove {
3588 target: settings::KeybindUpdateTarget {
3589 context: existing.context().and_then(KeybindContextString::local_str),
3590 keystrokes,
3591 action_name: existing.action().name,
3592 action_arguments: existing
3593 .action()
3594 .arguments
3595 .as_ref()
3596 .map(|arguments| arguments.text.as_ref()),
3597 },
3598 target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User),
3599 };
3600
3601 let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
3602 let updated_keymap_contents = settings::KeymapFile::update_keybinding(
3603 operation,
3604 keymap_contents,
3605 tab_size,
3606 keyboard_mapper,
3607 )
3608 .context("Failed to update keybinding")?;
3609 fs.write(
3610 paths::keymap_file().as_path(),
3611 updated_keymap_contents.as_bytes(),
3612 )
3613 .await
3614 .context("Failed to write keymap file")?;
3615
3616 telemetry::event!(
3617 "Keybinding Removed",
3618 new_keybinding = new_keybinding,
3619 removed_keybinding = removed_keybinding,
3620 source = source
3621 );
3622 Ok(())
3623}
3624
3625fn collect_contexts_from_assets() -> Vec<SharedString> {
3626 let mut keymap_assets = vec![
3627 util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
3628 util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
3629 ];
3630 keymap_assets.extend(
3631 BaseKeymap::OPTIONS
3632 .iter()
3633 .filter_map(|(_, base_keymap)| base_keymap.asset_path())
3634 .map(util::asset_str::<SettingsAssets>),
3635 );
3636
3637 let mut contexts = HashSet::default();
3638
3639 for keymap_asset in keymap_assets {
3640 let Ok(keymap) = KeymapFile::parse(&keymap_asset) else {
3641 continue;
3642 };
3643
3644 for section in keymap.sections() {
3645 let context_expr = §ion.context;
3646 let mut queue = Vec::new();
3647 let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else {
3648 continue;
3649 };
3650
3651 queue.push(root_context);
3652 while let Some(context) = queue.pop() {
3653 match context {
3654 Identifier(ident) => {
3655 contexts.insert(ident);
3656 }
3657 Equal(ident_a, ident_b) => {
3658 contexts.insert(ident_a);
3659 contexts.insert(ident_b);
3660 }
3661 NotEqual(ident_a, ident_b) => {
3662 contexts.insert(ident_a);
3663 contexts.insert(ident_b);
3664 }
3665 Descendant(ctx_a, ctx_b) => {
3666 queue.push(*ctx_a);
3667 queue.push(*ctx_b);
3668 }
3669 Not(ctx) => {
3670 queue.push(*ctx);
3671 }
3672 And(ctx_a, ctx_b) => {
3673 queue.push(*ctx_a);
3674 queue.push(*ctx_b);
3675 }
3676 Or(ctx_a, ctx_b) => {
3677 queue.push(*ctx_a);
3678 queue.push(*ctx_b);
3679 }
3680 }
3681 }
3682 }
3683 }
3684
3685 let mut contexts = contexts.into_iter().collect::<Vec<_>>();
3686 contexts.sort();
3687
3688 contexts
3689}
3690
3691fn normalized_ctx_eq(
3692 a: &gpui::KeyBindingContextPredicate,
3693 b: &gpui::KeyBindingContextPredicate,
3694) -> bool {
3695 use gpui::KeyBindingContextPredicate::*;
3696 return match (a, b) {
3697 (Identifier(_), Identifier(_)) => a == b,
3698 (Equal(a_left, a_right), Equal(b_left, b_right)) => {
3699 (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left)
3700 }
3701 (NotEqual(a_left, a_right), NotEqual(b_left, b_right)) => {
3702 (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left)
3703 }
3704 (Descendant(a_parent, a_child), Descendant(b_parent, b_child)) => {
3705 normalized_ctx_eq(a_parent, b_parent) && normalized_ctx_eq(a_child, b_child)
3706 }
3707 (Not(a_expr), Not(b_expr)) => normalized_ctx_eq(a_expr, b_expr),
3708 // Handle double negation: !(!a) == a
3709 (Not(a_expr), b) if matches!(a_expr.as_ref(), Not(_)) => {
3710 let Not(a_inner) = a_expr.as_ref() else {
3711 unreachable!();
3712 };
3713 normalized_ctx_eq(b, a_inner)
3714 }
3715 (a, Not(b_expr)) if matches!(b_expr.as_ref(), Not(_)) => {
3716 let Not(b_inner) = b_expr.as_ref() else {
3717 unreachable!();
3718 };
3719 normalized_ctx_eq(a, b_inner)
3720 }
3721 (And(a_left, a_right), And(b_left, b_right))
3722 if matches!(a_left.as_ref(), And(_, _))
3723 || matches!(a_right.as_ref(), And(_, _))
3724 || matches!(b_left.as_ref(), And(_, _))
3725 || matches!(b_right.as_ref(), And(_, _)) =>
3726 {
3727 let mut a_operands = Vec::new();
3728 flatten_and(a, &mut a_operands);
3729 let mut b_operands = Vec::new();
3730 flatten_and(b, &mut b_operands);
3731 compare_operand_sets(&a_operands, &b_operands)
3732 }
3733 (And(a_left, a_right), And(b_left, b_right)) => {
3734 (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right))
3735 || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left))
3736 }
3737 (Or(a_left, a_right), Or(b_left, b_right))
3738 if matches!(a_left.as_ref(), Or(_, _))
3739 || matches!(a_right.as_ref(), Or(_, _))
3740 || matches!(b_left.as_ref(), Or(_, _))
3741 || matches!(b_right.as_ref(), Or(_, _)) =>
3742 {
3743 let mut a_operands = Vec::new();
3744 flatten_or(a, &mut a_operands);
3745 let mut b_operands = Vec::new();
3746 flatten_or(b, &mut b_operands);
3747 compare_operand_sets(&a_operands, &b_operands)
3748 }
3749 (Or(a_left, a_right), Or(b_left, b_right)) => {
3750 (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right))
3751 || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left))
3752 }
3753 _ => false,
3754 };
3755
3756 fn flatten_and<'a>(
3757 pred: &'a gpui::KeyBindingContextPredicate,
3758 operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>,
3759 ) {
3760 use gpui::KeyBindingContextPredicate::*;
3761 match pred {
3762 And(left, right) => {
3763 flatten_and(left, operands);
3764 flatten_and(right, operands);
3765 }
3766 _ => operands.push(pred),
3767 }
3768 }
3769
3770 fn flatten_or<'a>(
3771 pred: &'a gpui::KeyBindingContextPredicate,
3772 operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>,
3773 ) {
3774 use gpui::KeyBindingContextPredicate::*;
3775 match pred {
3776 Or(left, right) => {
3777 flatten_or(left, operands);
3778 flatten_or(right, operands);
3779 }
3780 _ => operands.push(pred),
3781 }
3782 }
3783
3784 fn compare_operand_sets(
3785 a: &[&gpui::KeyBindingContextPredicate],
3786 b: &[&gpui::KeyBindingContextPredicate],
3787 ) -> bool {
3788 if a.len() != b.len() {
3789 return false;
3790 }
3791
3792 // For each operand in a, find a matching operand in b
3793 let mut b_matched = vec![false; b.len()];
3794 for a_operand in a {
3795 let mut found = false;
3796 for (b_idx, b_operand) in b.iter().enumerate() {
3797 if !b_matched[b_idx] && normalized_ctx_eq(a_operand, b_operand) {
3798 b_matched[b_idx] = true;
3799 found = true;
3800 break;
3801 }
3802 }
3803 if !found {
3804 return false;
3805 }
3806 }
3807
3808 true
3809 }
3810}
3811
3812impl SerializableItem for KeymapEditor {
3813 fn serialized_item_kind() -> &'static str {
3814 "KeymapEditor"
3815 }
3816
3817 fn cleanup(
3818 workspace_id: workspace::WorkspaceId,
3819 alive_items: Vec<workspace::ItemId>,
3820 _window: &mut Window,
3821 cx: &mut App,
3822 ) -> gpui::Task<gpui::Result<()>> {
3823 let db = KeybindingEditorDb::global(cx);
3824 workspace::delete_unloaded_items(alive_items, workspace_id, "keybinding_editors", &db, cx)
3825 }
3826
3827 fn deserialize(
3828 _project: Entity<project::Project>,
3829 workspace: WeakEntity<Workspace>,
3830 workspace_id: workspace::WorkspaceId,
3831 item_id: workspace::ItemId,
3832 window: &mut Window,
3833 cx: &mut App,
3834 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
3835 let db = KeybindingEditorDb::global(cx);
3836 window.spawn(cx, async move |cx| {
3837 if db.get_keybinding_editor(item_id, workspace_id)?.is_some() {
3838 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
3839 } else {
3840 Err(anyhow!("No keybinding editor to deserialize"))
3841 }
3842 })
3843 }
3844
3845 fn serialize(
3846 &mut self,
3847 workspace: &mut Workspace,
3848 item_id: workspace::ItemId,
3849 _closing: bool,
3850 _window: &mut Window,
3851 cx: &mut ui::Context<Self>,
3852 ) -> Option<gpui::Task<gpui::Result<()>>> {
3853 let workspace_id = workspace.database_id()?;
3854 let db = KeybindingEditorDb::global(cx);
3855 Some(cx.background_spawn(
3856 async move { db.save_keybinding_editor(item_id, workspace_id).await },
3857 ))
3858 }
3859
3860 fn should_serialize(&self, _event: &Self::Event) -> bool {
3861 false
3862 }
3863}
3864
3865mod persistence {
3866 use db::{query, sqlez::domain::Domain, sqlez_macros::sql};
3867 use workspace::WorkspaceDb;
3868
3869 pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
3870
3871 impl Domain for KeybindingEditorDb {
3872 const NAME: &str = stringify!(KeybindingEditorDb);
3873
3874 const MIGRATIONS: &[&str] = &[sql!(
3875 CREATE TABLE keybinding_editors (
3876 workspace_id INTEGER,
3877 item_id INTEGER UNIQUE,
3878
3879 PRIMARY KEY(workspace_id, item_id),
3880 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
3881 ON DELETE CASCADE
3882 ) STRICT;
3883 )];
3884 }
3885
3886 db::static_connection!(KeybindingEditorDb, [WorkspaceDb]);
3887
3888 impl KeybindingEditorDb {
3889 query! {
3890 pub async fn save_keybinding_editor(
3891 item_id: workspace::ItemId,
3892 workspace_id: workspace::WorkspaceId
3893 ) -> Result<()> {
3894 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
3895 VALUES (?, ?)
3896 }
3897 }
3898
3899 query! {
3900 pub fn get_keybinding_editor(
3901 item_id: workspace::ItemId,
3902 workspace_id: workspace::WorkspaceId
3903 ) -> Result<Option<workspace::ItemId>> {
3904 SELECT item_id
3905 FROM keybinding_editors
3906 WHERE item_id = ? AND workspace_id = ?
3907 }
3908 }
3909 }
3910}
3911
3912#[cfg(test)]
3913mod tests {
3914 use super::*;
3915
3916 #[test]
3917 fn normalized_ctx_cmp() {
3918 #[track_caller]
3919 fn cmp(a: &str, b: &str) -> bool {
3920 let a = gpui::KeyBindingContextPredicate::parse(a)
3921 .expect("Failed to parse keybinding context a");
3922 let b = gpui::KeyBindingContextPredicate::parse(b)
3923 .expect("Failed to parse keybinding context b");
3924 normalized_ctx_eq(&a, &b)
3925 }
3926
3927 // Basic equality - identical expressions
3928 assert!(cmp("a && b", "a && b"));
3929 assert!(cmp("a || b", "a || b"));
3930 assert!(cmp("a == b", "a == b"));
3931 assert!(cmp("a != b", "a != b"));
3932 assert!(cmp("a > b", "a > b"));
3933 assert!(cmp("!a", "!a"));
3934
3935 // AND operator - associative/commutative
3936 assert!(cmp("a && b", "b && a"));
3937 assert!(cmp("a && b && c", "c && b && a"));
3938 assert!(cmp("a && b && c", "b && a && c"));
3939 assert!(cmp("a && b && c && d", "d && c && b && a"));
3940
3941 // OR operator - associative/commutative
3942 assert!(cmp("a || b", "b || a"));
3943 assert!(cmp("a || b || c", "c || b || a"));
3944 assert!(cmp("a || b || c", "b || a || c"));
3945 assert!(cmp("a || b || c || d", "d || c || b || a"));
3946
3947 // Equality operator - associative/commutative
3948 assert!(cmp("a == b", "b == a"));
3949 assert!(cmp("x == y", "y == x"));
3950
3951 // Inequality operator - associative/commutative
3952 assert!(cmp("a != b", "b != a"));
3953 assert!(cmp("x != y", "y != x"));
3954
3955 // Complex nested expressions with associative operators
3956 assert!(cmp("(a && b) || c", "c || (a && b)"));
3957 assert!(cmp("(a && b) || c", "c || (b && a)"));
3958 assert!(cmp("(a || b) && c", "c && (a || b)"));
3959 assert!(cmp("(a || b) && c", "c && (b || a)"));
3960 assert!(cmp("(a && b) || (c && d)", "(c && d) || (a && b)"));
3961 assert!(cmp("(a && b) || (c && d)", "(d && c) || (b && a)"));
3962
3963 // Multiple levels of nesting
3964 assert!(cmp("((a && b) || c) && d", "d && ((a && b) || c)"));
3965 assert!(cmp("((a && b) || c) && d", "d && (c || (b && a))"));
3966 assert!(cmp("a && (b || (c && d))", "(b || (c && d)) && a"));
3967 assert!(cmp("a && (b || (c && d))", "(b || (d && c)) && a"));
3968
3969 // Negation with associative operators
3970 assert!(cmp("!a && b", "b && !a"));
3971 assert!(cmp("!a || b", "b || !a"));
3972 assert!(cmp("!(a && b) || c", "c || !(a && b)"));
3973 assert!(cmp("!(a && b) || c", "c || !(b && a)"));
3974
3975 // Descendant operator (>) - NOT associative/commutative
3976 assert!(cmp("a > b", "a > b"));
3977 assert!(!cmp("a > b", "b > a"));
3978 assert!(!cmp("a > b > c", "c > b > a"));
3979 assert!(!cmp("a > b > c", "a > c > b"));
3980
3981 // Mixed operators with descendant
3982 assert!(cmp("(a > b) && c", "c && (a > b)"));
3983 assert!(!cmp("(a > b) && c", "c && (b > a)"));
3984 assert!(cmp("(a > b) || (c > d)", "(c > d) || (a > b)"));
3985 assert!(!cmp("(a > b) || (c > d)", "(b > a) || (d > c)"));
3986
3987 // Negative cases - different operators
3988 assert!(!cmp("a && b", "a || b"));
3989 assert!(!cmp("a == b", "a != b"));
3990 assert!(!cmp("a && b", "a > b"));
3991 assert!(!cmp("a || b", "a > b"));
3992 assert!(!cmp("a == b", "a && b"));
3993 assert!(!cmp("a != b", "a || b"));
3994
3995 // Negative cases - different operands
3996 assert!(!cmp("a && b", "a && c"));
3997 assert!(!cmp("a && b", "c && d"));
3998 assert!(!cmp("a || b", "a || c"));
3999 assert!(!cmp("a || b", "c || d"));
4000 assert!(!cmp("a == b", "a == c"));
4001 assert!(!cmp("a != b", "a != c"));
4002 assert!(!cmp("a > b", "a > c"));
4003 assert!(!cmp("a > b", "c > b"));
4004
4005 // Negative cases - with negation
4006 assert!(!cmp("!a", "a"));
4007 assert!(!cmp("!a && b", "a && b"));
4008 assert!(!cmp("!(a && b)", "a && b"));
4009 assert!(!cmp("!a || b", "a || b"));
4010 assert!(!cmp("!(a || b)", "a || b"));
4011
4012 // Negative cases - complex expressions
4013 assert!(!cmp("(a && b) || c", "(a || b) && c"));
4014 assert!(!cmp("a && (b || c)", "a || (b && c)"));
4015 assert!(!cmp("(a && b) || (c && d)", "(a || b) && (c || d)"));
4016 assert!(!cmp("a > b && c", "a && b > c"));
4017
4018 // Edge cases - multiple same operands
4019 assert!(cmp("a && a", "a && a"));
4020 assert!(cmp("a || a", "a || a"));
4021 assert!(cmp("a && a && b", "b && a && a"));
4022 assert!(cmp("a || a || b", "b || a || a"));
4023
4024 // Edge cases - deeply nested
4025 assert!(cmp(
4026 "((a && b) || (c && d)) && ((e || f) && g)",
4027 "((e || f) && g) && ((c && d) || (a && b))"
4028 ));
4029 assert!(cmp(
4030 "((a && b) || (c && d)) && ((e || f) && g)",
4031 "(g && (f || e)) && ((d && c) || (b && a))"
4032 ));
4033
4034 // Edge cases - repeated patterns
4035 assert!(cmp("(a && b) || (a && b)", "(b && a) || (b && a)"));
4036 assert!(cmp("(a || b) && (a || b)", "(b || a) && (b || a)"));
4037
4038 // Negative cases - subtle differences
4039 assert!(!cmp("a && b && c", "a && b"));
4040 assert!(!cmp("a || b || c", "a || b"));
4041 assert!(!cmp("(a && b) || c", "a && (b || c)"));
4042
4043 // a > b > c is not the same as a > c, should not be equal
4044 assert!(!cmp("a > b > c", "a > c"));
4045
4046 // Double negation with complex expressions
4047 assert!(cmp("!(!(a && b))", "a && b"));
4048 assert!(cmp("!(!(a || b))", "a || b"));
4049 assert!(cmp("!(!(a > b))", "a > b"));
4050 assert!(cmp("!(!a) && b", "a && b"));
4051 assert!(cmp("!(!a) || b", "a || b"));
4052 assert!(cmp("!(!(a && b)) || c", "(a && b) || c"));
4053 assert!(cmp("!(!(a && b)) || c", "(b && a) || c"));
4054 assert!(cmp("!(!a)", "a"));
4055 assert!(cmp("a", "!(!a)"));
4056 assert!(cmp("!(!(!a))", "!a"));
4057 assert!(cmp("!(!(!(!a)))", "a"));
4058 }
4059}