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