diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 6e8d2bb5ae7ce079296b061a0c00616191b4382a..f21c202721fa29644e17df499fcfb288a72dc492 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -20,6 +20,7 @@ command_palette_hooks.workspace = true db.workspace = true fuzzy.workspace = true gpui.workspace = true +menu.workspace = true log.workspace = true picker.workspace = true postage.workspace = true diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 4b883d890b3ca5b54459bd0ead3322acfe5b6f41..dc71355d4d5ba29bdb6801a6ea80aaa808045e51 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -22,7 +22,7 @@ use persistence::COMMAND_PALETTE_HISTORY; use picker::{Picker, PickerDelegate}; use postage::{sink::Sink, stream::Stream}; use settings::Settings; -use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, h_flex, prelude::*, v_flex}; +use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::{ModalView, Workspace, WorkspaceSettings}; use zed_actions::{OpenZedUrl, command_palette::Toggle}; @@ -143,7 +143,7 @@ impl Focusable for CommandPalette { } impl Render for CommandPalette { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .key_context("CommandPalette") .w(rems(34.)) @@ -261,6 +261,17 @@ impl CommandPaletteDelegate { HashMap::new() } } + + fn selected_command(&self) -> Option<&Command> { + let action_ix = self + .matches + .get(self.selected_ix) + .map(|m| m.candidate_id) + .unwrap_or(self.selected_ix); + // this gets called in headless tests where there are no commands loaded + // so we need to return an Option here + self.commands.get(action_ix) + } } impl PickerDelegate for CommandPaletteDelegate { @@ -411,7 +422,20 @@ impl PickerDelegate for CommandPaletteDelegate { .log_err(); } - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { + if secondary { + let Some(selected_command) = self.selected_command() else { + return; + }; + let action_name = selected_command.action.name(); + let open_keymap = Box::new(zed_actions::ChangeKeybinding { + action: action_name.to_string(), + }); + window.dispatch_action(open_keymap, cx); + self.dismissed(window, cx); + return; + } + if self.matches.is_empty() { self.dismissed(window, cx); return; @@ -448,6 +472,7 @@ impl PickerDelegate for CommandPaletteDelegate { ) -> Option { let matching_command = self.matches.get(ix)?; let command = self.commands.get(matching_command.candidate_id)?; + Some( ListItem::new(ix) .inset(true) @@ -470,6 +495,59 @@ impl PickerDelegate for CommandPaletteDelegate { ), ) } + + fn render_footer( + &self, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + let selected_command = self.selected_command()?; + let keybind = + KeyBinding::for_action_in(&*selected_command.action, &self.previous_focus_handle, cx); + + let focus_handle = &self.previous_focus_handle; + let keybinding_buttons = if keybind.has_binding(window) { + Button::new("change", "Change Keybinding…") + .key_binding( + KeyBinding::for_action_in(&menu::SecondaryConfirm, focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(move |_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx); + }) + } else { + Button::new("add", "Add Keybinding…") + .key_binding( + KeyBinding::for_action_in(&menu::SecondaryConfirm, focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(move |_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx); + }) + }; + + Some( + h_flex() + .w_full() + .p_1p5() + .gap_1() + .justify_between() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child(keybinding_buttons) + .child( + Button::new("run-action", "Run") + .key_binding( + KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx) + }), + ) + .into_any(), + ) + } } pub fn humanize_action_name(name: &str) -> String { diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index 85bcc33cedfdda9eec30027058756e3a8ad39e5b..e3fb30d46eb57059afc53682c57be392ec8254ed 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -1,9 +1,10 @@ use std::{ + cell::RefCell, cmp::{self}, ops::{Not as _, Range}, rc::Rc, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; mod ui_components; @@ -41,7 +42,7 @@ use workspace::{ }; pub use ui_components::*; -use zed_actions::OpenKeymap; +use zed_actions::{ChangeKeybinding, OpenKeymap}; use crate::{ persistence::KEYBINDING_EDITORS, @@ -80,37 +81,77 @@ pub fn init(cx: &mut App) { let keymap_event_channel = KeymapEventChannel::new(); cx.set_global(keymap_event_channel); - cx.on_action(|_: &OpenKeymap, cx| { + fn common(filter: Option, cx: &mut App) { workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| { workspace - .with_local_workspace(window, cx, |workspace, window, cx| { + .with_local_workspace(window, cx, move |workspace, window, cx| { let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::()); - if let Some(existing) = existing { + 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), + 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) + } + }) } }) .detach(); }) - }); + } + + cx.on_action(|_: &OpenKeymap, cx| common(None, cx)); + cx.on_action(|action: &ChangeKeybinding, cx| common(Some(action.action.clone()), cx)); register_serializable_item::(cx); } +fn open_binding_modal_after_loading(cx: &mut Context) { + 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 {} @@ -1325,6 +1366,13 @@ impl KeymapEditor { editor.set_keystrokes(keystrokes, cx); }); } + + fn has_binding_for(&self, action_name: &str) -> bool { + self.keybindings + .iter() + .filter(|kb| kb.keystrokes().is_some()) + .any(|kb| kb.action().name == action_name) + } } struct HumanizedActionNameCache { diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index bf52d7be8c7e91b230eac295dff03f2679a004af..e22669995db416a3ec6884a79860e76610dd7d03 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -68,6 +68,18 @@ impl KeyBinding { pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self { Self::new(action, Some(focus.clone()), cx) } + pub fn has_binding(&self, window: &Window) -> bool { + match &self.source { + Source::Action { + action, + focus_handle: Some(focus), + } => window + .highest_precedence_binding_for_action_in(action.as_ref(), focus) + .or_else(|| window.highest_precedence_binding_for_action(action.as_ref())) + .is_some(), + _ => false, + } + } pub fn set_vim_mode(cx: &mut App, enabled: bool) { cx.set_global(VimStyle(enabled)); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 3506e492b77d1eca6a1dde84bf5ea0a2be107540..23e3465edd8e780d694b551f95004b556380c296 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -27,6 +27,13 @@ pub struct OpenZedUrl { pub url: String, } +/// Opens the keymap to either add a keybinding or change an existing one +#[derive(PartialEq, Clone, Default, Action, JsonSchema, Serialize, Deserialize)] +#[action(namespace = zed, no_json, no_register)] +pub struct ChangeKeybinding { + pub action: String, +} + actions!( zed, [ @@ -232,7 +239,7 @@ pub mod command_palette { command_palette, [ /// Toggles the command palette. - Toggle + Toggle, ] ); } diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 3c1ec57297fa08102bae1a8bc90b8f139b9eec09..6cb7808ae3e0917e086599c31c5f211c87844a11 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -23,7 +23,7 @@ For more information, see the documentation for [Vim mode](./vim.md) and [Helix ## Keymap Editor -You can access the keymap editor through the {#kb zed::OpenKeymap} action or by running {#action zed::OpenKeymap} action from the command palette +You can access the keymap editor through the {#kb zed::OpenKeymap} action or by running {#action zed::OpenKeymap} action from the command palette. You can easily add or change a keybind for an action with the `Change Keybinding` or `Add Keybinding` button on the command pallets left bottom corner. In there, you can see all of the existing actions in Zed as well as the associated keybindings set to them by default.