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