Add KeyBindings to CommandPalette (#3313)

Conrad Irwin created

Release Notes:

- N/A

Change summary

crates/command_palette2/src/command_palette.rs |  70 +-------
crates/gpui2/src/key_dispatch.rs               |  13 +
crates/gpui2/src/keymap/binding.rs             |  16 +
crates/gpui2/src/window.rs                     |  21 +
crates/ui2/src/components/keybinding.rs        | 157 ++++---------------
crates/ui2/src/components/palette.rs           |  54 ++----
crates/ui2/src/static_data.rs                  |  58 +-----
7 files changed, 116 insertions(+), 273 deletions(-)

Detailed changes

crates/command_palette2/src/command_palette.rs 🔗

@@ -11,7 +11,7 @@ use std::{
     sync::Arc,
 };
 use theme::ActiveTheme;
-use ui::{v_stack, HighlightedLabel, StyledExt};
+use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
@@ -318,66 +318,16 @@ impl PickerDelegate for CommandPaletteDelegate {
             .rounded_md()
             .when(selected, |this| this.bg(colors.ghost_element_selected))
             .hover(|this| this.bg(colors.ghost_element_hover))
-            .child(HighlightedLabel::new(
-                command.name.clone(),
-                r#match.positions.clone(),
-            ))
+            .child(
+                h_stack()
+                    .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 {

crates/gpui2/src/key_dispatch.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle,
-    FocusId, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels,
-    Style, StyleRefinement, ViewContext, WindowContext,
+    FocusId, KeyBinding, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent,
+    Pixels, Style, StyleRefinement, ViewContext, WindowContext,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -145,6 +145,15 @@ impl DispatchTree {
         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(
         &mut self,
         keystroke: &Keystroke,

crates/gpui2/src/keymap/binding.rs 🔗

@@ -3,9 +3,19 @@ use anyhow::Result;
 use smallvec::SmallVec;
 
 pub struct KeyBinding {
-    action: Box<dyn Action>,
-    pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
-    pub(super) context_predicate: Option<KeyBindingContextPredicate>,
+    pub(crate) action: Box<dyn Action>,
+    pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
+    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 {

crates/gpui2/src/window.rs 🔗

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

crates/ui2/src/components/keybinding.rs 🔗

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

crates/ui2/src/components/palette.rs 🔗

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

crates/ui2/src/static_data.rs 🔗

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