Get project compiling with type-safe actions

Nathan Sobo created

Change summary

gpui/src/app.rs                   |   2 
gpui/src/keymap.rs                |  18 ++-
gpui/src/platform/mac/platform.rs |   1 
server/src/rpc.rs                 |   4 
zed/src/file_finder.rs            |  85 ++++++++++---------
zed/src/lib.rs                    |   8 +
zed/src/main.rs                   |  13 +-
zed/src/menus.rs                  |  37 +++-----
zed/src/theme_selector.rs         |  54 ++++++------
zed/src/workspace.rs              | 138 ++++++++++++++++++--------------
zed/src/workspace/pane.rs         |  58 ++++++-------
zed/src/workspace/pane_group.rs   |   2 
zed/src/workspace/sidebar.rs      |  15 ++
13 files changed, 229 insertions(+), 206 deletions(-)

Detailed changes

gpui/src/app.rs πŸ”—

@@ -132,7 +132,7 @@ impl AnyAction for () {
 #[macro_export]
 macro_rules! action {
     ($name:ident, $arg:ty) => {
-        #[derive(Clone, Debug)]
+        #[derive(Clone)]
         pub struct $name(pub $arg);
 
         impl $crate::Action for $name {

gpui/src/keymap.rs πŸ”—

@@ -2,6 +2,7 @@ use anyhow::anyhow;
 use std::{
     any::Any,
     collections::{HashMap, HashSet},
+    fmt::Debug,
 };
 use tree_sitter::{Language, Node, Parser};
 
@@ -148,7 +149,7 @@ impl Keymap {
     }
 }
 
-mod menu {
+pub mod menu {
     use crate::action;
 
     action!(SelectPrev);
@@ -425,6 +426,11 @@ mod tests {
             }
         }
         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 {
@@ -472,12 +478,10 @@ mod tests {
     }
 
     impl Matcher {
-        fn test_keystroke<A: Action + Eq>(
-            &mut self,
-            keystroke: &str,
-            view_id: usize,
-            cx: &Context,
-        ) -> Option<A> {
+        fn test_keystroke<A>(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option<A>
+        where
+            A: Action + Debug + Eq,
+        {
             if let MatchResult::Action(action) =
                 self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx)
             {

server/src/rpc.rs πŸ”—

@@ -934,7 +934,7 @@ mod tests {
     use std::{path::Path, sync::Arc, time::Duration};
     use zed::{
         channel::{Channel, ChannelDetails, ChannelList},
-        editor::Editor,
+        editor::{Editor, Insert},
         fs::{FakeFs, Fs as _},
         language::LanguageRegistry,
         rpc::Client,
@@ -1023,7 +1023,7 @@ mod tests {
 
         // Edit the buffer as client B and see that edit as client A.
         editor_b.update(&mut cx_b, |editor, cx| {
-            editor.insert(&"ok, ".to_string(), cx)
+            editor.insert(&Insert("ok, ".into()), cx)
         });
         buffer_a
             .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")

zed/src/file_finder.rs πŸ”—

@@ -6,8 +6,13 @@ use crate::{
     worktree::{match_paths, PathMatch},
 };
 use gpui::{
+    action,
     elements::*,
-    keymap::{self, Binding},
+    keymap::{
+        self,
+        menu::{SelectNext, SelectPrev},
+        Binding,
+    },
     AppContext, Axis, Entity, MutableAppContext, RenderContext, Task, View, ViewContext,
     ViewHandle, WeakViewHandle,
 };
@@ -36,17 +41,27 @@ pub struct FileFinder {
     list_state: UniformListState,
 }
 
+action!(Toggle);
+action!(Confirm);
+action!(Select, Entry);
+
+#[derive(Clone)]
+pub struct Entry {
+    worktree_id: usize,
+    path: Arc<Path>,
+}
+
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action("file_finder:toggle", FileFinder::toggle);
-    cx.add_action("file_finder:confirm", FileFinder::confirm);
-    cx.add_action("file_finder:select", FileFinder::select);
-    cx.add_action("menu:select_prev", FileFinder::select_prev);
-    cx.add_action("menu:select_next", FileFinder::select_next);
+    cx.add_action(FileFinder::toggle);
+    cx.add_action(FileFinder::confirm);
+    cx.add_action(FileFinder::select);
+    cx.add_action(FileFinder::select_prev);
+    cx.add_action(FileFinder::select_next);
 
     cx.add_bindings(vec![
-        Binding::new("cmd-p", "file_finder:toggle", None),
-        Binding::new("escape", "file_finder:toggle", Some("FileFinder")),
-        Binding::new("enter", "file_finder:confirm", Some("FileFinder")),
+        Binding::new("cmd-p", Toggle, None),
+        Binding::new("escape", Toggle, Some("FileFinder")),
+        Binding::new("enter", Confirm, Some("FileFinder")),
     ]);
 }
 
@@ -196,10 +211,13 @@ impl FileFinder {
         )
         .with_style(&style.container);
 
-        let entry = (path_match.tree_id, path_match.path.clone());
+        let action = Select(Entry {
+            worktree_id: path_match.tree_id,
+            path: path_match.path.clone(),
+        });
         EventHandler::new(container.boxed())
             .on_mouse_down(move |cx| {
-                cx.dispatch_action("file_finder:select", entry.clone());
+                cx.dispatch_action(action.clone());
                 true
             })
             .named("match")
@@ -230,7 +248,7 @@ impl FileFinder {
         (file_name, file_name_positions, full_path, path_positions)
     }
 
-    fn toggle(workspace: &mut Workspace, _: &(), cx: &mut ViewContext<Workspace>) {
+    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
         workspace.toggle_modal(cx, |cx, workspace| {
             let handle = cx.handle();
             let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx));
@@ -328,7 +346,7 @@ impl FileFinder {
         0
     }
 
-    fn select_prev(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
         let mut selected_index = self.selected_index();
         if selected_index > 0 {
             selected_index -= 1;
@@ -339,7 +357,7 @@ impl FileFinder {
         cx.notify();
     }
 
-    fn select_next(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
         let mut selected_index = self.selected_index();
         if selected_index + 1 < self.matches.len() {
             selected_index += 1;
@@ -350,14 +368,14 @@ impl FileFinder {
         cx.notify();
     }
 
-    fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some(m) = self.matches.get(self.selected_index()) {
             cx.emit(Event::Selected(m.tree_id, m.path.clone()));
         }
     }
 
-    fn select(&mut self, (tree_id, path): &(usize, Arc<Path>), cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Selected(*tree_id, path.clone()));
+    fn select(&mut self, Select(entry): &Select, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Selected(entry.worktree_id, entry.path.clone()));
     }
 
     #[must_use]
@@ -417,7 +435,7 @@ impl FileFinder {
 mod tests {
     use super::*;
     use crate::{
-        editor,
+        editor::{self, Insert},
         fs::FakeFs,
         test::{build_app_state, temp_tree},
         workspace::Workspace,
@@ -447,12 +465,7 @@ mod tests {
             .unwrap();
         cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
             .await;
-        cx.dispatch_action(
-            window_id,
-            vec![workspace.id()],
-            "file_finder:toggle".into(),
-            (),
-        );
+        cx.dispatch_action(window_id, vec![workspace.id()], Toggle);
 
         let finder = cx.read(|cx| {
             workspace
@@ -466,26 +479,16 @@ mod tests {
         let query_buffer = cx.read(|cx| finder.read(cx).query_buffer.clone());
 
         let chain = vec![finder.id(), query_buffer.id()];
-        cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string());
-        cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string());
-        cx.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string());
+        cx.dispatch_action(window_id, chain.clone(), Insert("b".into()));
+        cx.dispatch_action(window_id, chain.clone(), Insert("n".into()));
+        cx.dispatch_action(window_id, chain.clone(), Insert("a".into()));
         finder
             .condition(&cx, |finder, _| finder.matches.len() == 2)
             .await;
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(
-            window_id,
-            vec![workspace.id(), finder.id()],
-            "menu:select_next",
-            (),
-        );
-        cx.dispatch_action(
-            window_id,
-            vec![workspace.id(), finder.id()],
-            "file_finder:confirm",
-            (),
-        );
+        cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], SelectNext);
+        cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], Confirm);
         active_pane
             .condition(&cx, |pane, _| pane.active_item().is_some())
             .await;
@@ -648,9 +651,9 @@ mod tests {
         finder.update(&mut cx, |f, cx| {
             assert_eq!(f.matches.len(), 2);
             assert_eq!(f.selected_index(), 0);
-            f.select_next(&(), cx);
+            f.select_next(&SelectNext, cx);
             assert_eq!(f.selected_index(), 1);
-            f.select_prev(&(), cx);
+            f.select_prev(&SelectPrev, cx);
             assert_eq!(f.selected_index(), 0);
         });
     }

zed/src/lib.rs πŸ”—

@@ -19,12 +19,16 @@ mod util;
 pub mod workspace;
 pub mod worktree;
 
+use gpui::action;
 pub use settings::Settings;
 
 use parking_lot::Mutex;
 use postage::watch;
 use std::sync::Arc;
 
+action!(About);
+action!(Quit);
+
 pub struct AppState {
     pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
     pub settings: watch::Receiver<Settings>,
@@ -35,9 +39,9 @@ pub struct AppState {
 }
 
 pub fn init(cx: &mut gpui::MutableAppContext) {
-    cx.add_global_action("app:quit", quit);
+    cx.add_global_action(quit);
 }
 
-fn quit(_: &(), cx: &mut gpui::MutableAppContext) {
+fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
     cx.platform().quit();
 }

zed/src/main.rs πŸ”—

@@ -10,7 +10,7 @@ use zed::{
     self, assets, editor, file_finder,
     fs::RealFs,
     language, menus, rpc, settings, theme_selector,
-    workspace::{self, OpenParams},
+    workspace::{self, OpenParams, OpenPaths},
     AppState,
 };
 
@@ -51,13 +51,10 @@ fn main() {
 
         let paths = collect_path_args();
         if !paths.is_empty() {
-            cx.dispatch_global_action(
-                "workspace:open_paths",
-                OpenParams {
-                    paths,
-                    app_state: app_state.clone(),
-                },
-            );
+            cx.dispatch_global_action(OpenPaths(OpenParams {
+                paths,
+                app_state: app_state.clone(),
+            }));
         }
     });
 }

zed/src/menus.rs πŸ”—

@@ -1,9 +1,11 @@
-use crate::AppState;
+use crate::{workspace, AppState};
 use gpui::{Menu, MenuItem};
 use std::sync::Arc;
 
 #[cfg(target_os = "macos")]
 pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
+    use crate::editor;
+
     vec![
         Menu {
             name: "Zed",
@@ -11,27 +13,23 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
                 MenuItem::Action {
                     name: "About Zed…",
                     keystroke: None,
-                    action: "app:about-zed",
-                    arg: None,
+                    action: Box::new(super::About),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Share",
                     keystroke: None,
-                    action: "workspace:share_worktree",
-                    arg: None,
+                    action: Box::new(workspace::ShareWorktree),
                 },
                 MenuItem::Action {
                     name: "Join",
                     keystroke: None,
-                    action: "workspace:join_worktree",
-                    arg: None,
+                    action: Box::new(workspace::JoinWorktree(state.clone())),
                 },
                 MenuItem::Action {
                     name: "Quit",
                     keystroke: Some("cmd-q"),
-                    action: "app:quit",
-                    arg: None,
+                    action: Box::new(super::Quit),
                 },
             ],
         },
@@ -41,15 +39,13 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
                 MenuItem::Action {
                     name: "New",
                     keystroke: Some("cmd-n"),
-                    action: "workspace:new_file",
-                    arg: Some(Box::new(state.clone())),
+                    action: Box::new(workspace::OpenNew(state.clone())),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Open…",
                     keystroke: Some("cmd-o"),
-                    action: "workspace:open",
-                    arg: Some(Box::new(state.clone())),
+                    action: Box::new(workspace::Open(state.clone())),
                 },
             ],
         },
@@ -59,33 +55,28 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
                 MenuItem::Action {
                     name: "Undo",
                     keystroke: Some("cmd-z"),
-                    action: "buffer:undo",
-                    arg: None,
+                    action: Box::new(editor::Undo),
                 },
                 MenuItem::Action {
                     name: "Redo",
                     keystroke: Some("cmd-Z"),
-                    action: "buffer:redo",
-                    arg: None,
+                    action: Box::new(editor::Redo),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Cut",
                     keystroke: Some("cmd-x"),
-                    action: "buffer:cut",
-                    arg: None,
+                    action: Box::new(editor::Cut),
                 },
                 MenuItem::Action {
                     name: "Copy",
                     keystroke: Some("cmd-c"),
-                    action: "buffer:copy",
-                    arg: None,
+                    action: Box::new(editor::Copy),
                 },
                 MenuItem::Action {
                     name: "Paste",
                     keystroke: Some("cmd-v"),
-                    action: "buffer:paste",
-                    arg: None,
+                    action: Box::new(editor::Paste),
                 },
             ],
         },

zed/src/theme_selector.rs πŸ”—

@@ -8,11 +8,12 @@ use crate::{
     AppState, Settings,
 };
 use gpui::{
+    action,
     elements::{
         Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement,
         UniformList, UniformListState,
     },
-    keymap::{self, Binding},
+    keymap::{self, menu, Binding},
     AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
     ViewContext, ViewHandle,
 };
@@ -29,19 +30,22 @@ pub struct ThemeSelector {
     selected_index: usize,
 }
 
+action!(Confirm);
+action!(Toggle, Arc<AppState>);
+action!(Reload, Arc<AppState>);
+
 pub fn init(cx: &mut MutableAppContext, app_state: &Arc<AppState>) {
-    cx.add_action("theme_selector:confirm", ThemeSelector::confirm);
-    cx.add_action("menu:select_prev", ThemeSelector::select_prev);
-    cx.add_action("menu:select_next", ThemeSelector::select_next);
-    cx.add_action("theme_selector:toggle", ThemeSelector::toggle);
-    cx.add_action("theme_selector:reload", ThemeSelector::reload);
+    cx.add_action(ThemeSelector::confirm);
+    cx.add_action(ThemeSelector::select_prev);
+    cx.add_action(ThemeSelector::select_next);
+    cx.add_action(ThemeSelector::toggle);
+    cx.add_action(ThemeSelector::reload);
 
     cx.add_bindings(vec![
-        Binding::new("cmd-k cmd-t", "theme_selector:toggle", None).with_arg(app_state.clone()),
-        Binding::new("cmd-k t", "theme_selector:reload", None).with_arg(app_state.clone()),
-        Binding::new("escape", "theme_selector:toggle", Some("ThemeSelector"))
-            .with_arg(app_state.clone()),
-        Binding::new("enter", "theme_selector:confirm", Some("ThemeSelector")),
+        Binding::new("cmd-k cmd-t", Toggle(app_state.clone()), None),
+        Binding::new("cmd-k t", Reload(app_state.clone()), None),
+        Binding::new("escape", Toggle(app_state.clone()), Some("ThemeSelector")),
+        Binding::new("enter", Confirm, Some("ThemeSelector")),
     ]);
 }
 
@@ -72,17 +76,13 @@ impl ThemeSelector {
         this
     }
 
-    fn toggle(
-        workspace: &mut Workspace,
-        app_state: &Arc<AppState>,
-        cx: &mut ViewContext<Workspace>,
-    ) {
+    fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
         workspace.toggle_modal(cx, |cx, _| {
             let selector = cx.add_view(|cx| {
                 Self::new(
-                    app_state.settings_tx.clone(),
-                    app_state.settings.clone(),
-                    app_state.themes.clone(),
+                    action.0.settings_tx.clone(),
+                    action.0.settings.clone(),
+                    action.0.themes.clone(),
                     cx,
                 )
             });
@@ -91,13 +91,13 @@ impl ThemeSelector {
         });
     }
 
-    fn reload(_: &mut Workspace, app_state: &Arc<AppState>, cx: &mut ViewContext<Workspace>) {
-        let current_theme_name = app_state.settings.borrow().theme.name.clone();
-        app_state.themes.clear();
-        match app_state.themes.get(&current_theme_name) {
+    fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
+        let current_theme_name = action.0.settings.borrow().theme.name.clone();
+        action.0.themes.clear();
+        match action.0.themes.get(&current_theme_name) {
             Ok(theme) => {
                 cx.notify_all();
-                app_state.settings_tx.lock().borrow_mut().theme = theme;
+                action.0.settings_tx.lock().borrow_mut().theme = theme;
             }
             Err(error) => {
                 log::error!("failed to load theme {}: {:?}", current_theme_name, error)
@@ -105,7 +105,7 @@ impl ThemeSelector {
         }
     }
 
-    fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
             match self.registry.get(&mat.string) {
                 Ok(theme) => {
@@ -118,7 +118,7 @@ impl ThemeSelector {
         }
     }
 
-    fn select_prev(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
         if self.selected_index > 0 {
             self.selected_index -= 1;
         }
@@ -126,7 +126,7 @@ impl ThemeSelector {
         cx.notify();
     }
 
-    fn select_next(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
         if self.selected_index + 1 < self.matches.len() {
             self.selected_index += 1;
         }

zed/src/workspace.rs πŸ”—

@@ -14,6 +14,7 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 use gpui::{
+    action,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     json::to_string_pretty,
@@ -36,37 +37,43 @@ use std::{
     sync::Arc,
 };
 
+action!(Open, Arc<AppState>);
+action!(OpenPaths, OpenParams);
+action!(OpenNew, Arc<AppState>);
+action!(ShareWorktree);
+action!(JoinWorktree, Arc<AppState>);
+action!(Save);
+action!(DebugElements);
+action!(ToggleSidebarItem, (Side, usize));
+
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_global_action("workspace:open", open);
-    cx.add_global_action(
-        "workspace:open_paths",
-        |params: &OpenParams, cx: &mut MutableAppContext| open_paths(params, cx).detach(),
-    );
-    cx.add_global_action("workspace:new_file", open_new);
-    cx.add_global_action("workspace:join_worktree", join_worktree);
-    cx.add_action("workspace:save", Workspace::save_active_item);
-    cx.add_action("workspace:debug_elements", Workspace::debug_elements);
-    cx.add_action("workspace:new_file", Workspace::open_new_file);
-    cx.add_action("workspace:share_worktree", Workspace::share_worktree);
-    cx.add_action("workspace:join_worktree", Workspace::join_worktree);
-    cx.add_action(
-        "workspace:toggle_sidebar_item",
-        Workspace::toggle_sidebar_item,
-    );
+    cx.add_global_action(open);
+    cx.add_global_action(|action: &OpenPaths, cx: &mut MutableAppContext| {
+        open_paths(action, cx).detach()
+    });
+    cx.add_global_action(open_new);
+    cx.add_global_action(join_worktree);
+    cx.add_action(Workspace::save_active_item);
+    cx.add_action(Workspace::debug_elements);
+    cx.add_action(Workspace::open_new_file);
+    cx.add_action(Workspace::share_worktree);
+    cx.add_action(Workspace::join_worktree);
+    cx.add_action(Workspace::toggle_sidebar_item);
     cx.add_bindings(vec![
-        Binding::new("cmd-s", "workspace:save", None),
-        Binding::new("cmd-alt-i", "workspace:debug_elements", None),
+        Binding::new("cmd-s", Save, None),
+        Binding::new("cmd-alt-i", DebugElements, None),
     ]);
     pane::init(cx);
 }
 
+#[derive(Clone)]
 pub struct OpenParams {
     pub paths: Vec<PathBuf>,
     pub app_state: Arc<AppState>,
 }
 
-fn open(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
-    let app_state = app_state.clone();
+fn open(action: &Open, cx: &mut MutableAppContext) {
+    let app_state = action.0.clone();
     cx.prompt_for_paths(
         PathPromptOptions {
             files: true,
@@ -75,22 +82,22 @@ fn open(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
         },
         move |paths, cx| {
             if let Some(paths) = paths {
-                cx.dispatch_global_action("workspace:open_paths", OpenParams { paths, app_state });
+                cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
             }
         },
     );
 }
 
-fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) -> Task<()> {
-    log::info!("open paths {:?}", params.paths);
+fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
+    log::info!("open paths {:?}", action.0.paths);
 
     // Open paths in existing workspace if possible
     for window_id in cx.window_ids().collect::<Vec<_>>() {
         if let Some(handle) = cx.root_view::<Workspace>(window_id) {
             let task = handle.update(cx, |view, cx| {
-                if view.contains_paths(&params.paths, cx.as_ref()) {
+                if view.contains_paths(&action.0.paths, cx.as_ref()) {
                     log::info!("open paths on existing workspace");
-                    Some(view.open_paths(&params.paths, cx))
+                    Some(view.open_paths(&action.0.paths, cx))
                 } else {
                     None
                 }
@@ -106,23 +113,26 @@ fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) -> Task<()> {
 
     // Add a new workspace if necessary
 
-    let (_, workspace) =
-        cx.add_window(window_options(), |cx| Workspace::new(&params.app_state, cx));
-    workspace.update(cx, |workspace, cx| workspace.open_paths(&params.paths, cx))
+    let (_, workspace) = cx.add_window(window_options(), |cx| {
+        Workspace::new(&action.0.app_state, cx)
+    });
+    workspace.update(cx, |workspace, cx| {
+        workspace.open_paths(&action.0.paths, cx)
+    })
 }
 
-fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
+fn open_new(action: &OpenNew, cx: &mut MutableAppContext) {
     cx.add_window(window_options(), |cx| {
-        let mut view = Workspace::new(app_state.as_ref(), cx);
-        view.open_new_file(&app_state, cx);
+        let mut view = Workspace::new(action.0.as_ref(), cx);
+        view.open_new_file(&action, cx);
         view
     });
 }
 
-fn join_worktree(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
+fn join_worktree(action: &JoinWorktree, cx: &mut MutableAppContext) {
     cx.add_window(window_options(), |cx| {
-        let mut view = Workspace::new(app_state.as_ref(), cx);
-        view.join_worktree(&(), cx);
+        let mut view = Workspace::new(action.0.as_ref(), cx);
+        view.join_worktree(action, cx);
         view
     });
 }
@@ -544,7 +554,7 @@ impl Workspace {
         }
     }
 
-    pub fn open_new_file(&mut self, _: &Arc<AppState>, cx: &mut ViewContext<Self>) {
+    pub fn open_new_file(&mut self, _: &OpenNew, cx: &mut ViewContext<Self>) {
         let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
         let buffer_view =
             cx.add_view(|cx| Editor::for_buffer(buffer.clone(), self.settings.clone(), cx));
@@ -677,7 +687,7 @@ impl Workspace {
         self.active_pane().read(cx).active_item()
     }
 
-    pub fn save_active_item(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
         if let Some(item) = self.active_item(cx) {
             let handle = cx.handle();
             if item.entry_id(cx.as_ref()).is_none() {
@@ -744,7 +754,7 @@ impl Workspace {
 
     pub fn toggle_sidebar_item(
         &mut self,
-        (side, item_ix): &(Side, usize),
+        ToggleSidebarItem((side, item_ix)): &ToggleSidebarItem,
         cx: &mut ViewContext<Self>,
     ) {
         let sidebar = match side {
@@ -755,7 +765,7 @@ impl Workspace {
         cx.notify();
     }
 
-    pub fn debug_elements(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
         match to_string_pretty(&cx.debug_elements()) {
             Ok(json) => {
                 let kib = json.len() as f32 / 1024.;
@@ -771,7 +781,7 @@ impl Workspace {
         };
     }
 
-    fn share_worktree(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    fn share_worktree(&mut self, _: &ShareWorktree, cx: &mut ViewContext<Self>) {
         let rpc = self.rpc.clone();
         let platform = cx.platform();
 
@@ -803,7 +813,7 @@ impl Workspace {
         .detach();
     }
 
-    fn join_worktree(&mut self, _: &(), cx: &mut ViewContext<Self>) {
+    fn join_worktree(&mut self, _: &JoinWorktree, cx: &mut ViewContext<Self>) {
         let rpc = self.rpc.clone();
         let languages = self.languages.clone();
 
@@ -1000,7 +1010,7 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
 mod tests {
     use super::*;
     use crate::{
-        editor::Editor,
+        editor::{Editor, Insert},
         fs::FakeFs,
         test::{build_app_state, temp_tree},
         worktree::WorktreeHandle,
@@ -1029,13 +1039,13 @@ mod tests {
 
         cx.update(|cx| {
             open_paths(
-                &OpenParams {
+                &OpenPaths(OpenParams {
                     paths: vec![
                         dir.path().join("a").to_path_buf(),
                         dir.path().join("b").to_path_buf(),
                     ],
                     app_state: app_state.clone(),
-                },
+                }),
                 cx,
             )
         })
@@ -1044,10 +1054,10 @@ mod tests {
 
         cx.update(|cx| {
             open_paths(
-                &OpenParams {
+                &OpenPaths(OpenParams {
                     paths: vec![dir.path().join("a").to_path_buf()],
                     app_state: app_state.clone(),
-                },
+                }),
                 cx,
             )
         })
@@ -1060,13 +1070,13 @@ mod tests {
 
         cx.update(|cx| {
             open_paths(
-                &OpenParams {
+                &OpenPaths(OpenParams {
                     paths: vec![
                         dir.path().join("b").to_path_buf(),
                         dir.path().join("c").to_path_buf(),
                     ],
                     app_state: app_state.clone(),
-                },
+                }),
                 cx,
             )
         })
@@ -1284,14 +1294,14 @@ mod tests {
             item.to_any().downcast::<Editor>().unwrap()
         });
 
-        cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&"x".to_string(), cx)));
+        cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&Insert("x".into()), cx)));
         fs::write(dir.path().join("a.txt"), "changed").unwrap();
         editor
             .condition(&cx, |editor, cx| editor.has_conflict(cx))
             .await;
         cx.read(|cx| assert!(editor.is_dirty(cx)));
 
-        cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&(), cx)));
+        cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&Save, cx)));
         cx.simulate_prompt_answer(window_id, 0);
         editor
             .condition(&cx, |editor, cx| !editor.is_dirty(cx))
@@ -1323,7 +1333,7 @@ mod tests {
 
         // Create a new untitled buffer
         let editor = workspace.update(&mut cx, |workspace, cx| {
-            workspace.open_new_file(&app_state, cx);
+            workspace.open_new_file(&OpenNew(app_state.clone()), cx);
             workspace
                 .active_item(cx)
                 .unwrap()
@@ -1335,12 +1345,14 @@ mod tests {
         editor.update(&mut cx, |editor, cx| {
             assert!(!editor.is_dirty(cx.as_ref()));
             assert_eq!(editor.title(cx.as_ref()), "untitled");
-            editor.insert(&"hi".to_string(), cx);
+            editor.insert(&Insert("hi".into()), cx);
             assert!(editor.is_dirty(cx.as_ref()));
         });
 
         // Save the buffer. This prompts for a filename.
-        workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
+        workspace.update(&mut cx, |workspace, cx| {
+            workspace.save_active_item(&Save, cx)
+        });
         cx.simulate_new_path_selection(|parent_dir| {
             assert_eq!(parent_dir, dir.path());
             Some(parent_dir.join("the-new-name"))
@@ -1361,10 +1373,12 @@ mod tests {
 
         // Edit the file and save it again. This time, there is no filename prompt.
         editor.update(&mut cx, |editor, cx| {
-            editor.insert(&" there".to_string(), cx);
+            editor.insert(&Insert(" there".into()), cx);
             assert_eq!(editor.is_dirty(cx.as_ref()), true);
         });
-        workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
+        workspace.update(&mut cx, |workspace, cx| {
+            workspace.save_active_item(&Save, cx)
+        });
         assert!(!cx.did_prompt_for_new_path());
         editor
             .condition(&cx, |editor, cx| !editor.is_dirty(cx))
@@ -1374,7 +1388,7 @@ mod tests {
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
         workspace.update(&mut cx, |workspace, cx| {
-            workspace.open_new_file(&app_state, cx);
+            workspace.open_new_file(&OpenNew(app_state.clone()), cx);
             workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
             assert!(workspace
                 .open_entry((tree.id(), Path::new("the-new-name").into()), cx)
@@ -1398,7 +1412,7 @@ mod tests {
         cx.update(init);
 
         let app_state = cx.read(build_app_state);
-        cx.dispatch_global_action("workspace:new_file", app_state);
+        cx.dispatch_global_action(OpenNew(app_state));
         let window_id = *cx.window_ids().first().unwrap();
         let workspace = cx.root_view::<Workspace>(window_id).unwrap();
         let editor = workspace.update(&mut cx, |workspace, cx| {
@@ -1414,7 +1428,9 @@ mod tests {
             assert!(editor.text(cx).is_empty());
         });
 
-        workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
+        workspace.update(&mut cx, |workspace, cx| {
+            workspace.save_active_item(&Save, cx)
+        });
 
         let dir = TempDir::new("test-new-empty-workspace").unwrap();
         cx.simulate_new_path_selection(|_| {
@@ -1467,7 +1483,11 @@ mod tests {
             );
         });
 
-        cx.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
+        cx.dispatch_action(
+            window_id,
+            vec![pane_1.id()],
+            pane::Split(SplitDirection::Right),
+        );
         cx.update(|cx| {
             let pane_2 = workspace.read(cx).active_pane().clone();
             assert_ne!(pane_1, pane_2);
@@ -1475,7 +1495,7 @@ mod tests {
             let pane2_item = pane_2.read(cx).active_item().unwrap();
             assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone()));
 
-            cx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
+            cx.dispatch_action(window_id, vec![pane_2.id()], &CloseActiveItem);
             let workspace = workspace.read(cx);
             assert_eq!(workspace.panes.len(), 1);
             assert_eq!(workspace.active_pane(), &pane_1);

zed/src/workspace/pane.rs πŸ”—

@@ -1,6 +1,7 @@
 use super::{ItemViewHandle, SplitDirection};
 use crate::{settings::Settings, theme};
 use gpui::{
+    action,
     color::Color,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
@@ -11,46 +12,41 @@ use gpui::{
 use postage::watch;
 use std::{cmp, path::Path, sync::Arc};
 
+action!(Split, SplitDirection);
+action!(ActivateItem, usize);
+action!(ActivatePrevItem);
+action!(ActivateNextItem);
+action!(CloseActiveItem);
+action!(CloseItem, usize);
+
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(
-        "pane:activate_item",
-        |pane: &mut Pane, index: &usize, cx| {
-            pane.activate_item(*index, cx);
-        },
-    );
-    cx.add_action("pane:activate_prev_item", |pane: &mut Pane, _: &(), cx| {
+    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
+        pane.activate_item(action.0, cx);
+    });
+    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
         pane.activate_prev_item(cx);
     });
-    cx.add_action("pane:activate_next_item", |pane: &mut Pane, _: &(), cx| {
+    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
         pane.activate_next_item(cx);
     });
-    cx.add_action("pane:close_active_item", |pane: &mut Pane, _: &(), cx| {
+    cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
         pane.close_active_item(cx);
     });
-    cx.add_action("pane:close_item", |pane: &mut Pane, item_id: &usize, cx| {
-        pane.close_item(*item_id, cx);
-    });
-    cx.add_action("pane:split_up", |pane: &mut Pane, _: &(), cx| {
-        pane.split(SplitDirection::Up, cx);
-    });
-    cx.add_action("pane:split_down", |pane: &mut Pane, _: &(), cx| {
-        pane.split(SplitDirection::Down, cx);
-    });
-    cx.add_action("pane:split_left", |pane: &mut Pane, _: &(), cx| {
-        pane.split(SplitDirection::Left, cx);
+    cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
+        pane.close_item(action.0, cx);
     });
-    cx.add_action("pane:split_right", |pane: &mut Pane, _: &(), cx| {
-        pane.split(SplitDirection::Right, cx);
+    cx.add_action(|pane: &mut Pane, action: &Split, cx| {
+        pane.split(action.0, cx);
     });
 
     cx.add_bindings(vec![
-        Binding::new("shift-cmd-{", "pane:activate_prev_item", Some("Pane")),
-        Binding::new("shift-cmd-}", "pane:activate_next_item", Some("Pane")),
-        Binding::new("cmd-w", "pane:close_active_item", Some("Pane")),
-        Binding::new("cmd-k up", "pane:split_up", Some("Pane")),
-        Binding::new("cmd-k down", "pane:split_down", Some("Pane")),
-        Binding::new("cmd-k left", "pane:split_left", Some("Pane")),
-        Binding::new("cmd-k right", "pane:split_right", Some("Pane")),
+        Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
+        Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
+        Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
+        Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
+        Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
+        Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
+        Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
     ]);
 }
 
@@ -253,7 +249,7 @@ impl Pane {
                         ConstrainedBox::new(
                             EventHandler::new(container.boxed())
                                 .on_mouse_down(move |cx| {
-                                    cx.dispatch_action("pane:activate_item", ix);
+                                    cx.dispatch_action(ActivateItem(ix));
                                     true
                                 })
                                 .boxed(),
@@ -338,7 +334,7 @@ impl Pane {
                     icon.boxed()
                 }
             })
-            .on_click(move |cx| cx.dispatch_action("pane:close_item", item_id))
+            .on_click(move |cx| cx.dispatch_action(CloseItem(item_id)))
             .named("close-tab-icon")
         } else {
             let diameter = 8.;

zed/src/workspace/sidebar.rs πŸ”—

@@ -1,5 +1,6 @@
 use crate::Settings;
 use gpui::{
+    action,
     elements::{
         Align, ConstrainedBox, Container, Flex, MouseEventHandler, ParentElement as _, Svg,
     },
@@ -23,6 +24,14 @@ struct Item {
     view: AnyViewHandle,
 }
 
+action!(ToggleSidebarItem, ToggleArg);
+
+#[derive(Clone)]
+pub struct ToggleArg {
+    side: Side,
+    item_index: usize,
+}
+
 impl Sidebar {
     pub fn new(side: Side) -> Self {
         Self {
@@ -59,8 +68,8 @@ impl Sidebar {
 
         Container::new(
             Flex::column()
-                .with_children(self.items.iter().enumerate().map(|(item_ix, item)| {
-                    let theme = if Some(item_ix) == self.active_item_ix {
+                .with_children(self.items.iter().enumerate().map(|(item_index, item)| {
+                    let theme = if Some(item_index) == self.active_item_ix {
                         &settings.theme.active_sidebar_icon
                     } else {
                         &settings.theme.sidebar_icon
@@ -81,7 +90,7 @@ impl Sidebar {
                         .boxed()
                     })
                     .on_click(move |cx| {
-                        cx.dispatch_action("workspace:toggle_sidebar_item", (side, item_ix))
+                        cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index }))
                     })
                     .boxed()
                 }))