1use std::{ops::Range, sync::Arc};
2
3use anyhow::{Context as _, anyhow};
4use collections::HashSet;
5use editor::{Editor, EditorEvent};
6use feature_flags::FeatureFlagViewExt;
7use fs::Fs;
8use fuzzy::{StringMatch, StringMatchCandidate};
9use gpui::{
10 AppContext as _, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
11 Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription,
12 WeakEntity, actions, div, transparent_black,
13};
14use language::{Language, LanguageConfig};
15use settings::KeybindSource;
16
17use util::ResultExt;
18
19use ui::{
20 ActiveTheme as _, App, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString,
21 Styled as _, Tooltip, Window, prelude::*, right_click_menu,
22};
23use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
24
25use crate::{
26 SettingsUiFeatureFlag,
27 keybindings::persistence::KEYBINDING_EDITORS,
28 ui_components::table::{Table, TableInteractionState},
29};
30
31actions!(
32 zed,
33 [
34 /// Opens the keymap editor.
35 OpenKeymapEditor
36 ]
37);
38
39const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
40actions!(
41 keymap_editor,
42 [
43 /// Edits the selected key binding.
44 EditBinding,
45 /// Copies the action name to clipboard.
46 CopyAction,
47 /// Copies the context predicate to clipboard.
48 CopyContext
49 ]
50);
51
52pub fn init(cx: &mut App) {
53 let keymap_event_channel = KeymapEventChannel::new();
54 cx.set_global(keymap_event_channel);
55
56 cx.on_action(|_: &OpenKeymapEditor, cx| {
57 workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
58 let existing = workspace
59 .active_pane()
60 .read(cx)
61 .items()
62 .find_map(|item| item.downcast::<KeymapEditor>());
63
64 if let Some(existing) = existing {
65 workspace.activate_item(&existing, true, true, window, cx);
66 } else {
67 let keymap_editor =
68 cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
69 workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
70 }
71 });
72 });
73
74 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
75 let Some(window) = window else { return };
76
77 let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
78
79 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
80 filter.hide_action_types(&keymap_ui_actions);
81 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
82 });
83
84 cx.observe_flag::<SettingsUiFeatureFlag, _>(
85 window,
86 move |is_enabled, _workspace, _, cx| {
87 if is_enabled {
88 command_palette_hooks::CommandPaletteFilter::update_global(
89 cx,
90 |filter, _cx| {
91 filter.show_action_types(keymap_ui_actions.iter());
92 filter.show_namespace(KEYMAP_EDITOR_NAMESPACE);
93 },
94 );
95 } else {
96 command_palette_hooks::CommandPaletteFilter::update_global(
97 cx,
98 |filter, _cx| {
99 filter.hide_action_types(&keymap_ui_actions);
100 filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE);
101 },
102 );
103 }
104 },
105 )
106 .detach();
107 })
108 .detach();
109
110 register_serializable_item::<KeymapEditor>(cx);
111}
112
113pub struct KeymapEventChannel {}
114
115impl Global for KeymapEventChannel {}
116
117impl KeymapEventChannel {
118 fn new() -> Self {
119 Self {}
120 }
121
122 pub fn trigger_keymap_changed(cx: &mut App) {
123 let Some(_event_channel) = cx.try_global::<Self>() else {
124 // don't panic if no global defined. This usually happens in tests
125 return;
126 };
127 cx.update_global(|_event_channel: &mut Self, _| {
128 /* triggers observers in KeymapEditors */
129 });
130 }
131}
132
133struct KeymapEditor {
134 workspace: WeakEntity<Workspace>,
135 focus_handle: FocusHandle,
136 _keymap_subscription: Subscription,
137 keybindings: Vec<ProcessedKeybinding>,
138 // corresponds 1 to 1 with keybindings
139 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
140 matches: Vec<StringMatch>,
141 table_interaction_state: Entity<TableInteractionState>,
142 filter_editor: Entity<Editor>,
143 selected_index: Option<usize>,
144}
145
146impl EventEmitter<()> for KeymapEditor {}
147
148impl Focusable for KeymapEditor {
149 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
150 return self.filter_editor.focus_handle(cx);
151 }
152}
153
154impl KeymapEditor {
155 fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
156 let focus_handle = cx.focus_handle();
157
158 let _keymap_subscription =
159 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
160 let table_interaction_state = TableInteractionState::new(window, cx);
161
162 let filter_editor = cx.new(|cx| {
163 let mut editor = Editor::single_line(window, cx);
164 editor.set_placeholder_text("Filter action names…", cx);
165 editor
166 });
167
168 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
169 if !matches!(e, EditorEvent::BufferEdited) {
170 return;
171 }
172
173 this.update_matches(cx);
174 })
175 .detach();
176
177 let mut this = Self {
178 workspace,
179 keybindings: vec![],
180 string_match_candidates: Arc::new(vec![]),
181 matches: vec![],
182 focus_handle: focus_handle.clone(),
183 _keymap_subscription,
184 table_interaction_state,
185 filter_editor,
186 selected_index: None,
187 };
188
189 this.update_keybindings(cx);
190
191 this
192 }
193
194 fn current_query(&self, cx: &mut Context<Self>) -> String {
195 self.filter_editor.read(cx).text(cx)
196 }
197
198 fn update_matches(&self, cx: &mut Context<Self>) {
199 let query = self.current_query(cx);
200
201 cx.spawn(async move |this, cx| Self::process_query(this, query, cx).await)
202 .detach();
203 }
204
205 async fn process_query(
206 this: WeakEntity<Self>,
207 query: String,
208 cx: &mut AsyncApp,
209 ) -> anyhow::Result<()> {
210 let query = command_palette::normalize_action_query(&query);
211 let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
212 (this.string_match_candidates.clone(), this.keybindings.len())
213 })?;
214 let executor = cx.background_executor().clone();
215 let mut matches = fuzzy::match_strings(
216 &string_match_candidates,
217 &query,
218 true,
219 true,
220 keybind_count,
221 &Default::default(),
222 executor,
223 )
224 .await;
225 this.update(cx, |this, cx| {
226 if query.is_empty() {
227 // apply default sort
228 // sorts by source precedence, and alphabetically by action name within each source
229 matches.sort_by_key(|match_item| {
230 let keybind = &this.keybindings[match_item.candidate_id];
231 let source = keybind.source.as_ref().map(|s| s.0);
232 use KeybindSource::*;
233 let source_precedence = match source {
234 Some(User) => 0,
235 Some(Vim) => 1,
236 Some(Base) => 2,
237 Some(Default) => 3,
238 None => 4,
239 };
240 return (source_precedence, keybind.action.as_ref());
241 });
242 }
243 this.selected_index.take();
244 this.scroll_to_item(0, ScrollStrategy::Top, cx);
245 this.matches = matches;
246 cx.notify();
247 })
248 }
249
250 fn process_bindings(
251 json_language: Arc<Language>,
252 cx: &mut App,
253 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
254 let key_bindings_ptr = cx.key_bindings();
255 let lock = key_bindings_ptr.borrow();
256 let key_bindings = lock.bindings();
257 let mut unmapped_action_names =
258 HashSet::from_iter(cx.all_action_names().into_iter().copied());
259 let action_documentation = cx.action_documentation();
260
261 let mut processed_bindings = Vec::new();
262 let mut string_match_candidates = Vec::new();
263
264 for key_binding in key_bindings {
265 let source = key_binding.meta().map(settings::KeybindSource::from_meta);
266
267 let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
268 let ui_key_binding = Some(
269 ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
270 .vim_mode(source == Some(settings::KeybindSource::Vim)),
271 );
272
273 let context = key_binding
274 .predicate()
275 .map(|predicate| KeybindContextString::Local(predicate.to_string().into()))
276 .unwrap_or(KeybindContextString::Global);
277
278 let source = source.map(|source| (source, source.name().into()));
279
280 let action_name = key_binding.action().name();
281 unmapped_action_names.remove(&action_name);
282 let action_input = key_binding
283 .action_input()
284 .map(|input| SyntaxHighlightedText::new(input, json_language.clone()));
285 let action_docs = action_documentation.get(action_name).copied();
286
287 let index = processed_bindings.len();
288 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
289 processed_bindings.push(ProcessedKeybinding {
290 keystroke_text: keystroke_text.into(),
291 ui_key_binding,
292 action: action_name.into(),
293 action_input,
294 action_docs,
295 context: Some(context),
296 source,
297 });
298 string_match_candidates.push(string_match_candidate);
299 }
300
301 let empty = SharedString::new_static("");
302 for action_name in unmapped_action_names.into_iter() {
303 let index = processed_bindings.len();
304 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
305 processed_bindings.push(ProcessedKeybinding {
306 keystroke_text: empty.clone(),
307 ui_key_binding: None,
308 action: action_name.into(),
309 action_input: None,
310 action_docs: action_documentation.get(action_name).copied(),
311 context: None,
312 source: None,
313 });
314 string_match_candidates.push(string_match_candidate);
315 }
316
317 (processed_bindings, string_match_candidates)
318 }
319
320 fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
321 let workspace = self.workspace.clone();
322 cx.spawn(async move |this, cx| {
323 let json_language = Self::load_json_language(workspace, cx).await;
324
325 let query = this.update(cx, |this, cx| {
326 let (key_bindings, string_match_candidates) =
327 Self::process_bindings(json_language.clone(), cx);
328 this.keybindings = key_bindings;
329 this.string_match_candidates = Arc::new(string_match_candidates);
330 this.matches = this
331 .string_match_candidates
332 .iter()
333 .enumerate()
334 .map(|(ix, candidate)| StringMatch {
335 candidate_id: ix,
336 score: 0.0,
337 positions: vec![],
338 string: candidate.string.clone(),
339 })
340 .collect();
341 this.current_query(cx)
342 })?;
343 // calls cx.notify
344 Self::process_query(this, query, cx).await
345 })
346 .detach_and_log_err(cx);
347 }
348
349 async fn load_json_language(
350 workspace: WeakEntity<Workspace>,
351 cx: &mut AsyncApp,
352 ) -> Arc<Language> {
353 let json_language_task = workspace
354 .read_with(cx, |workspace, cx| {
355 workspace
356 .project()
357 .read(cx)
358 .languages()
359 .language_for_name("JSON")
360 })
361 .context("Failed to load JSON language")
362 .log_err();
363 let json_language = match json_language_task {
364 Some(task) => task.await.context("Failed to load JSON language").log_err(),
365 None => None,
366 };
367 return json_language.unwrap_or_else(|| {
368 Arc::new(Language::new(
369 LanguageConfig {
370 name: "JSON".into(),
371 ..Default::default()
372 },
373 Some(tree_sitter_json::LANGUAGE.into()),
374 ))
375 });
376 }
377
378 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
379 let mut dispatch_context = KeyContext::new_with_defaults();
380 dispatch_context.add("KeymapEditor");
381 dispatch_context.add("menu");
382
383 dispatch_context
384 }
385
386 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
387 let index = usize::min(index, self.matches.len().saturating_sub(1));
388 self.table_interaction_state.update(cx, |this, _cx| {
389 this.scroll_handle.scroll_to_item(index, strategy);
390 });
391 }
392
393 fn focus_search(
394 &mut self,
395 _: &search::FocusSearch,
396 window: &mut Window,
397 cx: &mut Context<Self>,
398 ) {
399 if !self
400 .filter_editor
401 .focus_handle(cx)
402 .contains_focused(window, cx)
403 {
404 window.focus(&self.filter_editor.focus_handle(cx));
405 } else {
406 self.filter_editor.update(cx, |editor, cx| {
407 editor.select_all(&Default::default(), window, cx);
408 });
409 }
410 self.selected_index.take();
411 }
412
413 fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
414 self.selected_index
415 .and_then(|match_index| self.matches.get(match_index))
416 .map(|r#match| r#match.candidate_id)
417 .and_then(|keybind_index| self.keybindings.get(keybind_index))
418 }
419
420 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
421 if let Some(selected) = self.selected_index {
422 let selected = selected + 1;
423 if selected >= self.matches.len() {
424 self.select_last(&Default::default(), window, cx);
425 } else {
426 self.selected_index = Some(selected);
427 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
428 cx.notify();
429 }
430 } else {
431 self.select_first(&Default::default(), window, cx);
432 }
433 }
434
435 fn select_previous(
436 &mut self,
437 _: &menu::SelectPrevious,
438 window: &mut Window,
439 cx: &mut Context<Self>,
440 ) {
441 if let Some(selected) = self.selected_index {
442 if selected == 0 {
443 return;
444 }
445
446 let selected = selected - 1;
447
448 if selected >= self.matches.len() {
449 self.select_last(&Default::default(), window, cx);
450 } else {
451 self.selected_index = Some(selected);
452 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
453 cx.notify();
454 }
455 } else {
456 self.select_last(&Default::default(), window, cx);
457 }
458 }
459
460 fn select_first(
461 &mut self,
462 _: &menu::SelectFirst,
463 _window: &mut Window,
464 cx: &mut Context<Self>,
465 ) {
466 if self.matches.get(0).is_some() {
467 self.selected_index = Some(0);
468 self.scroll_to_item(0, ScrollStrategy::Center, cx);
469 cx.notify();
470 }
471 }
472
473 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
474 if self.matches.last().is_some() {
475 let index = self.matches.len() - 1;
476 self.selected_index = Some(index);
477 self.scroll_to_item(index, ScrollStrategy::Center, cx);
478 cx.notify();
479 }
480 }
481
482 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
483 self.edit_selected_keybinding(window, cx);
484 }
485
486 fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
487 let Some(keybind) = self.selected_binding() else {
488 return;
489 };
490 self.workspace
491 .update(cx, |workspace, cx| {
492 let fs = workspace.app_state().fs.clone();
493 workspace.toggle_modal(window, cx, |window, cx| {
494 let modal = KeybindingEditorModal::new(keybind.clone(), fs, window, cx);
495 window.focus(&modal.focus_handle(cx));
496 modal
497 });
498 })
499 .log_err();
500 }
501
502 fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
503 self.edit_selected_keybinding(window, cx);
504 }
505
506 fn copy_context_to_clipboard(
507 &mut self,
508 _: &CopyContext,
509 _window: &mut Window,
510 cx: &mut Context<Self>,
511 ) {
512 let context = self
513 .selected_binding()
514 .and_then(|binding| binding.context.as_ref())
515 .and_then(KeybindContextString::local_str)
516 .map(|context| context.to_string());
517 let Some(context) = context else {
518 return;
519 };
520 cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
521 }
522
523 fn copy_action_to_clipboard(
524 &mut self,
525 _: &CopyAction,
526 _window: &mut Window,
527 cx: &mut Context<Self>,
528 ) {
529 let action = self
530 .selected_binding()
531 .map(|binding| binding.action.to_string());
532 let Some(action) = action else {
533 return;
534 };
535 cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
536 }
537}
538
539#[derive(Clone)]
540struct ProcessedKeybinding {
541 keystroke_text: SharedString,
542 ui_key_binding: Option<ui::KeyBinding>,
543 action: SharedString,
544 action_input: Option<SyntaxHighlightedText>,
545 action_docs: Option<&'static str>,
546 context: Option<KeybindContextString>,
547 source: Option<(KeybindSource, SharedString)>,
548}
549
550#[derive(Clone, Debug, IntoElement)]
551enum KeybindContextString {
552 Global,
553 Local(SharedString),
554}
555
556impl KeybindContextString {
557 const GLOBAL: SharedString = SharedString::new_static("<global>");
558
559 pub fn local(&self) -> Option<&SharedString> {
560 match self {
561 KeybindContextString::Global => None,
562 KeybindContextString::Local(name) => Some(name),
563 }
564 }
565
566 pub fn local_str(&self) -> Option<&str> {
567 match self {
568 KeybindContextString::Global => None,
569 KeybindContextString::Local(name) => Some(name),
570 }
571 }
572}
573
574impl RenderOnce for KeybindContextString {
575 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
576 match self {
577 KeybindContextString::Global => KeybindContextString::GLOBAL.clone(),
578 KeybindContextString::Local(name) => name,
579 }
580 }
581}
582
583impl Item for KeymapEditor {
584 type Event = ();
585
586 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
587 "Keymap Editor".into()
588 }
589}
590
591impl Render for KeymapEditor {
592 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
593 let row_count = self.matches.len();
594 let theme = cx.theme();
595
596 v_flex()
597 .id("keymap-editor")
598 .track_focus(&self.focus_handle)
599 .key_context(self.dispatch_context(window, cx))
600 .on_action(cx.listener(Self::select_next))
601 .on_action(cx.listener(Self::select_previous))
602 .on_action(cx.listener(Self::select_first))
603 .on_action(cx.listener(Self::select_last))
604 .on_action(cx.listener(Self::focus_search))
605 .on_action(cx.listener(Self::confirm))
606 .on_action(cx.listener(Self::edit_binding))
607 .on_action(cx.listener(Self::copy_action_to_clipboard))
608 .on_action(cx.listener(Self::copy_context_to_clipboard))
609 .size_full()
610 .p_2()
611 .gap_1()
612 .bg(theme.colors().editor_background)
613 .child(
614 h_flex()
615 .key_context({
616 let mut context = KeyContext::new_with_defaults();
617 context.add("BufferSearchBar");
618 context
619 })
620 .h_8()
621 .pl_2()
622 .pr_1()
623 .py_1()
624 .border_1()
625 .border_color(theme.colors().border)
626 .rounded_lg()
627 .child(self.filter_editor.clone()),
628 )
629 .child(
630 Table::new()
631 .interactable(&self.table_interaction_state)
632 .striped()
633 .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
634 .header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
635 .uniform_list(
636 "keymap-editor-table",
637 row_count,
638 cx.processor(move |this, range: Range<usize>, _window, _cx| {
639 range
640 .filter_map(|index| {
641 let candidate_id = this.matches.get(index)?.candidate_id;
642 let binding = &this.keybindings[candidate_id];
643
644 let action = div()
645 .child(binding.action.clone())
646 .id(("keymap action", index))
647 .tooltip({
648 let action_name = binding.action.clone();
649 let action_docs = binding.action_docs;
650 move |_, cx| {
651 let action_tooltip = Tooltip::new(
652 command_palette::humanize_action_name(
653 &action_name,
654 ),
655 );
656 let action_tooltip = match action_docs {
657 Some(docs) => action_tooltip.meta(docs),
658 None => action_tooltip,
659 };
660 cx.new(|_| action_tooltip).into()
661 }
662 })
663 .into_any_element();
664 let keystrokes = binding.ui_key_binding.clone().map_or(
665 binding.keystroke_text.clone().into_any_element(),
666 IntoElement::into_any_element,
667 );
668 let action_input = binding
669 .action_input
670 .clone()
671 .map_or(gpui::Empty.into_any_element(), |input| {
672 input.into_any_element()
673 });
674 let context = binding
675 .context
676 .clone()
677 .map_or(gpui::Empty.into_any_element(), |context| {
678 context.into_any_element()
679 });
680 let source = binding
681 .source
682 .clone()
683 .map(|(_source, name)| name)
684 .unwrap_or_default()
685 .into_any_element();
686 Some([action, action_input, keystrokes, context, source])
687 })
688 .collect()
689 }),
690 )
691 .map_row(
692 cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
693 let is_selected = this.selected_index == Some(row_index);
694 let row = row
695 .id(("keymap-table-row", row_index))
696 .on_click(cx.listener(move |this, _event, _window, _cx| {
697 this.selected_index = Some(row_index);
698 }))
699 .border_2()
700 .border_color(transparent_black())
701 .when(is_selected, |row| {
702 row.border_color(cx.theme().colors().panel_focused_border)
703 });
704
705 right_click_menu(("keymap-table-row-menu", row_index))
706 .trigger({
707 let this = cx.weak_entity();
708 move |is_menu_open: bool, _window, cx| {
709 if is_menu_open {
710 this.update(cx, |this, cx| {
711 if this.selected_index != Some(row_index) {
712 this.selected_index = Some(row_index);
713 cx.notify();
714 }
715 })
716 .ok();
717 }
718 row
719 }
720 })
721 .menu({
722 let this = cx.weak_entity();
723 move |window, cx| build_keybind_context_menu(&this, window, cx)
724 })
725 .into_any_element()
726 }),
727 ),
728 )
729 }
730}
731
732#[derive(Debug, Clone, IntoElement)]
733struct SyntaxHighlightedText {
734 text: SharedString,
735 language: Arc<Language>,
736}
737
738impl SyntaxHighlightedText {
739 pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
740 Self {
741 text: text.into(),
742 language,
743 }
744 }
745}
746
747impl RenderOnce for SyntaxHighlightedText {
748 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
749 let text_style = window.text_style();
750 let syntax_theme = cx.theme().syntax();
751
752 let text = self.text.clone();
753
754 let highlights = self
755 .language
756 .highlight_text(&text.as_ref().into(), 0..text.len());
757 let mut runs = Vec::with_capacity(highlights.len());
758 let mut offset = 0;
759
760 for (highlight_range, highlight_id) in highlights {
761 // Add un-highlighted text before the current highlight
762 if highlight_range.start > offset {
763 runs.push(text_style.to_run(highlight_range.start - offset));
764 }
765
766 let mut run_style = text_style.clone();
767 if let Some(highlight_style) = highlight_id.style(syntax_theme) {
768 run_style = run_style.highlight(highlight_style);
769 }
770 // add the highlighted range
771 runs.push(run_style.to_run(highlight_range.len()));
772 offset = highlight_range.end;
773 }
774
775 // Add any remaining un-highlighted text
776 if offset < text.len() {
777 runs.push(text_style.to_run(text.len() - offset));
778 }
779
780 return StyledText::new(text).with_runs(runs);
781 }
782}
783
784struct KeybindingEditorModal {
785 editing_keybind: ProcessedKeybinding,
786 keybind_editor: Entity<KeystrokeInput>,
787 fs: Arc<dyn Fs>,
788 error: Option<String>,
789}
790
791impl ModalView for KeybindingEditorModal {}
792
793impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
794
795impl Focusable for KeybindingEditorModal {
796 fn focus_handle(&self, cx: &App) -> FocusHandle {
797 self.keybind_editor.focus_handle(cx)
798 }
799}
800
801impl KeybindingEditorModal {
802 pub fn new(
803 editing_keybind: ProcessedKeybinding,
804 fs: Arc<dyn Fs>,
805 _window: &mut Window,
806 cx: &mut App,
807 ) -> Self {
808 let keybind_editor = cx.new(KeystrokeInput::new);
809 Self {
810 editing_keybind,
811 fs,
812 keybind_editor,
813 error: None,
814 }
815 }
816}
817
818impl Render for KeybindingEditorModal {
819 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
820 let theme = cx.theme().colors();
821
822 return v_flex()
823 .w(rems(34.))
824 .elevation_3(cx)
825 .child(
826 v_flex()
827 .p_3()
828 .gap_2()
829 .child(
830 v_flex().child(Label::new("Edit Keystroke")).child(
831 Label::new(
832 "Input the desired keystroke for the selected action and hit save.",
833 )
834 .color(Color::Muted),
835 ),
836 )
837 .child(self.keybind_editor.clone()),
838 )
839 .child(
840 h_flex()
841 .p_2()
842 .w_full()
843 .gap_1()
844 .justify_end()
845 .border_t_1()
846 .border_color(cx.theme().colors().border_variant)
847 .child(
848 Button::new("cancel", "Cancel")
849 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
850 )
851 .child(Button::new("save-btn", "Save").on_click(cx.listener(
852 |this, _event, _window, cx| {
853 let existing_keybind = this.editing_keybind.clone();
854 let fs = this.fs.clone();
855 let new_keystrokes = this
856 .keybind_editor
857 .read_with(cx, |editor, _| editor.keystrokes.clone());
858 if new_keystrokes.is_empty() {
859 this.error = Some("Keystrokes cannot be empty".to_string());
860 cx.notify();
861 return;
862 }
863 let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
864 cx.spawn(async move |this, cx| {
865 if let Err(err) = save_keybinding_update(
866 existing_keybind,
867 &new_keystrokes,
868 &fs,
869 tab_size,
870 )
871 .await
872 {
873 this.update(cx, |this, cx| {
874 this.error = Some(err.to_string());
875 cx.notify();
876 })
877 .log_err();
878 }
879 })
880 .detach();
881 },
882 ))),
883 )
884 .when_some(self.error.clone(), |this, error| {
885 this.child(
886 div()
887 .bg(theme.background)
888 .border_color(theme.border)
889 .border_2()
890 .rounded_md()
891 .child(error),
892 )
893 });
894 }
895}
896
897async fn save_keybinding_update(
898 existing: ProcessedKeybinding,
899 new_keystrokes: &[Keystroke],
900 fs: &Arc<dyn Fs>,
901 tab_size: usize,
902) -> anyhow::Result<()> {
903 let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
904 .await
905 .context("Failed to load keymap file")?;
906
907 let existing_keystrokes = existing
908 .ui_key_binding
909 .as_ref()
910 .map(|keybinding| keybinding.keystrokes.as_slice())
911 .unwrap_or_default();
912
913 let context = existing
914 .context
915 .as_ref()
916 .and_then(KeybindContextString::local_str);
917
918 let input = existing
919 .action_input
920 .as_ref()
921 .map(|input| input.text.as_ref());
922
923 let operation = if existing.ui_key_binding.is_some() {
924 settings::KeybindUpdateOperation::Replace {
925 target: settings::KeybindUpdateTarget {
926 context,
927 keystrokes: existing_keystrokes,
928 action_name: &existing.action,
929 use_key_equivalents: false,
930 input,
931 },
932 target_source: existing
933 .source
934 .map(|(source, _name)| source)
935 .unwrap_or(KeybindSource::User),
936 source: settings::KeybindUpdateTarget {
937 context,
938 keystrokes: new_keystrokes,
939 action_name: &existing.action,
940 use_key_equivalents: false,
941 input,
942 },
943 }
944 } else {
945 anyhow::bail!("Adding new bindings not implemented yet");
946 };
947 let updated_keymap_contents =
948 settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
949 .context("Failed to update keybinding")?;
950 fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
951 .await
952 .context("Failed to write keymap file")?;
953 Ok(())
954}
955
956struct KeystrokeInput {
957 keystrokes: Vec<Keystroke>,
958 focus_handle: FocusHandle,
959}
960
961impl KeystrokeInput {
962 fn new(cx: &mut Context<Self>) -> Self {
963 let focus_handle = cx.focus_handle();
964 Self {
965 keystrokes: Vec::new(),
966 focus_handle,
967 }
968 }
969
970 fn on_modifiers_changed(
971 &mut self,
972 event: &ModifiersChangedEvent,
973 _window: &mut Window,
974 cx: &mut Context<Self>,
975 ) {
976 if let Some(last) = self.keystrokes.last_mut()
977 && last.key.is_empty()
978 {
979 if !event.modifiers.modified() {
980 self.keystrokes.pop();
981 } else {
982 last.modifiers = event.modifiers;
983 }
984 } else {
985 self.keystrokes.push(Keystroke {
986 modifiers: event.modifiers,
987 key: "".to_string(),
988 key_char: None,
989 });
990 }
991 cx.stop_propagation();
992 cx.notify();
993 }
994
995 fn on_key_down(
996 &mut self,
997 event: &gpui::KeyDownEvent,
998 _window: &mut Window,
999 cx: &mut Context<Self>,
1000 ) {
1001 if event.is_held {
1002 return;
1003 }
1004 if let Some(last) = self.keystrokes.last_mut()
1005 && last.key.is_empty()
1006 {
1007 *last = event.keystroke.clone();
1008 } else {
1009 self.keystrokes.push(event.keystroke.clone());
1010 }
1011 cx.stop_propagation();
1012 cx.notify();
1013 }
1014
1015 fn on_key_up(
1016 &mut self,
1017 event: &gpui::KeyUpEvent,
1018 _window: &mut Window,
1019 cx: &mut Context<Self>,
1020 ) {
1021 if let Some(last) = self.keystrokes.last_mut()
1022 && !last.key.is_empty()
1023 && last.modifiers == event.keystroke.modifiers
1024 {
1025 self.keystrokes.push(Keystroke {
1026 modifiers: event.keystroke.modifiers,
1027 key: "".to_string(),
1028 key_char: None,
1029 });
1030 }
1031 cx.stop_propagation();
1032 cx.notify();
1033 }
1034}
1035
1036impl Focusable for KeystrokeInput {
1037 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1038 self.focus_handle.clone()
1039 }
1040}
1041
1042impl Render for KeystrokeInput {
1043 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1044 let colors = cx.theme().colors();
1045
1046 return h_flex()
1047 .id("keybinding_input")
1048 .track_focus(&self.focus_handle)
1049 .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
1050 .on_key_down(cx.listener(Self::on_key_down))
1051 .on_key_up(cx.listener(Self::on_key_up))
1052 .focus(|mut style| {
1053 style.border_color = Some(colors.border_focused);
1054 style
1055 })
1056 .py_2()
1057 .px_3()
1058 .gap_2()
1059 .min_h_8()
1060 .w_full()
1061 .justify_between()
1062 .bg(colors.editor_background)
1063 .border_1()
1064 .rounded_md()
1065 .flex_1()
1066 .overflow_hidden()
1067 .child(
1068 h_flex()
1069 .w_full()
1070 .min_w_0()
1071 .justify_center()
1072 .flex_wrap()
1073 .gap(ui::DynamicSpacing::Base04.rems(cx))
1074 .children(self.keystrokes.iter().map(|keystroke| {
1075 h_flex().children(ui::render_keystroke(
1076 keystroke,
1077 None,
1078 Some(rems(0.875).into()),
1079 ui::PlatformStyle::platform(),
1080 false,
1081 ))
1082 })),
1083 )
1084 .child(
1085 h_flex()
1086 .gap_0p5()
1087 .flex_none()
1088 .child(
1089 IconButton::new("backspace-btn", IconName::Delete)
1090 .tooltip(Tooltip::text("Delete Keystroke"))
1091 .on_click(cx.listener(|this, _event, _window, cx| {
1092 this.keystrokes.pop();
1093 cx.notify();
1094 })),
1095 )
1096 .child(
1097 IconButton::new("clear-btn", IconName::Eraser)
1098 .tooltip(Tooltip::text("Clear Keystrokes"))
1099 .on_click(cx.listener(|this, _event, _window, cx| {
1100 this.keystrokes.clear();
1101 cx.notify();
1102 })),
1103 ),
1104 );
1105 }
1106}
1107
1108fn build_keybind_context_menu(
1109 this: &WeakEntity<KeymapEditor>,
1110 window: &mut Window,
1111 cx: &mut App,
1112) -> Entity<ContextMenu> {
1113 ContextMenu::build(window, cx, |menu, _window, cx| {
1114 let Some(this) = this.upgrade() else {
1115 return menu;
1116 };
1117 let selected_binding = this.read_with(cx, |this, _cx| this.selected_binding().cloned());
1118 let Some(selected_binding) = selected_binding else {
1119 return menu;
1120 };
1121
1122 let selected_binding_has_context = selected_binding
1123 .context
1124 .as_ref()
1125 .and_then(KeybindContextString::local)
1126 .is_some();
1127
1128 menu.action("Edit Binding", Box::new(EditBinding))
1129 .action("Copy action", Box::new(CopyAction))
1130 .action_disabled_when(
1131 !selected_binding_has_context,
1132 "Copy Context",
1133 Box::new(CopyContext),
1134 )
1135 })
1136}
1137
1138impl SerializableItem for KeymapEditor {
1139 fn serialized_item_kind() -> &'static str {
1140 "KeymapEditor"
1141 }
1142
1143 fn cleanup(
1144 workspace_id: workspace::WorkspaceId,
1145 alive_items: Vec<workspace::ItemId>,
1146 _window: &mut Window,
1147 cx: &mut App,
1148 ) -> gpui::Task<gpui::Result<()>> {
1149 workspace::delete_unloaded_items(
1150 alive_items,
1151 workspace_id,
1152 "keybinding_editors",
1153 &KEYBINDING_EDITORS,
1154 cx,
1155 )
1156 }
1157
1158 fn deserialize(
1159 _project: Entity<project::Project>,
1160 workspace: WeakEntity<Workspace>,
1161 workspace_id: workspace::WorkspaceId,
1162 item_id: workspace::ItemId,
1163 window: &mut Window,
1164 cx: &mut App,
1165 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
1166 window.spawn(cx, async move |cx| {
1167 if KEYBINDING_EDITORS
1168 .get_keybinding_editor(item_id, workspace_id)?
1169 .is_some()
1170 {
1171 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
1172 } else {
1173 Err(anyhow!("No keybinding editor to deserialize"))
1174 }
1175 })
1176 }
1177
1178 fn serialize(
1179 &mut self,
1180 workspace: &mut Workspace,
1181 item_id: workspace::ItemId,
1182 _closing: bool,
1183 _window: &mut Window,
1184 cx: &mut ui::Context<Self>,
1185 ) -> Option<gpui::Task<gpui::Result<()>>> {
1186 let workspace_id = workspace.database_id()?;
1187 Some(cx.background_spawn(async move {
1188 KEYBINDING_EDITORS
1189 .save_keybinding_editor(item_id, workspace_id)
1190 .await
1191 }))
1192 }
1193
1194 fn should_serialize(&self, _event: &Self::Event) -> bool {
1195 false
1196 }
1197}
1198
1199mod persistence {
1200 use db::{define_connection, query, sqlez_macros::sql};
1201 use workspace::WorkspaceDb;
1202
1203 define_connection! {
1204 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
1205 &[sql!(
1206 CREATE TABLE keybinding_editors (
1207 workspace_id INTEGER,
1208 item_id INTEGER UNIQUE,
1209
1210 PRIMARY KEY(workspace_id, item_id),
1211 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1212 ON DELETE CASCADE
1213 ) STRICT;
1214 )];
1215 }
1216
1217 impl KeybindingEditorDb {
1218 query! {
1219 pub async fn save_keybinding_editor(
1220 item_id: workspace::ItemId,
1221 workspace_id: workspace::WorkspaceId
1222 ) -> Result<()> {
1223 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
1224 VALUES (?, ?)
1225 }
1226 }
1227
1228 query! {
1229 pub fn get_keybinding_editor(
1230 item_id: workspace::ItemId,
1231 workspace_id: workspace::WorkspaceId
1232 ) -> Result<Option<workspace::ItemId>> {
1233 SELECT item_id
1234 FROM keybinding_editors
1235 WHERE item_id = ? AND workspace_id = ?
1236 }
1237 }
1238 }
1239}