From 3901d9d544496af7752d13a7e6ea8ecfdc185f28 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Apr 2022 13:50:00 -0700 Subject: [PATCH] Display key bindings in the command palette They still need to be styled. Co-authored-by: Antonio Scandurra --- crates/command_palette/src/command_palette.rs | 91 +++++++++++++++---- crates/gpui/src/app.rs | 16 +++- crates/gpui/src/keymap.rs | 55 +++++++++-- 3 files changed, 132 insertions(+), 30 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 7e1c2b43b1be72b2d5c9780ab9e599138c0ae501..d8b83ce328a9e74a5bb3ad12d9e3451d17b50f96 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,13 +1,13 @@ -use std::cmp; - use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, - elements::{ChildView, Label}, + elements::{ChildView, Flex, Label, ParentElement}, + keymap::Keystroke, Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, }; use selector::{SelectorModal, SelectorModalDelegate}; use settings::Settings; +use std::cmp; use workspace::Workspace; mod selector; @@ -21,7 +21,7 @@ actions!(command_palette, [Toggle]); pub struct CommandPalette { selector: ViewHandle>, - actions: Vec<(&'static str, Box)>, + actions: Vec, matches: Vec, selected_ix: usize, focused_view_id: usize, @@ -31,13 +31,27 @@ pub enum Event { Dismissed, } +struct Command { + name: &'static str, + action: Box, + keystrokes: Vec, + has_multiple_bindings: bool, +} + impl CommandPalette { - pub fn new( - focused_view_id: usize, - actions: Vec<(&'static str, Box)>, - cx: &mut ViewContext, - ) -> Self { + pub fn new(focused_view_id: usize, cx: &mut ViewContext) -> Self { let this = cx.weak_handle(); + let actions = cx + .available_actions(cx.window_id(), focused_view_id) + .map(|(name, action, bindings)| Command { + name, + action, + keystrokes: bindings + .last() + .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()), + has_multiple_bindings: bindings.len() > 1, + }) + .collect(); let selector = cx.add_view(|cx| SelectorModal::new(this, cx)); Self { selector, @@ -54,12 +68,11 @@ impl CommandPalette { let focused_view_id = cx.focused_view_id(window_id).unwrap_or(workspace.id()); cx.as_mut().defer(move |cx| { - let actions = cx.available_actions(window_id, focused_view_id); + let this = cx.add_view(window_id, |cx| Self::new(focused_view_id, cx)); workspace.update(cx, |workspace, cx| { workspace.toggle_modal(cx, |cx, _| { - let selector = cx.add_view(|cx| Self::new(focused_view_id, actions, cx)); - cx.subscribe(&selector, Self::on_event).detach(); - selector + cx.subscribe(&this, Self::on_event).detach(); + this }); }); }); @@ -119,10 +132,10 @@ impl SelectorModalDelegate for CommandPalette { .actions .iter() .enumerate() - .map(|(ix, (name, _))| StringMatchCandidate { + .map(|(ix, command)| StringMatchCandidate { id: ix, - string: name.to_string(), - char_bag: name.chars().collect(), + string: command.name.to_string(), + char_bag: command.name.chars().collect(), }) .collect::>(); cx.spawn(move |this, mut cx| async move { @@ -157,13 +170,15 @@ impl SelectorModalDelegate for CommandPalette { cx.dispatch_action_at( window_id, self.focused_view_id, - self.actions[action_ix].1.as_ref(), + self.actions[action_ix].action.as_ref(), ) } cx.emit(Event::Dismissed); } fn render_match(&self, ix: usize, selected: bool, cx: &gpui::AppContext) -> gpui::ElementBox { + let mat = &self.matches[ix]; + let command = &self.actions[mat.candidate_id]; let settings = cx.global::(); let theme = &settings.theme.selector; let style = if selected { @@ -171,9 +186,49 @@ impl SelectorModalDelegate for CommandPalette { } else { &theme.item }; - Label::new(self.matches[ix].string.clone(), style.label.clone()) + + Flex::row() + .with_child(Label::new(mat.string.clone(), style.label.clone()).boxed()) + .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.into(), style.label.clone()).boxed()) + } else { + None + } + }), + ) + .with_child(Label::new(keystroke.key.clone(), style.label.clone()).boxed()) + .contained() + .with_margin_left(5.0) + .flex_float() + .boxed() + })) + .with_children(if command.has_multiple_bindings { + Some(Label::new("+".into(), style.label.clone()).boxed()) + } else { + None + }) .contained() .with_style(style.container) .boxed() } } + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command") + .field("name", &self.name) + .field("keystrokes", &self.keystrokes) + .finish() + } +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f8721770820ce73f44fa771c200578e7df30a5fc..683ea46999297e162ba8fa4228b6b9a917b290c0 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3,7 +3,7 @@ pub mod action; use crate::{ elements::ElementBox, executor::{self, Task}, - keymap::{self, Keystroke}, + keymap::{self, Binding, Keystroke}, platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, @@ -17,6 +17,7 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use platform::Event; use postage::oneshot; +use smallvec::SmallVec; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, @@ -1309,7 +1310,7 @@ impl MutableAppContext { &self, window_id: usize, view_id: usize, - ) -> Vec<(&'static str, Box)> { + ) -> impl Iterator, SmallVec<[&Binding; 1]>)> { let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect(); let presenter = self @@ -1330,14 +1331,19 @@ impl MutableAppContext { self.action_deserializers .iter() - .filter_map(|(name, (type_id, deserialize))| { + .filter_map(move |(name, (type_id, deserialize))| { if action_types.contains(type_id) { - Some((*name, deserialize("{}").ok()?)) + Some(( + *name, + deserialize("{}").ok()?, + self.keystroke_matcher + .bindings_for_action_type(*type_id) + .collect(), + )) } else { None } }) - .collect() } pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) { diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 723403f16012641924051ac4dd5d80b61cfbbb6f..c42fbff907061e5e6813da2411477b62a697c28c 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -1,7 +1,8 @@ use crate::Action; use anyhow::{anyhow, Result}; +use smallvec::SmallVec; use std::{ - any::Any, + any::{Any, TypeId}, collections::{HashMap, HashSet}, fmt::Debug, }; @@ -23,7 +24,10 @@ struct Pending { } #[derive(Default)] -pub struct Keymap(Vec); +pub struct Keymap { + bindings: Vec, + binding_indices_by_action_type: HashMap>, +} pub struct Binding { keystrokes: Vec, @@ -111,6 +115,10 @@ impl Matcher { self.keymap.clear(); } + pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator { + self.keymap.bindings_for_action_type(action_type) + } + pub fn clear_pending(&mut self) { self.pending.clear(); } @@ -132,7 +140,7 @@ impl Matcher { pending.keystrokes.push(keystroke); let mut retain_pending = false; - for binding in self.keymap.0.iter().rev() { + for binding in self.keymap.bindings.iter().rev() { if binding.keystrokes.starts_with(&pending.keystrokes) && binding.context.as_ref().map(|c| c.eval(cx)).unwrap_or(true) { @@ -163,15 +171,44 @@ impl Default for Matcher { impl Keymap { pub fn new(bindings: Vec) -> Self { - Self(bindings) + let mut binding_indices_by_action_type = HashMap::new(); + for (ix, binding) in bindings.iter().enumerate() { + binding_indices_by_action_type + .entry(binding.action.as_any().type_id()) + .or_insert_with(|| SmallVec::new()) + .push(ix); + } + Self { + binding_indices_by_action_type, + bindings, + } + } + + fn bindings_for_action_type<'a>( + &'a self, + action_type: TypeId, + ) -> impl Iterator { + self.binding_indices_by_action_type + .get(&action_type) + .map(SmallVec::as_slice) + .unwrap_or(&[]) + .iter() + .map(|ix| &self.bindings[*ix]) } fn add_bindings>(&mut self, bindings: T) { - self.0.extend(bindings.into_iter()); + for binding in bindings { + self.binding_indices_by_action_type + .entry(binding.action.as_any().type_id()) + .or_default() + .push(self.bindings.len()); + self.bindings.push(binding); + } } fn clear(&mut self) { - self.0.clear(); + self.bindings.clear(); + self.binding_indices_by_action_type.clear(); } } @@ -198,6 +235,10 @@ impl Binding { context, }) } + + pub fn keystrokes(&self) -> &[Keystroke] { + &self.keystrokes + } } impl Keystroke { @@ -446,7 +487,7 @@ mod tests { a: &'static str, } - let keymap = Keymap(vec![ + let keymap = Keymap::new(vec![ Binding::new("a", A("x".to_string()), Some("a")), Binding::new("b", B, Some("a")), Binding::new("a b", Ab, Some("a || b")),