Open keymap editor from command palette entry (#40825)

Release Notes:

- Adds footer to the command palette with buttons to add or change the
selected actions keybinding. Both open the keymap editor though the add
button takes you directly to the modal for recording a new keybind.


https://github.com/user-attachments/assets/0ee6b91e-b1dd-4d7f-ad64-cc79689ceeb2

---------

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
David Kleingeld
2025-10-28 15:14:58 +00:00
committed by GitHub
co-authored by Joseph T. Lyons Julia Ryan Danilo Leal
parent c656101862
commit 233a1eb46e
6 changed files with 158 additions and 12 deletions
+1
View File
@@ -20,6 +20,7 @@ command_palette_hooks.workspace = true
db.workspace = true db.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
menu.workspace = true
log.workspace = true log.workspace = true
picker.workspace = true picker.workspace = true
postage.workspace = true postage.workspace = true
+81 -3
View File
@@ -22,7 +22,7 @@ use persistence::COMMAND_PALETTE_HISTORY;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use postage::{sink::Sink, stream::Stream}; use postage::{sink::Sink, stream::Stream};
use settings::Settings; 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 util::ResultExt;
use workspace::{ModalView, Workspace, WorkspaceSettings}; use workspace::{ModalView, Workspace, WorkspaceSettings};
use zed_actions::{OpenZedUrl, command_palette::Toggle}; use zed_actions::{OpenZedUrl, command_palette::Toggle};
@@ -143,7 +143,7 @@ impl Focusable for CommandPalette {
} }
impl Render for CommandPalette { impl Render for CommandPalette {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.key_context("CommandPalette") .key_context("CommandPalette")
.w(rems(34.)) .w(rems(34.))
@@ -261,6 +261,17 @@ impl CommandPaletteDelegate {
HashMap::new() 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 { impl PickerDelegate for CommandPaletteDelegate {
@@ -411,7 +422,20 @@ impl PickerDelegate for CommandPaletteDelegate {
.log_err(); .log_err();
} }
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
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() { if self.matches.is_empty() {
self.dismissed(window, cx); self.dismissed(window, cx);
return; return;
@@ -448,6 +472,7 @@ impl PickerDelegate for CommandPaletteDelegate {
) -> Option<Self::ListItem> { ) -> Option<Self::ListItem> {
let matching_command = self.matches.get(ix)?; let matching_command = self.matches.get(ix)?;
let command = self.commands.get(matching_command.candidate_id)?; let command = self.commands.get(matching_command.candidate_id)?;
Some( Some(
ListItem::new(ix) ListItem::new(ix)
.inset(true) .inset(true)
@@ -470,6 +495,59 @@ impl PickerDelegate for CommandPaletteDelegate {
), ),
) )
} }
fn render_footer(
&self,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
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 { pub fn humanize_action_name(name: &str) -> String {
+55 -7
View File
@@ -1,9 +1,10 @@
use std::{ use std::{
cell::RefCell,
cmp::{self}, cmp::{self},
ops::{Not as _, Range}, ops::{Not as _, Range},
rc::Rc, rc::Rc,
sync::Arc, sync::Arc,
time::Duration, time::{Duration, Instant},
}; };
mod ui_components; mod ui_components;
@@ -41,7 +42,7 @@ use workspace::{
}; };
pub use ui_components::*; pub use ui_components::*;
use zed_actions::OpenKeymap; use zed_actions::{ChangeKeybinding, OpenKeymap};
use crate::{ use crate::{
persistence::KEYBINDING_EDITORS, persistence::KEYBINDING_EDITORS,
@@ -80,37 +81,77 @@ pub fn init(cx: &mut App) {
let keymap_event_channel = KeymapEventChannel::new(); let keymap_event_channel = KeymapEventChannel::new();
cx.set_global(keymap_event_channel); cx.set_global(keymap_event_channel);
cx.on_action(|_: &OpenKeymap, cx| { fn common(filter: Option<String>, cx: &mut App) {
workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| { workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
workspace workspace
.with_local_workspace(window, cx, |workspace, window, cx| { .with_local_workspace(window, cx, move |workspace, window, cx| {
let existing = workspace let existing = workspace
.active_pane() .active_pane()
.read(cx) .read(cx)
.items() .items()
.find_map(|item| item.downcast::<KeymapEditor>()); .find_map(|item| item.downcast::<KeymapEditor>());
if let Some(existing) = existing { let keymap_editor = if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx); workspace.activate_item(&existing, true, true, window, cx);
existing
} else { } else {
let keymap_editor = let keymap_editor =
cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx)); cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
workspace.add_item_to_active_pane( workspace.add_item_to_active_pane(
Box::new(keymap_editor), Box::new(keymap_editor.clone()),
None, None,
true, true,
window, window,
cx, 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(); .detach();
}) })
}); }
cx.on_action(|_: &OpenKeymap, cx| common(None, cx));
cx.on_action(|action: &ChangeKeybinding, cx| common(Some(action.action.clone()), cx));
register_serializable_item::<KeymapEditor>(cx); 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 {} pub struct KeymapEventChannel {}
impl Global for KeymapEventChannel {} impl Global for KeymapEventChannel {}
@@ -1325,6 +1366,13 @@ impl KeymapEditor {
editor.set_keystrokes(keystrokes, cx); 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 { struct HumanizedActionNameCache {
+12
View File
@@ -68,6 +68,18 @@ impl KeyBinding {
pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self { pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self {
Self::new(action, Some(focus.clone()), cx) 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) { pub fn set_vim_mode(cx: &mut App, enabled: bool) {
cx.set_global(VimStyle(enabled)); cx.set_global(VimStyle(enabled));
+8 -1
View File
@@ -27,6 +27,13 @@ pub struct OpenZedUrl {
pub url: String, 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!( actions!(
zed, zed,
[ [
@@ -232,7 +239,7 @@ pub mod command_palette {
command_palette, command_palette,
[ [
/// Toggles the command palette. /// Toggles the command palette.
Toggle Toggle,
] ]
); );
} }
+1 -1
View File
@@ -23,7 +23,7 @@ For more information, see the documentation for [Vim mode](./vim.md) and [Helix
## Keymap Editor ## 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. In there, you can see all of the existing actions in Zed as well as the associated keybindings set to them by default.