wip

Junkui Zhang created

Change summary

crates/gpui/src/key_dispatch.rs        |  7 +-
crates/gpui/src/keymap.rs              | 31 ++++++++++
crates/gpui/src/keymap/binding.rs      | 35 ++++++++++--
crates/gpui/src/platform/keystroke.rs  | 25 ++++++--
crates/gpui/src/window.rs              |  2 
crates/settings/src/keymap_file.rs     |  7 +-
crates/ui/src/components/keybinding.rs | 75 ++++++++++++++++++---------
7 files changed, 133 insertions(+), 49 deletions(-)

Detailed changes

crates/gpui/src/key_dispatch.rs 🔗

@@ -51,7 +51,7 @@
 ///
 use crate::{
     Action, ActionRegistry, App, BindingIndex, DispatchPhase, EntityId, FocusId, KeyBinding,
-    KeyContext, Keymap, Keystroke, ModifiersChangedEvent, Window,
+    KeyContext, KeybindingKeystroke, Keymap, Keystroke, ModifiersChangedEvent, Window,
 };
 use collections::FxHashMap;
 use smallvec::SmallVec;
@@ -444,10 +444,11 @@ impl DispatchTree {
     fn binding_matches_predicate_and_not_shadowed(
         keymap: &Keymap,
         binding_index: BindingIndex,
-        keystrokes: &[Keystroke],
+        keystrokes: &[KeybindingKeystroke],
         context_stack: &[KeyContext],
     ) -> bool {
-        let (bindings, _) = keymap.bindings_for_input_with_indices(&keystrokes, context_stack);
+        let (bindings, _) =
+            keymap.bindings_for_keybinding_keystroke_with_indices(&keystrokes, context_stack);
         if let Some((highest_precedence_index, _)) = bindings.iter().next() {
             binding_index == *highest_precedence_index
         } else {

crates/gpui/src/keymap.rs 🔗

@@ -4,7 +4,7 @@ mod context;
 pub use binding::*;
 pub use context::*;
 
-use crate::{Action, Keystroke, is_no_action};
+use crate::{Action, KeybindingKeystroke, Keystroke, is_no_action};
 use collections::HashMap;
 use smallvec::SmallVec;
 use std::any::TypeId;
@@ -177,10 +177,37 @@ impl Keymap {
                     .map(|pending| (BindingIndex(ix), binding, pending))
             });
 
+        self.bindings_for_keystrokes_with_indices_inner(possibilities, context_stack)
+    }
+
+    /// TODO:
+    pub fn bindings_for_keybinding_keystroke_with_indices(
+        &self,
+        input: &[KeybindingKeystroke],
+        context_stack: &[KeyContext],
+    ) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) {
+        let possibilities = self
+            .bindings()
+            .enumerate()
+            .rev()
+            .filter_map(|(ix, binding)| {
+                binding
+                    .match_keybinding_keystrokes(input)
+                    .map(|pending| (BindingIndex(ix), binding, pending))
+            });
+
+        self.bindings_for_keystrokes_with_indices_inner(possibilities, context_stack)
+    }
+
+    fn bindings_for_keystrokes_with_indices_inner<'a>(
+        &'a self,
+        possibilities: impl Iterator<Item = (BindingIndex, &'a KeyBinding, bool)>,
+        context_stack: &[KeyContext],
+    ) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) {
         let mut bindings: SmallVec<[(BindingIndex, KeyBinding, usize); 1]> = SmallVec::new();
 
         // (pending, is_no_action, depth, keystrokes)
-        let mut pending_info_opt: Option<(bool, bool, usize, &[Keystroke])> = None;
+        let mut pending_info_opt: Option<(bool, bool, usize, &[KeybindingKeystroke])> = None;
 
         'outer: for (binding_index, binding, pending) in possibilities {
             for depth in (0..=context_stack.len()).rev() {

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

@@ -2,13 +2,16 @@ use std::rc::Rc;
 
 use collections::HashMap;
 
-use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
+use crate::{
+    Action, InvalidKeystrokeError, KeyBindingContextPredicate, KeybindingKeystroke, Keystroke,
+    SharedString,
+};
 use smallvec::SmallVec;
 
 /// A keybinding and its associated metadata, from the keymap.
 pub struct KeyBinding {
     pub(crate) action: Box<dyn Action>,
-    pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
+    pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
     pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
     pub(crate) meta: Option<KeyBindingMetaIndex>,
     /// The json input string used when building the keybinding, if any
@@ -46,16 +49,17 @@ impl KeyBinding {
         key_equivalents: Option<&HashMap<char, char>>,
         action_input: Option<SharedString>,
     ) -> std::result::Result<Self, InvalidKeystrokeError> {
-        let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
+        let mut keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
             .split_whitespace()
-            .map(Keystroke::parse)
+            .map(KeybindingKeystroke::parse)
             .collect::<std::result::Result<_, _>>()?;
 
         if let Some(equivalents) = key_equivalents {
             for keystroke in keystrokes.iter_mut() {
-                if keystroke.key.chars().count() == 1 {
-                    if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) {
-                        keystroke.key = key.to_string();
+                if keystroke.inner.key.chars().count() == 1 {
+                    if let Some(key) = equivalents.get(&keystroke.inner.key.chars().next().unwrap())
+                    {
+                        keystroke.inner.key = key.to_string();
                     }
                 }
             }
@@ -96,8 +100,23 @@ impl KeyBinding {
         Some(self.keystrokes.len() > typed.len())
     }
 
+    /// TODO:
+    pub fn match_keybinding_keystrokes(&self, typed: &[KeybindingKeystroke]) -> Option<bool> {
+        if self.keystrokes.len() < typed.len() {
+            return None;
+        }
+
+        for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
+            if !typed.inner.should_match(target) {
+                return None;
+            }
+        }
+
+        Some(self.keystrokes.len() > typed.len())
+    }
+
     /// Get the keystrokes associated with this binding
-    pub fn keystrokes(&self) -> &[Keystroke] {
+    pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
         self.keystrokes.as_slice()
     }
 

crates/gpui/src/platform/keystroke.rs 🔗

@@ -22,7 +22,8 @@ pub struct Keystroke {
 }
 
 /// TODO:
-pub struct KeybindKeystroke {
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct KeybindingKeystroke {
     /// TODO:
     pub inner: Keystroke,
     /// TODO:
@@ -65,7 +66,7 @@ impl Keystroke {
     ///
     /// This method assumes that `self` was typed and `target' is in the keymap, and checks
     /// both possibilities for self against the target.
-    pub fn should_match(&self, target: &Keystroke) -> bool {
+    pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
         #[cfg(not(target_os = "windows"))]
         if let Some(key_char) = self
             .key_char
@@ -78,7 +79,7 @@ impl Keystroke {
                 ..Default::default()
             };
 
-            if &target.key == key_char && target.modifiers == ime_modifiers {
+            if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
                 return true;
             }
         }
@@ -90,12 +91,12 @@ impl Keystroke {
             .filter(|key_char| key_char != &&self.key)
         {
             // On Windows, if key_char is set, then the typed keystroke produced the key_char
-            if &target.key == key_char && target.modifiers == Modifiers::none() {
+            if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
                 return true;
             }
         }
 
-        target.modifiers == self.modifiers && target.key == self.key
+        target.inner.modifiers == self.modifiers && target.inner.key == self.key
     }
 
     /// key syntax is:
@@ -273,7 +274,7 @@ impl Keystroke {
     }
 }
 
-impl KeybindKeystroke {
+impl KeybindingKeystroke {
     /// TODO:
     pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
         let keystroke = Keystroke::parse(source)?;
@@ -281,12 +282,22 @@ impl KeybindKeystroke {
             mut modifiers, key, ..
         } = keystroke.clone();
         let (key, modifiers) = temp_keyboard_mapper(key, modifiers);
-        Ok(KeybindKeystroke {
+        Ok(KeybindingKeystroke {
             inner: keystroke,
             modifiers,
             key,
         })
     }
+
+    /// TODO:
+    pub fn to_string(&self) -> String {
+        let keystroke = Keystroke {
+            modifiers: self.modifiers,
+            key: self.key.clone(),
+            key_char: None,
+        };
+        keystroke.to_string()
+    }
 }
 
 fn temp_keyboard_mapper(key: String, mut modifiers: Modifiers) -> (String, Modifiers) {

crates/gpui/src/window.rs 🔗

@@ -3316,7 +3316,7 @@ impl Window {
                 binding
                     .keystrokes()
                     .iter()
-                    .map(ToString::to_string)
+                    .map(|ks| ks.to_string())
                     .collect::<Vec<_>>()
                     .join(" ")
             })

crates/settings/src/keymap_file.rs 🔗

@@ -3,7 +3,8 @@ use collections::{BTreeMap, HashMap, IndexMap};
 use fs::Fs;
 use gpui::{
     Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
-    KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString,
+    KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke,
+    NoAction, SharedString,
 };
 use schemars::{JsonSchema, json_schema};
 use serde::Deserialize;
@@ -787,7 +788,7 @@ pub enum KeybindUpdateOperation<'a> {
 
 pub struct KeybindUpdateTarget<'a> {
     pub context: Option<&'a str>,
-    pub keystrokes: &'a [Keystroke],
+    pub keystrokes: &'a [KeybindingKeystroke],
     pub action_name: &'a str,
     pub use_key_equivalents: bool,
     pub input: Option<&'a str>,
@@ -810,7 +811,7 @@ impl<'a> KeybindUpdateTarget<'a> {
     fn keystrokes_unparsed(&self) -> String {
         let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
         for keystroke in self.keystrokes {
-            keystrokes.push_str(&keystroke.unparse());
+            keystrokes.push_str(&keystroke.inner.unparse());
             keystrokes.push(' ');
         }
         keystrokes.pop();

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

@@ -1,8 +1,8 @@
 use crate::PlatformStyle;
 use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
 use gpui::{
-    Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window,
-    relative,
+    Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
+    Modifiers, Window, relative,
 };
 use itertools::Itertools;
 
@@ -13,7 +13,7 @@ pub struct KeyBinding {
     /// More than one keystroke produces a chord.
     ///
     /// This should always contain at least one keystroke.
-    pub keystrokes: Vec<Keystroke>,
+    pub keystrokes: Vec<KeybindingKeystroke>,
 
     /// The [`PlatformStyle`] to use when displaying this keybinding.
     platform_style: PlatformStyle,
@@ -59,7 +59,7 @@ impl KeyBinding {
         cx.try_global::<VimStyle>().is_some_and(|g| g.0)
     }
 
-    pub fn new(keystrokes: Vec<Keystroke>, cx: &App) -> Self {
+    pub fn new(keystrokes: Vec<KeybindingKeystroke>, cx: &App) -> Self {
         Self {
             keystrokes,
             platform_style: PlatformStyle::platform(),
@@ -99,16 +99,16 @@ impl KeyBinding {
 }
 
 fn render_key(
-    keystroke: &Keystroke,
+    key: &str,
     color: Option<Color>,
     platform_style: PlatformStyle,
     size: impl Into<Option<AbsoluteLength>>,
 ) -> AnyElement {
-    let key_icon = icon_for_key(keystroke, platform_style);
+    let key_icon = icon_for_key(key, platform_style);
     match key_icon {
         Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
         None => {
-            let key = util::capitalize(&keystroke.key);
+            let key = util::capitalize(key);
             Key::new(&key, color).size(size).into_any_element()
         }
     }
@@ -149,7 +149,7 @@ impl RenderOnce for KeyBinding {
 }
 
 pub fn render_keystroke(
-    keystroke: &Keystroke,
+    keystroke: &KeybindingKeystroke,
     color: Option<Color>,
     size: impl Into<Option<AbsoluteLength>>,
     platform_style: PlatformStyle,
@@ -163,9 +163,17 @@ pub fn render_keystroke(
     let size = size.into();
 
     if use_text {
-        let element = Key::new(keystroke_text(&keystroke, platform_style, vim_mode), color)
-            .size(size)
-            .into_any_element();
+        let element = Key::new(
+            keystroke_text(
+                &keystroke.modifiers,
+                &keystroke.key,
+                platform_style,
+                vim_mode,
+            ),
+            color,
+        )
+        .size(size)
+        .into_any_element();
         vec![element]
     } else {
         let mut elements = Vec::new();
@@ -176,13 +184,13 @@ pub fn render_keystroke(
             size,
             true,
         ));
-        elements.push(render_key(&keystroke, color, platform_style, size));
+        elements.push(render_key(&keystroke.key, color, platform_style, size));
         elements
     }
 }
 
-fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
-    match keystroke.key.as_str() {
+fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
+    match key {
         "left" => Some(IconName::ArrowLeft),
         "right" => Some(IconName::ArrowRight),
         "up" => Some(IconName::ArrowUp),
@@ -382,27 +390,44 @@ pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option
     Some(text_for_keystrokes(key_binding.keystrokes(), cx))
 }
 
-pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
+pub fn text_for_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String {
     let platform_style = PlatformStyle::platform();
     let vim_enabled = cx.try_global::<VimStyle>().is_some();
     keystrokes
         .iter()
-        .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
+        .map(|keystroke| {
+            keystroke_text(
+                &keystroke.modifiers,
+                &keystroke.key,
+                platform_style,
+                vim_enabled,
+            )
+        })
         .join(" ")
 }
 
 pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
     let platform_style = PlatformStyle::platform();
     let vim_enabled = cx.try_global::<VimStyle>().is_some();
-    keystroke_text(keystroke, platform_style, vim_enabled)
+    keystroke_text(
+        &keystroke.modifiers,
+        &keystroke.key,
+        platform_style,
+        vim_enabled,
+    )
 }
 
 /// Returns a textual representation of the given [`Keystroke`].
-fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String {
+fn keystroke_text(
+    modifiers: &Modifiers,
+    key: &str,
+    platform_style: PlatformStyle,
+    vim_mode: bool,
+) -> String {
     let mut text = String::new();
     let delimiter = '-';
 
-    if keystroke.modifiers.function {
+    if modifiers.function {
         match vim_mode {
             false => text.push_str("Fn"),
             true => text.push_str("fn"),
@@ -411,7 +436,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
         text.push(delimiter);
     }
 
-    if keystroke.modifiers.control {
+    if modifiers.control {
         match (platform_style, vim_mode) {
             (PlatformStyle::Mac, false) => text.push_str("Control"),
             (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
@@ -421,7 +446,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
         text.push(delimiter);
     }
 
-    if keystroke.modifiers.platform {
+    if modifiers.platform {
         match (platform_style, vim_mode) {
             (PlatformStyle::Mac, false) => text.push_str("Command"),
             (PlatformStyle::Mac, true) => text.push_str("cmd"),
@@ -434,7 +459,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
         text.push(delimiter);
     }
 
-    if keystroke.modifiers.alt {
+    if modifiers.alt {
         match (platform_style, vim_mode) {
             (PlatformStyle::Mac, false) => text.push_str("Option"),
             (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
@@ -444,7 +469,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
         text.push(delimiter);
     }
 
-    if keystroke.modifiers.shift {
+    if modifiers.shift {
         match (platform_style, vim_mode) {
             (_, false) => text.push_str("Shift"),
             (_, true) => text.push_str("shift"),
@@ -453,9 +478,9 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
     }
 
     if vim_mode {
-        text.push_str(&keystroke.key)
+        text.push_str(key)
     } else {
-        let key = match keystroke.key.as_str() {
+        let key = match key {
             "pageup" => "PageUp",
             "pagedown" => "PageDown",
             key => &util::capitalize(key),