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