3960 lines · 150.0 KB · Rust
RawDownload
use std::{    cell::RefCell,    cmp::{self},    ops::{Not as _, Range},    rc::Rc,    sync::Arc,    time::{Duration, Instant},}; mod action_completion_provider;mod ui_components; use anyhow::{Context as _, anyhow};use collections::{HashMap, HashSet};use editor::{CompletionProvider, Editor, EditorEvent, EditorMode, SizingBehavior};use fs::Fs;use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,    FocusHandle, Focusable, Global, IsZero,    KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},    KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy,    ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,    actions, anchored, deferred, div,};use language::{Language, LanguageConfig, ToOffset as _};use notifications::status_toast::{StatusToast, ToastIcon};use project::{CompletionDisplayOptions, Project};use settings::{    BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size,};use ui::{    ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,    Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section,    SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState,    TableResizeBehavior, Tooltip, Window, prelude::*,};use ui_input::InputField;use util::ResultExt;use workspace::{    Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,    register_serializable_item,}; pub use ui_components::*;use zed_actions::{ChangeKeybinding, OpenKeymap}; use crate::{    action_completion_provider::ActionCompletionProvider,    persistence::KEYBINDING_EDITORS,    ui_components::keystroke_input::{        ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording,    },}; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");const COLS: usize = 6; actions!(    keymap_editor,    [        /// Edits the selected key binding.        EditBinding,        /// Creates a new key binding for the selected action.        CreateBinding,        /// Creates a new key binding from scratch, prompting for the action.        OpenCreateKeybindingModal,        /// Deletes the selected key binding.        DeleteBinding,        /// Copies the action name to clipboard.        CopyAction,        /// Copies the context predicate to clipboard.        CopyContext,        /// Toggles Conflict Filtering        ToggleConflictFilter,        /// Toggle Keystroke search        ToggleKeystrokeSearch,        /// Toggles exact matching for keystroke search        ToggleExactKeystrokeMatching,        /// Shows matching keystrokes for the currently selected binding        ShowMatchingKeybinds    ]); pub fn init(cx: &mut App) {    let keymap_event_channel = KeymapEventChannel::new();    cx.set_global(keymap_event_channel);     fn open_keymap_editor(        filter: Option<String>,        workspace: &mut Workspace,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        let existing = workspace            .active_pane()            .read(cx)            .items()            .find_map(|item| item.downcast::<KeymapEditor>());         let keymap_editor = if let Some(existing) = existing {            workspace.activate_item(&existing, true, true, window, cx);            existing        } else {            let keymap_editor = cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));            workspace.add_item_to_active_pane(                Box::new(keymap_editor.clone()),                None,                true,                window,                cx,            );            keymap_editor        };         if let Some(filter) = filter {            keymap_editor.update(cx, |editor, cx| {                editor.filter_editor.update(cx, |editor, cx| {                    editor.clear(window, cx);                    editor.insert(&filter, window, cx);                });                if !editor.has_binding_for(&filter) {                    open_binding_modal_after_loading(cx)                }            })        }    }     cx.observe_new(|workspace: &mut Workspace, _window, _cx| {        workspace            .register_action(|workspace, _: &OpenKeymap, window, cx| {                open_keymap_editor(None, workspace, window, cx);            })            .register_action(|workspace, action: &ChangeKeybinding, window, cx| {                open_keymap_editor(Some(action.action.clone()), workspace, window, cx);            });    })    .detach();     register_serializable_item::<KeymapEditor>(cx);} fn open_binding_modal_after_loading(cx: &mut Context<KeymapEditor>) {    let started_at = Instant::now();    let observer = Rc::new(RefCell::new(None));    let handle = {        let observer = Rc::clone(&observer);        cx.observe(&cx.entity(), move |editor, _, cx| {            let subscription = observer.borrow_mut().take();             if started_at.elapsed().as_secs() > 10 {                return;            }            if !editor.matches.is_empty() {                editor.selected_index = Some(0);                cx.dispatch_action(&CreateBinding);                return;            }             *observer.borrow_mut() = subscription;        })    };    *observer.borrow_mut() = Some(handle);} pub struct KeymapEventChannel {} impl Global for KeymapEventChannel {} impl KeymapEventChannel {    fn new() -> Self {        Self {}    }     pub fn trigger_keymap_changed(cx: &mut App) {        let Some(_event_channel) = cx.try_global::<Self>() else {            // don't panic if no global defined. This usually happens in tests            return;        };        cx.update_global(|_event_channel: &mut Self, _| {            /* triggers observers in KeymapEditors */        });    }} #[derive(Default, PartialEq)]enum SearchMode {    #[default]    Normal,    KeyStroke {        exact_match: bool,    },} impl SearchMode {    fn invert(&self) -> Self {        match self {            SearchMode::Normal => SearchMode::KeyStroke { exact_match: true },            SearchMode::KeyStroke { .. } => SearchMode::Normal,        }    }     fn exact_match(&self) -> bool {        match self {            SearchMode::Normal => false,            SearchMode::KeyStroke { exact_match } => *exact_match,        }    }} #[derive(Default, PartialEq, Copy, Clone)]enum FilterState {    #[default]    All,    Conflicts,} impl FilterState {    fn invert(&self) -> Self {        match self {            FilterState::All => FilterState::Conflicts,            FilterState::Conflicts => FilterState::All,        }    }} #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]struct ActionMapping {    keystrokes: Rc<[KeybindingKeystroke]>,    context: Option<SharedString>,} #[derive(Debug)]struct KeybindConflict {    first_conflict_index: usize,    remaining_conflict_amount: usize,} #[derive(Clone, Copy, PartialEq)]struct ConflictOrigin {    override_source: KeybindSource,    overridden_source: Option<KeybindSource>,    index: usize,} impl ConflictOrigin {    fn new(source: KeybindSource, index: usize) -> Self {        Self {            override_source: source,            index,            overridden_source: None,        }    }     fn with_overridden_source(self, source: KeybindSource) -> Self {        Self {            overridden_source: Some(source),            ..self        }    }     fn get_conflict_with(&self, other: &Self) -> Option<Self> {        if self.override_source == KeybindSource::User            && other.override_source == KeybindSource::User        {            Some(                Self::new(KeybindSource::User, other.index)                    .with_overridden_source(self.override_source),            )        } else if self.override_source > other.override_source {            Some(other.with_overridden_source(self.override_source))        } else {            None        }    }     fn is_user_keybind_conflict(&self) -> bool {        self.override_source == KeybindSource::User            && self.overridden_source == Some(KeybindSource::User)    }} #[derive(Default)]struct ConflictState {    conflicts: Vec<Option<ConflictOrigin>>,    keybind_mapping: ConflictKeybindMapping,    has_user_conflicts: bool,} type ConflictKeybindMapping = HashMap<    Rc<[KeybindingKeystroke]>,    Vec<(        Option<gpui::KeyBindingContextPredicate>,        Vec<ConflictOrigin>,    )>,>; impl ConflictState {    fn new(key_bindings: &[ProcessedBinding]) -> Self {        let mut action_keybind_mapping = ConflictKeybindMapping::default();         let mut largest_index = 0;        for (index, binding) in key_bindings            .iter()            .enumerate()            .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information()))        {            let mapping = binding.get_action_mapping();            let predicate = mapping                .context                .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());            let entry = action_keybind_mapping                .entry(mapping.keystrokes.clone())                .or_default();            let origin = ConflictOrigin::new(binding.source, index);            if let Some((_, origins)) =                entry                    .iter_mut()                    .find(|(other_predicate, _)| match (&predicate, other_predicate) {                        (None, None) => true,                        (Some(a), Some(b)) => normalized_ctx_eq(a, b),                        _ => false,                    })            {                origins.push(origin);            } else {                entry.push((predicate, vec![origin]));            }            largest_index = index;        }         let mut conflicts = vec![None; largest_index + 1];        let mut has_user_conflicts = false;         for entries in action_keybind_mapping.values_mut() {            for (_, indices) in entries.iter_mut() {                indices.sort_unstable_by_key(|origin| origin.override_source);                let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else {                    continue;                };                 for origin in indices.iter() {                    conflicts[origin.index] =                        origin.get_conflict_with(if origin == fst { snd } else { fst })                }                 has_user_conflicts |= fst.override_source == KeybindSource::User                    && snd.override_source == KeybindSource::User;            }        }         Self {            conflicts,            keybind_mapping: action_keybind_mapping,            has_user_conflicts,        }    }     fn conflicting_indices_for_mapping(        &self,        action_mapping: &ActionMapping,        keybind_idx: Option<usize>,    ) -> Option<KeybindConflict> {        let ActionMapping {            keystrokes,            context,        } = action_mapping;        let predicate = context            .as_deref()            .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());        self.keybind_mapping.get(keystrokes).and_then(|entries| {            entries                .iter()                .find_map(|(other_predicate, indices)| {                    match (&predicate, other_predicate) {                        (None, None) => true,                        (Some(pred), Some(other)) => normalized_ctx_eq(pred, other),                        _ => false,                    }                    .then_some(indices)                })                .and_then(|indices| {                    let mut indices = indices                        .iter()                        .filter(|&conflict| Some(conflict.index) != keybind_idx);                    indices.next().map(|origin| KeybindConflict {                        first_conflict_index: origin.index,                        remaining_conflict_amount: indices.count(),                    })                })        })    }     fn conflict_for_idx(&self, idx: usize) -> Option<ConflictOrigin> {        self.conflicts.get(idx).copied().flatten()    }     fn has_user_conflict(&self, candidate_idx: usize) -> bool {        self.conflict_for_idx(candidate_idx)            .is_some_and(|conflict| conflict.is_user_keybind_conflict())    }     fn any_user_binding_conflicts(&self) -> bool {        self.has_user_conflicts    }} struct KeymapEditor {    workspace: WeakEntity<Workspace>,    focus_handle: FocusHandle,    _keymap_subscription: Subscription,    keybindings: Vec<ProcessedBinding>,    keybinding_conflict_state: ConflictState,    filter_state: FilterState,    search_mode: SearchMode,    search_query_debounce: Option<Task<()>>,    // corresponds 1 to 1 with keybindings    string_match_candidates: Arc<Vec<StringMatchCandidate>>,    matches: Vec<StringMatch>,    table_interaction_state: Entity<TableInteractionState>,    filter_editor: Entity<Editor>,    keystroke_editor: Entity<KeystrokeInput>,    selected_index: Option<usize>,    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,    previous_edit: Option<PreviousEdit>,    humanized_action_names: HumanizedActionNameCache,    current_widths: Entity<TableColumnWidths>,    show_hover_menus: bool,    actions_with_schemas: HashSet<&'static str>,    /// In order for the JSON LSP to run in the actions arguments editor, we    /// require a backing file In order to avoid issues (primarily log spam)    /// with drop order between the buffer, file, worktree, etc, we create a    /// temporary directory for these backing files in the keymap editor struct    /// instead of here. This has the added benefit of only having to create a    /// worktree and directory once, although the perf improvement is negligible.    action_args_temp_dir_worktree: Option<Entity<project::Worktree>>,    action_args_temp_dir: Option<tempfile::TempDir>,} enum PreviousEdit {    /// When deleting, we want to maintain the same scroll position    ScrollBarOffset(Point<Pixels>),    /// When editing or creating, because the new keybinding could be in a different position in the sort order    /// we store metadata about the new binding (either the modified version or newly created one)    /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches    /// this metadata, we set the selected index to it and scroll to it,    /// and if we don't find it, we scroll to 0 and don't set a selected index    Keybinding {        action_mapping: ActionMapping,        action_name: &'static str,        /// The scrollbar position to fallback to if we don't find the keybinding during a refresh        /// this can happen if there's a filter applied to the search and the keybinding modification        /// filters the binding from the search results        fallback: Point<Pixels>,    },} impl EventEmitter<()> for KeymapEditor {} impl Focusable for KeymapEditor {    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {        if self.selected_index.is_some() {            self.focus_handle.clone()        } else {            self.filter_editor.focus_handle(cx)        }    }}/// Helper function to check if two keystroke sequences match exactlyfn keystrokes_match_exactly(    keystrokes1: &[KeybindingKeystroke],    keystrokes2: &[KeybindingKeystroke],) -> bool {    keystrokes1.len() == keystrokes2.len()        && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {            k1.inner().key == k2.inner().key && k1.inner().modifiers == k2.inner().modifiers        })} impl KeymapEditor {    fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {        let _keymap_subscription =            cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed);        let table_interaction_state = cx.new(|cx| {            TableInteractionState::new(cx)                .with_custom_scrollbar(ui::Scrollbars::for_settings::<editor::EditorSettings>())        });         let keystroke_editor = cx.new(|cx| {            let mut keystroke_editor = KeystrokeInput::new(None, window, cx);            keystroke_editor.set_search(true);            keystroke_editor        });         let filter_editor = cx.new(|cx| {            let mut editor = Editor::single_line(window, cx);            editor.set_placeholder_text("Filter action names…", window, cx);            editor        }); 

Lines 1–500 of 3960.