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