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