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