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    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!(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_from_gpui(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        v_flex()
 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            .pt_4()
 590            .px_4()
 591            .gap_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                    .h_8()
 600                    .pl_2()
 601                    .pr_1()
 602                    .py_1()
 603                    .border_1()
 604                    .border_color(theme.colors().border)
 605                    .rounded_lg()
 606                    .child(self.filter_editor.clone()),
 607            )
 608            .child(
 609                Table::new()
 610                    .interactable(&self.table_interaction_state)
 611                    .striped()
 612                    .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
 613                    .header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
 614                    .uniform_list(
 615                        "keymap-editor-table",
 616                        row_count,
 617                        cx.processor(move |this, range: Range<usize>, _window, _cx| {
 618                            range
 619                                .filter_map(|index| {
 620                                    let candidate_id = this.matches.get(index)?.candidate_id;
 621                                    let binding = &this.keybindings[candidate_id];
 622
 623                                    let action = binding.action.clone().into_any_element();
 624                                    let keystrokes = binding.ui_key_binding.clone().map_or(
 625                                        binding.keystroke_text.clone().into_any_element(),
 626                                        IntoElement::into_any_element,
 627                                    );
 628                                    let action_input = binding
 629                                        .action_input
 630                                        .clone()
 631                                        .map_or(gpui::Empty.into_any_element(), |input| {
 632                                            input.into_any_element()
 633                                        });
 634                                    let context = binding
 635                                        .context
 636                                        .clone()
 637                                        .map_or(gpui::Empty.into_any_element(), |context| {
 638                                            context.into_any_element()
 639                                        });
 640                                    let source = binding
 641                                        .source
 642                                        .clone()
 643                                        .map(|(_source, name)| name)
 644                                        .unwrap_or_default()
 645                                        .into_any_element();
 646                                    Some([action, action_input, keystrokes, context, source])
 647                                })
 648                                .collect()
 649                        }),
 650                    )
 651                    .map_row(
 652                        cx.processor(|this, (row_index, row): (usize, Div), _window, cx| {
 653                            let is_selected = this.selected_index == Some(row_index);
 654                            let row = row
 655                                .id(("keymap-table-row", row_index))
 656                                .on_click(cx.listener(move |this, _event, _window, _cx| {
 657                                    this.selected_index = Some(row_index);
 658                                }))
 659                                .border_2()
 660                                .border_color(transparent_black())
 661                                .when(is_selected, |row| {
 662                                    row.border_color(cx.theme().colors().panel_focused_border)
 663                                });
 664
 665                            right_click_menu(("keymap-table-row-menu", row_index))
 666                                .trigger({
 667                                    let this = cx.weak_entity();
 668                                    move |is_menu_open: bool, _window, cx| {
 669                                        if is_menu_open {
 670                                            this.update(cx, |this, cx| {
 671                                                if this.selected_index != Some(row_index) {
 672                                                    this.selected_index = Some(row_index);
 673                                                    cx.notify();
 674                                                }
 675                                            })
 676                                            .ok();
 677                                        }
 678                                        row
 679                                    }
 680                                })
 681                                .menu({
 682                                    let this = cx.weak_entity();
 683                                    move |window, cx| build_keybind_context_menu(&this, window, cx)
 684                                })
 685                                .into_any_element()
 686                        }),
 687                    ),
 688            )
 689    }
 690}
 691
 692#[derive(Debug, Clone, IntoElement)]
 693struct SyntaxHighlightedText {
 694    text: SharedString,
 695    language: Arc<Language>,
 696}
 697
 698impl SyntaxHighlightedText {
 699    pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
 700        Self {
 701            text: text.into(),
 702            language,
 703        }
 704    }
 705}
 706
 707impl RenderOnce for SyntaxHighlightedText {
 708    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 709        let text_style = window.text_style();
 710        let syntax_theme = cx.theme().syntax();
 711
 712        let text = self.text.clone();
 713
 714        let highlights = self
 715            .language
 716            .highlight_text(&text.as_ref().into(), 0..text.len());
 717        let mut runs = Vec::with_capacity(highlights.len());
 718        let mut offset = 0;
 719
 720        for (highlight_range, highlight_id) in highlights {
 721            // Add un-highlighted text before the current highlight
 722            if highlight_range.start > offset {
 723                runs.push(text_style.to_run(highlight_range.start - offset));
 724            }
 725
 726            let mut run_style = text_style.clone();
 727            if let Some(highlight_style) = highlight_id.style(syntax_theme) {
 728                run_style = run_style.highlight(highlight_style);
 729            }
 730            // add the highlighted range
 731            runs.push(run_style.to_run(highlight_range.len()));
 732            offset = highlight_range.end;
 733        }
 734
 735        // Add any remaining un-highlighted text
 736        if offset < text.len() {
 737            runs.push(text_style.to_run(text.len() - offset));
 738        }
 739
 740        return StyledText::new(text).with_runs(runs);
 741    }
 742}
 743
 744struct KeybindingEditorModal {
 745    editing_keybind: ProcessedKeybinding,
 746    keybind_editor: Entity<KeystrokeInput>,
 747    fs: Arc<dyn Fs>,
 748    error: Option<String>,
 749}
 750
 751impl ModalView for KeybindingEditorModal {}
 752
 753impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
 754
 755impl Focusable for KeybindingEditorModal {
 756    fn focus_handle(&self, cx: &App) -> FocusHandle {
 757        self.keybind_editor.focus_handle(cx)
 758    }
 759}
 760
 761impl KeybindingEditorModal {
 762    pub fn new(
 763        editing_keybind: ProcessedKeybinding,
 764        fs: Arc<dyn Fs>,
 765        _window: &mut Window,
 766        cx: &mut App,
 767    ) -> Self {
 768        let keybind_editor = cx.new(KeystrokeInput::new);
 769        Self {
 770            editing_keybind,
 771            fs,
 772            keybind_editor,
 773            error: None,
 774        }
 775    }
 776}
 777
 778impl Render for KeybindingEditorModal {
 779    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 780        let theme = cx.theme().colors();
 781
 782        return v_flex()
 783            .w(rems(36.))
 784            .elevation_3(cx)
 785            .child(
 786                v_flex()
 787                    .pt_2()
 788                    .px_4()
 789                    .pb_4()
 790                    .gap_2()
 791                    .child(Label::new("Input desired keystroke, then hit save"))
 792                    .child(self.keybind_editor.clone()),
 793            )
 794            .child(
 795                h_flex()
 796                    .p_2()
 797                    .w_full()
 798                    .gap_1()
 799                    .justify_end()
 800                    .border_t_1()
 801                    .border_color(cx.theme().colors().border_variant)
 802                    .child(
 803                        Button::new("cancel", "Cancel")
 804                            .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
 805                    )
 806                    .child(
 807                        Button::new("save-btn", "Save Keybinding").on_click(cx.listener(
 808                            |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
 865    let existing_keystrokes = existing
 866        .ui_key_binding
 867        .as_ref()
 868        .map(|keybinding| keybinding.keystrokes.as_slice())
 869        .unwrap_or_default();
 870
 871    let context = existing
 872        .context
 873        .as_ref()
 874        .and_then(KeybindContextString::local_str);
 875
 876    let input = existing
 877        .action_input
 878        .as_ref()
 879        .map(|input| input.text.as_ref());
 880
 881    let operation = if existing.ui_key_binding.is_some() {
 882        settings::KeybindUpdateOperation::Replace {
 883            target: settings::KeybindUpdateTarget {
 884                context,
 885                keystrokes: existing_keystrokes,
 886                action_name: &existing.action,
 887                use_key_equivalents: false,
 888                input,
 889            },
 890            target_source: existing
 891                .source
 892                .map(|(source, _name)| source)
 893                .unwrap_or(KeybindSource::User),
 894            source: settings::KeybindUpdateTarget {
 895                context,
 896                keystrokes: new_keystrokes,
 897                action_name: &existing.action,
 898                use_key_equivalents: false,
 899                input,
 900            },
 901        }
 902    } else {
 903        anyhow::bail!("Adding new bindings not implemented yet");
 904    };
 905    let updated_keymap_contents =
 906        settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
 907            .context("Failed to update keybinding")?;
 908    fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
 909        .await
 910        .context("Failed to write keymap file")?;
 911    Ok(())
 912}
 913
 914struct KeystrokeInput {
 915    keystrokes: Vec<Keystroke>,
 916    focus_handle: FocusHandle,
 917}
 918
 919impl KeystrokeInput {
 920    fn new(cx: &mut Context<Self>) -> Self {
 921        let focus_handle = cx.focus_handle();
 922        Self {
 923            keystrokes: Vec::new(),
 924            focus_handle,
 925        }
 926    }
 927
 928    fn on_modifiers_changed(
 929        &mut self,
 930        event: &ModifiersChangedEvent,
 931        _window: &mut Window,
 932        cx: &mut Context<Self>,
 933    ) {
 934        if let Some(last) = self.keystrokes.last_mut()
 935            && last.key.is_empty()
 936        {
 937            if !event.modifiers.modified() {
 938                self.keystrokes.pop();
 939            } else {
 940                last.modifiers = event.modifiers;
 941            }
 942        } else {
 943            self.keystrokes.push(Keystroke {
 944                modifiers: event.modifiers,
 945                key: "".to_string(),
 946                key_char: None,
 947            });
 948        }
 949        cx.stop_propagation();
 950        cx.notify();
 951    }
 952
 953    fn on_key_down(
 954        &mut self,
 955        event: &gpui::KeyDownEvent,
 956        _window: &mut Window,
 957        cx: &mut Context<Self>,
 958    ) {
 959        if event.is_held {
 960            return;
 961        }
 962        if let Some(last) = self.keystrokes.last_mut()
 963            && last.key.is_empty()
 964        {
 965            *last = event.keystroke.clone();
 966        } else {
 967            self.keystrokes.push(event.keystroke.clone());
 968        }
 969        cx.stop_propagation();
 970        cx.notify();
 971    }
 972
 973    fn on_key_up(
 974        &mut self,
 975        event: &gpui::KeyUpEvent,
 976        _window: &mut Window,
 977        cx: &mut Context<Self>,
 978    ) {
 979        if let Some(last) = self.keystrokes.last_mut()
 980            && !last.key.is_empty()
 981            && last.modifiers == event.keystroke.modifiers
 982        {
 983            self.keystrokes.push(Keystroke {
 984                modifiers: event.keystroke.modifiers,
 985                key: "".to_string(),
 986                key_char: None,
 987            });
 988        }
 989        cx.stop_propagation();
 990        cx.notify();
 991    }
 992}
 993
 994impl Focusable for KeystrokeInput {
 995    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 996        self.focus_handle.clone()
 997    }
 998}
 999
1000impl Render for KeystrokeInput {
1001    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1002        let colors = cx.theme().colors();
1003
1004        return h_flex()
1005            .id("keybinding_input")
1006            .track_focus(&self.focus_handle)
1007            .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
1008            .on_key_down(cx.listener(Self::on_key_down))
1009            .on_key_up(cx.listener(Self::on_key_up))
1010            .focus(|mut style| {
1011                style.border_color = Some(colors.border_focused);
1012                style
1013            })
1014            .py_2()
1015            .px_3()
1016            .gap_2()
1017            .min_h_8()
1018            .w_full()
1019            .justify_between()
1020            .bg(colors.editor_background)
1021            .border_1()
1022            .rounded_md()
1023            .flex_1()
1024            .overflow_hidden()
1025            .child(
1026                h_flex()
1027                    .w_full()
1028                    .min_w_0()
1029                    .justify_center()
1030                    .flex_wrap()
1031                    .gap(ui::DynamicSpacing::Base04.rems(cx))
1032                    .children(self.keystrokes.iter().map(|keystroke| {
1033                        h_flex().children(ui::render_keystroke(
1034                            keystroke,
1035                            None,
1036                            Some(rems(0.875).into()),
1037                            ui::PlatformStyle::platform(),
1038                            false,
1039                        ))
1040                    })),
1041            )
1042            .child(
1043                h_flex()
1044                    .gap_0p5()
1045                    .flex_none()
1046                    .child(
1047                        IconButton::new("backspace-btn", IconName::Delete)
1048                            .tooltip(Tooltip::text("Delete Keystroke"))
1049                            .on_click(cx.listener(|this, _event, _window, cx| {
1050                                this.keystrokes.pop();
1051                                cx.notify();
1052                            })),
1053                    )
1054                    .child(
1055                        IconButton::new("clear-btn", IconName::Eraser)
1056                            .tooltip(Tooltip::text("Clear Keystrokes"))
1057                            .on_click(cx.listener(|this, _event, _window, cx| {
1058                                this.keystrokes.clear();
1059                                cx.notify();
1060                            })),
1061                    ),
1062            );
1063    }
1064}
1065
1066fn build_keybind_context_menu(
1067    this: &WeakEntity<KeymapEditor>,
1068    window: &mut Window,
1069    cx: &mut App,
1070) -> Entity<ContextMenu> {
1071    ContextMenu::build(window, cx, |menu, _window, cx| {
1072        let Some(this) = this.upgrade() else {
1073            return menu;
1074        };
1075        let selected_binding = this.read_with(cx, |this, _cx| this.selected_binding().cloned());
1076        let Some(selected_binding) = selected_binding else {
1077            return menu;
1078        };
1079
1080        let selected_binding_has_context = selected_binding
1081            .context
1082            .as_ref()
1083            .and_then(KeybindContextString::local)
1084            .is_some();
1085
1086        menu.action("Edit Binding", Box::new(EditBinding))
1087            .action("Copy action", Box::new(CopyAction))
1088            .action_disabled_when(
1089                !selected_binding_has_context,
1090                "Copy Context",
1091                Box::new(CopyContext),
1092            )
1093    })
1094}
1095
1096impl SerializableItem for KeymapEditor {
1097    fn serialized_item_kind() -> &'static str {
1098        "KeymapEditor"
1099    }
1100
1101    fn cleanup(
1102        workspace_id: workspace::WorkspaceId,
1103        alive_items: Vec<workspace::ItemId>,
1104        _window: &mut Window,
1105        cx: &mut App,
1106    ) -> gpui::Task<gpui::Result<()>> {
1107        workspace::delete_unloaded_items(
1108            alive_items,
1109            workspace_id,
1110            "keybinding_editors",
1111            &KEYBINDING_EDITORS,
1112            cx,
1113        )
1114    }
1115
1116    fn deserialize(
1117        _project: Entity<project::Project>,
1118        workspace: WeakEntity<Workspace>,
1119        workspace_id: workspace::WorkspaceId,
1120        item_id: workspace::ItemId,
1121        window: &mut Window,
1122        cx: &mut App,
1123    ) -> gpui::Task<gpui::Result<Entity<Self>>> {
1124        window.spawn(cx, async move |cx| {
1125            if KEYBINDING_EDITORS
1126                .get_keybinding_editor(item_id, workspace_id)?
1127                .is_some()
1128            {
1129                cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
1130            } else {
1131                Err(anyhow!("No keybinding editor to deserialize"))
1132            }
1133        })
1134    }
1135
1136    fn serialize(
1137        &mut self,
1138        workspace: &mut Workspace,
1139        item_id: workspace::ItemId,
1140        _closing: bool,
1141        _window: &mut Window,
1142        cx: &mut ui::Context<Self>,
1143    ) -> Option<gpui::Task<gpui::Result<()>>> {
1144        let workspace_id = workspace.database_id()?;
1145        Some(cx.background_spawn(async move {
1146            KEYBINDING_EDITORS
1147                .save_keybinding_editor(item_id, workspace_id)
1148                .await
1149        }))
1150    }
1151
1152    fn should_serialize(&self, _event: &Self::Event) -> bool {
1153        false
1154    }
1155}
1156
1157mod persistence {
1158    use db::{define_connection, query, sqlez_macros::sql};
1159    use workspace::WorkspaceDb;
1160
1161    define_connection! {
1162        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
1163            &[sql!(
1164                CREATE TABLE keybinding_editors (
1165                    workspace_id INTEGER,
1166                    item_id INTEGER UNIQUE,
1167
1168                    PRIMARY KEY(workspace_id, item_id),
1169                    FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1170                    ON DELETE CASCADE
1171                ) STRICT;
1172            )];
1173    }
1174
1175    impl KeybindingEditorDb {
1176        query! {
1177            pub async fn save_keybinding_editor(
1178                item_id: workspace::ItemId,
1179                workspace_id: workspace::WorkspaceId
1180            ) -> Result<()> {
1181                INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
1182                VALUES (?, ?)
1183            }
1184        }
1185
1186        query! {
1187            pub fn get_keybinding_editor(
1188                item_id: workspace::ItemId,
1189                workspace_id: workspace::WorkspaceId
1190            ) -> Result<Option<workspace::ItemId>> {
1191                SELECT item_id
1192                FROM keybinding_editors
1193                WHERE item_id = ? AND workspace_id = ?
1194            }
1195        }
1196    }
1197}