Add KeyBindings to CommandPalette

This commit is contained in:
Conrad Irwin 2023-11-13 15:33:22 -07:00
parent b918c27cf9
commit 25bc898807
7 changed files with 116 additions and 273 deletions

View file

@ -11,7 +11,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{v_stack, HighlightedLabel, StyledExt}; use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
use util::{ use util::{
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
ResultExt, ResultExt,
@ -318,66 +318,16 @@ impl PickerDelegate for CommandPaletteDelegate {
.rounded_md() .rounded_md()
.when(selected, |this| this.bg(colors.ghost_element_selected)) .when(selected, |this| this.bg(colors.ghost_element_selected))
.hover(|this| this.bg(colors.ghost_element_hover)) .hover(|this| this.bg(colors.ghost_element_hover))
.child(HighlightedLabel::new( .child(
command.name.clone(), h_stack()
r#match.positions.clone(), .justify_between()
)) .child(HighlightedLabel::new(
command.name.clone(),
r#match.positions.clone(),
))
.children(KeyBinding::for_action(&*command.action, cx)),
)
} }
// fn render_match(
// &self,
// ix: usize,
// mouse_state: &mut MouseState,
// selected: bool,
// cx: &gpui::AppContext,
// ) -> AnyElement<Picker<Self>> {
// let mat = &self.matches[ix];
// let command = &self.actions[mat.candidate_id];
// let theme = theme::current(cx);
// let style = theme.picker.item.in_state(selected).style_for(mouse_state);
// let key_style = &theme.command_palette.key.in_state(selected);
// let keystroke_spacing = theme.command_palette.keystroke_spacing;
// Flex::row()
// .with_child(
// Label::new(mat.string.clone(), style.label.clone())
// .with_highlights(mat.positions.clone()),
// )
// .with_children(command.keystrokes.iter().map(|keystroke| {
// Flex::row()
// .with_children(
// [
// (keystroke.ctrl, "^"),
// (keystroke.alt, "⌥"),
// (keystroke.cmd, "⌘"),
// (keystroke.shift, "⇧"),
// ]
// .into_iter()
// .filter_map(|(modifier, label)| {
// if modifier {
// Some(
// Label::new(label, key_style.label.clone())
// .contained()
// .with_style(key_style.container),
// )
// } else {
// None
// }
// }),
// )
// .with_child(
// Label::new(keystroke.key.clone(), key_style.label.clone())
// .contained()
// .with_style(key_style.container),
// )
// .contained()
// .with_margin_left(keystroke_spacing)
// .flex_float()
// }))
// .contained()
// .with_style(style.container)
// .into_any()
// }
} }
fn humanize_action_name(name: &str) -> String { fn humanize_action_name(name: &str) -> String {

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle,
FocusId, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent,
Style, StyleRefinement, ViewContext, WindowContext, Pixels, Style, StyleRefinement, ViewContext, WindowContext,
}; };
use collections::HashMap; use collections::HashMap;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -145,6 +145,15 @@ impl DispatchTree {
actions actions
} }
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
self.keymap
.lock()
.bindings_for_action(action.type_id())
.filter(|candidate| candidate.action.partial_eq(action))
.cloned()
.collect()
}
pub fn dispatch_key( pub fn dispatch_key(
&mut self, &mut self,
keystroke: &Keystroke, keystroke: &Keystroke,

View file

@ -3,9 +3,19 @@ use anyhow::Result;
use smallvec::SmallVec; use smallvec::SmallVec;
pub struct KeyBinding { pub struct KeyBinding {
action: Box<dyn Action>, pub(crate) action: Box<dyn Action>,
pub(super) keystrokes: SmallVec<[Keystroke; 2]>, pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
pub(super) context_predicate: Option<KeyBindingContextPredicate>, pub(crate) context_predicate: Option<KeyBindingContextPredicate>,
}
impl Clone for KeyBinding {
fn clone(&self) -> Self {
KeyBinding {
action: self.action.boxed_clone(),
keystrokes: self.keystrokes.clone(),
context_predicate: self.context_predicate.clone(),
}
}
} }
impl KeyBinding { impl KeyBinding {

View file

@ -3,13 +3,13 @@ use crate::{
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
InputEvent, IsZero, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, Modifiers, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext,
MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path,
PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::HashMap; use collections::HashMap;
@ -1377,6 +1377,13 @@ impl<'a> WindowContext<'a> {
Vec::new() Vec::new()
} }
} }
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
self.window
.current_frame
.dispatch_tree
.bindings_for_action(action)
}
} }
impl Context for WindowContext<'_> { impl Context for WindowContext<'_> {

View file

@ -1,50 +1,42 @@
use std::collections::HashSet; use gpui::Action;
use strum::EnumIter;
use strum::{EnumIter, IntoEnumIterator};
use crate::prelude::*; use crate::prelude::*;
#[derive(Component)] #[derive(Component)]
pub struct Keybinding { pub struct KeyBinding {
/// A keybinding consists of a key and a set of modifier keys. /// A keybinding consists of a key and a set of modifier keys.
/// More then one keybinding produces a chord. /// More then one keybinding produces a chord.
/// ///
/// This should always contain at least one element. /// This should always contain at least one element.
keybinding: Vec<(String, ModifierKeys)>, key_binding: gpui::KeyBinding,
} }
impl Keybinding { impl KeyBinding {
pub fn new(key: String, modifiers: ModifierKeys) -> Self { pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
Self { // todo! this last is arbitrary, we want to prefer users key bindings over defaults,
keybinding: vec![(key, modifiers)], // and vim over normal (in vim mode), etc.
} let key_binding = cx.bindings_for_action(action).last().cloned()?;
Some(Self::new(key_binding))
} }
pub fn new_chord( pub fn new(key_binding: gpui::KeyBinding) -> Self {
first_note: (String, ModifierKeys), Self { key_binding }
second_note: (String, ModifierKeys),
) -> Self {
Self {
keybinding: vec![first_note, second_note],
}
} }
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> { fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div() div()
.flex() .flex()
.gap_2() .gap_2()
.children(self.keybinding.iter().map(|(key, modifiers)| { .children(self.key_binding.keystrokes().iter().map(|keystroke| {
div() div()
.flex() .flex()
.gap_1() .gap_1()
.children(ModifierKey::iter().filter_map(|modifier| { .when(keystroke.modifiers.control, |el| el.child(Key::new("^")))
if modifiers.0.contains(&modifier) { .when(keystroke.modifiers.alt, |el| el.child(Key::new("")))
Some(Key::new(modifier.glyph().to_string())) .when(keystroke.modifiers.command, |el| el.child(Key::new("")))
} else { .when(keystroke.modifiers.shift, |el| el.child(Key::new("")))
None .child(Key::new(keystroke.key.clone()))
}
}))
.child(Key::new(key.clone()))
})) }))
} }
} }
@ -81,76 +73,6 @@ pub enum ModifierKey {
Shift, Shift,
} }
impl ModifierKey {
/// Returns the glyph for the [`ModifierKey`].
pub fn glyph(&self) -> char {
match self {
Self::Control => '^',
Self::Alt => '⌥',
Self::Command => '⌘',
Self::Shift => '⇧',
}
}
}
#[derive(Clone)]
pub struct ModifierKeys(HashSet<ModifierKey>);
impl ModifierKeys {
pub fn new() -> Self {
Self(HashSet::new())
}
pub fn all() -> Self {
Self(HashSet::from_iter(ModifierKey::iter()))
}
pub fn add(mut self, modifier: ModifierKey) -> Self {
self.0.insert(modifier);
self
}
pub fn control(mut self, control: bool) -> Self {
if control {
self.0.insert(ModifierKey::Control);
} else {
self.0.remove(&ModifierKey::Control);
}
self
}
pub fn alt(mut self, alt: bool) -> Self {
if alt {
self.0.insert(ModifierKey::Alt);
} else {
self.0.remove(&ModifierKey::Alt);
}
self
}
pub fn command(mut self, command: bool) -> Self {
if command {
self.0.insert(ModifierKey::Command);
} else {
self.0.remove(&ModifierKey::Command);
}
self
}
pub fn shift(mut self, shift: bool) -> Self {
if shift {
self.0.insert(ModifierKey::Shift);
} else {
self.0.remove(&ModifierKey::Shift);
}
self
}
}
#[cfg(feature = "stories")] #[cfg(feature = "stories")]
pub use stories::*; pub use stories::*;
@ -158,29 +80,38 @@ pub use stories::*;
mod stories { mod stories {
use super::*; use super::*;
use crate::Story; use crate::Story;
use gpui::{Div, Render}; use gpui::{action, Div, Render};
use itertools::Itertools; use itertools::Itertools;
pub struct KeybindingStory; pub struct KeybindingStory;
#[action]
struct NoAction {}
pub fn binding(key: &str) -> gpui::KeyBinding {
gpui::KeyBinding::new(key, NoAction {}, None)
}
impl Render for KeybindingStory { impl Render for KeybindingStory {
type Element = Div<Self>; type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let all_modifier_permutations = ModifierKey::iter().permutations(2); let all_modifier_permutations =
["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, Keybinding>(cx)) .child(Story::title_for::<_, KeyBinding>(cx))
.child(Story::label(cx, "Single Key")) .child(Story::label(cx, "Single Key"))
.child(Keybinding::new("Z".to_string(), ModifierKeys::new())) .child(KeyBinding::new(binding("Z")))
.child(Story::label(cx, "Single Key with Modifier")) .child(Story::label(cx, "Single Key with Modifier"))
.child( .child(
div() div()
.flex() .flex()
.gap_3() .gap_3()
.children(ModifierKey::iter().map(|modifier| { .child(KeyBinding::new(binding("ctrl-c")))
Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier)) .child(KeyBinding::new(binding("alt-c")))
})), .child(KeyBinding::new(binding("cmd-c")))
.child(KeyBinding::new(binding("shift-c"))),
) )
.child(Story::label(cx, "Single Key with Modifier (Permuted)")) .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
.child( .child(
@ -194,29 +125,17 @@ mod stories {
.gap_4() .gap_4()
.py_3() .py_3()
.children(chunk.map(|permutation| { .children(chunk.map(|permutation| {
let mut modifiers = ModifierKeys::new(); KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
for modifier in permutation {
modifiers = modifiers.add(modifier);
}
Keybinding::new("X".to_string(), modifiers)
})) }))
}), }),
), ),
) )
.child(Story::label(cx, "Single Key with All Modifiers")) .child(Story::label(cx, "Single Key with All Modifiers"))
.child(Keybinding::new("Z".to_string(), ModifierKeys::all())) .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
.child(Story::label(cx, "Chord")) .child(Story::label(cx, "Chord"))
.child(Keybinding::new_chord( .child(KeyBinding::new(binding("a z")))
("A".to_string(), ModifierKeys::new()),
("Z".to_string(), ModifierKeys::new()),
))
.child(Story::label(cx, "Chord with Modifier")) .child(Story::label(cx, "Chord with Modifier"))
.child(Keybinding::new_chord( .child(KeyBinding::new(binding("ctrl-a shift-z")))
("A".to_string(), ModifierKeys::new().control(true)),
("Z".to_string(), ModifierKeys::new().shift(true)),
))
} }
} }
} }

View file

@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use crate::{h_stack, v_stack, Keybinding, Label, LabelColor}; use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor};
#[derive(Component)] #[derive(Component)]
pub struct Palette { pub struct Palette {
@ -108,7 +108,7 @@ impl Palette {
pub struct PaletteItem { pub struct PaletteItem {
pub label: SharedString, pub label: SharedString,
pub sublabel: Option<SharedString>, pub sublabel: Option<SharedString>,
pub keybinding: Option<Keybinding>, pub keybinding: Option<KeyBinding>,
} }
impl PaletteItem { impl PaletteItem {
@ -132,7 +132,7 @@ impl PaletteItem {
pub fn keybinding<K>(mut self, keybinding: K) -> Self pub fn keybinding<K>(mut self, keybinding: K) -> Self
where where
K: Into<Option<Keybinding>>, K: Into<Option<KeyBinding>>,
{ {
self.keybinding = keybinding.into(); self.keybinding = keybinding.into();
self self
@ -161,7 +161,7 @@ pub use stories::*;
mod stories { mod stories {
use gpui::{Div, Render}; use gpui::{Div, Render};
use crate::{ModifierKeys, Story}; use crate::{binding, Story};
use super::*; use super::*;
@ -181,46 +181,24 @@ mod stories {
Palette::new("palette-2") Palette::new("palette-2")
.placeholder("Execute a command...") .placeholder("Execute a command...")
.items(vec![ .items(vec![
PaletteItem::new("theme selector: toggle").keybinding( PaletteItem::new("theme selector: toggle")
Keybinding::new_chord( .keybinding(KeyBinding::new(binding("cmd-k cmd-t"))),
("k".to_string(), ModifierKeys::new().command(true)), PaletteItem::new("assistant: inline assist")
("t".to_string(), ModifierKeys::new().command(true)), .keybinding(KeyBinding::new(binding("cmd-enter"))),
), PaletteItem::new("assistant: quote selection")
), .keybinding(KeyBinding::new(binding("cmd-<"))),
PaletteItem::new("assistant: inline assist").keybinding( PaletteItem::new("assistant: toggle focus")
Keybinding::new( .keybinding(KeyBinding::new(binding("cmd-?"))),
"enter".to_string(),
ModifierKeys::new().command(true),
),
),
PaletteItem::new("assistant: quote selection").keybinding(
Keybinding::new(
">".to_string(),
ModifierKeys::new().command(true),
),
),
PaletteItem::new("assistant: toggle focus").keybinding(
Keybinding::new(
"?".to_string(),
ModifierKeys::new().command(true),
),
),
PaletteItem::new("auto update: check"), PaletteItem::new("auto update: check"),
PaletteItem::new("auto update: view release notes"), PaletteItem::new("auto update: view release notes"),
PaletteItem::new("branches: open recent").keybinding( PaletteItem::new("branches: open recent")
Keybinding::new( .keybinding(KeyBinding::new(binding("cmd-alt-b"))),
"b".to_string(),
ModifierKeys::new().command(true).alt(true),
),
),
PaletteItem::new("chat panel: toggle focus"), PaletteItem::new("chat panel: toggle focus"),
PaletteItem::new("cli: install"), PaletteItem::new("cli: install"),
PaletteItem::new("client: sign in"), PaletteItem::new("client: sign in"),
PaletteItem::new("client: sign out"), PaletteItem::new("client: sign out"),
PaletteItem::new("editor: cancel").keybinding(Keybinding::new( PaletteItem::new("editor: cancel")
"escape".to_string(), .keybinding(KeyBinding::new(binding("escape"))),
ModifierKeys::new(),
)),
]), ]),
) )
} }

View file

@ -7,12 +7,12 @@ use gpui::{AppContext, ViewContext};
use rand::Rng; use rand::Rng;
use theme2::ActiveTheme; use theme2::ActiveTheme;
use crate::HighlightedText; use crate::{binding, HighlightedText};
use crate::{ use crate::{
Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, HighlightedLine, Icon, KeyBinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream,
MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus, MicStatus, Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus,
PlayerWithCallStatus, PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus,
}; };
use crate::{ListItem, NotificationAction}; use crate::{ListItem, NotificationAction};
@ -701,46 +701,16 @@ pub fn static_collab_panel_channels() -> Vec<ListItem> {
pub fn example_editor_actions() -> Vec<PaletteItem> { pub fn example_editor_actions() -> Vec<PaletteItem> {
vec![ vec![
PaletteItem::new("New File").keybinding(Keybinding::new( PaletteItem::new("New File").keybinding(KeyBinding::new(binding("cmd-n"))),
"N".to_string(), PaletteItem::new("Open File").keybinding(KeyBinding::new(binding("cmd-o"))),
ModifierKeys::new().command(true), PaletteItem::new("Save File").keybinding(KeyBinding::new(binding("cmd-s"))),
)), PaletteItem::new("Cut").keybinding(KeyBinding::new(binding("cmd-x"))),
PaletteItem::new("Open File").keybinding(Keybinding::new( PaletteItem::new("Copy").keybinding(KeyBinding::new(binding("cmd-c"))),
"O".to_string(), PaletteItem::new("Paste").keybinding(KeyBinding::new(binding("cmd-v"))),
ModifierKeys::new().command(true), PaletteItem::new("Undo").keybinding(KeyBinding::new(binding("cmd-z"))),
)), PaletteItem::new("Redo").keybinding(KeyBinding::new(binding("cmd-shift-z"))),
PaletteItem::new("Save File").keybinding(Keybinding::new( PaletteItem::new("Find").keybinding(KeyBinding::new(binding("cmd-f"))),
"S".to_string(), PaletteItem::new("Replace").keybinding(KeyBinding::new(binding("cmd-r"))),
ModifierKeys::new().command(true),
)),
PaletteItem::new("Cut").keybinding(Keybinding::new(
"X".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("Copy").keybinding(Keybinding::new(
"C".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("Paste").keybinding(Keybinding::new(
"V".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("Undo").keybinding(Keybinding::new(
"Z".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("Redo").keybinding(Keybinding::new(
"Z".to_string(),
ModifierKeys::new().command(true).shift(true),
)),
PaletteItem::new("Find").keybinding(Keybinding::new(
"F".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("Replace").keybinding(Keybinding::new(
"R".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("Jump to Line"), PaletteItem::new("Jump to Line"),
PaletteItem::new("Select All"), PaletteItem::new("Select All"),
PaletteItem::new("Deselect All"), PaletteItem::new("Deselect All"),