Make toggle dock actions appear in the command palette (#2525)

Max Brunsfeld created

This makes the `Toggle{Left,Right,Bottom}Dock` actions deserializable
from empty JSON, so that they can be constructed for the command
palette. It also fixes a bug in GPUI's `available_actions` method, in
which we'd include key bindings for actions of the same type but
different values.

Note that, for now, the command palette will perform the *focusing*
version of the actions. I'm not totally sure this is the right behavior,
but it seems more useful to me.

Release Notes:

N/A

Change summary

crates/gpui/src/app.rs            | 64 ++++++++++++++++++++++++++++----
crates/gpui/src/app/window.rs     | 23 ++++++-----
crates/workspace/src/workspace.rs |  7 +++
3 files changed, 74 insertions(+), 20 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -3390,15 +3390,14 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
         self.keystroke_matcher
             .bindings_for_action_type(action.as_any().type_id())
             .find_map(|b| {
-                handler_depth
-                    .map(|highest_handler| {
-                        if (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..])) {
-                            Some(b.keystrokes().into())
-                        } else {
-                            None
-                        }
-                    })
-                    .flatten()
+                let highest_handler = handler_depth?;
+                if action.eq(b.action())
+                    && (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..]))
+                {
+                    Some(b.keystrokes().into())
+                } else {
+                    None
+                }
             })
     }
 
@@ -6090,6 +6089,53 @@ mod tests {
         }
     }
 
+    #[crate::test(self)]
+    fn test_keystrokes_for_action_with_data(cx: &mut TestAppContext) {
+        #[derive(Clone, Debug, Deserialize, PartialEq)]
+        struct ActionWithArg {
+            #[serde(default)]
+            arg: bool,
+        }
+
+        struct View;
+        impl super::Entity for View {
+            type Event = ();
+        }
+        impl super::View for View {
+            fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+                Empty::new().into_any()
+            }
+            fn ui_name() -> &'static str {
+                "View"
+            }
+        }
+
+        impl_actions!(test, [ActionWithArg]);
+
+        let (window_id, view) = cx.add_window(|_| View);
+        cx.update(|cx| {
+            cx.add_global_action(|_: &ActionWithArg, _| {});
+            cx.add_bindings(vec![
+                Binding::new("a", ActionWithArg { arg: false }, None),
+                Binding::new("shift-a", ActionWithArg { arg: true }, None),
+            ]);
+        });
+
+        let actions = cx.available_actions(window_id, view.id());
+        assert_eq!(
+            actions[0].1.as_any().downcast_ref::<ActionWithArg>(),
+            Some(&ActionWithArg { arg: false })
+        );
+        assert_eq!(
+            actions[0]
+                .2
+                .iter()
+                .map(|b| b.keystrokes()[0].clone())
+                .collect::<Vec<_>>(),
+            vec![Keystroke::parse("a").unwrap()],
+        );
+    }
+
     #[crate::test(self)]
     async fn test_model_condition(cx: &mut TestAppContext) {
         struct Counter(usize);

crates/gpui/src/app/window.rs 🔗

@@ -394,17 +394,18 @@ impl<'a> WindowContext<'a> {
             .iter()
             .filter_map(move |(name, (type_id, deserialize))| {
                 if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
-                    Some((
-                        *name,
-                        deserialize("{}").ok()?,
-                        self.keystroke_matcher
-                            .bindings_for_action_type(*type_id)
-                            .filter(|b| {
-                                (0..=action_depth).any(|depth| b.match_context(&contexts[depth..]))
-                            })
-                            .cloned()
-                            .collect(),
-                    ))
+                    let action = deserialize("{}").ok()?;
+                    let bindings = self
+                        .keystroke_matcher
+                        .bindings_for_action_type(*type_id)
+                        .filter(|b| {
+                            action.eq(b.action())
+                                && (0..=action_depth)
+                                    .any(|depth| b.match_context(&contexts[depth..]))
+                        })
+                        .cloned()
+                        .collect();
+                    Some((*name, action, bindings))
                 } else {
                     None
                 }

crates/workspace/src/workspace.rs 🔗

@@ -105,16 +105,19 @@ pub struct RemoveWorktreeFromProject(pub WorktreeId);
 
 #[derive(Copy, Clone, Default, Deserialize, PartialEq)]
 pub struct ToggleLeftDock {
+    #[serde(default = "default_true")]
     pub focus: bool,
 }
 
 #[derive(Copy, Clone, Default, Deserialize, PartialEq)]
 pub struct ToggleBottomDock {
+    #[serde(default = "default_true")]
     pub focus: bool,
 }
 
 #[derive(Copy, Clone, Default, Deserialize, PartialEq)]
 pub struct ToggleRightDock {
+    #[serde(default = "default_true")]
     pub focus: bool,
 }
 
@@ -3378,6 +3381,10 @@ fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
     Some(vec2f(width as f32, height as f32))
 }
 
+fn default_true() -> bool {
+    true
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;