keybindings.rs

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