Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/command_palette/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 81 additions & 3 deletions crates/command_palette/src/command_palette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -143,7 +143,7 @@ impl Focusable 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()
.key_context("CommandPalette")
.w(rems(34.))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -411,7 +422,20 @@ impl PickerDelegate for CommandPaletteDelegate {
.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() {
self.dismissed(window, cx);
return;
Expand Down Expand Up @@ -448,6 +472,7 @@ impl PickerDelegate for CommandPaletteDelegate {
) -> Option<Self::ListItem> {
let matching_command = self.matches.get(ix)?;
let command = self.commands.get(matching_command.candidate_id)?;

Some(
ListItem::new(ix)
.inset(true)
Expand All @@ -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 {
Expand Down
62 changes: 55 additions & 7 deletions crates/keymap_editor/src/keymap_editor.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -41,7 +42,7 @@ use workspace::{
};

pub use ui_components::*;
use zed_actions::OpenKeymap;
use zed_actions::{ChangeKeybinding, OpenKeymap};

use crate::{
persistence::KEYBINDING_EDITORS,
Expand Down Expand Up @@ -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<String>, 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::<KeymapEditor>());

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::<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 {}
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions crates/ui/src/components/keybinding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
9 changes: 8 additions & 1 deletion crates/zed_actions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
[
Expand Down Expand Up @@ -232,7 +239,7 @@ pub mod command_palette {
command_palette,
[
/// Toggles the command palette.
Toggle
Toggle,
]
);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/src/key-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading