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