Allow actions to be deserialized from JSON

Max Brunsfeld created

Introduce separate macro for implementing 'internal' actions which
are not intended to be loaded from keymaps.

Change summary

Cargo.lock                                  |   3 
crates/editor/src/editor.rs                 | 241 ++++++++++++----------
crates/file_finder/src/file_finder.rs       |   4 
crates/gpui/src/app.rs                      | 147 ++++++++++---
crates/gpui/src/app/action.rs               | 111 +++++++--
crates/gpui/src/gpui.rs                     |   3 
crates/gpui/src/keymap.rs                   |  64 +++--
crates/gpui/src/views/select.rs             |   4 
crates/project_panel/src/project_panel.rs   |   4 
crates/search/Cargo.toml                    |   1 
crates/search/src/buffer_search.rs          | 112 ++++++---
crates/search/src/project_search.rs         |  37 ++-
crates/search/src/search.rs                 |   8 
crates/server/Cargo.toml                    |   1 
crates/server/src/rpc.rs                    |   2 
crates/theme_selector/src/theme_selector.rs |  30 +-
crates/vim/Cargo.toml                       |   1 
crates/vim/src/mode.rs                      |   5 
crates/vim/src/normal.rs                    |   7 
crates/vim/src/vim.rs                       |   3 
crates/workspace/src/pane.rs                |  29 +-
crates/workspace/src/pane_group.rs          |   4 
crates/workspace/src/sidebar.rs             |   6 
crates/workspace/src/workspace.rs           |  13 +
crates/zed/src/main.rs                      |   2 
crates/zed/src/zed.rs                       |  42 ++-
26 files changed, 554 insertions(+), 330 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4269,6 +4269,7 @@ dependencies = [
  "log",
  "postage",
  "project",
+ "serde",
  "serde_json",
  "settings",
  "theme",
@@ -5748,6 +5749,7 @@ dependencies = [
  "language",
  "log",
  "project",
+ "serde",
  "settings",
  "util",
  "workspace",
@@ -6133,6 +6135,7 @@ dependencies = [
  "sha-1 0.9.6",
  "sqlx 0.5.5",
  "surf",
+ "theme",
  "tide",
  "tide-compress",
  "time 0.2.27",

crates/editor/src/editor.rs 🔗

@@ -22,7 +22,7 @@ use gpui::{
     executor,
     fonts::{self, HighlightStyle, TextStyle},
     geometry::vector::{vec2f, Vector2F},
-    impl_actions,
+    impl_actions, impl_internal_actions,
     keymap::Binding,
     platform::CursorStyle,
     text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
@@ -66,8 +66,11 @@ const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
 
-#[derive(Clone)]
-pub struct SelectNext(pub bool);
+#[derive(Clone, Deserialize)]
+pub struct SelectNext {
+    #[serde(default)]
+    pub replace_newest: bool,
+}
 
 #[derive(Clone)]
 pub struct GoToDiagnostic(pub Direction);
@@ -81,44 +84,26 @@ pub struct Select(pub SelectPhase);
 #[derive(Clone)]
 pub struct Input(pub String);
 
-#[derive(Clone)]
-pub struct Tab(pub Direction);
-
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct SelectToBeginningOfLine {
+    #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct SelectToEndOfLine {
+    #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[derive(Clone)]
-pub struct ToggleCodeActions(pub bool);
+#[derive(Clone, Deserialize)]
+pub struct ToggleCodeActions(#[serde(default)] pub bool);
 
-#[derive(Clone)]
-pub struct ConfirmCompletion(pub Option<usize>);
+#[derive(Clone, Deserialize)]
+pub struct ConfirmCompletion(#[serde(default)] pub Option<usize>);
 
-#[derive(Clone)]
-pub struct ConfirmCodeAction(pub Option<usize>);
-
-impl_actions!(
-    editor,
-    [
-        SelectNext,
-        GoToDiagnostic,
-        Scroll,
-        Select,
-        Input,
-        Tab,
-        SelectToBeginningOfLine,
-        SelectToEndOfLine,
-        ToggleCodeActions,
-        ConfirmCompletion,
-        ConfirmCodeAction,
-    ]
-);
+#[derive(Clone, Deserialize)]
+pub struct ConfirmCodeAction(#[serde(default)] pub Option<usize>);
 
 actions!(
     editor,
@@ -127,6 +112,8 @@ actions!(
         Backspace,
         Delete,
         Newline,
+        GoToNextDiagnostic,
+        GoToPrevDiagnostic,
         Indent,
         Outdent,
         DeleteLine,
@@ -172,6 +159,8 @@ actions!(
         SplitSelectionIntoLines,
         AddSelectionAbove,
         AddSelectionBelow,
+        Tab,
+        TabPrev,
         ToggleComments,
         SelectLargerSyntaxNode,
         SelectSmallerSyntaxNode,
@@ -193,6 +182,20 @@ actions!(
     ]
 );
 
+impl_actions!(
+    editor,
+    [
+        SelectNext,
+        SelectToBeginningOfLine,
+        SelectToEndOfLine,
+        ToggleCodeActions,
+        ConfirmCompletion,
+        ConfirmCodeAction,
+    ]
+);
+
+impl_internal_actions!(editor, [Scroll, Select, Input]);
+
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
 
@@ -226,8 +229,8 @@ pub fn init(cx: &mut MutableAppContext) {
             Some("Editor && showing_code_actions"),
         ),
         Binding::new("enter", ConfirmRename, Some("Editor && renaming")),
-        Binding::new("tab", Tab(Direction::Next), Some("Editor")),
-        Binding::new("shift-tab", Tab(Direction::Prev), Some("Editor")),
+        Binding::new("tab", Tab, Some("Editor")),
+        Binding::new("shift-tab", TabPrev, Some("Editor")),
         Binding::new(
             "tab",
             ConfirmCompletion(None),
@@ -346,8 +349,20 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")),
         Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")),
         Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")),
-        Binding::new("cmd-d", SelectNext(false), Some("Editor")),
-        Binding::new("cmd-k cmd-d", SelectNext(true), Some("Editor")),
+        Binding::new(
+            "cmd-d",
+            SelectNext {
+                replace_newest: false,
+            },
+            Some("Editor"),
+        ),
+        Binding::new(
+            "cmd-k cmd-d",
+            SelectNext {
+                replace_newest: true,
+            },
+            Some("Editor"),
+        ),
         Binding::new("cmd-/", ToggleComments, Some("Editor")),
         Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")),
         Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")),
@@ -355,8 +370,8 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
         Binding::new("cmd-u", UndoSelection, Some("Editor")),
         Binding::new("cmd-shift-U", RedoSelection, Some("Editor")),
-        Binding::new("f8", GoToDiagnostic(Direction::Next), Some("Editor")),
-        Binding::new("shift-f8", GoToDiagnostic(Direction::Prev), Some("Editor")),
+        Binding::new("f8", GoToNextDiagnostic, Some("Editor")),
+        Binding::new("shift-f8", GoToPrevDiagnostic, Some("Editor")),
         Binding::new("f2", Rename, Some("Editor")),
         Binding::new("f12", GoToDefinition, Some("Editor")),
         Binding::new("alt-shift-f12", FindAllReferences, Some("Editor")),
@@ -435,7 +450,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::move_to_enclosing_bracket);
     cx.add_action(Editor::undo_selection);
     cx.add_action(Editor::redo_selection);
-    cx.add_action(Editor::go_to_diagnostic);
+    cx.add_action(Editor::go_to_next_diagnostic);
+    cx.add_action(Editor::go_to_prev_diagnostic);
     cx.add_action(Editor::go_to_definition);
     cx.add_action(Editor::page_up);
     cx.add_action(Editor::page_down);
@@ -2940,8 +2956,8 @@ impl Editor {
         self.move_to_snippet_tabstop(Bias::Right, cx)
     }
 
-    pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) {
-        self.move_to_snippet_tabstop(Bias::Left, cx);
+    pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        self.move_to_snippet_tabstop(Bias::Left, cx)
     }
 
     pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext<Self>) -> bool {
@@ -3046,54 +3062,46 @@ impl Editor {
         });
     }
 
-    pub fn tab(&mut self, &Tab(direction): &Tab, cx: &mut ViewContext<Self>) {
-        match direction {
-            Direction::Prev => {
-                if !self.snippet_stack.is_empty() {
-                    self.move_to_prev_snippet_tabstop(cx);
-                    return;
-                }
+    pub fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
+        if self.move_to_prev_snippet_tabstop(cx) {
+            return;
+        }
 
-                self.outdent(&Outdent, cx);
-            }
-            Direction::Next => {
-                if self.move_to_next_snippet_tabstop(cx) {
-                    return;
-                }
+        self.outdent(&Outdent, cx);
+    }
 
-                let mut selections = self.local_selections::<Point>(cx);
-                if selections.iter().all(|s| s.is_empty()) {
-                    self.transact(cx, |this, cx| {
-                        this.buffer.update(cx, |buffer, cx| {
-                            for selection in &mut selections {
-                                let language_name =
-                                    buffer.language_at(selection.start, cx).map(|l| l.name());
-                                let tab_size =
-                                    cx.global::<Settings>().tab_size(language_name.as_deref());
-                                let char_column = buffer
-                                    .read(cx)
-                                    .text_for_range(
-                                        Point::new(selection.start.row, 0)..selection.start,
-                                    )
-                                    .flat_map(str::chars)
-                                    .count();
-                                let chars_to_next_tab_stop =
-                                    tab_size - (char_column as u32 % tab_size);
-                                buffer.edit(
-                                    [selection.start..selection.start],
-                                    " ".repeat(chars_to_next_tab_stop as usize),
-                                    cx,
-                                );
-                                selection.start.column += chars_to_next_tab_stop;
-                                selection.end = selection.start;
-                            }
-                        });
-                        this.update_selections(selections, Some(Autoscroll::Fit), cx);
-                    });
-                } else {
-                    self.indent(&Indent, cx);
-                }
-            }
+    pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+        if self.move_to_next_snippet_tabstop(cx) {
+            return;
+        }
+
+        let mut selections = self.local_selections::<Point>(cx);
+        if selections.iter().all(|s| s.is_empty()) {
+            self.transact(cx, |this, cx| {
+                this.buffer.update(cx, |buffer, cx| {
+                    for selection in &mut selections {
+                        let language_name =
+                            buffer.language_at(selection.start, cx).map(|l| l.name());
+                        let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
+                        let char_column = buffer
+                            .read(cx)
+                            .text_for_range(Point::new(selection.start.row, 0)..selection.start)
+                            .flat_map(str::chars)
+                            .count();
+                        let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
+                        buffer.edit(
+                            [selection.start..selection.start],
+                            " ".repeat(chars_to_next_tab_stop as usize),
+                            cx,
+                        );
+                        selection.start.column += chars_to_next_tab_stop;
+                        selection.end = selection.start;
+                    }
+                });
+                this.update_selections(selections, Some(Autoscroll::Fit), cx);
+            });
+        } else {
+            self.indent(&Indent, cx);
         }
     }
 
@@ -4237,7 +4245,6 @@ impl Editor {
 
     pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) {
         self.push_to_selection_history();
-        let replace_newest = action.0;
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
         let mut selections = self.local_selections::<usize>(cx);
@@ -4276,7 +4283,7 @@ impl Editor {
                 }
 
                 if let Some(next_selected_range) = next_selected_range {
-                    if replace_newest {
+                    if action.replace_newest {
                         if let Some(newest_id) =
                             selections.iter().max_by_key(|s| s.id).map(|s| s.id)
                         {
@@ -4547,11 +4554,15 @@ impl Editor {
         self.selection_history.mode = SelectionHistoryMode::Normal;
     }
 
-    pub fn go_to_diagnostic(
-        &mut self,
-        &GoToDiagnostic(direction): &GoToDiagnostic,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext<Self>) {
+        self.go_to_diagnostic(Direction::Next, cx)
+    }
+
+    fn go_to_prev_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext<Self>) {
+        self.go_to_diagnostic(Direction::Prev, cx)
+    }
+
+    pub fn go_to_diagnostic(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         let buffer = self.buffer.read(cx).snapshot(cx);
         let selection = self.newest_selection_with_snapshot::<usize>(&buffer);
         let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
@@ -7771,7 +7782,7 @@ mod tests {
             );
 
             // indent from mid-tabstop to full tabstop
-            view.tab(&Tab(Direction::Next), cx);
+            view.tab(&Tab, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7782,7 +7793,7 @@ mod tests {
             );
 
             // outdent from 1 tabstop to 0 tabstops
-            view.tab(&Tab(Direction::Prev), cx);
+            view.tab_prev(&TabPrev, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7803,7 +7814,7 @@ mod tests {
             );
 
             // indent and outdent affect only the preceding line
-            view.tab(&Tab(Direction::Next), cx);
+            view.tab(&Tab, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7812,7 +7823,7 @@ mod tests {
                     ] four"},
                 cx,
             );
-            view.tab(&Tab(Direction::Prev), cx);
+            view.tab_prev(&TabPrev, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7831,7 +7842,7 @@ mod tests {
                      four"},
                 cx,
             );
-            view.tab(&Tab(Direction::Next), cx);
+            view.tab(&Tab, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7849,7 +7860,7 @@ mod tests {
                      four"},
                 cx,
             );
-            view.tab(&Tab(Direction::Prev), cx);
+            view.tab_prev(&TabPrev, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7939,7 +7950,7 @@ mod tests {
                 cx,
             );
 
-            editor.tab(&Tab(Direction::Next), cx);
+            editor.tab(&Tab, cx);
             assert_text_with_selections(
                 &mut editor,
                 indoc! {"
@@ -7950,7 +7961,7 @@ mod tests {
                 "},
                 cx,
             );
-            editor.tab(&Tab(Direction::Prev), cx);
+            editor.tab_prev(&TabPrev, cx);
             assert_text_with_selections(
                 &mut editor,
                 indoc! {"
@@ -8693,10 +8704,20 @@ mod tests {
 
         view.update(cx, |view, cx| {
             view.select_ranges([ranges[1].start + 1..ranges[1].start + 1], None, cx);
-            view.select_next(&SelectNext(false), cx);
+            view.select_next(
+                &SelectNext {
+                    replace_newest: false,
+                },
+                cx,
+            );
             assert_eq!(view.selected_ranges(cx), &ranges[1..2]);
 
-            view.select_next(&SelectNext(false), cx);
+            view.select_next(
+                &SelectNext {
+                    replace_newest: false,
+                },
+                cx,
+            );
             assert_eq!(view.selected_ranges(cx), &ranges[1..3]);
 
             view.undo_selection(&UndoSelection, cx);
@@ -8705,10 +8726,20 @@ mod tests {
             view.redo_selection(&RedoSelection, cx);
             assert_eq!(view.selected_ranges(cx), &ranges[1..3]);
 
-            view.select_next(&SelectNext(false), cx);
+            view.select_next(
+                &SelectNext {
+                    replace_newest: false,
+                },
+                cx,
+            );
             assert_eq!(view.selected_ranges(cx), &ranges[1..4]);
 
-            view.select_next(&SelectNext(false), cx);
+            view.select_next(
+                &SelectNext {
+                    replace_newest: false,
+                },
+                cx,
+            );
             assert_eq!(view.selected_ranges(cx), &ranges[0..4]);
         });
     }

crates/file_finder/src/file_finder.rs 🔗

@@ -3,7 +3,7 @@ use fuzzy::PathMatch;
 use gpui::{
     actions,
     elements::*,
-    impl_actions,
+    impl_internal_actions,
     keymap::{self, Binding},
     AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
@@ -41,8 +41,8 @@ pub struct FileFinder {
 #[derive(Clone)]
 pub struct Select(pub ProjectPath);
 
-impl_actions!(file_finder, [Select]);
 actions!(file_finder, [Toggle]);
+impl_internal_actions!(file_finder, [Select]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(FileFinder::toggle);

crates/gpui/src/app.rs 🔗

@@ -715,12 +715,14 @@ type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
+type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
 
 pub struct MutableAppContext {
     weak_self: Option<rc::Weak<RefCell<Self>>>,
     foreground_platform: Rc<dyn platform::ForegroundPlatform>,
     assets: Arc<AssetCache>,
     cx: AppContext,
+    action_deserializers: HashMap<&'static str, DeserializeActionCallback>,
     capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
@@ -773,6 +775,7 @@ impl MutableAppContext {
                 font_cache,
                 platform,
             },
+            action_deserializers: HashMap::new(),
             capture_actions: HashMap::new(),
             actions: HashMap::new(),
             global_actions: HashMap::new(),
@@ -857,6 +860,18 @@ impl MutableAppContext {
             .and_then(|(presenter, _)| presenter.borrow().debug_elements(self))
     }
 
+    pub fn deserialize_action(
+        &self,
+        name: &str,
+        argument: Option<&str>,
+    ) -> Result<Box<dyn Action>> {
+        let callback = self
+            .action_deserializers
+            .get(name)
+            .ok_or_else(|| anyhow!("unknown action {}", name))?;
+        callback(argument.unwrap_or("{}"))
+    }
+
     pub fn add_action<A, V, F>(&mut self, handler: F)
     where
         A: Action,
@@ -899,6 +914,10 @@ impl MutableAppContext {
             },
         );
 
+        self.action_deserializers
+            .entry(A::qualified_name())
+            .or_insert(A::from_json_str);
+
         let actions = if capture {
             &mut self.capture_actions
         } else {
@@ -934,6 +953,10 @@ impl MutableAppContext {
             handler(action, cx);
         });
 
+        self.action_deserializers
+            .entry(A::qualified_name())
+            .or_insert(A::from_json_str);
+
         if self
             .global_actions
             .insert(TypeId::of::<A>(), handler)
@@ -4575,7 +4598,8 @@ impl RefCounts {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{elements::*, impl_actions};
+    use crate::{actions, elements::*, impl_actions};
+    use serde::Deserialize;
     use smol::future::poll_once;
     use std::{
         cell::Cell,
@@ -5683,6 +5707,42 @@ mod tests {
         );
     }
 
+    #[crate::test(self)]
+    fn test_deserialize_actions(cx: &mut MutableAppContext) {
+        #[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
+        pub struct ComplexAction {
+            arg: String,
+            count: usize,
+        }
+
+        actions!(test::something, [SimpleAction]);
+        impl_actions!(test::something, [ComplexAction]);
+
+        cx.add_global_action(move |_: &SimpleAction, _: &mut MutableAppContext| {});
+        cx.add_global_action(move |_: &ComplexAction, _: &mut MutableAppContext| {});
+
+        let action1 = cx
+            .deserialize_action(
+                "test::something::ComplexAction",
+                Some(r#"{"arg": "a", "count": 5}"#),
+            )
+            .unwrap();
+        let action2 = cx
+            .deserialize_action("test::something::SimpleAction", None)
+            .unwrap();
+        assert_eq!(
+            action1.as_any().downcast_ref::<ComplexAction>().unwrap(),
+            &ComplexAction {
+                arg: "a".to_string(),
+                count: 5,
+            }
+        );
+        assert_eq!(
+            action2.as_any().downcast_ref::<SimpleAction>().unwrap(),
+            &SimpleAction
+        );
+    }
+
     #[crate::test(self)]
     fn test_dispatch_action(cx: &mut MutableAppContext) {
         struct ViewA {
@@ -5721,32 +5781,32 @@ mod tests {
             }
         }
 
-        #[derive(Clone)]
-        pub struct Action(pub &'static str);
+        #[derive(Clone, Deserialize)]
+        pub struct Action(pub String);
 
         impl_actions!(test, [Action]);
 
         let actions = Rc::new(RefCell::new(Vec::new()));
 
-        {
+        cx.add_global_action({
             let actions = actions.clone();
-            cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| {
+            move |_: &Action, _: &mut MutableAppContext| {
                 actions.borrow_mut().push("global".to_string());
-            });
-        }
+            }
+        });
 
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut ViewA, action: &Action, cx| {
+            move |view: &mut ViewA, action: &Action, cx| {
                 assert_eq!(action.0, "bar");
                 cx.propagate_action();
                 actions.borrow_mut().push(format!("{} a", view.id));
-            });
-        }
+            }
+        });
 
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut ViewA, _: &Action, cx| {
+            move |view: &mut ViewA, _: &Action, cx| {
                 if view.id != 1 {
                     cx.add_view(|cx| {
                         cx.propagate_action(); // Still works on a nested ViewContext
@@ -5754,32 +5814,32 @@ mod tests {
                     });
                 }
                 actions.borrow_mut().push(format!("{} b", view.id));
-            });
-        }
+            }
+        });
 
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
+            move |view: &mut ViewB, _: &Action, cx| {
                 cx.propagate_action();
                 actions.borrow_mut().push(format!("{} c", view.id));
-            });
-        }
+            }
+        });
 
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
+            move |view: &mut ViewB, _: &Action, cx| {
                 cx.propagate_action();
                 actions.borrow_mut().push(format!("{} d", view.id));
-            });
-        }
+            }
+        });
 
-        {
+        cx.capture_action({
             let actions = actions.clone();
-            cx.capture_action(move |view: &mut ViewA, _: &Action, cx| {
+            move |view: &mut ViewA, _: &Action, cx| {
                 cx.propagate_action();
                 actions.borrow_mut().push(format!("{} capture", view.id));
-            });
-        }
+            }
+        });
 
         let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 });
         let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 });
@@ -5789,7 +5849,7 @@ mod tests {
         cx.dispatch_action(
             window_id,
             vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()],
-            &Action("bar"),
+            &Action("bar".to_string()),
         );
 
         assert_eq!(
@@ -5812,7 +5872,7 @@ mod tests {
         cx.dispatch_action(
             window_id,
             vec![view_2.id(), view_3.id(), view_4.id()],
-            &Action("bar"),
+            &Action("bar".to_string()),
         );
 
         assert_eq!(
@@ -5832,8 +5892,8 @@ mod tests {
 
     #[crate::test(self)]
     fn test_dispatch_keystroke(cx: &mut MutableAppContext) {
-        #[derive(Clone)]
-        pub struct Action(pub &'static str);
+        #[derive(Clone, Deserialize)]
+        pub struct Action(String);
 
         impl_actions!(test, [Action]);
 
@@ -5887,16 +5947,20 @@ mod tests {
         // "a" and "b" in its context, but not "c".
         cx.add_bindings(vec![keymap::Binding::new(
             "a",
-            Action("a"),
+            Action("a".to_string()),
             Some("a && b && !c"),
         )]);
 
-        cx.add_bindings(vec![keymap::Binding::new("b", Action("b"), None)]);
+        cx.add_bindings(vec![keymap::Binding::new(
+            "b",
+            Action("b".to_string()),
+            None,
+        )]);
 
         let actions = Rc::new(RefCell::new(Vec::new()));
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut View, action: &Action, cx| {
+            move |view: &mut View, action: &Action, cx| {
                 if action.0 == "a" {
                     actions.borrow_mut().push(format!("{} a", view.id));
                 } else {
@@ -5905,14 +5969,15 @@ mod tests {
                         .push(format!("{} {}", view.id, action.0));
                     cx.propagate_action();
                 }
-            });
-        }
-        {
+            }
+        });
+
+        cx.add_global_action({
             let actions = actions.clone();
-            cx.add_global_action(move |action: &Action, _| {
+            move |action: &Action, _| {
                 actions.borrow_mut().push(format!("global {}", action.0));
-            });
-        }
+            }
+        });
 
         cx.dispatch_keystroke(
             window_id,

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

@@ -2,55 +2,108 @@ use std::any::{Any, TypeId};
 
 pub trait Action: 'static {
     fn id(&self) -> TypeId;
-    fn namespace(&self) -> &'static str;
     fn name(&self) -> &'static str;
     fn as_any(&self) -> &dyn Any;
     fn boxed_clone(&self) -> Box<dyn Action>;
-    fn boxed_clone_as_any(&self) -> Box<dyn Any>;
+
+    fn qualified_name() -> &'static str
+    where
+        Self: Sized;
+    fn from_json_str(json: &str) -> anyhow::Result<Box<dyn Action>>
+    where
+        Self: Sized;
 }
 
+/// Define a set of unit struct types that all implement the `Action` trait.
+///
+/// The first argument is a namespace that will be associated with each of
+/// the given action types, to ensure that they have globally unique
+/// qualified names for use in keymap files.
 #[macro_export]
-macro_rules! impl_actions {
+macro_rules! actions {
     ($namespace:path, [ $($name:ident),* $(,)? ]) => {
         $(
-            impl $crate::action::Action for $name {
-                fn id(&self) -> std::any::TypeId {
-                    std::any::TypeId::of::<$name>()
-                }
-
-                fn namespace(&self) -> &'static str {
-                    stringify!($namespace)
-                }
-
-                fn name(&self) -> &'static str {
-                    stringify!($name)
-                }
-
-                fn as_any(&self) -> &dyn std::any::Any {
-                    self
-                }
-
-                fn boxed_clone(&self) -> Box<dyn $crate::action::Action> {
-                    Box::new(self.clone())
+            #[derive(Clone, Debug, Default, PartialEq, Eq)]
+            pub struct $name;
+            $crate::__impl_action! {
+                $namespace,
+                $name,
+                fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+                    Ok(Box::new(Self))
                 }
+            }
+        )*
+    };
+}
 
-                fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
-                    Box::new(self.clone())
+/// Implement the `Action` trait for a set of existing types.
+///
+/// The first argument is a namespace that will be associated with each of
+/// the given action types, to ensure that they have globally unique
+/// qualified names for use in keymap files.
+#[macro_export]
+macro_rules! impl_actions {
+    ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+        $(
+            $crate::__impl_action! {
+                $namespace,
+                $name,
+                fn from_json_str(json: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+                    Ok(Box::new($crate::serde_json::from_str::<Self>(json)?))
                 }
             }
         )*
     };
 }
 
+/// Implement the `Action` trait for a set of existing types that are
+/// not intended to be constructed via a keymap file, but only dispatched
+/// internally.
 #[macro_export]
-macro_rules! actions {
+macro_rules! impl_internal_actions {
     ($namespace:path, [ $($name:ident),* $(,)? ]) => {
-
         $(
-            #[derive(Clone, Debug, Default, PartialEq, Eq)]
-            pub struct $name;
+            $crate::__impl_action! {
+                $namespace,
+                $name,
+                fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+                    Err($crate::anyhow::anyhow!("internal action"))
+                }
+            }
         )*
+    };
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __impl_action {
+    ($namespace:path, $name:ident, $from_json_fn:item) => {
+        impl $crate::action::Action for $name {
+            fn name(&self) -> &'static str {
+                stringify!($name)
+            }
+
+            fn qualified_name() -> &'static str {
+                concat!(
+                    stringify!($namespace),
+                    "::",
+                    stringify!($name),
+                )
+            }
+
+            fn id(&self) -> std::any::TypeId {
+                std::any::TypeId::of::<$name>()
+            }
+
+            fn as_any(&self) -> &dyn std::any::Any {
+                self
+            }
+
+            fn boxed_clone(&self) -> Box<dyn $crate::Action> {
+                Box::new(self.clone())
+            }
 
-        $crate::impl_actions!($namespace, [ $($name),* ]);
+            $from_json_fn
+        }
     };
 }

crates/gpui/src/gpui.rs 🔗

@@ -33,3 +33,6 @@ pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, Prom
 pub use presenter::{
     Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
 };
+
+pub use anyhow;
+pub use serde_json;

crates/gpui/src/keymap.rs 🔗

@@ -328,6 +328,8 @@ impl ContextPredicate {
 
 #[cfg(test)]
 mod tests {
+    use serde::Deserialize;
+
     use crate::{actions, impl_actions};
 
     use super::*;
@@ -419,30 +421,18 @@ mod tests {
 
     #[test]
     fn test_matcher() -> anyhow::Result<()> {
-        #[derive(Clone)]
-        pub struct A(pub &'static str);
+        #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+        pub struct A(pub String);
         impl_actions!(test, [A]);
         actions!(test, [B, Ab]);
 
-        impl PartialEq for A {
-            fn eq(&self, other: &Self) -> bool {
-                self.0 == other.0
-            }
-        }
-        impl Eq for A {}
-        impl Debug for A {
-            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-                write!(f, "A({:?})", &self.0)
-            }
-        }
-
         #[derive(Clone, Debug, Eq, PartialEq)]
         struct ActionArg {
             a: &'static str,
         }
 
         let keymap = Keymap(vec![
-            Binding::new("a", A("x"), Some("a")),
+            Binding::new("a", A("x".to_string()), Some("a")),
             Binding::new("b", B, Some("a")),
             Binding::new("a b", Ab, Some("a || b")),
         ]);
@@ -456,40 +446,54 @@ mod tests {
         let mut matcher = Matcher::new(keymap);
 
         // Basic match
-        assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
+        assert_eq!(
+            downcast(&matcher.test_keystroke("a", 1, &ctx_a)),
+            Some(&A("x".to_string()))
+        );
 
         // Multi-keystroke match
-        assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
-        assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
+        assert!(matcher.test_keystroke("a", 1, &ctx_b).is_none());
+        assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_b)), Some(&Ab));
 
         // Failed matches don't interfere with matching subsequent keys
-        assert_eq!(matcher.test_keystroke::<A>("x", 1, &ctx_a), None);
-        assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
+        assert!(matcher.test_keystroke("x", 1, &ctx_a).is_none());
+        assert_eq!(
+            downcast(&matcher.test_keystroke("a", 1, &ctx_a)),
+            Some(&A("x".to_string()))
+        );
 
         // Pending keystrokes are cleared when the context changes
-        assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
-        assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B));
+        assert!(&matcher.test_keystroke("a", 1, &ctx_b).is_none());
+        assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_a)), Some(&B));
 
         let mut ctx_c = Context::default();
         ctx_c.set.insert("c".into());
 
         // Pending keystrokes are maintained per-view
-        assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
-        assert_eq!(matcher.test_keystroke::<A>("a", 2, &ctx_c), None);
-        assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
+        assert!(matcher.test_keystroke("a", 1, &ctx_b).is_none());
+        assert!(matcher.test_keystroke("a", 2, &ctx_c).is_none());
+        assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_b)), Some(&Ab));
 
         Ok(())
     }
 
+    fn downcast<'a, A: Action>(action: &'a Option<Box<dyn Action>>) -> Option<&'a A> {
+        action
+            .as_ref()
+            .and_then(|action| action.as_any().downcast_ref())
+    }
+
     impl Matcher {
-        fn test_keystroke<A>(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option<A>
-        where
-            A: Action + Debug + Eq,
-        {
+        fn test_keystroke(
+            &mut self,
+            keystroke: &str,
+            view_id: usize,
+            cx: &Context,
+        ) -> Option<Box<dyn Action>> {
             if let MatchResult::Action(action) =
                 self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx)
             {
-                Some(*action.boxed_clone_as_any().downcast().unwrap())
+                Some(action.boxed_clone())
             } else {
                 None
             }

crates/gpui/src/views/select.rs 🔗

@@ -1,3 +1,5 @@
+use serde::Deserialize;
+
 use crate::{
     actions, elements::*, impl_actions, AppContext, Entity, MutableAppContext, RenderContext, View,
     ViewContext, WeakViewHandle,
@@ -25,7 +27,7 @@ pub enum ItemType {
     Unselected,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct SelectItem(pub usize);
 
 actions!(select, [ToggleSelect]);

crates/project_panel/src/project_panel.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
         Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
         Svg, UniformList, UniformListState,
     },
-    impl_actions,
+    impl_internal_actions,
     keymap::{self, Binding},
     platform::CursorStyle,
     AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
@@ -54,7 +54,7 @@ pub struct ToggleExpanded(pub ProjectEntryId);
 pub struct Open(pub ProjectEntryId);
 
 actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]);
-impl_actions!(project_panel, [Open, ToggleExpanded]);
+impl_internal_actions!(project_panel, [Open, ToggleExpanded]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectPanel::expand_selected_entry);

crates/search/Cargo.toml 🔗

@@ -20,6 +20,7 @@ workspace = { path = "../workspace" }
 anyhow = "1.0"
 log = "0.4"
 postage = { version = "0.4.1", features = ["futures-traits"] }
+serde = { version = "1", features = ["derive"] }
 
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }

crates/search/src/buffer_search.rs 🔗

@@ -1,25 +1,32 @@
-use crate::{active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch};
+use crate::{
+    active_match_index, match_index_for_direction, Direction, SearchOption, SelectNextMatch,
+    SelectPrevMatch,
+};
 use collections::HashMap;
 use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
 use gpui::{
-    actions, elements::*, impl_actions, keymap::Binding, platform::CursorStyle, AppContext, Entity,
-    MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle,
+    actions, elements::*, impl_actions, impl_internal_actions, keymap::Binding,
+    platform::CursorStyle, AppContext, Entity, MutableAppContext, RenderContext, Subscription,
+    Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::OffsetRangeExt;
 use project::search::SearchQuery;
+use serde::Deserialize;
 use settings::Settings;
 use std::ops::Range;
 use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView};
 
-#[derive(Clone)]
-pub struct Deploy(pub bool);
+#[derive(Clone, Deserialize)]
+pub struct Deploy {
+    pub focus: bool,
+}
 
 #[derive(Clone)]
 pub struct ToggleSearchOption(pub SearchOption);
 
 actions!(buffer_search, [Dismiss, FocusEditor]);
-impl_actions!(buffer_search, [Deploy, ToggleSearchOption]);
+impl_actions!(buffer_search, [Deploy]);
+impl_internal_actions!(buffer_search, [ToggleSearchOption]);
 
 pub enum Event {
     UpdateLocation,
@@ -27,29 +34,31 @@ pub enum Event {
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_bindings([
-        Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
-        Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
-        Binding::new("escape", Dismiss, Some("BufferSearchBar")),
-        Binding::new("cmd-f", FocusEditor, Some("BufferSearchBar")),
         Binding::new(
-            "enter",
-            SelectMatch(Direction::Next),
-            Some("BufferSearchBar"),
+            "cmd-f",
+            Deploy { focus: true },
+            Some("Editor && mode == full"),
         ),
         Binding::new(
-            "shift-enter",
-            SelectMatch(Direction::Prev),
-            Some("BufferSearchBar"),
+            "cmd-e",
+            Deploy { focus: false },
+            Some("Editor && mode == full"),
         ),
-        Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
-        Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
+        Binding::new("escape", Dismiss, Some("BufferSearchBar")),
+        Binding::new("cmd-f", FocusEditor, Some("BufferSearchBar")),
+        Binding::new("enter", SelectNextMatch, Some("BufferSearchBar")),
+        Binding::new("shift-enter", SelectPrevMatch, Some("BufferSearchBar")),
+        Binding::new("cmd-g", SelectNextMatch, Some("Pane")),
+        Binding::new("cmd-shift-G", SelectPrevMatch, Some("Pane")),
     ]);
     cx.add_action(BufferSearchBar::deploy);
     cx.add_action(BufferSearchBar::dismiss);
     cx.add_action(BufferSearchBar::focus_editor);
     cx.add_action(BufferSearchBar::toggle_search_option);
-    cx.add_action(BufferSearchBar::select_match);
-    cx.add_action(BufferSearchBar::select_match_on_pane);
+    cx.add_action(BufferSearchBar::select_next_match);
+    cx.add_action(BufferSearchBar::select_prev_match);
+    cx.add_action(BufferSearchBar::select_next_match_on_pane);
+    cx.add_action(BufferSearchBar::select_prev_match_on_pane);
 }
 
 pub struct BufferSearchBar {
@@ -325,14 +334,17 @@ impl BufferSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
+        .on_click(move |cx| match direction {
+            Direction::Prev => cx.dispatch_action(SelectPrevMatch),
+            Direction::Next => cx.dispatch_action(SelectNextMatch),
+        })
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }
 
-    fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext<Pane>) {
+    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
-            if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) {
+            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, cx)) {
                 return;
             }
         }
@@ -368,7 +380,15 @@ impl BufferSearchBar {
         cx.notify();
     }
 
-    fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
+    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
+        self.select_match(Direction::Next, cx);
+    }
+
+    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
+        self.select_match(Direction::Prev, cx);
+    }
+
+    fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         if let Some(index) = self.active_match_index {
             if let Some(editor) = self.active_editor.as_ref() {
                 editor.update(cx, |editor, cx| {
@@ -389,9 +409,23 @@ impl BufferSearchBar {
         }
     }
 
-    fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
+    fn select_next_match_on_pane(
+        pane: &mut Pane,
+        action: &SelectNextMatch,
+        cx: &mut ViewContext<Pane>,
+    ) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
+        }
+    }
+
+    fn select_prev_match_on_pane(
+        pane: &mut Pane,
+        action: &SelectPrevMatch,
+        cx: &mut ViewContext<Pane>,
+    ) {
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
-            search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
+            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
         }
     }
 
@@ -699,7 +733,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(0));
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -710,7 +744,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -721,7 +755,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -732,7 +766,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -743,7 +777,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -754,7 +788,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -765,7 +799,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -782,7 +816,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(1));
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -799,7 +833,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(1));
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -816,7 +850,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(2));
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -833,7 +867,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(2));
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -850,7 +884,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(0));
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]

crates/search/src/project_search.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch,
-    ToggleSearchOption,
+    active_match_index, match_index_for_direction, Direction, SearchOption, SelectNextMatch,
+    SelectPrevMatch, ToggleSearchOption,
 };
 use collections::HashMap;
 use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
@@ -34,14 +34,15 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
         Binding::new("enter", Search, Some("ProjectSearchBar")),
         Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchBar")),
-        Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
-        Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
+        Binding::new("cmd-g", SelectNextMatch, Some("Pane")),
+        Binding::new("cmd-shift-G", SelectPrevMatch, Some("Pane")),
     ]);
     cx.add_action(ProjectSearchView::deploy);
     cx.add_action(ProjectSearchBar::search);
     cx.add_action(ProjectSearchBar::search_in_new);
     cx.add_action(ProjectSearchBar::toggle_search_option);
-    cx.add_action(ProjectSearchBar::select_match);
+    cx.add_action(ProjectSearchBar::select_next_match);
+    cx.add_action(ProjectSearchBar::select_prev_match);
     cx.add_action(ProjectSearchBar::toggle_focus);
     cx.capture_action(ProjectSearchBar::tab);
 }
@@ -545,18 +546,23 @@ impl ProjectSearchBar {
         }
     }
 
-    fn select_match(
-        pane: &mut Pane,
-        &SelectMatch(direction): &SelectMatch,
-        cx: &mut ViewContext<Pane>,
-    ) {
+    fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext<Pane>) {
         if let Some(search_view) = pane
             .active_item()
             .and_then(|item| item.downcast::<ProjectSearchView>())
         {
-            search_view.update(cx, |search_view, cx| {
-                search_view.select_match(direction, cx);
-            });
+            search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx));
+        } else {
+            cx.propagate_action();
+        }
+    }
+
+    fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx));
         } else {
             cx.propagate_action();
         }
@@ -635,7 +641,10 @@ impl ProjectSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
+        .on_click(move |cx| match direction {
+            Direction::Prev => cx.dispatch_action(SelectPrevMatch),
+            Direction::Next => cx.dispatch_action(SelectNextMatch),
+        })
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }

crates/search/src/search.rs 🔗

@@ -1,6 +1,6 @@
 pub use buffer_search::BufferSearchBar;
 use editor::{Anchor, MultiBufferSnapshot};
-use gpui::{impl_actions, MutableAppContext};
+use gpui::{actions, impl_internal_actions, MutableAppContext};
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
 use std::{
     cmp::{self, Ordering},
@@ -18,10 +18,8 @@ pub fn init(cx: &mut MutableAppContext) {
 #[derive(Clone)]
 pub struct ToggleSearchOption(pub SearchOption);
 
-#[derive(Clone)]
-pub struct SelectMatch(pub Direction);
-
-impl_actions!(search, [ToggleSearchOption, SelectMatch]);
+actions!(search, [SelectNextMatch, SelectPrevMatch]);
+impl_internal_actions!(search, [ToggleSearchOption]);
 
 #[derive(Clone, Copy)]
 pub enum SearchOption {

crates/server/Cargo.toml 🔗

@@ -64,6 +64,7 @@ language = { path = "../language", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
+theme = { path = "../theme" }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"

crates/server/src/rpc.rs 🔗

@@ -1117,6 +1117,7 @@ mod tests {
         },
         time::Duration,
     };
+    use theme::ThemeRegistry;
     use util::TryFutureExt;
     use workspace::{Item, SplitDirection, ToggleFollow, Workspace, WorkspaceParams};
 
@@ -5633,6 +5634,7 @@ mod tests {
                         project: project.clone(),
                         user_store: self.user_store.clone(),
                         languages: self.language_registry.clone(),
+                        themes: ThemeRegistry::new((), cx.font_cache().clone()),
                         channel_list: cx.add_model(|cx| {
                             ChannelList::new(self.user_store.clone(), self.client.clone(), cx)
                         }),

crates/theme_selector/src/theme_selector.rs 🔗

@@ -1,8 +1,8 @@
 use editor::Editor;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
+    actions,
     elements::*,
-    impl_actions,
     keymap::{self, Binding},
     AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
     ViewContext, ViewHandle,
@@ -25,15 +25,9 @@ pub struct ThemeSelector {
     selection_completed: bool,
 }
 
-#[derive(Clone)]
-pub struct Toggle(pub Arc<ThemeRegistry>);
+actions!(theme_selector, [Toggle, Reload]);
 
-#[derive(Clone)]
-pub struct Reload(pub Arc<ThemeRegistry>);
-
-impl_actions!(theme_selector, [Toggle, Reload]);
-
-pub fn init(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
+pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ThemeSelector::confirm);
     cx.add_action(ThemeSelector::select_prev);
     cx.add_action(ThemeSelector::select_next);
@@ -41,9 +35,9 @@ pub fn init(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
     cx.add_action(ThemeSelector::reload);
 
     cx.add_bindings(vec![
-        Binding::new("cmd-k cmd-t", Toggle(themes.clone()), None),
-        Binding::new("cmd-k t", Reload(themes.clone()), None),
-        Binding::new("escape", Toggle(themes.clone()), Some("ThemeSelector")),
+        Binding::new("cmd-k cmd-t", Toggle, None),
+        Binding::new("cmd-k t", Reload, None),
+        Binding::new("escape", Toggle, Some("ThemeSelector")),
     ]);
 }
 
@@ -79,18 +73,20 @@ impl ThemeSelector {
         this
     }
 
-    fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
+    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+        let themes = workspace.themes();
         workspace.toggle_modal(cx, |cx, _| {
-            let selector = cx.add_view(|cx| Self::new(action.0.clone(), cx));
+            let selector = cx.add_view(|cx| Self::new(themes, cx));
             cx.subscribe(&selector, Self::on_event).detach();
             selector
         });
     }
 
-    fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
+    fn reload(workspace: &mut Workspace, _: &Reload, cx: &mut ViewContext<Workspace>) {
         let current_theme_name = cx.global::<Settings>().theme.name.clone();
-        action.0.clear();
-        match action.0.get(&current_theme_name) {
+        let themes = workspace.themes();
+        themes.clear();
+        match themes.get(&current_theme_name) {
             Ok(theme) => {
                 Self::set_theme(theme, cx);
                 log::info!("reloaded theme {}", current_theme_name);

crates/vim/Cargo.toml 🔗

@@ -12,6 +12,7 @@ collections = { path = "../collections" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+serde = { version = "1", features = ["derive"] }
 settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 log = "0.4"

crates/vim/src/mode.rs 🔗

@@ -1,7 +1,8 @@
 use editor::CursorShape;
 use gpui::keymap::Context;
+use serde::Deserialize;
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
 pub enum Mode {
     Normal(NormalState),
     Insert,
@@ -44,7 +45,7 @@ impl Default for Mode {
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
 pub enum NormalState {
     None,
     GPrefix,

crates/vim/src/normal.rs 🔗

@@ -4,15 +4,16 @@ use crate::{mode::NormalState, Mode, SwitchMode, VimState};
 use editor::{char_kind, movement, Bias};
 use gpui::{actions, impl_actions, keymap::Binding, MutableAppContext, ViewContext};
 use language::SelectionGoal;
+use serde::Deserialize;
 use workspace::Workspace;
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 struct MoveToNextWordStart(pub bool);
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 struct MoveToNextWordEnd(pub bool);
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 struct MoveToPreviousWordStart(pub bool);
 
 impl_actions!(

crates/vim/src/vim.rs 🔗

@@ -8,12 +8,13 @@ mod vim_test_context;
 use collections::HashMap;
 use editor::{CursorShape, Editor};
 use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
+use serde::Deserialize;
 
 use mode::Mode;
 use settings::Settings;
 use workspace::{self, Workspace};
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct SwitchMode(pub Mode);
 
 impl_actions!(vim, [SwitchMode]);

crates/workspace/src/pane.rs 🔗

@@ -7,13 +7,14 @@ use gpui::{
     actions,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    impl_actions,
+    impl_actions, impl_internal_actions,
     keymap::Binding,
     platform::{CursorStyle, NavigationDirection},
     AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{ProjectEntryId, ProjectPath};
+use serde::Deserialize;
 use settings::Settings;
 use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
 use util::ResultExt;
@@ -28,13 +29,16 @@ actions!(
     ]
 );
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct Split(pub SplitDirection);
 
 #[derive(Clone)]
-pub struct CloseItem(pub CloseItemParams);
+pub struct CloseItem {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct ActivateItem(pub usize);
 
 #[derive(Clone)]
@@ -43,13 +47,8 @@ pub struct GoBack(pub Option<WeakViewHandle<Pane>>);
 #[derive(Clone)]
 pub struct GoForward(pub Option<WeakViewHandle<Pane>>);
 
-impl_actions!(pane, [Split, CloseItem, ActivateItem, GoBack, GoForward,]);
-
-#[derive(Clone)]
-pub struct CloseItemParams {
-    pub item_id: usize,
-    pub pane: WeakViewHandle<Pane>,
-}
+impl_actions!(pane, [Split]);
+impl_internal_actions!(pane, [CloseItem, ActivateItem, GoBack, GoForward]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -66,8 +65,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_async_action(Pane::close_active_item);
     cx.add_async_action(Pane::close_inactive_items);
     cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
-        let pane = action.0.pane.upgrade(cx)?;
-        Some(Pane::close_item(workspace, pane, action.0.item_id, cx))
+        let pane = action.pane.upgrade(cx)?;
+        Some(Pane::close_item(workspace, pane, action.item_id, cx))
     });
     cx.add_action(|pane: &mut Pane, action: &Split, cx| {
         pane.split(action.0, cx);
@@ -747,10 +746,10 @@ impl Pane {
                                             .on_click({
                                                 let pane = pane.clone();
                                                 move |cx| {
-                                                    cx.dispatch_action(CloseItem(CloseItemParams {
+                                                    cx.dispatch_action(CloseItem {
                                                         item_id,
                                                         pane: pane.clone(),
-                                                    }))
+                                                    })
                                                 }
                                             })
                                             .named("close-tab-icon")

crates/workspace/src/pane_group.rs 🔗

@@ -4,6 +4,7 @@ use client::PeerId;
 use collections::HashMap;
 use gpui::{elements::*, Axis, Border, ViewHandle};
 use project::Collaborator;
+use serde::Deserialize;
 use theme::Theme;
 
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -254,7 +255,8 @@ impl PaneAxis {
     }
 }
 
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Deserialize)]
+#[serde(rename_all = "snake_case")]
 pub enum SplitDirection {
     Up,
     Down,

crates/workspace/src/sidebar.rs 🔗

@@ -1,5 +1,7 @@
 use super::Workspace;
-use gpui::{elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, RenderContext};
+use gpui::{
+    elements::*, impl_internal_actions, platform::CursorStyle, AnyViewHandle, RenderContext,
+};
 use std::{cell::RefCell, rc::Rc};
 use theme::Theme;
 
@@ -27,7 +29,7 @@ pub struct ToggleSidebarItem(pub SidebarItemId);
 #[derive(Clone)]
 pub struct ToggleSidebarItemFocus(pub SidebarItemId);
 
-impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
+impl_internal_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
 
 #[derive(Clone)]
 pub struct SidebarItemId {

crates/workspace/src/workspace.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{
     color::Color,
     elements::*,
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
-    impl_actions,
+    impl_internal_actions,
     json::{self, to_string_pretty, ToJson},
     keymap::Binding,
     platform::{CursorStyle, WindowOptions},
@@ -101,7 +101,7 @@ pub struct ToggleFollow(pub PeerId);
 #[derive(Clone)]
 pub struct JoinProject(pub JoinProjectParams);
 
-impl_actions!(
+impl_internal_actions!(
     workspace,
     [Open, OpenNew, OpenPaths, ToggleFollow, JoinProject]
 );
@@ -630,6 +630,7 @@ pub struct WorkspaceParams {
     pub client: Arc<Client>,
     pub fs: Arc<dyn Fs>,
     pub languages: Arc<LanguageRegistry>,
+    pub themes: Arc<ThemeRegistry>,
     pub user_store: ModelHandle<UserStore>,
     pub channel_list: ModelHandle<ChannelList>,
 }
@@ -659,6 +660,7 @@ impl WorkspaceParams {
             channel_list: cx
                 .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
             client,
+            themes: ThemeRegistry::new((), cx.font_cache().clone()),
             fs,
             languages,
             user_store,
@@ -677,6 +679,7 @@ impl WorkspaceParams {
             ),
             client: app_state.client.clone(),
             fs: app_state.fs.clone(),
+            themes: app_state.themes.clone(),
             languages: app_state.languages.clone(),
             user_store: app_state.user_store.clone(),
             channel_list: app_state.channel_list.clone(),
@@ -694,6 +697,7 @@ pub struct Workspace {
     user_store: ModelHandle<client::UserStore>,
     remote_entity_subscription: Option<Subscription>,
     fs: Arc<dyn Fs>,
+    themes: Arc<ThemeRegistry>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     left_sidebar: Sidebar,
@@ -802,6 +806,7 @@ impl Workspace {
             remote_entity_subscription: None,
             user_store: params.user_store.clone(),
             fs: params.fs.clone(),
+            themes: params.themes.clone(),
             left_sidebar: Sidebar::new(Side::Left),
             right_sidebar: Sidebar::new(Side::Right),
             project: params.project.clone(),
@@ -834,6 +839,10 @@ impl Workspace {
         &self.project
     }
 
+    pub fn themes(&self) -> Arc<ThemeRegistry> {
+        self.themes.clone()
+    }
+
     pub fn worktrees<'a>(
         &self,
         cx: &'a AppContext,

crates/zed/src/main.rs 🔗

@@ -146,7 +146,7 @@ fn main() {
         });
         journal::init(app_state.clone(), cx);
         zed::init(&app_state, cx);
-        theme_selector::init(app_state.themes.clone(), cx);
+        theme_selector::init(cx);
 
         cx.set_menus(menus::menus(&app_state.clone()));
 

crates/zed/src/zed.rs 🔗

@@ -14,7 +14,6 @@ pub use editor;
 use gpui::{
     actions,
     geometry::vector::vec2f,
-    impl_actions,
     keymap::Binding,
     platform::{WindowBounds, WindowOptions},
     ModelHandle, ViewContext,
@@ -30,12 +29,16 @@ use std::{path::PathBuf, sync::Arc};
 pub use workspace;
 use workspace::{AppState, Workspace, WorkspaceParams};
 
-actions!(zed, [About, Quit, OpenSettings]);
-
-#[derive(Clone)]
-pub struct AdjustBufferFontSize(pub f32);
-
-impl_actions!(zed, [AdjustBufferFontSize]);
+actions!(
+    zed,
+    [
+        About,
+        Quit,
+        OpenSettings,
+        IncreaseBufferFontSize,
+        DecreaseBufferFontSize
+    ]
+);
 
 const MIN_FONT_SIZE: f32 = 6.0;
 
@@ -48,16 +51,18 @@ lazy_static! {
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     cx.add_global_action(quit);
-    cx.add_global_action({
-        move |action: &AdjustBufferFontSize, cx| {
-            cx.update_global::<Settings, _, _>(|settings, cx| {
-                settings.buffer_font_size =
-                    (settings.buffer_font_size + action.0).max(MIN_FONT_SIZE);
-                cx.refresh_windows();
-            });
-        }
+    cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
+        cx.update_global::<Settings, _, _>(|settings, cx| {
+            settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
+            cx.refresh_windows();
+        });
+    });
+    cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
+        cx.update_global::<Settings, _, _>(|settings, cx| {
+            settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
+            cx.refresh_windows();
+        });
     });
-
     cx.add_action({
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
@@ -100,8 +105,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     workspace::lsp_status::init(cx);
 
     cx.add_bindings(vec![
-        Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
-        Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
+        Binding::new("cmd-=", IncreaseBufferFontSize, None),
+        Binding::new("cmd--", DecreaseBufferFontSize, None),
         Binding::new("cmd-,", OpenSettings, None),
     ])
 }
@@ -134,6 +139,7 @@ pub fn build_workspace(
         client: app_state.client.clone(),
         fs: app_state.fs.clone(),
         languages: app_state.languages.clone(),
+        themes: app_state.themes.clone(),
         user_store: app_state.user_store.clone(),
         channel_list: app_state.channel_list.clone(),
     };