Merge pull request #2420 from zed-industries/simplify-action-dispatch

Antonio Scandurra created

Remove `impl_internal_actions!` macro

Change summary

crates/collab/src/tests/integration_tests.rs        |  60 -
crates/collab_ui/src/collab_titlebar_item.rs        |  64 
crates/collab_ui/src/collab_ui.rs                   |  98 --
crates/collab_ui/src/contact_list.rs                | 113 +-
crates/collab_ui/src/contact_notification.rs        |  35 
crates/collab_ui/src/contacts_popover.rs            |  40 
crates/collab_ui/src/incoming_call_notification.rs  |  50 
crates/collab_ui/src/notifications.rs               |  24 
crates/collab_ui/src/project_shared_notification.rs |  46 
crates/context_menu/src/context_menu.rs             | 184 ++--
crates/copilot/src/copilot.rs                       |   2 
crates/copilot/src/sign_in.rs                       |  16 
crates/copilot_button/src/copilot_button.rs         | 287 +++----
crates/diagnostics/src/diagnostics.rs               |  11 
crates/editor/src/editor.rs                         |  56 
crates/editor/src/editor_tests.rs                   |  12 
crates/editor/src/element.rs                        | 196 +++--
crates/editor/src/hover_popover.rs                  |  42 -
crates/editor/src/items.rs                          |   6 
crates/editor/src/link_go_to_definition.rs          | 143 ---
crates/editor/src/mouse_context_menu.rs             |  46 -
crates/editor/src/scroll.rs                         |  24 
crates/editor/src/scroll/actions.rs                 |  24 
crates/gpui/src/app/action.rs                       |  18 
crates/gpui/src/presenter/event_context.rs          | 100 --
crates/gpui/src/presenter/event_dispatcher.rs       | 309 ---------
crates/menu/src/menu.rs                             |   5 
crates/picker/src/picker.rs                         |  10 
crates/project_panel/src/project_panel.rs           | 147 +--
crates/recent_projects/src/recent_projects.rs       |  61 +
crates/search/src/project_search.rs                 |   5 
crates/terminal_view/src/terminal_button.rs         |  70 -
crates/terminal_view/src/terminal_element.rs        |  29 
crates/terminal_view/src/terminal_view.rs           |  18 
crates/theme/src/ui.rs                              |  18 
crates/workspace/src/dock.rs                        |  68 -
crates/workspace/src/dock/toggle_dock_button.rs     |  12 
crates/workspace/src/item.rs                        |   4 
crates/workspace/src/notifications.rs               |  28 
crates/workspace/src/pane.rs                        | 307 +++-----
crates/workspace/src/pane/dragged_item_receiver.rs  |  99 ++
crates/workspace/src/pane_group.rs                  |  34 
crates/workspace/src/workspace.rs                   | 512 ++++++--------
crates/zed/src/main.rs                              |  16 
crates/zed/src/zed.rs                               |   9 
45 files changed, 1,312 insertions(+), 2,146 deletions(-)

Detailed changes

crates/collab/src/tests/integration_tests.rs 🔗

@@ -38,9 +38,7 @@ use std::{
     },
 };
 use unindent::Unindent as _;
-use workspace::{
-    item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace,
-};
+use workspace::{item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, Workspace};
 
 #[ctor::ctor]
 fn init_logger() {
@@ -6119,9 +6117,7 @@ async fn test_basic_following(
     // When client B starts following client A, all visible view states are replicated to client B.
     workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(peer_id_a), cx)
-                .unwrap()
+            workspace.toggle_follow(peer_id_a, cx).unwrap()
         })
         .await
         .unwrap();
@@ -6160,9 +6156,7 @@ async fn test_basic_following(
     // Client C also follows client A.
     workspace_c
         .update(cx_c, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(peer_id_a), cx)
-                .unwrap()
+            workspace.toggle_follow(peer_id_a, cx).unwrap()
         })
         .await
         .unwrap();
@@ -6197,7 +6191,7 @@ async fn test_basic_following(
 
     // Client C unfollows client A.
     workspace_c.update(cx_c, |workspace, cx| {
-        workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
+        workspace.toggle_follow(peer_id_a, cx);
     });
 
     // All clients see that clients B is following client A.
@@ -6220,7 +6214,7 @@ async fn test_basic_following(
 
     // Client C re-follows client A.
     workspace_c.update(cx_c, |workspace, cx| {
-        workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
+        workspace.toggle_follow(peer_id_a, cx);
     });
 
     // All clients see that clients B and C are following client A.
@@ -6244,9 +6238,7 @@ async fn test_basic_following(
     // Client D follows client C.
     workspace_d
         .update(cx_d, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(peer_id_c), cx)
-                .unwrap()
+            workspace.toggle_follow(peer_id_c, cx).unwrap()
         })
         .await
         .unwrap();
@@ -6436,9 +6428,7 @@ async fn test_basic_following(
     // Client A starts following client B.
     workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(peer_id_b), cx)
-                .unwrap()
+            workspace.toggle_follow(peer_id_b, cx).unwrap()
         })
         .await
         .unwrap();
@@ -6707,9 +6697,7 @@ async fn test_following_tab_order(
     //Follow client B as client A
     workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(client_b_id), cx)
-                .unwrap()
+            workspace.toggle_follow(client_b_id, cx).unwrap()
         })
         .await
         .unwrap();
@@ -6824,9 +6812,7 @@ async fn test_peers_following_each_other(
     workspace_a
         .update(cx_a, |workspace, cx| {
             let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
-            workspace
-                .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
-                .unwrap()
+            workspace.toggle_follow(leader_id, cx).unwrap()
         })
         .await
         .unwrap();
@@ -6840,9 +6826,7 @@ async fn test_peers_following_each_other(
     workspace_b
         .update(cx_b, |workspace, cx| {
             let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
-            workspace
-                .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
-                .unwrap()
+            workspace.toggle_follow(leader_id, cx).unwrap()
         })
         .await
         .unwrap();
@@ -6988,9 +6972,7 @@ async fn test_auto_unfollowing(
     });
     workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(leader_id), cx)
-                .unwrap()
+            workspace.toggle_follow(leader_id, cx).unwrap()
         })
         .await
         .unwrap();
@@ -7015,9 +6997,7 @@ async fn test_auto_unfollowing(
 
     workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(leader_id), cx)
-                .unwrap()
+            workspace.toggle_follow(leader_id, cx).unwrap()
         })
         .await
         .unwrap();
@@ -7035,9 +7015,7 @@ async fn test_auto_unfollowing(
 
     workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(leader_id), cx)
-                .unwrap()
+            workspace.toggle_follow(leader_id, cx).unwrap()
         })
         .await
         .unwrap();
@@ -7057,9 +7035,7 @@ async fn test_auto_unfollowing(
 
     workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace
-                .toggle_follow(&ToggleFollow(leader_id), cx)
-                .unwrap()
+            workspace.toggle_follow(leader_id, cx).unwrap()
         })
         .await
         .unwrap();
@@ -7134,14 +7110,10 @@ async fn test_peers_simultaneously_following_each_other(
     });
 
     let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
-        workspace
-            .toggle_follow(&ToggleFollow(client_b_id), cx)
-            .unwrap()
+        workspace.toggle_follow(client_b_id, cx).unwrap()
     });
     let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
-        workspace
-            .toggle_follow(&ToggleFollow(client_a_id), cx)
-            .unwrap()
+        workspace.toggle_follow(client_a_id, cx).unwrap()
     });
 
     futures::try_join!(a_follow_b, b_follow_a).unwrap();

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -13,7 +13,6 @@ use gpui::{
     color::Color,
     elements::*,
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
-    impl_internal_actions,
     json::{self, ToJson},
     platform::{CursorStyle, MouseButton},
     AppContext, Entity, ImageData, ModelHandle, SceneBuilder, Subscription, View, ViewContext,
@@ -23,7 +22,7 @@ use settings::Settings;
 use std::{ops::Range, sync::Arc};
 use theme::{AvatarStyle, Theme};
 use util::ResultExt;
-use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
+use workspace::{FollowNextCollaborator, Workspace};
 
 actions!(
     collab,
@@ -36,17 +35,11 @@ actions!(
     ]
 );
 
-impl_internal_actions!(collab, [LeaveCall]);
-
-#[derive(Copy, Clone, PartialEq)]
-pub(crate) struct LeaveCall;
-
 pub fn init(cx: &mut AppContext) {
     cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
     cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
     cx.add_action(CollabTitlebarItem::share_project);
     cx.add_action(CollabTitlebarItem::unshare_project);
-    cx.add_action(CollabTitlebarItem::leave_call);
     cx.add_action(CollabTitlebarItem::toggle_user_menu);
 }
 
@@ -136,7 +129,7 @@ impl View for CollabTitlebarItem {
 impl CollabTitlebarItem {
     pub fn new(
         workspace: &ViewHandle<Workspace>,
-        user_store: &ModelHandle<UserStore>,
+        user_store: ModelHandle<UserStore>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let active_call = ActiveCall::global(cx);
@@ -146,9 +139,9 @@ impl CollabTitlebarItem {
         subscriptions.push(cx.observe_window_activation(|this, active, cx| {
             this.window_activation_changed(active, cx)
         }));
-        subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
+        subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
         subscriptions.push(
-            cx.subscribe(user_store, move |this, user_store, event, cx| {
+            cx.subscribe(&user_store, move |this, user_store, event, cx| {
                 if let Some(workspace) = this.workspace.upgrade(cx) {
                     workspace.update(cx, |workspace, cx| {
                         if let client::Event::Contact { user, kind } = event {
@@ -257,9 +250,7 @@ impl CollabTitlebarItem {
     pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
         if self.contacts_popover.take().is_none() {
             if let Some(workspace) = self.workspace.upgrade(cx) {
-                let project = workspace.read(cx).project().clone();
-                let user_store = workspace.read(cx).user_store().clone();
-                let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
+                let view = cx.add_view(|cx| ContactsPopover::new(&workspace, cx));
                 cx.subscribe(&view, |this, _, event, cx| {
                     match event {
                         contacts_popover::Event::Dismissed => {
@@ -301,13 +292,19 @@ impl CollabTitlebarItem {
                             .with_style(item_style.container)
                             .into_any()
                     })),
-                    ContextMenuItem::item("Sign out", SignOut),
-                    ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
+                    ContextMenuItem::action("Sign out", SignOut),
+                    ContextMenuItem::action(
+                        "Send Feedback",
+                        feedback::feedback_editor::GiveFeedback,
+                    ),
                 ]
             } else {
                 vec![
-                    ContextMenuItem::item("Sign in", SignIn),
-                    ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
+                    ContextMenuItem::action("Sign in", SignIn),
+                    ContextMenuItem::action(
+                        "Send Feedback",
+                        feedback::feedback_editor::GiveFeedback,
+                    ),
                 ]
             };
 
@@ -315,12 +312,6 @@ impl CollabTitlebarItem {
         });
     }
 
-    fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| call.hang_up(cx))
-            .detach_and_log_err(cx);
-    }
-
     fn render_toggle_contacts_button(
         &self,
         theme: &Theme,
@@ -740,14 +731,22 @@ impl CollabTitlebarItem {
 
         if let Some(location) = location {
             if let Some(replica_id) = replica_id {
+                enum ToggleFollow {}
+
                 content = MouseEventHandler::<ToggleFollow, Self>::new(
                     replica_id.into(),
                     cx,
                     move |_, _| content,
                 )
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(ToggleFollow(peer_id))
+                .on_click(MouseButton::Left, move |_, item, cx| {
+                    if let Some(workspace) = item.workspace.upgrade(cx) {
+                        if let Some(task) = workspace
+                            .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
+                        {
+                            task.detach_and_log_err(cx);
+                        }
+                    }
                 })
                 .with_tooltip::<ToggleFollow>(
                     peer_id.as_u64() as usize,
@@ -762,6 +761,8 @@ impl CollabTitlebarItem {
                 )
                 .into_any();
             } else if let ParticipantLocation::SharedProject { project_id } = location {
+                enum JoinProject {}
+
                 let user_id = user.id;
                 content = MouseEventHandler::<JoinProject, Self>::new(
                     peer_id.as_u64() as usize,
@@ -769,11 +770,12 @@ impl CollabTitlebarItem {
                     move |_, _| content,
                 )
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(JoinProject {
-                        project_id,
-                        follow_user_id: user_id,
-                    })
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    if let Some(workspace) = this.workspace.upgrade(cx) {
+                        let app_state = workspace.read(cx).app_state().clone();
+                        workspace::join_remote_project(project_id, user_id, app_state, cx)
+                            .detach_and_log_err(cx);
+                    }
                 })
                 .with_tooltip::<JoinProject>(
                     peer_id.as_u64() as usize,

crates/collab_ui/src/collab_ui.rs 🔗

@@ -10,29 +10,24 @@ mod notifications;
 mod project_shared_notification;
 mod sharing_status_indicator;
 
-use anyhow::anyhow;
 use call::ActiveCall;
 pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
 use gpui::{actions, AppContext, Task};
 use std::sync::Arc;
-use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
+use workspace::AppState;
 
 actions!(collab, [ToggleScreenSharing]);
 
-pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     collab_titlebar_item::init(cx);
-    contact_notification::init(cx);
     contact_list::init(cx);
     contact_finder::init(cx);
     contacts_popover::init(cx);
-    incoming_call_notification::init(cx);
-    project_shared_notification::init(cx);
+    incoming_call_notification::init(&app_state, cx);
+    project_shared_notification::init(&app_state, cx);
     sharing_status_indicator::init(cx);
 
     cx.add_global_action(toggle_screen_sharing);
-    cx.add_global_action(move |action: &JoinProject, cx| {
-        join_project(action, app_state.clone(), cx);
-    });
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -47,88 +42,3 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
         toggle_screen_sharing.detach_and_log_err(cx);
     }
 }
-
-fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut AppContext) {
-    let project_id = action.project_id;
-    let follow_user_id = action.follow_user_id;
-    cx.spawn(|mut cx| async move {
-        let existing_workspace = cx.update(|cx| {
-            cx.window_ids()
-                .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
-                .find(|workspace| {
-                    workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
-                })
-        });
-
-        let workspace = if let Some(existing_workspace) = existing_workspace {
-            existing_workspace.downgrade()
-        } else {
-            let active_call = cx.read(ActiveCall::global);
-            let room = active_call
-                .read_with(&cx, |call, _| call.room().cloned())
-                .ok_or_else(|| anyhow!("not in a call"))?;
-            let project = room
-                .update(&mut cx, |room, cx| {
-                    room.join_project(
-                        project_id,
-                        app_state.languages.clone(),
-                        app_state.fs.clone(),
-                        cx,
-                    )
-                })
-                .await?;
-
-            let (_, workspace) = cx.add_window(
-                (app_state.build_window_options)(None, None, cx.platform().as_ref()),
-                |cx| {
-                    let mut workspace = Workspace::new(
-                        Default::default(),
-                        0,
-                        project,
-                        app_state.dock_default_item_factory,
-                        app_state.background_actions,
-                        cx,
-                    );
-                    (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-                    workspace
-                },
-            );
-            workspace.downgrade()
-        };
-
-        cx.activate_window(workspace.window_id());
-        cx.platform().activate(true);
-
-        workspace.update(&mut cx, |workspace, cx| {
-            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-                let follow_peer_id = room
-                    .read(cx)
-                    .remote_participants()
-                    .iter()
-                    .find(|(_, participant)| participant.user.id == follow_user_id)
-                    .map(|(_, p)| p.peer_id)
-                    .or_else(|| {
-                        // If we couldn't follow the given user, follow the host instead.
-                        let collaborator = workspace
-                            .project()
-                            .read(cx)
-                            .collaborators()
-                            .values()
-                            .find(|collaborator| collaborator.replica_id == 0)?;
-                        Some(collaborator.peer_id)
-                    });
-
-                if let Some(follow_peer_id) = follow_peer_id {
-                    if !workspace.is_being_followed(follow_peer_id) {
-                        workspace
-                            .toggle_follow(&ToggleFollow(follow_peer_id), cx)
-                            .map(|follow| follow.detach_and_log_err(cx));
-                    }
-                }
-            }
-        })?;
-
-        anyhow::Ok(())
-    })
-    .detach_and_log_err(cx);
-}

crates/collab_ui/src/contact_list.rs 🔗

@@ -1,4 +1,3 @@
-use super::collab_titlebar_item::LeaveCall;
 use crate::contacts_popover;
 use call::ActiveCall;
 use client::{proto::PeerId, Contact, User, UserStore};
@@ -8,10 +7,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    impl_actions, impl_internal_actions,
+    impl_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, PromptLevel},
-    AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle,
+    AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::Project;
@@ -19,10 +18,9 @@ use serde::Deserialize;
 use settings::Settings;
 use std::{mem, sync::Arc};
 use theme::IconButton;
-use workspace::{JoinProject, OpenSharedScreen};
+use workspace::Workspace;
 
 impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
-impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(ContactList::remove_contact);
@@ -31,17 +29,6 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ContactList::select_next);
     cx.add_action(ContactList::select_prev);
     cx.add_action(ContactList::confirm);
-    cx.add_action(ContactList::toggle_expanded);
-    cx.add_action(ContactList::call);
-}
-
-#[derive(Clone, PartialEq)]
-struct ToggleExpanded(Section);
-
-#[derive(Clone, PartialEq)]
-struct Call {
-    recipient_user_id: u64,
-    initial_project: Option<ModelHandle<Project>>,
 }
 
 #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
@@ -161,6 +148,7 @@ pub struct ContactList {
     match_candidates: Vec<StringMatchCandidate>,
     list_state: ListState<Self>,
     project: ModelHandle<Project>,
+    workspace: WeakViewHandle<Workspace>,
     user_store: ModelHandle<UserStore>,
     filter_editor: ViewHandle<Editor>,
     collapsed_sections: Vec<Section>,
@@ -169,11 +157,7 @@ pub struct ContactList {
 }
 
 impl ContactList {
-    pub fn new(
-        project: ModelHandle<Project>,
-        user_store: ModelHandle<UserStore>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
         let filter_editor = cx.add_view(|cx| {
             let mut editor = Editor::single_line(
                 Some(Arc::new(|theme| {
@@ -278,6 +262,7 @@ impl ContactList {
         });
 
         let active_call = ActiveCall::global(cx);
+        let user_store = workspace.read(cx).user_store().clone();
         let mut subscriptions = Vec::new();
         subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
         subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
@@ -290,7 +275,8 @@ impl ContactList {
             match_candidates: Default::default(),
             filter_editor,
             _subscriptions: subscriptions,
-            project,
+            project: workspace.read(cx).project().clone(),
+            workspace: workspace.downgrade(),
             user_store,
         };
         this.update_entries(cx);
@@ -403,18 +389,11 @@ impl ContactList {
             if let Some(entry) = self.entries.get(selection) {
                 match entry {
                     ContactEntry::Header(section) => {
-                        let section = *section;
-                        self.toggle_expanded(&ToggleExpanded(section), cx);
+                        self.toggle_expanded(*section, cx);
                     }
                     ContactEntry::Contact { contact, calling } => {
                         if contact.online && !contact.busy && !calling {
-                            self.call(
-                                &Call {
-                                    recipient_user_id: contact.user.id,
-                                    initial_project: Some(self.project.clone()),
-                                },
-                                cx,
-                            );
+                            self.call(contact.user.id, Some(self.project.clone()), cx);
                         }
                     }
                     ContactEntry::ParticipantProject {
@@ -422,13 +401,23 @@ impl ContactList {
                         host_user_id,
                         ..
                     } => {
-                        cx.dispatch_global_action(JoinProject {
-                            project_id: *project_id,
-                            follow_user_id: *host_user_id,
-                        });
+                        if let Some(workspace) = self.workspace.upgrade(cx) {
+                            let app_state = workspace.read(cx).app_state().clone();
+                            workspace::join_remote_project(
+                                *project_id,
+                                *host_user_id,
+                                app_state,
+                                cx,
+                            )
+                            .detach_and_log_err(cx);
+                        }
                     }
                     ContactEntry::ParticipantScreen { peer_id, .. } => {
-                        cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
+                        if let Some(workspace) = self.workspace.upgrade(cx) {
+                            workspace.update(cx, |workspace, cx| {
+                                workspace.open_shared_screen(*peer_id, cx)
+                            });
+                        }
                     }
                     _ => {}
                 }
@@ -436,8 +425,7 @@ impl ContactList {
         }
     }
 
-    fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
-        let section = action.0;
+    fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
             self.collapsed_sections.remove(ix);
         } else {
@@ -798,6 +786,8 @@ impl ContactList {
         theme: &theme::ContactList,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
+        enum JoinProject {}
+
         let font_cache = cx.font_cache();
         let host_avatar_height = theme
             .contact_avatar
@@ -873,12 +863,13 @@ impl ContactList {
         } else {
             CursorStyle::Arrow
         })
-        .on_click(MouseButton::Left, move |_, _, cx| {
+        .on_click(MouseButton::Left, move |_, this, cx| {
             if !is_current {
-                cx.dispatch_global_action(JoinProject {
-                    project_id,
-                    follow_user_id: host_user_id,
-                });
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    let app_state = workspace.read(cx).app_state().clone();
+                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+                        .detach_and_log_err(cx);
+                }
             }
         })
         .into_any()
@@ -891,6 +882,8 @@ impl ContactList {
         theme: &theme::ContactList,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
+        enum OpenSharedScreen {}
+
         let font_cache = cx.font_cache();
         let host_avatar_height = theme
             .contact_avatar
@@ -971,8 +964,12 @@ impl ContactList {
             },
         )
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            cx.dispatch_action(OpenSharedScreen { peer_id });
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            if let Some(workspace) = this.workspace.upgrade(cx) {
+                workspace.update(cx, |workspace, cx| {
+                    workspace.open_shared_screen(peer_id, cx)
+                });
+            }
         })
         .into_any()
     }
@@ -1004,7 +1001,11 @@ impl ContactList {
                         .contained()
                         .with_style(style.container)
                 })
-                .on_click(MouseButton::Left, |_, _, cx| cx.dispatch_action(LeaveCall))
+                .on_click(MouseButton::Left, |_, _, cx| {
+                    ActiveCall::global(cx)
+                        .update(cx, |call, cx| call.hang_up(cx))
+                        .detach_and_log_err(cx);
+                })
                 .aligned(),
             )
         } else {
@@ -1043,8 +1044,8 @@ impl ContactList {
                 .with_style(header_style.container)
         })
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            cx.dispatch_action(ToggleExpanded(section))
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            this.toggle_expanded(section, cx);
         })
         .into_any()
     }
@@ -1142,12 +1143,9 @@ impl ContactList {
                             .style_for(&mut Default::default(), is_selected),
                     )
             })
-            .on_click(MouseButton::Left, move |_, _, cx| {
+            .on_click(MouseButton::Left, move |_, this, cx| {
                 if online && !busy {
-                    cx.dispatch_action(Call {
-                        recipient_user_id: user_id,
-                        initial_project: Some(initial_project.clone()),
-                    });
+                    this.call(user_id, Some(initial_project.clone()), cx);
                 }
             });
 
@@ -1269,9 +1267,12 @@ impl ContactList {
             .into_any()
     }
 
-    fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
-        let recipient_user_id = action.recipient_user_id;
-        let initial_project = action.initial_project.clone();
+    fn call(
+        &mut self,
+        recipient_user_id: u64,
+        initial_project: Option<ModelHandle<Project>>,
+        cx: &mut ViewContext<Self>,
+    ) {
         ActiveCall::global(cx)
             .update(cx, |call, cx| {
                 call.invite(recipient_user_id, initial_project, cx)

crates/collab_ui/src/contact_notification.rs 🔗

@@ -2,18 +2,9 @@ use std::sync::Arc;
 
 use crate::notifications::render_user_notification;
 use client::{ContactEventKind, User, UserStore};
-use gpui::{
-    elements::*, impl_internal_actions, AppContext, Entity, ModelHandle, View, ViewContext,
-};
+use gpui::{elements::*, Entity, ModelHandle, View, ViewContext};
 use workspace::notifications::Notification;
 
-impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ContactNotification::dismiss);
-    cx.add_action(ContactNotification::respond_to_contact_request);
-}
-
 pub struct ContactNotification {
     user_store: ModelHandle<UserStore>,
     user: Arc<User>,
@@ -48,20 +39,18 @@ impl View for ContactNotification {
                 self.user.clone(),
                 "wants to add you as a contact",
                 Some("They won't be alerted if you decline."),
-                Dismiss(self.user.id),
+                |notification, cx| notification.dismiss(cx),
                 vec![
                     (
                         "Decline",
-                        Box::new(RespondToContactRequest {
-                            user_id: self.user.id,
-                            accept: false,
+                        Box::new(|notification, cx| {
+                            notification.respond_to_contact_request(false, cx)
                         }),
                     ),
                     (
                         "Accept",
-                        Box::new(RespondToContactRequest {
-                            user_id: self.user.id,
-                            accept: true,
+                        Box::new(|notification, cx| {
+                            notification.respond_to_contact_request(true, cx)
                         }),
                     ),
                 ],
@@ -71,7 +60,7 @@ impl View for ContactNotification {
                 self.user.clone(),
                 "accepted your contact request",
                 None,
-                Dismiss(self.user.id),
+                |notification, cx| notification.dismiss(cx),
                 vec![],
                 cx,
             ),
@@ -113,7 +102,7 @@ impl ContactNotification {
         }
     }
 
-    fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
         self.user_store.update(cx, |store, cx| {
             store
                 .dismiss_contact_request(self.user.id, cx)
@@ -122,14 +111,10 @@ impl ContactNotification {
         cx.emit(Event::Dismiss);
     }
 
-    fn respond_to_contact_request(
-        &mut self,
-        action: &RespondToContactRequest,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn respond_to_contact_request(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
         self.user_store
             .update(cx, |store, cx| {
-                store.respond_to_contact_request(action.user_id, action.accept, cx)
+                store.respond_to_contact_request(self.user.id, accept, cx)
             })
             .detach();
     }

crates/collab_ui/src/contacts_popover.rs 🔗

@@ -6,11 +6,11 @@ use crate::{
 use client::UserStore;
 use gpui::{
     actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
-    ViewContext, ViewHandle,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use picker::PickerEvent;
-use project::Project;
 use settings::Settings;
+use workspace::Workspace;
 
 actions!(contacts_popover, [ToggleContactFinder]);
 
@@ -29,23 +29,17 @@ enum Child {
 
 pub struct ContactsPopover {
     child: Child,
-    project: ModelHandle<Project>,
     user_store: ModelHandle<UserStore>,
+    workspace: WeakViewHandle<Workspace>,
     _subscription: Option<gpui::Subscription>,
 }
 
 impl ContactsPopover {
-    pub fn new(
-        project: ModelHandle<Project>,
-        user_store: ModelHandle<UserStore>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
         let mut this = Self {
-            child: Child::ContactList(
-                cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
-            ),
-            project,
-            user_store,
+            child: Child::ContactList(cx.add_view(|cx| ContactList::new(workspace, cx))),
+            user_store: workspace.read(cx).user_store().clone(),
+            workspace: workspace.downgrade(),
             _subscription: None,
         };
         this.show_contact_list(String::new(), cx);
@@ -74,16 +68,16 @@ impl ContactsPopover {
     }
 
     fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
-        let child = cx.add_view(|cx| {
-            ContactList::new(self.project.clone(), self.user_store.clone(), cx)
-                .with_editor_text(editor_text, cx)
-        });
-        cx.focus(&child);
-        self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
-            crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
-        }));
-        self.child = Child::ContactList(child);
-        cx.notify();
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let child = cx
+                .add_view(|cx| ContactList::new(&workspace, cx).with_editor_text(editor_text, cx));
+            cx.focus(&child);
+            self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
+                crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
+            }));
+            self.child = Child::ContactList(child);
+            cx.notify();
+        }
     }
 }
 

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -1,22 +1,20 @@
+use std::sync::{Arc, Weak};
+
 use call::{ActiveCall, IncomingCall};
 use client::proto;
 use futures::StreamExt;
 use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    impl_internal_actions,
     platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
     AnyElement, AppContext, Entity, View, ViewContext,
 };
 use settings::Settings;
 use util::ResultExt;
-use workspace::JoinProject;
-
-impl_internal_actions!(incoming_call_notification, [RespondToCall]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(IncomingCallNotification::respond_to_call);
+use workspace::AppState;
 
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    let app_state = Arc::downgrade(app_state);
     let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
     cx.spawn(|mut cx| async move {
         let mut notification_windows = Vec::new();
@@ -48,7 +46,7 @@ pub fn init(cx: &mut AppContext) {
                             is_movable: false,
                             screen: Some(screen),
                         },
-                        |_| IncomingCallNotification::new(incoming_call.clone()),
+                        |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
                     );
 
                     notification_windows.push(window_id);
@@ -66,28 +64,34 @@ struct RespondToCall {
 
 pub struct IncomingCallNotification {
     call: IncomingCall,
+    app_state: Weak<AppState>,
 }
 
 impl IncomingCallNotification {
-    pub fn new(call: IncomingCall) -> Self {
-        Self { call }
+    pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
+        Self { call, app_state }
     }
 
-    fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
+    fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
         let active_call = ActiveCall::global(cx);
-        if action.accept {
+        if accept {
             let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
             let caller_user_id = self.call.calling_user.id;
             let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
-            cx.spawn(|_, mut cx| async move {
+            cx.spawn(|this, mut cx| async move {
                 join.await?;
                 if let Some(project_id) = initial_project_id {
-                    cx.update(|cx| {
-                        cx.dispatch_global_action(JoinProject {
-                            project_id,
-                            follow_user_id: caller_user_id,
-                        })
-                    });
+                    this.update(&mut cx, |this, cx| {
+                        if let Some(app_state) = this.app_state.upgrade() {
+                            workspace::join_remote_project(
+                                project_id,
+                                caller_user_id,
+                                app_state,
+                                cx,
+                            )
+                            .detach_and_log_err(cx);
+                        }
+                    })?;
                 }
                 anyhow::Ok(())
             })
@@ -174,8 +178,8 @@ impl IncomingCallNotification {
                         .with_style(theme.accept_button.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, _, cx| {
-                    cx.dispatch_action(RespondToCall { accept: true });
+                .on_click(MouseButton::Left, |_, this, cx| {
+                    this.respond(true, cx);
                 })
                 .flex(1., true),
             )
@@ -188,8 +192,8 @@ impl IncomingCallNotification {
                         .with_style(theme.decline_button.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, _, cx| {
-                    cx.dispatch_action(RespondToCall { accept: false });
+                .on_click(MouseButton::Left, |_, this, cx| {
+                    this.respond(false, cx);
                 })
                 .flex(1., true),
             )

crates/collab_ui/src/notifications.rs 🔗

@@ -2,7 +2,7 @@ use client::User;
 use gpui::{
     elements::*,
     platform::{CursorStyle, MouseButton},
-    Action, AnyElement, Element, View, ViewContext,
+    AnyElement, Element, View, ViewContext,
 };
 use settings::Settings;
 use std::sync::Arc;
@@ -10,14 +10,18 @@ use std::sync::Arc;
 enum Dismiss {}
 enum Button {}
 
-pub fn render_user_notification<V: View, A: Action + Clone>(
+pub fn render_user_notification<F, V>(
     user: Arc<User>,
     title: &'static str,
     body: Option<&'static str>,
-    dismiss_action: A,
-    buttons: Vec<(&'static str, Box<dyn Action>)>,
+    on_dismiss: F,
+    buttons: Vec<(&'static str, Box<dyn Fn(&mut V, &mut ViewContext<V>)>)>,
     cx: &mut ViewContext<V>,
-) -> AnyElement<V> {
+) -> AnyElement<V>
+where
+    F: 'static + Fn(&mut V, &mut ViewContext<V>),
+    V: View,
+{
     let theme = cx.global::<Settings>().theme.clone();
     let theme = &theme.contact_notification;
 
@@ -64,9 +68,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                     })
                     .with_cursor_style(CursorStyle::PointingHand)
                     .with_padding(Padding::uniform(5.))
-                    .on_click(MouseButton::Left, move |_, _, cx| {
-                        cx.dispatch_any_action(dismiss_action.boxed_clone())
-                    })
+                    .on_click(MouseButton::Left, move |_, view, cx| on_dismiss(view, cx))
                     .aligned()
                     .constrained()
                     .with_height(
@@ -90,7 +92,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
             Some(
                 Flex::row()
                     .with_children(buttons.into_iter().enumerate().map(
-                        |(ix, (message, action))| {
+                        |(ix, (message, handler))| {
                             MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
                                 let button = theme.button.style_for(state, false);
                                 Label::new(message, button.text.clone())
@@ -98,9 +100,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                                     .with_style(button.container)
                             })
                             .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(MouseButton::Left, move |_, _, cx| {
-                                cx.dispatch_any_action(action.boxed_clone())
-                            })
+                            .on_click(MouseButton::Left, move |_, view, cx| handler(view, cx))
                         },
                     ))
                     .aligned()

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -2,22 +2,17 @@ use call::{room, ActiveCall};
 use client::User;
 use collections::HashMap;
 use gpui::{
-    actions,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
     AppContext, Entity, View, ViewContext,
 };
 use settings::Settings;
-use std::sync::Arc;
-use workspace::JoinProject;
-
-actions!(project_shared_notification, [DismissProject]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ProjectSharedNotification::join);
-    cx.add_action(ProjectSharedNotification::dismiss);
+use std::sync::{Arc, Weak};
+use workspace::AppState;
 
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    let app_state = Arc::downgrade(app_state);
     let active_call = ActiveCall::global(cx);
     let mut notification_windows = HashMap::default();
     cx.subscribe(&active_call, move |_, event, cx| match event {
@@ -50,6 +45,7 @@ pub fn init(cx: &mut AppContext) {
                             owner.clone(),
                             *project_id,
                             worktree_root_names.clone(),
+                            app_state.clone(),
                         )
                     },
                 );
@@ -82,23 +78,33 @@ pub struct ProjectSharedNotification {
     project_id: u64,
     worktree_root_names: Vec<String>,
     owner: Arc<User>,
+    app_state: Weak<AppState>,
 }
 
 impl ProjectSharedNotification {
-    fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
+    fn new(
+        owner: Arc<User>,
+        project_id: u64,
+        worktree_root_names: Vec<String>,
+        app_state: Weak<AppState>,
+    ) -> Self {
         Self {
             project_id,
             worktree_root_names,
             owner,
+            app_state,
         }
     }
 
-    fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
+    fn join(&mut self, cx: &mut ViewContext<Self>) {
         cx.remove_window();
-        cx.propagate_action();
+        if let Some(app_state) = self.app_state.upgrade() {
+            workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
+                .detach_and_log_err(cx);
+        }
     }
 
-    fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
         cx.remove_window();
     }
 
@@ -161,9 +167,6 @@ impl ProjectSharedNotification {
         enum Open {}
         enum Dismiss {}
 
-        let project_id = self.project_id;
-        let owner_user_id = self.owner.id;
-
         Flex::column()
             .with_child(
                 MouseEventHandler::<Open, Self>::new(0, cx, |_, cx| {
@@ -174,12 +177,7 @@ impl ProjectSharedNotification {
                         .with_style(theme.open_button.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(JoinProject {
-                        project_id,
-                        follow_user_id: owner_user_id,
-                    });
-                })
+                .on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
                 .flex(1., true),
             )
             .with_child(
@@ -191,8 +189,8 @@ impl ProjectSharedNotification {
                         .with_style(theme.dismiss_button.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, _, cx| {
-                    cx.dispatch_action(DismissProject);
+                .on_click(MouseButton::Left, |_, this, cx| {
+                    this.dismiss(cx);
                 })
                 .flex(1., true),
             )

crates/context_menu/src/context_menu.rs 🔗

@@ -2,7 +2,6 @@ use gpui::{
     anyhow,
     elements::*,
     geometry::vector::Vector2F,
-    impl_internal_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton},
     Action, AnyViewHandle, AppContext, Axis, Entity, MouseState, SizeConstraint, Subscription,
@@ -10,19 +9,13 @@ use gpui::{
 };
 use menu::*;
 use settings::Settings;
-use std::{any::TypeId, borrow::Cow, time::Duration};
-
-#[derive(Copy, Clone, PartialEq)]
-struct Clicked;
-
-impl_internal_actions!(context_menu, [Clicked]);
+use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(ContextMenu::select_first);
     cx.add_action(ContextMenu::select_last);
     cx.add_action(ContextMenu::select_next);
     cx.add_action(ContextMenu::select_prev);
-    cx.add_action(ContextMenu::clicked);
     cx.add_action(ContextMenu::confirm);
     cx.add_action(ContextMenu::cancel);
 }
@@ -37,21 +30,43 @@ pub enum ContextMenuItemLabel {
     Element(ContextMenuItemBuilder),
 }
 
-pub enum ContextMenuAction {
-    ParentAction {
-        action: Box<dyn Action>,
-    },
-    ViewAction {
-        action: Box<dyn Action>,
-        for_view: usize,
-    },
+impl From<Cow<'static, str>> for ContextMenuItemLabel {
+    fn from(s: Cow<'static, str>) -> Self {
+        Self::String(s)
+    }
+}
+
+impl From<&'static str> for ContextMenuItemLabel {
+    fn from(s: &'static str) -> Self {
+        Self::String(s.into())
+    }
+}
+
+impl From<String> for ContextMenuItemLabel {
+    fn from(s: String) -> Self {
+        Self::String(s.into())
+    }
+}
+
+impl<T> From<T> for ContextMenuItemLabel
+where
+    T: 'static + Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>,
+{
+    fn from(f: T) -> Self {
+        Self::Element(Box::new(f))
+    }
 }
 
-impl ContextMenuAction {
-    fn id(&self) -> TypeId {
+pub enum ContextMenuItemAction {
+    Action(Box<dyn Action>),
+    Handler(Arc<dyn Fn(&mut ViewContext<ContextMenu>)>),
+}
+
+impl Clone for ContextMenuItemAction {
+    fn clone(&self) -> Self {
         match self {
-            ContextMenuAction::ParentAction { action } => action.id(),
-            ContextMenuAction::ViewAction { action, .. } => action.id(),
+            Self::Action(action) => Self::Action(action.boxed_clone()),
+            Self::Handler(handler) => Self::Handler(handler.clone()),
         }
     }
 }
@@ -59,42 +74,27 @@ impl ContextMenuAction {
 pub enum ContextMenuItem {
     Item {
         label: ContextMenuItemLabel,
-        action: ContextMenuAction,
+        action: ContextMenuItemAction,
     },
     Static(StaticItem),
     Separator,
 }
 
 impl ContextMenuItem {
-    pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self {
+    pub fn action(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
         Self::Item {
-            label: ContextMenuItemLabel::Element(label),
-            action: ContextMenuAction::ParentAction {
-                action: Box::new(action),
-            },
+            label: label.into(),
+            action: ContextMenuItemAction::Action(Box::new(action)),
         }
     }
 
-    pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
-        Self::Item {
-            label: ContextMenuItemLabel::String(label.into()),
-            action: ContextMenuAction::ParentAction {
-                action: Box::new(action),
-            },
-        }
-    }
-
-    pub fn item_for_view(
-        label: impl Into<Cow<'static, str>>,
-        view_id: usize,
-        action: impl 'static + Action,
+    pub fn handler(
+        label: impl Into<ContextMenuItemLabel>,
+        handler: impl 'static + Fn(&mut ViewContext<ContextMenu>),
     ) -> Self {
         Self::Item {
-            label: ContextMenuItemLabel::String(label.into()),
-            action: ContextMenuAction::ViewAction {
-                action: Box::new(action),
-                for_view: view_id,
-            },
+            label: label.into(),
+            action: ContextMenuItemAction::Handler(Arc::new(handler)),
         }
     }
 
@@ -108,7 +108,10 @@ impl ContextMenuItem {
 
     fn action_id(&self) -> Option<TypeId> {
         match self {
-            ContextMenuItem::Item { action, .. } => Some(action.id()),
+            ContextMenuItem::Item { action, .. } => match action {
+                ContextMenuItemAction::Action(action) => Some(action.id()),
+                ContextMenuItemAction::Handler(_) => None,
+            },
             ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
         }
     }
@@ -218,22 +221,20 @@ impl ContextMenu {
         }
     }
 
-    fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
-        self.clicked = true;
-    }
-
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.selected_index {
             if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
                 match action {
-                    ContextMenuAction::ParentAction { action } => {
-                        cx.dispatch_any_action(action.boxed_clone())
-                    }
-                    ContextMenuAction::ViewAction { action, for_view } => {
+                    ContextMenuItemAction::Action(action) => {
                         let window_id = cx.window_id();
-                        cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone())
+                        cx.dispatch_any_action_at(
+                            window_id,
+                            self.parent_view_id,
+                            action.boxed_clone(),
+                        );
                     }
-                };
+                    ContextMenuItemAction::Handler(handler) => handler(cx),
+                }
                 self.reset(cx);
             }
         }
@@ -375,22 +376,17 @@ impl ContextMenu {
                                     &mut Default::default(),
                                     Some(ix) == self.selected_index,
                                 );
-                                let (action, view_id) = match action {
-                                    ContextMenuAction::ParentAction { action } => {
-                                        (action.boxed_clone(), self.parent_view_id)
-                                    }
-                                    ContextMenuAction::ViewAction { action, for_view } => {
-                                        (action.boxed_clone(), *for_view)
-                                    }
-                                };
 
-                                KeystrokeLabel::new(
-                                    view_id,
-                                    action.boxed_clone(),
-                                    style.keystroke.container,
-                                    style.keystroke.text.clone(),
-                                )
-                                .into_any()
+                                match action {
+                                    ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
+                                        self.parent_view_id,
+                                        action.boxed_clone(),
+                                        style.keystroke.container,
+                                        style.keystroke.text.clone(),
+                                    )
+                                    .into_any(),
+                                    ContextMenuItemAction::Handler(_) => Empty::new().into_any(),
+                                }
                             }
 
                             ContextMenuItem::Static(_) => Empty::new().into_any(),
@@ -422,18 +418,23 @@ impl ContextMenu {
                 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
                     match item {
                         ContextMenuItem::Item { label, action } => {
-                            let (action, view_id) = match action {
-                                ContextMenuAction::ParentAction { action } => {
-                                    (action.boxed_clone(), self.parent_view_id)
-                                }
-                                ContextMenuAction::ViewAction { action, for_view } => {
-                                    (action.boxed_clone(), *for_view)
-                                }
-                            };
-
+                            let action = action.clone();
+                            let view_id = self.parent_view_id;
                             MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
                                 let style =
                                     style.item.style_for(state, Some(ix) == self.selected_index);
+                                let keystroke = match &action {
+                                    ContextMenuItemAction::Action(action) => Some(
+                                        KeystrokeLabel::new(
+                                            view_id,
+                                            action.boxed_clone(),
+                                            style.keystroke.container,
+                                            style.keystroke.text.clone(),
+                                        )
+                                        .flex_float(),
+                                    ),
+                                    ContextMenuItemAction::Handler(_) => None,
+                                };
 
                                 Flex::row()
                                     .with_child(match label {
@@ -446,25 +447,26 @@ impl ContextMenu {
                                             element(state, style)
                                         }
                                     })
-                                    .with_child({
-                                        KeystrokeLabel::new(
-                                            view_id,
-                                            action.boxed_clone(),
-                                            style.keystroke.container,
-                                            style.keystroke.text.clone(),
-                                        )
-                                        .flex_float()
-                                    })
+                                    .with_children(keystroke)
                                     .contained()
                                     .with_style(style.container)
                             })
                             .with_cursor_style(CursorStyle::PointingHand)
                             .on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
                             .on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
-                            .on_click(MouseButton::Left, move |_, _, cx| {
-                                cx.dispatch_action(Clicked);
+                            .on_click(MouseButton::Left, move |_, menu, cx| {
+                                menu.clicked = true;
                                 let window_id = cx.window_id();
-                                cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
+                                match &action {
+                                    ContextMenuItemAction::Action(action) => {
+                                        cx.dispatch_any_action_at(
+                                            window_id,
+                                            view_id,
+                                            action.boxed_clone(),
+                                        );
+                                    }
+                                    ContextMenuItemAction::Handler(handler) => handler(cx),
+                                }
                             })
                             .on_drag(MouseButton::Left, |_, _, _| {})
                             .into_any()

crates/copilot/src/copilot.rs 🔗

@@ -458,7 +458,7 @@ impl Copilot {
         }
     }
 
-    fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+    pub fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         if let CopilotServer::Running(server) = &mut self.server {
             let task = match &server.sign_in_status {
                 SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {

crates/copilot/src/sign_in.rs 🔗

@@ -2,7 +2,6 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
 use gpui::{
     elements::*,
     geometry::rect::RectF,
-    impl_internal_actions,
     platform::{WindowBounds, WindowKind, WindowOptions},
     AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
     ViewHandle,
@@ -10,11 +9,6 @@ use gpui::{
 use settings::Settings;
 use theme::ui::modal;
 
-#[derive(PartialEq, Eq, Debug, Clone)]
-struct ClickedConnect;
-
-impl_internal_actions!(copilot_verification, [ClickedConnect]);
-
 #[derive(PartialEq, Eq, Debug, Clone)]
 struct CopyUserCode;
 
@@ -68,12 +62,6 @@ pub fn init(cx: &mut AppContext) {
             }
         })
         .detach();
-
-        cx.add_action(
-            |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
-                code_verification.connect_clicked = true;
-            },
-        );
     }
 }
 
@@ -219,9 +207,9 @@ impl CopilotCodeVerification {
                 cx,
                 {
                     let verification_uri = data.verification_uri.clone();
-                    move |_, _, cx| {
+                    move |_, verification, cx| {
                         cx.platform().open_url(&verification_uri);
-                        cx.dispatch_action(ClickedConnect)
+                        verification.connect_clicked = true;
                     }
                 },
             ))

crates/copilot_button/src/copilot_button.rs 🔗

@@ -1,145 +1,24 @@
-use std::sync::Arc;
-
 use context_menu::{ContextMenu, ContextMenuItem};
+use copilot::{Copilot, Reinstall, SignOut, Status};
 use editor::Editor;
 use gpui::{
     elements::*,
-    impl_internal_actions,
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, Element, Entity, MouseState, Subscription, View, ViewContext,
-    ViewHandle,
+    ViewHandle, WindowContext,
 };
 use settings::{settings_file::SettingsFile, Settings};
+use std::sync::Arc;
+use util::ResultExt;
 use workspace::{
-    item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast,
-    StatusItemView,
+    item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView, Toast,
+    Workspace,
 };
 
-use copilot::{Copilot, Reinstall, SignIn, SignOut, Status};
-
 const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
 const COPILOT_STARTING_TOAST_ID: usize = 1337;
 const COPILOT_ERROR_TOAST_ID: usize = 1338;
 
-#[derive(Clone, PartialEq)]
-pub struct DeployCopilotMenu;
-
-#[derive(Clone, PartialEq)]
-pub struct DeployCopilotStartMenu;
-
-#[derive(Clone, PartialEq)]
-pub struct HideCopilot;
-
-#[derive(Clone, PartialEq)]
-pub struct InitiateSignIn;
-
-#[derive(Clone, PartialEq)]
-pub struct ToggleCopilotForLanguage {
-    language: Arc<str>,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct ToggleCopilotGlobally;
-
-// TODO: Make the other code path use `get_or_insert` logic for this modal
-#[derive(Clone, PartialEq)]
-pub struct DeployCopilotModal;
-
-impl_internal_actions!(
-    copilot,
-    [
-        DeployCopilotMenu,
-        DeployCopilotStartMenu,
-        HideCopilot,
-        InitiateSignIn,
-        DeployCopilotModal,
-        ToggleCopilotForLanguage,
-        ToggleCopilotGlobally,
-    ]
-);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(CopilotButton::deploy_copilot_menu);
-    cx.add_action(CopilotButton::deploy_copilot_start_menu);
-    cx.add_action(
-        |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
-            let language = action.language.clone();
-            let show_copilot_suggestions = cx
-                .global::<Settings>()
-                .show_copilot_suggestions(Some(&language));
-
-            SettingsFile::update(cx, move |file_contents| {
-                file_contents.languages.insert(
-                    language,
-                    settings::EditorSettings {
-                        show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
-                        ..Default::default()
-                    },
-                );
-            })
-        },
-    );
-
-    cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
-        let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
-        SettingsFile::update(cx, move |file_contents| {
-            file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
-        })
-    });
-
-    cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| {
-        SettingsFile::update(cx, move |file_contents| {
-            file_contents.features.copilot = Some(false)
-        })
-    });
-
-    cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| {
-        let Some(copilot) = Copilot::global(cx) else {
-            return;
-        };
-        let status = copilot.read(cx).status();
-
-        match status {
-            Status::Starting { task } => {
-                cx.dispatch_action(workspace::Toast::new(
-                    COPILOT_STARTING_TOAST_ID,
-                    "Copilot is starting...",
-                ));
-                let window_id = cx.window_id();
-                let task = task.to_owned();
-                cx.spawn(|handle, mut cx| async move {
-                    task.await;
-                    cx.update(|cx| {
-                        if let Some(copilot) = Copilot::global(cx) {
-                            let status = copilot.read(cx).status();
-                            match status {
-                                Status::Authorized => cx.dispatch_action_at(
-                                    window_id,
-                                    handle.id(),
-                                    workspace::Toast::new(
-                                        COPILOT_STARTING_TOAST_ID,
-                                        "Copilot has started!",
-                                    ),
-                                ),
-                                _ => {
-                                    cx.dispatch_action_at(
-                                        window_id,
-                                        handle.id(),
-                                        DismissToast::new(COPILOT_STARTING_TOAST_ID),
-                                    );
-                                    cx.dispatch_action_at(window_id, handle.id(), SignIn)
-                                }
-                            }
-                        }
-                    })
-                })
-                .detach();
-            }
-            _ => cx.dispatch_action(SignIn),
-        }
-    })
-}
-
 pub struct CopilotButton {
     popup_menu: ViewHandle<ContextMenu>,
     editor_subscription: Option<(Subscription, usize)>,
@@ -217,15 +96,25 @@ impl View for CopilotButton {
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, {
                     let status = status.clone();
-                    move |_, _, cx| match status {
-                        Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
-                        Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
-                            COPILOT_ERROR_TOAST_ID,
-                            format!("Copilot can't be started: {}", e),
-                            "Reinstall Copilot",
-                            Reinstall,
-                        )),
-                        _ => cx.dispatch_action(DeployCopilotStartMenu),
+                    move |_, this, cx| match status {
+                        Status::Authorized => this.deploy_copilot_menu(cx),
+                        Status::Error(ref e) => {
+                            if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>()
+                            {
+                                workspace.update(cx, |workspace, cx| {
+                                    workspace.show_toast(
+                                        Toast::new_action(
+                                            COPILOT_ERROR_TOAST_ID,
+                                            format!("Copilot can't be started: {}", e),
+                                            "Reinstall Copilot",
+                                            Reinstall,
+                                        ),
+                                        cx,
+                                    );
+                                });
+                            }
+                        }
+                        _ => this.deploy_copilot_start_menu(cx),
                     }
                 })
                 .with_tooltip::<Self>(
@@ -264,15 +153,15 @@ impl CopilotButton {
         }
     }
 
-    pub fn deploy_copilot_start_menu(
-        &mut self,
-        _: &DeployCopilotStartMenu,
-        cx: &mut ViewContext<Self>,
-    ) {
+    pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
         let mut menu_options = Vec::with_capacity(2);
 
-        menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
-        menu_options.push(ContextMenuItem::item("Disable Copilot", HideCopilot));
+        menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
+            initiate_sign_in(cx)
+        }));
+        menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| {
+            hide_copilot(cx)
+        }));
 
         self.popup_menu.update(cx, |menu, cx| {
             menu.show(
@@ -284,53 +173,48 @@ impl CopilotButton {
         });
     }
 
-    pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
+    pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
         let settings = cx.global::<Settings>();
 
         let mut menu_options = Vec::with_capacity(6);
 
-        if let Some(language) = &self.language {
+        if let Some(language) = self.language.clone() {
             let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
-
-            menu_options.push(ContextMenuItem::item(
+            menu_options.push(ContextMenuItem::handler(
                 format!(
                     "{} Suggestions for {}",
                     if language_enabled { "Hide" } else { "Show" },
                     language
                 ),
-                ToggleCopilotForLanguage {
-                    language: language.to_owned(),
-                },
+                move |cx| toggle_copilot_for_language(language.clone(), cx),
             ));
         }
 
         let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
-        menu_options.push(ContextMenuItem::item(
+        menu_options.push(ContextMenuItem::handler(
             if globally_enabled {
                 "Hide Suggestions for All Files"
             } else {
                 "Show Suggestions for All Files"
             },
-            ToggleCopilotGlobally,
+            |cx| toggle_copilot_globally(cx),
         ));
 
         menu_options.push(ContextMenuItem::Separator);
 
         let icon_style = settings.theme.copilot.out_link_icon.clone();
-        menu_options.push(ContextMenuItem::element_item(
-            Box::new(
-                move |state: &mut MouseState, style: &theme::ContextMenuItem| {
-                    Flex::row()
-                        .with_child(Label::new("Copilot Settings", style.label.clone()))
-                        .with_child(theme::ui::icon(icon_style.style_for(state, false)))
-                        .align_children_center()
-                        .into_any()
-                },
-            ),
+        menu_options.push(ContextMenuItem::action(
+            move |state: &mut MouseState, style: &theme::ContextMenuItem| {
+                Flex::row()
+                    .with_child(Label::new("Copilot Settings", style.label.clone()))
+                    .with_child(theme::ui::icon(icon_style.style_for(state, false)))
+                    .align_children_center()
+                    .into_any()
+            },
             OsOpen::new(COPILOT_SETTINGS_URL),
         ));
 
-        menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
+        menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
 
         self.popup_menu.update(cx, |menu, cx| {
             menu.show(
@@ -375,3 +259,80 @@ impl StatusItemView for CopilotButton {
         cx.notify();
     }
 }
+
+fn toggle_copilot_globally(cx: &mut AppContext) {
+    let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
+    SettingsFile::update(cx, move |file_contents| {
+        file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
+    });
+}
+
+fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
+    let show_copilot_suggestions = cx
+        .global::<Settings>()
+        .show_copilot_suggestions(Some(&language));
+
+    SettingsFile::update(cx, move |file_contents| {
+        file_contents.languages.insert(
+            language,
+            settings::EditorSettings {
+                show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
+                ..Default::default()
+            },
+        );
+    })
+}
+
+fn hide_copilot(cx: &mut AppContext) {
+    SettingsFile::update(cx, move |file_contents| {
+        file_contents.features.copilot = Some(false)
+    })
+}
+
+fn initiate_sign_in(cx: &mut WindowContext) {
+    let Some(copilot) = Copilot::global(cx) else {
+        return;
+    };
+    let status = copilot.read(cx).status();
+
+    match status {
+        Status::Starting { task } => {
+            let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() else {
+                return;
+            };
+
+            workspace.update(cx, |workspace, cx| {
+                workspace.show_toast(
+                    Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
+                    cx,
+                )
+            });
+            let workspace = workspace.downgrade();
+            cx.spawn(|mut cx| async move {
+                task.await;
+                if let Some(copilot) = cx.read(Copilot::global) {
+                    workspace
+                        .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
+                            Status::Authorized => workspace.show_toast(
+                                Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
+                                cx,
+                            ),
+                            _ => {
+                                workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
+                                copilot
+                                    .update(cx, |copilot, cx| copilot.sign_in(cx))
+                                    .detach_and_log_err(cx);
+                            }
+                        })
+                        .log_err();
+                }
+            })
+            .detach();
+        }
+        _ => {
+            copilot
+                .update(cx, |copilot, cx| copilot.sign_in(cx))
+                .detach_and_log_err(cx);
+        }
+    }
+}

crates/diagnostics/src/diagnostics.rs 🔗

@@ -10,8 +10,8 @@ use editor::{
     Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
 };
 use gpui::{
-    actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
-    AppContext, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
+    ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::{
     Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
@@ -38,8 +38,6 @@ use workspace::{
 
 actions!(diagnostics, [Deploy]);
 
-impl_internal_actions!(diagnostics, [Jump]);
-
 const CONTEXT_LINE_COUNT: u32 = 1;
 
 pub fn init(cx: &mut AppContext) {
@@ -551,6 +549,11 @@ impl Item for ProjectDiagnosticsEditor {
         false
     }
 
+    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
+    }
+
     fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, cx))

crates/editor/src/editor.rs 🔗

@@ -37,7 +37,7 @@ use gpui::{
     executor,
     fonts::{self, HighlightStyle, TextStyle},
     geometry::vector::Vector2F,
-    impl_actions, impl_internal_actions,
+    impl_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton},
     serde_json::{self, json},
@@ -45,7 +45,7 @@ use gpui::{
     ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
-use hover_popover::{hide_hover, HideHover, HoverState};
+use hover_popover::{hide_hover, HoverState};
 pub use items::MAX_TAB_TITLE_LEN;
 use itertools::Itertools;
 pub use language::{char_kind, CharKind};
@@ -86,7 +86,7 @@ use std::{
 pub use sum_tree::Bias;
 use theme::{DiagnosticStyle, Theme};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
-use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
+use workspace::{ItemNavHistory, ViewId, Workspace};
 
 use crate::git::diff_hunk_to_display;
 
@@ -104,16 +104,6 @@ pub struct SelectNext {
     pub replace_newest: bool,
 }
 
-#[derive(Clone, PartialEq)]
-pub struct Select(pub SelectPhase);
-
-#[derive(Clone, Debug, PartialEq)]
-pub struct Jump {
-    path: ProjectPath,
-    position: Point,
-    anchor: language::Anchor,
-}
-
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct SelectToBeginningOfLine {
     #[serde(default)]
@@ -285,8 +275,6 @@ impl_actions!(
     ]
 );
 
-impl_internal_actions!(editor, [Select, Jump]);
-
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
 enum InputComposition {}
@@ -299,7 +287,6 @@ pub enum Direction {
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::new_file);
-    cx.add_action(Editor::select);
     cx.add_action(Editor::cancel);
     cx.add_action(Editor::newline);
     cx.add_action(Editor::newline_above);
@@ -381,7 +368,6 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::show_completions);
     cx.add_action(Editor::toggle_code_actions);
     cx.add_action(Editor::open_excerpts);
-    cx.add_action(Editor::jump);
     cx.add_action(Editor::toggle_soft_wrap);
     cx.add_action(Editor::reveal_in_finder);
     cx.add_action(Editor::copy_path);
@@ -400,8 +386,6 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::copilot_suggest);
 
     hover_popover::init(cx);
-    link_go_to_definition::init(cx);
-    mouse_context_menu::init(cx);
     scroll::actions::init(cx);
 
     workspace::register_project_item::<Editor>(cx);
@@ -509,7 +493,7 @@ pub struct Editor {
     pending_rename: Option<RenameState>,
     searchable: bool,
     cursor_shape: CursorShape,
-    workspace_id: Option<WorkspaceId>,
+    workspace: Option<(WeakViewHandle<Workspace>, i64)>,
     keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
     input_enabled: bool,
     read_only: bool,
@@ -1282,7 +1266,7 @@ impl Editor {
             searchable: true,
             override_text_style: None,
             cursor_shape: Default::default(),
-            workspace_id: None,
+            workspace: None,
             keymap_context_layers: Default::default(),
             input_enabled: true,
             read_only: false,
@@ -1495,7 +1479,7 @@ impl Editor {
                 }
             }
 
-            hide_hover(self, &HideHover, cx);
+            hide_hover(self, cx);
 
             if old_cursor_position.to_display_point(&display_map).row()
                 != new_cursor_position.to_display_point(&display_map).row()
@@ -1563,7 +1547,7 @@ impl Editor {
         });
     }
 
-    fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
+    fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
         self.hide_context_menu(cx);
 
         match phase {
@@ -1571,20 +1555,20 @@ impl Editor {
                 position,
                 add,
                 click_count,
-            } => self.begin_selection(*position, *add, *click_count, cx),
+            } => self.begin_selection(position, add, click_count, cx),
             SelectPhase::BeginColumnar {
                 position,
                 goal_column,
-            } => self.begin_columnar_selection(*position, *goal_column, cx),
+            } => self.begin_columnar_selection(position, goal_column, cx),
             SelectPhase::Extend {
                 position,
                 click_count,
-            } => self.extend_selection(*position, *click_count, cx),
+            } => self.extend_selection(position, click_count, cx),
             SelectPhase::Update {
                 position,
                 goal_column,
                 scroll_position,
-            } => self.update_selection(*position, *goal_column, *scroll_position, cx),
+            } => self.update_selection(position, goal_column, scroll_position, cx),
             SelectPhase::End => self.end_selection(cx),
         }
     }
@@ -1879,7 +1863,7 @@ impl Editor {
             return;
         }
 
-        if hide_hover(self, &HideHover, cx) {
+        if hide_hover(self, cx) {
             return;
         }
 
@@ -6756,10 +6740,14 @@ impl Editor {
         });
     }
 
-    fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
-        let editor = workspace.open_path(action.path.clone(), None, true, cx);
-        let position = action.position;
-        let anchor = action.anchor;
+    fn jump(
+        workspace: &mut Workspace,
+        path: ProjectPath,
+        position: Point,
+        anchor: language::Anchor,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let editor = workspace.open_path(path, None, true, cx);
         cx.spawn(|_, mut cx| async move {
             let editor = editor
                 .await?
@@ -7025,7 +7013,7 @@ impl View for Editor {
 
         if font_changed {
             cx.defer(move |editor, cx: &mut ViewContext<Editor>| {
-                hide_hover(editor, &HideHover, cx);
+                hide_hover(editor, cx);
                 hide_link_definition(editor, cx);
             });
         }
@@ -7074,7 +7062,7 @@ impl View for Editor {
         self.buffer
             .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
         self.hide_context_menu(cx);
-        hide_hover(self, &HideHover, cx);
+        hide_hover(self, cx);
         cx.emit(Event::Blurred);
         cx.notify();
     }

crates/editor/src/editor_tests.rs 🔗

@@ -24,7 +24,7 @@ use util::{
 };
 use workspace::{
     item::{FollowableItem, Item, ItemHandle},
-    NavigationEntry, Pane, ViewId,
+    NavigationEntry, ViewId,
 };
 
 #[gpui::test]
@@ -486,12 +486,15 @@ fn test_clone(cx: &mut TestAppContext) {
 }
 
 #[gpui::test]
-fn test_navigation_history(cx: &mut TestAppContext) {
+async fn test_navigation_history(cx: &mut TestAppContext) {
     cx.update(|cx| cx.set_global(Settings::test(cx)));
     cx.set_global(DragAndDrop::<Workspace>::default());
     use workspace::item::Item;
-    let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx));
 
+    let fs = FakeFs::new(cx.background());
+    let project = Project::test(fs, [], cx).await;
+    let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+    let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
     cx.add_view(&pane, |cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
         let mut editor = build_editor(buffer.clone(), cx);
@@ -5576,7 +5579,8 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
     Settings::test_async(cx);
     let fs = FakeFs::new(cx.background());
     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-    let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx));
+    let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+    let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
     let leader = pane.update(cx, |_, cx| {
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

crates/editor/src/element.rs 🔗

@@ -1,20 +1,19 @@
 use super::{
     display_map::{BlockContext, ToDisplayPoint},
-    Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Select, SelectPhase, SoftWrap,
-    ToPoint, MAX_LINE_LEN,
+    Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, SelectPhase, SoftWrap, ToPoint,
+    MAX_LINE_LEN,
 };
 use crate::{
     display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
-        HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
+        hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
+        MIN_POPOVER_LINE_HEIGHT,
     },
     link_go_to_definition::{
-        GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
+        go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
     },
-    mouse_context_menu::DeployMouseContextMenu,
-    scroll::actions::Scroll,
-    EditorStyle, GutterHover, UnfoldAt,
+    mouse_context_menu, EditorStyle, GutterHover, UnfoldAt,
 };
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
@@ -115,9 +114,10 @@ impl EditorElement {
             )
             .on_down(MouseButton::Left, {
                 let position_map = position_map.clone();
-                move |e, _, cx| {
+                move |event, editor, cx| {
                     if !Self::mouse_down(
-                        e.platform_event,
+                        editor,
+                        event.platform_event,
                         position_map.as_ref(),
                         text_bounds,
                         gutter_bounds,
@@ -129,8 +129,9 @@ impl EditorElement {
             })
             .on_down(MouseButton::Right, {
                 let position_map = position_map.clone();
-                move |event, _, cx| {
+                move |event, editor, cx| {
                     if !Self::mouse_right_down(
+                        editor,
                         event.position,
                         position_map.as_ref(),
                         text_bounds,
@@ -144,12 +145,12 @@ impl EditorElement {
                 let position_map = position_map.clone();
                 move |event, editor, cx| {
                     if !Self::mouse_up(
+                        editor,
                         event.position,
                         event.cmd,
                         event.shift,
                         position_map.as_ref(),
                         text_bounds,
-                        editor,
                         cx,
                     ) {
                         cx.propagate_event()
@@ -160,10 +161,10 @@ impl EditorElement {
                 let position_map = position_map.clone();
                 move |event, editor, cx| {
                     if !Self::mouse_dragged(
+                        editor,
                         event.platform_event,
                         position_map.as_ref(),
                         text_bounds,
-                        editor,
                         cx,
                     ) {
                         cx.propagate_event()
@@ -172,24 +173,31 @@ impl EditorElement {
             })
             .on_move({
                 let position_map = position_map.clone();
-                move |e, _, cx| {
-                    if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
+                move |event, editor, cx| {
+                    if !Self::mouse_moved(
+                        editor,
+                        event.platform_event,
+                        &position_map,
+                        text_bounds,
+                        cx,
+                    ) {
                         cx.propagate_event()
                     }
                 }
             })
-            .on_move_out(move |_, _: &mut Editor, cx| {
+            .on_move_out(move |_, editor: &mut Editor, cx| {
                 if has_popovers {
-                    cx.dispatch_action(HideHover);
+                    hide_hover(editor, cx);
                 }
             })
             .on_scroll({
                 let position_map = position_map.clone();
-                move |e, _, cx| {
+                move |event, editor, cx| {
                     if !Self::scroll(
-                        e.position,
-                        *e.delta.raw(),
-                        e.delta.precise(),
+                        editor,
+                        event.position,
+                        *event.delta.raw(),
+                        event.delta.precise(),
                         &position_map,
                         bounds,
                         cx,
@@ -212,6 +220,7 @@ impl EditorElement {
     }
 
     fn mouse_down(
+        editor: &mut Editor,
         MouseButtonEvent {
             position,
             modifiers:
@@ -239,27 +248,37 @@ impl EditorElement {
         let (position, target_position) = position_map.point_for_position(text_bounds, position);
 
         if shift && alt {
-            cx.dispatch_action(Select(SelectPhase::BeginColumnar {
-                position,
-                goal_column: target_position.column(),
-            }));
+            editor.select(
+                SelectPhase::BeginColumnar {
+                    position,
+                    goal_column: target_position.column(),
+                },
+                cx,
+            );
         } else if shift && !ctrl && !alt && !cmd {
-            cx.dispatch_action(Select(SelectPhase::Extend {
-                position,
-                click_count,
-            }));
+            editor.select(
+                SelectPhase::Extend {
+                    position,
+                    click_count,
+                },
+                cx,
+            );
         } else {
-            cx.dispatch_action(Select(SelectPhase::Begin {
-                position,
-                add: alt,
-                click_count,
-            }));
+            editor.select(
+                SelectPhase::Begin {
+                    position,
+                    add: alt,
+                    click_count,
+                },
+                cx,
+            );
         }
 
         true
     }
 
     fn mouse_right_down(
+        editor: &mut Editor,
         position: Vector2F,
         position_map: &PositionMap,
         text_bounds: RectF,
@@ -270,38 +289,45 @@ impl EditorElement {
         }
 
         let (point, _) = position_map.point_for_position(text_bounds, position);
-
-        cx.dispatch_action(DeployMouseContextMenu { position, point });
+        mouse_context_menu::deploy_context_menu(editor, position, point, cx);
         true
     }
 
     fn mouse_up(
+        editor: &mut Editor,
         position: Vector2F,
         cmd: bool,
         shift: bool,
         position_map: &PositionMap,
         text_bounds: RectF,
-        editor: &mut Editor,
         cx: &mut EventContext<Editor>,
     ) -> bool {
         let end_selection = editor.has_pending_selection();
         let pending_nonempty_selections = editor.has_pending_nonempty_selection();
 
         if end_selection {
-            cx.dispatch_action(Select(SelectPhase::End));
+            editor.select(SelectPhase::End, cx);
         }
 
-        if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
-            let (point, target_point) = position_map.point_for_position(text_bounds, position);
+        if let Some(workspace) = editor
+            .workspace
+            .as_ref()
+            .and_then(|(workspace, _)| workspace.upgrade(cx))
+        {
+            if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
+                let (point, target_point) = position_map.point_for_position(text_bounds, position);
 
-            if point == target_point {
-                if shift {
-                    cx.dispatch_action(GoToFetchedTypeDefinition { point });
-                } else {
-                    cx.dispatch_action(GoToFetchedDefinition { point });
-                }
+                if point == target_point {
+                    workspace.update(cx, |workspace, cx| {
+                        if shift {
+                            go_to_fetched_type_definition(workspace, point, cx);
+                        } else {
+                            go_to_fetched_definition(workspace, point, cx);
+                        }
+                    });
 
-                return true;
+                    return true;
+                }
             }
         }
 
@@ -309,6 +335,7 @@ impl EditorElement {
     }
 
     fn mouse_dragged(
+        editor: &mut Editor,
         MouseMovedEvent {
             modifiers: Modifiers { cmd, shift, .. },
             position,
@@ -316,7 +343,6 @@ impl EditorElement {
         }: MouseMovedEvent,
         position_map: &PositionMap,
         text_bounds: RectF,
-        editor: &mut Editor,
         cx: &mut EventContext<Editor>,
     ) -> bool {
         // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
@@ -332,11 +358,7 @@ impl EditorElement {
             None
         };
 
-        cx.dispatch_action(UpdateGoToDefinitionLink {
-            point,
-            cmd_held: cmd,
-            shift_held: shift,
-        });
+        update_go_to_definition_link(editor, point, cmd, shift, cx);
 
         if editor.has_pending_selection() {
             let mut scroll_delta = Vector2F::zero();
@@ -368,22 +390,25 @@ impl EditorElement {
             let (position, target_position) =
                 position_map.point_for_position(text_bounds, position);
 
-            cx.dispatch_action(Select(SelectPhase::Update {
-                position,
-                goal_column: target_position.column(),
-                scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
-                    .clamp(Vector2F::zero(), position_map.scroll_max),
-            }));
-
-            cx.dispatch_action(HoverAt { point });
+            editor.select(
+                SelectPhase::Update {
+                    position,
+                    goal_column: target_position.column(),
+                    scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
+                        .clamp(Vector2F::zero(), position_map.scroll_max),
+                },
+                cx,
+            );
+            hover_at(editor, point, cx);
             true
         } else {
-            cx.dispatch_action(HoverAt { point });
+            hover_at(editor, point, cx);
             false
         }
     }
 
     fn mouse_moved(
+        editor: &mut Editor,
         MouseMovedEvent {
             modifiers: Modifiers { shift, cmd, .. },
             position,
@@ -397,18 +422,14 @@ impl EditorElement {
         // Don't trigger hover popover if mouse is hovering over context menu
         let point = position_to_display_point(position, text_bounds, position_map);
 
-        cx.dispatch_action(UpdateGoToDefinitionLink {
-            point,
-            cmd_held: cmd,
-            shift_held: shift,
-        });
-
-        cx.dispatch_action(HoverAt { point });
+        update_go_to_definition_link(editor, point, cmd, shift, cx);
+        hover_at(editor, point, cx);
 
         true
     }
 
     fn scroll(
+        editor: &mut Editor,
         position: Vector2F,
         mut delta: Vector2F,
         precise: bool,
@@ -436,11 +457,7 @@ impl EditorElement {
         let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
         let y = (scroll_position.y() * line_height - delta.y()) / line_height;
         let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
-
-        cx.dispatch_action(Scroll {
-            scroll_position,
-            axis,
-        });
+        editor.scroll(scroll_position, axis, cx);
 
         true
     }
@@ -1421,18 +1438,15 @@ impl EditorElement {
                 } => {
                     let id = *id;
                     let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
-                        let jump_position = range
+                        let jump_path = ProjectPath {
+                            worktree_id: file.worktree_id(cx),
+                            path: file.path.clone(),
+                        };
+                        let jump_anchor = range
                             .primary
                             .as_ref()
                             .map_or(range.context.start, |primary| primary.start);
-                        let jump_action = crate::Jump {
-                            path: ProjectPath {
-                                worktree_id: file.worktree_id(cx),
-                                path: file.path.clone(),
-                            },
-                            position: language::ToPoint::to_point(&jump_position, buffer),
-                            anchor: jump_position,
-                        };
+                        let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
 
                         enum JumpIcon {}
                         MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
@@ -1449,8 +1463,22 @@ impl EditorElement {
                                 .with_height(style.button_width)
                         })
                         .with_cursor_style(CursorStyle::PointingHand)
-                        .on_click(MouseButton::Left, move |_, _, cx| {
-                            cx.dispatch_action(jump_action.clone())
+                        .on_click(MouseButton::Left, move |_, editor, cx| {
+                            if let Some(workspace) = editor
+                                .workspace
+                                .as_ref()
+                                .and_then(|(workspace, _)| workspace.upgrade(cx))
+                            {
+                                workspace.update(cx, |workspace, cx| {
+                                    Editor::jump(
+                                        workspace,
+                                        jump_path.clone(),
+                                        jump_position,
+                                        jump_anchor,
+                                        cx,
+                                    );
+                                });
+                            }
                         })
                         .with_tooltip::<JumpIcon>(
                             id.into(),

crates/editor/src/hover_popover.rs 🔗

@@ -3,7 +3,6 @@ use gpui::{
     actions,
     elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
     fonts::{HighlightStyle, Underline, Weight},
-    impl_internal_actions,
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
 };
@@ -25,21 +24,10 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
 pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
 pub const HOVER_POPOVER_GAP: f32 = 10.;
 
-#[derive(Clone, PartialEq)]
-pub struct HoverAt {
-    pub point: Option<DisplayPoint>,
-}
-
-#[derive(Copy, Clone, PartialEq)]
-pub struct HideHover;
-
 actions!(editor, [Hover]);
-impl_internal_actions!(editor, [HoverAt, HideHover]);
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(hover);
-    cx.add_action(hover_at);
-    cx.add_action(hide_hover);
 }
 
 /// Bindable action which uses the most recent selection head to trigger a hover
@@ -50,12 +38,12 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
 
 /// The internal hover action dispatches between `show_hover` or `hide_hover`
 /// depending on whether a point to hover over is provided.
-pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Editor>) {
+pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
     if cx.global::<Settings>().hover_popover_enabled {
-        if let Some(point) = action.point {
+        if let Some(point) = point {
             show_hover(editor, point, false, cx);
         } else {
-            hide_hover(editor, &HideHover, cx);
+            hide_hover(editor, cx);
         }
     }
 }
@@ -63,7 +51,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
 /// Hides the type information popup.
 /// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 /// selections changed.
-pub fn hide_hover(editor: &mut Editor, _: &HideHover, cx: &mut ViewContext<Editor>) -> bool {
+pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
     let did_hide = editor.hover_state.info_popover.take().is_some()
         | editor.hover_state.diagnostic_popover.take().is_some();
 
@@ -130,7 +118,7 @@ fn show_hover(
                 // Hover triggered from same location as last time. Don't show again.
                 return;
             } else {
-                hide_hover(editor, &HideHover, cx);
+                hide_hover(editor, cx);
             }
         }
     }
@@ -736,15 +724,7 @@ mod tests {
             fn test() { printˇln!(); }
         "});
 
-        cx.update_editor(|editor, cx| {
-            hover_at(
-                editor,
-                &HoverAt {
-                    point: Some(hover_point),
-                },
-                cx,
-            )
-        });
+        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
         assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
 
         // After delay, hover should be visible.
@@ -783,15 +763,7 @@ mod tests {
         let mut request = cx
             .lsp
             .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
-        cx.update_editor(|editor, cx| {
-            hover_at(
-                editor,
-                &HoverAt {
-                    point: Some(hover_point),
-                },
-                cx,
-            )
-        });
+        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
         cx.foreground()
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         request.next().await;

crates/editor/src/items.rs 🔗

@@ -794,7 +794,7 @@ impl Item for Editor {
     fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
         let workspace_id = workspace.database_id();
         let item_id = cx.view_id();
-        self.workspace_id = Some(workspace_id);
+        self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
 
         fn serialize(
             buffer: ModelHandle<Buffer>,
@@ -819,9 +819,9 @@ impl Item for Editor {
             serialize(buffer.clone(), workspace_id, item_id, cx);
 
             cx.subscribe(&buffer, |this, buffer, event, cx| {
-                if let Some(workspace_id) = this.workspace_id {
+                if let Some((_, workspace_id)) = this.workspace.as_ref() {
                     if let language::Event::FileHandleChanged = event {
-                        serialize(buffer, workspace_id, cx.view_id(), cx);
+                        serialize(buffer, *workspace_id, cx.view_id(), cx);
                     }
                 }
             })
@@ -1,6 +1,6 @@
 use std::ops::Range;
 
-use gpui::{impl_internal_actions, AppContext, Task, ViewContext};
+use gpui::{Task, ViewContext};
 use language::{Bias, ToOffset};
 use project::LocationLink;
 use settings::Settings;
@@ -8,42 +8,9 @@ use util::TryFutureExt;
 use workspace::Workspace;
 
 use crate::{
-    Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, Select,
-    SelectPhase,
+    Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, SelectPhase,
 };
 
-#[derive(Clone, PartialEq)]
-pub struct UpdateGoToDefinitionLink {
-    pub point: Option<DisplayPoint>,
-    pub cmd_held: bool,
-    pub shift_held: bool,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct GoToFetchedDefinition {
-    pub point: DisplayPoint,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct GoToFetchedTypeDefinition {
-    pub point: DisplayPoint,
-}
-
-impl_internal_actions!(
-    editor,
-    [
-        UpdateGoToDefinitionLink,
-        GoToFetchedDefinition,
-        GoToFetchedTypeDefinition
-    ]
-);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(update_go_to_definition_link);
-    cx.add_action(go_to_fetched_definition);
-    cx.add_action(go_to_fetched_type_definition);
-}
-
 #[derive(Debug, Default)]
 pub struct LinkGoToDefinitionState {
     pub last_mouse_location: Option<Anchor>,
@@ -55,11 +22,9 @@ pub struct LinkGoToDefinitionState {
 
 pub fn update_go_to_definition_link(
     editor: &mut Editor,
-    &UpdateGoToDefinitionLink {
-        point,
-        cmd_held,
-        shift_held,
-    }: &UpdateGoToDefinitionLink,
+    point: Option<DisplayPoint>,
+    cmd_held: bool,
+    shift_held: bool,
     cx: &mut ViewContext<Editor>,
 ) {
     let pending_nonempty_selection = editor.has_pending_nonempty_selection();
@@ -286,7 +251,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 
 pub fn go_to_fetched_definition(
     workspace: &mut Workspace,
-    &GoToFetchedDefinition { point }: &GoToFetchedDefinition,
+    point: DisplayPoint,
     cx: &mut ViewContext<Workspace>,
 ) {
     go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx);
@@ -294,7 +259,7 @@ pub fn go_to_fetched_definition(
 
 pub fn go_to_fetched_type_definition(
     workspace: &mut Workspace,
-    &GoToFetchedTypeDefinition { point }: &GoToFetchedTypeDefinition,
+    point: DisplayPoint,
     cx: &mut ViewContext<Workspace>,
 ) {
     go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx);
@@ -334,11 +299,11 @@ fn go_to_fetched_definition_of_kind(
     } else {
         editor_handle.update(cx, |editor, cx| {
             editor.select(
-                &Select(SelectPhase::Begin {
+                SelectPhase::Begin {
                     position: point,
                     add: false,
                     click_count: 1,
-                }),
+                },
                 cx,
             );
         });
@@ -411,15 +376,7 @@ mod tests {
 
         // Press cmd+shift to trigger highlight
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                &UpdateGoToDefinitionLink {
-                    point: Some(hover_point),
-                    cmd_held: true,
-                    shift_held: true,
-                },
-                cx,
-            );
+            update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -470,11 +427,7 @@ mod tests {
             });
 
         cx.update_workspace(|workspace, cx| {
-            go_to_fetched_type_definition(
-                workspace,
-                &GoToFetchedTypeDefinition { point: hover_point },
-                cx,
-            );
+            go_to_fetched_type_definition(workspace, hover_point, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -527,15 +480,7 @@ mod tests {
         });
 
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                &UpdateGoToDefinitionLink {
-                    point: Some(hover_point),
-                    cmd_held: true,
-                    shift_held: false,
-                },
-                cx,
-            );
+            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -569,15 +514,7 @@ mod tests {
             ])))
         });
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                &UpdateGoToDefinitionLink {
-                    point: Some(hover_point),
-                    cmd_held: true,
-                    shift_held: false,
-                },
-                cx,
-            );
+            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -599,15 +536,7 @@ mod tests {
                 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
             });
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                &UpdateGoToDefinitionLink {
-                    point: Some(hover_point),
-                    cmd_held: true,
-                    shift_held: false,
-                },
-                cx,
-            );
+            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -624,15 +553,7 @@ mod tests {
             fn do_work() { teˇst(); }
         "});
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                &UpdateGoToDefinitionLink {
-                    point: Some(hover_point),
-                    cmd_held: false,
-                    shift_held: false,
-                },
-                cx,
-            );
+            update_go_to_definition_link(editor, Some(hover_point), false, false, cx);
         });
         cx.foreground().run_until_parked();
 
@@ -691,15 +612,7 @@ mod tests {
 
         // Moving the mouse restores the highlights.
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                &UpdateGoToDefinitionLink {
-                    point: Some(hover_point),
-                    cmd_held: true,
-                    shift_held: false,
-                },
-                cx,
-            );
+            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
         });
         cx.foreground().run_until_parked();
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@@ -713,15 +626,7 @@ mod tests {
             fn do_work() { tesˇt(); }
         "});
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                &UpdateGoToDefinitionLink {
-                    point: Some(hover_point),
-                    cmd_held: true,
-                    shift_held: false,
-                },
-                cx,
-            );
+            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
         });
         cx.foreground().run_until_parked();
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@@ -731,7 +636,7 @@ mod tests {
 
         // Cmd click with existing definition doesn't re-request and dismisses highlight
         cx.update_workspace(|workspace, cx| {
-            go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
+            go_to_fetched_definition(workspace, hover_point, cx);
         });
         // Assert selection moved to to definition
         cx.lsp
@@ -772,7 +677,7 @@ mod tests {
             ])))
         });
         cx.update_workspace(|workspace, cx| {
-            go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
+            go_to_fetched_definition(workspace, hover_point, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -817,15 +722,7 @@ mod tests {
             });
         });
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                &UpdateGoToDefinitionLink {
-                    point: Some(hover_point),
-                    cmd_held: true,
-                    shift_held: false,
-                },
-                cx,
-            );
+            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
         });
         cx.foreground().run_until_parked();
         assert!(requests.try_next().is_err());

crates/editor/src/mouse_context_menu.rs 🔗

@@ -1,29 +1,14 @@
-use context_menu::ContextMenuItem;
-use gpui::{
-    elements::AnchorCorner, geometry::vector::Vector2F, impl_internal_actions, AppContext,
-    ViewContext,
-};
-
 use crate::{
     DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
     Rename, RevealInFinder, SelectMode, ToggleCodeActions,
 };
-
-#[derive(Clone, PartialEq)]
-pub struct DeployMouseContextMenu {
-    pub position: Vector2F,
-    pub point: DisplayPoint,
-}
-
-impl_internal_actions!(editor, [DeployMouseContextMenu]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(deploy_context_menu);
-}
+use context_menu::ContextMenuItem;
+use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext};
 
 pub fn deploy_context_menu(
     editor: &mut Editor,
-    &DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
+    position: Vector2F,
+    point: DisplayPoint,
     cx: &mut ViewContext<Editor>,
 ) {
     if !editor.focused {
@@ -51,18 +36,18 @@ pub fn deploy_context_menu(
             position,
             AnchorCorner::TopLeft,
             vec![
-                ContextMenuItem::item("Rename Symbol", Rename),
-                ContextMenuItem::item("Go to Definition", GoToDefinition),
-                ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
-                ContextMenuItem::item("Find All References", FindAllReferences),
-                ContextMenuItem::item(
+                ContextMenuItem::action("Rename Symbol", Rename),
+                ContextMenuItem::action("Go to Definition", GoToDefinition),
+                ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
+                ContextMenuItem::action("Find All References", FindAllReferences),
+                ContextMenuItem::action(
                     "Code Actions",
                     ToggleCodeActions {
                         deployed_from_indicator: false,
                     },
                 ),
                 ContextMenuItem::Separator,
-                ContextMenuItem::item("Reveal in Finder", RevealInFinder),
+                ContextMenuItem::action("Reveal in Finder", RevealInFinder),
             ],
             cx,
         );
@@ -98,16 +83,7 @@ mod tests {
                 do_wˇork();
             }
         "});
-        cx.update_editor(|editor, cx| {
-            deploy_context_menu(
-                editor,
-                &DeployMouseContextMenu {
-                    position: Default::default(),
-                    point,
-                },
-                cx,
-            )
-        });
+        cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
 
         cx.assert_editor_state(indoc! {"
             fn test() {

crates/editor/src/scroll.rs 🔗

@@ -17,7 +17,7 @@ use workspace::WorkspaceId;
 
 use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
-    hover_popover::{hide_hover, HideHover},
+    hover_popover::hide_hover,
     persistence::DB,
     Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
 };
@@ -307,14 +307,10 @@ impl Editor {
     ) {
         let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
 
-        hide_hover(self, &HideHover, cx);
-        self.scroll_manager.set_scroll_position(
-            scroll_position,
-            &map,
-            local,
-            self.workspace_id,
-            cx,
-        );
+        hide_hover(self, cx);
+        let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
+        self.scroll_manager
+            .set_scroll_position(scroll_position, &map, local, workspace_id, cx);
     }
 
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@@ -323,13 +319,14 @@ impl Editor {
     }
 
     pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
-        hide_hover(self, &HideHover, cx);
+        hide_hover(self, cx);
+        let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
         let top_row = scroll_anchor
             .top_anchor
             .to_point(&self.buffer().read(cx).snapshot(cx))
             .row;
         self.scroll_manager
-            .set_anchor(scroll_anchor, top_row, true, self.workspace_id, cx);
+            .set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
     }
 
     pub(crate) fn set_scroll_anchor_remote(
@@ -337,13 +334,14 @@ impl Editor {
         scroll_anchor: ScrollAnchor,
         cx: &mut ViewContext<Self>,
     ) {
-        hide_hover(self, &HideHover, cx);
+        hide_hover(self, cx);
+        let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
         let top_row = scroll_anchor
             .top_anchor
             .to_point(&self.buffer().read(cx).snapshot(cx))
             .row;
         self.scroll_manager
-            .set_anchor(scroll_anchor, top_row, false, self.workspace_id, cx);
+            .set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
     }
 
     pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {

crates/editor/src/scroll/actions.rs 🔗

@@ -1,6 +1,4 @@
-use gpui::{
-    actions, geometry::vector::Vector2F, impl_internal_actions, AppContext, Axis, ViewContext,
-};
+use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext};
 use language::Bias;
 
 use crate::{Editor, EditorMode};
@@ -23,17 +21,8 @@ actions!(
     ]
 );
 
-#[derive(Clone, PartialEq)]
-pub struct Scroll {
-    pub scroll_position: Vector2F,
-    pub axis: Option<Axis>,
-}
-
-impl_internal_actions!(editor, [Scroll]);
-
 pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::next_screen);
-    cx.add_action(Editor::scroll);
     cx.add_action(Editor::scroll_cursor_top);
     cx.add_action(Editor::scroll_cursor_center);
     cx.add_action(Editor::scroll_cursor_bottom);
@@ -75,9 +64,14 @@ impl Editor {
         Some(())
     }
 
-    fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
-        self.scroll_manager.update_ongoing_scroll(action.axis);
-        self.set_scroll_position(action.scroll_position, cx);
+    pub fn scroll(
+        &mut self,
+        scroll_position: Vector2F,
+        axis: Option<Axis>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.scroll_manager.update_ongoing_scroll(axis);
+        self.set_scroll_position(scroll_position, cx);
     }
 
     fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {

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

@@ -66,24 +66,6 @@ macro_rules! impl_actions {
     };
 }
 
-/// 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! impl_internal_actions {
-    ($namespace:path, [ $($name:ident),* $(,)? ]) => {
-        $(
-            $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 {

crates/gpui/src/presenter/event_context.rs 🔗

@@ -1,100 +0,0 @@
-use std::ops::{Deref, DerefMut};
-
-use collections::{HashMap, HashSet};
-
-use crate::{Action, ElementBox, Event, FontCache, MutableAppContext, TextLayoutCache};
-
-pub struct EventContext<'a> {
-    rendered_views: &'a mut HashMap<usize, ElementBox>,
-    pub font_cache: &'a FontCache,
-    pub text_layout_cache: &'a TextLayoutCache,
-    pub app: &'a mut MutableAppContext,
-    pub window_id: usize,
-    pub notify_count: usize,
-    view_stack: Vec<usize>,
-    pub(crate) handled: bool,
-    pub(crate) invalidated_views: HashSet<usize>,
-}
-
-impl<'a> EventContext<'a> {
-    pub(crate) fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool {
-        if let Some(mut element) = self.rendered_views.remove(&view_id) {
-            let result =
-                self.with_current_view(view_id, |this| element.dispatch_event(event, this));
-            self.rendered_views.insert(view_id, element);
-            result
-        } else {
-            false
-        }
-    }
-
-    pub(crate) fn with_current_view<F, T>(&mut self, view_id: usize, f: F) -> T
-    where
-        F: FnOnce(&mut Self) -> T,
-    {
-        self.view_stack.push(view_id);
-        let result = f(self);
-        self.view_stack.pop();
-        result
-    }
-
-    pub fn window_id(&self) -> usize {
-        self.window_id
-    }
-
-    pub fn view_id(&self) -> Option<usize> {
-        self.view_stack.last().copied()
-    }
-
-    pub fn is_parent_view_focused(&self) -> bool {
-        if let Some(parent_view_id) = self.view_stack.last() {
-            self.app.focused_view_id(self.window_id) == Some(*parent_view_id)
-        } else {
-            false
-        }
-    }
-
-    pub fn focus_parent_view(&mut self) {
-        if let Some(parent_view_id) = self.view_stack.last() {
-            self.app.focus(self.window_id, Some(*parent_view_id))
-        }
-    }
-
-    pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
-        self.app
-            .dispatch_any_action_at(self.window_id, *self.view_stack.last().unwrap(), action)
-    }
-
-    pub fn dispatch_action<A: Action>(&mut self, action: A) {
-        self.dispatch_any_action(Box::new(action));
-    }
-
-    pub fn notify(&mut self) {
-        self.notify_count += 1;
-        if let Some(view_id) = self.view_stack.last() {
-            self.invalidated_views.insert(*view_id);
-        }
-    }
-
-    pub fn notify_count(&self) -> usize {
-        self.notify_count
-    }
-
-    pub fn propogate_event(&mut self) {
-        self.handled = false;
-    }
-}
-
-impl<'a> Deref for EventContext<'a> {
-    type Target = MutableAppContext;
-
-    fn deref(&self) -> &Self::Target {
-        self.app
-    }
-}
-
-impl<'a> DerefMut for EventContext<'a> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        self.app
-    }
-}

crates/gpui/src/presenter/event_dispatcher.rs 🔗

@@ -1,309 +0,0 @@
-use std::sync::Arc;
-
-use collections::{HashMap, HashSet};
-use pathfinder_geometry::vector::Vector2F;
-
-use crate::{
-    scene::{
-        ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
-        MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
-    },
-    CursorRegion, CursorStyle, ElementBox, Event, EventContext, FontCache, MouseButton,
-    MouseMovedEvent, MouseRegion, MouseRegionId, MutableAppContext, Scene, TextLayoutCache,
-};
-
-pub struct EventDispatcher {
-    window_id: usize,
-    font_cache: Arc<FontCache>,
-
-    last_mouse_moved_event: Option<Event>,
-    cursor_regions: Vec<CursorRegion>,
-    mouse_regions: Vec<(MouseRegion, usize)>,
-    clicked_regions: Vec<MouseRegion>,
-    clicked_button: Option<MouseButton>,
-    mouse_position: Vector2F,
-    hovered_region_ids: HashSet<MouseRegionId>,
-}
-
-impl EventDispatcher {
-    pub fn new(window_id: usize, font_cache: Arc<FontCache>) -> Self {
-        Self {
-            window_id,
-            font_cache,
-
-            last_mouse_moved_event: Default::default(),
-            cursor_regions: Default::default(),
-            mouse_regions: Default::default(),
-            clicked_regions: Default::default(),
-            clicked_button: Default::default(),
-            mouse_position: Default::default(),
-            hovered_region_ids: Default::default(),
-        }
-    }
-
-    pub fn clicked_region_ids(&self) -> Option<(Vec<MouseRegionId>, MouseButton)> {
-        self.clicked_button.map(|button| {
-            (
-                self.clicked_regions
-                    .iter()
-                    .filter_map(MouseRegion::id)
-                    .collect(),
-                button,
-            )
-        })
-    }
-
-    pub fn hovered_region_ids(&self) -> HashSet<MouseRegionId> {
-        self.hovered_region_ids.clone()
-    }
-
-    pub fn update_mouse_regions(&mut self, scene: &Scene) {
-        self.cursor_regions = scene.cursor_regions();
-        self.mouse_regions = scene.mouse_regions();
-    }
-
-    pub fn redispatch_mouse_moved_event<'a>(&'a mut self, cx: &mut EventContext<'a>) {
-        if let Some(event) = self.last_mouse_moved_event.clone() {
-            self.dispatch_event(event, true, cx);
-        }
-    }
-
-    pub fn dispatch_event<'a>(
-        &'a mut self,
-        event: Event,
-        event_reused: bool,
-        cx: &mut EventContext<'a>,
-    ) -> bool {
-        let root_view_id = cx.root_view_id(self.window_id);
-        if root_view_id.is_none() {
-            return false;
-        }
-
-        let root_view_id = root_view_id.unwrap();
-        //1. Allocate the correct set of GPUI events generated from the platform events
-        // -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
-        // -> Also moves around mouse related state
-        let events_to_send = self.select_region_events(&event, cx, event_reused);
-
-        // For a given platform event, potentially multiple mouse region events can be created. For a given
-        // region event, dispatch continues until a mouse region callback fails to propogate (handled is set to true)
-        // If no region handles any of the produced platform events, we fallback to the old dispatch event style.
-        let mut invalidated_views: HashSet<usize> = Default::default();
-        let mut any_event_handled = false;
-        for mut region_event in events_to_send {
-            //2. Find mouse regions relevant to each region_event. For example, if the event is click, select
-            // the clicked_regions that overlap with the mouse position
-            let valid_regions = self.select_relevant_mouse_regions(&region_event);
-            let hovered_region_ids = self.hovered_region_ids.clone();
-
-            //3. Dispatch region event ot each valid mouse region
-            for valid_region in valid_regions.into_iter() {
-                region_event.set_region(valid_region.bounds);
-                if let MouseRegionEvent::Hover(e) = &mut region_event {
-                    e.started = valid_region
-                        .id()
-                        .map(|region_id| hovered_region_ids.contains(&region_id))
-                        .unwrap_or(false)
-                }
-
-                if let Some(callback) = valid_region.handlers.get(&region_event.handler_key()) {
-                    if !event_reused {
-                        invalidated_views.insert(valid_region.view_id);
-                    }
-
-                    cx.handled = true;
-                    cx.with_current_view(valid_region.view_id, {
-                        let region_event = region_event.clone();
-                        |cx| {
-                            callback(region_event, cx);
-                        }
-                    });
-
-                    // For bubbling events, if the event was handled, don't continue dispatching
-                    // This only makes sense for local events.
-                    if cx.handled && region_event.is_local() {
-                        break;
-                    }
-                }
-            }
-
-            // Keep track if any platform event was handled
-            any_event_handled = any_event_handled && cx.handled;
-        }
-
-        if !any_event_handled {
-            // No platform event was handled, so fall back to old mouse event dispatch style
-            any_event_handled = cx.dispatch_event(root_view_id, &event);
-        }
-
-        // Notify any views which have been validated from event callbacks
-        for view_id in invalidated_views {
-            cx.notify_view(self.window_id, view_id);
-        }
-
-        any_event_handled
-    }
-
-    fn select_region_events(
-        &mut self,
-        event: &Event,
-        cx: &mut MutableAppContext,
-        event_reused: bool,
-    ) -> Vec<MouseRegionEvent> {
-        let mut events_to_send = Vec::new();
-        match event {
-            Event::MouseDown(e) => {
-                //Click events are weird because they can be fired after a drag event.
-                //MDN says that browsers handle this by starting from 'the most
-                //specific ancestor element that contained both [positions]'
-                //So we need to store the overlapping regions on mouse down.
-                self.clicked_regions = self
-                    .mouse_regions
-                    .iter()
-                    .filter_map(|(region, _)| {
-                        region
-                            .bounds
-                            .contains_point(e.position)
-                            .then(|| region.clone())
-                    })
-                    .collect();
-                self.clicked_button = Some(e.button);
-
-                events_to_send.push(MouseRegionEvent::Down(DownRegionEvent {
-                    region: Default::default(),
-                    platform_event: e.clone(),
-                }));
-                events_to_send.push(MouseRegionEvent::DownOut(DownOutRegionEvent {
-                    region: Default::default(),
-                    platform_event: e.clone(),
-                }));
-            }
-            Event::MouseUp(e) => {
-                //NOTE: The order of event pushes is important! MouseUp events MUST be fired
-                //before click events, and so the UpRegionEvent events need to be pushed before
-                //ClickRegionEvents
-                events_to_send.push(MouseRegionEvent::Up(UpRegionEvent {
-                    region: Default::default(),
-                    platform_event: e.clone(),
-                }));
-                events_to_send.push(MouseRegionEvent::UpOut(UpOutRegionEvent {
-                    region: Default::default(),
-                    platform_event: e.clone(),
-                }));
-                events_to_send.push(MouseRegionEvent::Click(ClickRegionEvent {
-                    region: Default::default(),
-                    platform_event: e.clone(),
-                }));
-            }
-            Event::MouseMoved(
-                e @ MouseMovedEvent {
-                    position,
-                    pressed_button,
-                    ..
-                },
-            ) => {
-                let mut style_to_assign = CursorStyle::Arrow;
-                for region in self.cursor_regions.iter().rev() {
-                    if region.bounds.contains_point(*position) {
-                        style_to_assign = region.style;
-                        break;
-                    }
-                }
-
-                cx.platform().set_cursor_style(style_to_assign);
-
-                if !event_reused {
-                    if pressed_button.is_some() {
-                        events_to_send.push(MouseRegionEvent::Drag(DragRegionEvent {
-                            region: Default::default(),
-                            prev_mouse_position: self.mouse_position,
-                            platform_event: e.clone(),
-                        }));
-                    }
-                    events_to_send.push(MouseRegionEvent::Move(MoveRegionEvent {
-                        region: Default::default(),
-                        platform_event: e.clone(),
-                    }));
-                }
-
-                events_to_send.push(MouseRegionEvent::Hover(HoverRegionEvent {
-                    region: Default::default(),
-                    platform_event: e.clone(),
-                    started: false,
-                }));
-
-                self.last_mouse_moved_event = Some(event.clone());
-            }
-            _ => {}
-        }
-        if let Some(position) = event.position() {
-            self.mouse_position = position;
-        }
-        events_to_send
-    }
-
-    fn select_relevant_mouse_regions(
-        &mut self,
-        region_event: &MouseRegionEvent,
-    ) -> Vec<MouseRegion> {
-        let mut valid_regions = Vec::new();
-        //GPUI elements are arranged by depth but sibling elements can register overlapping
-        //mouse regions. As such, hover events are only fired on overlapping elements which
-        //are at the same depth as the deepest element which overlaps with the mouse.
-        if let MouseRegionEvent::Hover(_) = *region_event {
-            let mut top_most_depth = None;
-            let mouse_position = self.mouse_position.clone();
-            for (region, depth) in self.mouse_regions.iter().rev() {
-                let contains_mouse = region.bounds.contains_point(mouse_position);
-
-                if contains_mouse && top_most_depth.is_none() {
-                    top_most_depth = Some(depth);
-                }
-
-                if let Some(region_id) = region.id() {
-                    //This unwrap relies on short circuiting boolean expressions
-                    //The right side of the && is only executed when contains_mouse
-                    //is true, and we know above that when contains_mouse is true
-                    //top_most_depth is set
-                    if contains_mouse && depth == top_most_depth.unwrap() {
-                        //Ensure that hover entrance events aren't sent twice
-                        if self.hovered_region_ids.insert(region_id) {
-                            valid_regions.push(region.clone());
-                        }
-                    } else {
-                        //Ensure that hover exit events aren't sent twice
-                        if self.hovered_region_ids.remove(&region_id) {
-                            valid_regions.push(region.clone());
-                        }
-                    }
-                }
-            }
-        } else if let MouseRegionEvent::Click(e) = region_event {
-            //Clear stored clicked_regions
-            let clicked_regions = std::mem::replace(&mut self.clicked_regions, Vec::new());
-            self.clicked_button = None;
-
-            //Find regions which still overlap with the mouse since the last MouseDown happened
-            for clicked_region in clicked_regions.into_iter().rev() {
-                if clicked_region.bounds.contains_point(e.position) {
-                    valid_regions.push(clicked_region);
-                }
-            }
-        } else if region_event.is_local() {
-            for (mouse_region, _) in self.mouse_regions.iter().rev() {
-                //Contains
-                if mouse_region.bounds.contains_point(self.mouse_position) {
-                    valid_regions.push(mouse_region.clone());
-                }
-            }
-        } else {
-            for (mouse_region, _) in self.mouse_regions.iter().rev() {
-                //NOT contains
-                if !mouse_region.bounds.contains_point(self.mouse_position) {
-                    valid_regions.push(mouse_region.clone());
-                }
-            }
-        }
-        valid_regions
-    }
-}

crates/menu/src/menu.rs 🔗

@@ -1,6 +1,3 @@
-#[derive(Clone, PartialEq)]
-pub struct SelectIndex(pub usize);
-
 gpui::actions!(
     menu,
     [
@@ -12,5 +9,3 @@ gpui::actions!(
         SelectLast
     ]
 );
-
-gpui::impl_internal_actions!(menu, [SelectIndex]);

crates/picker/src/picker.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext,
     ViewHandle,
 };
-use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
+use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
 use parking_lot::Mutex;
 use std::{cmp, sync::Arc};
 use util::ResultExt;
@@ -104,8 +104,8 @@ impl<D: PickerDelegate> View for Picker<D> {
                                 // Capture mouse events
                                 .on_down(MouseButton::Left, |_, _, _| {})
                                 .on_up(MouseButton::Left, |_, _, _| {})
-                                .on_click(MouseButton::Left, move |_, _, cx| {
-                                    cx.dispatch_action(SelectIndex(ix))
+                                .on_click(MouseButton::Left, move |_, picker, cx| {
+                                    picker.select_index(ix, cx);
                                 })
                                 .with_cursor_style(CursorStyle::PointingHand)
                                 .into_any()
@@ -151,7 +151,6 @@ impl<D: PickerDelegate> Picker<D> {
         cx.add_action(Self::select_last);
         cx.add_action(Self::select_next);
         cx.add_action(Self::select_prev);
-        cx.add_action(Self::select_index);
         cx.add_action(Self::confirm);
         cx.add_action(Self::cancel);
     }
@@ -265,8 +264,7 @@ impl<D: PickerDelegate> Picker<D> {
         cx.notify();
     }
 
-    pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext<Self>) {
-        let index = action.0;
+    pub fn select_index(&mut self, index: usize, cx: &mut ViewContext<Self>) {
         if self.delegate.match_count() > 0 {
             self.confirmed = true;
             self.delegate.set_selected_index(index, cx);

crates/project_panel/src/project_panel.rs 🔗

@@ -10,7 +10,6 @@ use gpui::{
         ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
     },
     geometry::vector::Vector2F,
-    impl_internal_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, PromptLevel},
     AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
@@ -88,28 +87,6 @@ pub struct EntryDetails {
     is_cut: bool,
 }
 
-#[derive(Clone, PartialEq)]
-pub struct ToggleExpanded(pub ProjectEntryId);
-
-#[derive(Clone, PartialEq)]
-pub struct Open {
-    pub entry_id: ProjectEntryId,
-    pub change_focus: bool,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct MoveProjectEntry {
-    pub entry_to_move: ProjectEntryId,
-    pub destination: ProjectEntryId,
-    pub destination_is_file: bool,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct DeployContextMenu {
-    pub position: Vector2F,
-    pub entry_id: ProjectEntryId,
-}
-
 actions!(
     project_panel,
     [
@@ -128,19 +105,12 @@ actions!(
         ToggleFocus
     ]
 );
-impl_internal_actions!(
-    project_panel,
-    [Open, ToggleExpanded, DeployContextMenu, MoveProjectEntry]
-);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(ProjectPanel::deploy_context_menu);
     cx.add_action(ProjectPanel::expand_selected_entry);
     cx.add_action(ProjectPanel::collapse_selected_entry);
-    cx.add_action(ProjectPanel::toggle_expanded);
     cx.add_action(ProjectPanel::select_prev);
     cx.add_action(ProjectPanel::select_next);
-    cx.add_action(ProjectPanel::open_entry);
     cx.add_action(ProjectPanel::new_file);
     cx.add_action(ProjectPanel::new_directory);
     cx.add_action(ProjectPanel::rename);
@@ -157,7 +127,6 @@ pub fn init(cx: &mut AppContext) {
             this.paste(action, cx);
         },
     );
-    cx.add_action(ProjectPanel::move_entry);
 }
 
 pub enum Event {
@@ -277,10 +246,14 @@ impl ProjectPanel {
         project_panel
     }
 
-    fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
+    fn deploy_context_menu(
+        &mut self,
+        position: Vector2F,
+        entry_id: ProjectEntryId,
+        cx: &mut ViewContext<Self>,
+    ) {
         let project = self.project.read(cx);
 
-        let entry_id = action.entry_id;
         let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
             id
         } else {
@@ -296,43 +269,43 @@ impl ProjectPanel {
         if let Some((worktree, entry)) = self.selected_entry(cx) {
             let is_root = Some(entry) == worktree.root_entry();
             if !project.is_remote() {
-                menu_entries.push(ContextMenuItem::item(
+                menu_entries.push(ContextMenuItem::action(
                     "Add Folder to Project",
                     workspace::AddFolderToProject,
                 ));
                 if is_root {
-                    menu_entries.push(ContextMenuItem::item(
-                        "Remove from Project",
-                        workspace::RemoveWorktreeFromProject(worktree_id),
-                    ));
+                    let project = self.project.clone();
+                    menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
+                        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
+                    }));
                 }
             }
-            menu_entries.push(ContextMenuItem::item("New File", NewFile));
-            menu_entries.push(ContextMenuItem::item("New Folder", NewDirectory));
+            menu_entries.push(ContextMenuItem::action("New File", NewFile));
+            menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
             menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::item("Cut", Cut));
-            menu_entries.push(ContextMenuItem::item("Copy", Copy));
+            menu_entries.push(ContextMenuItem::action("Cut", Cut));
+            menu_entries.push(ContextMenuItem::action("Copy", Copy));
             menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath));
-            menu_entries.push(ContextMenuItem::item(
+            menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
+            menu_entries.push(ContextMenuItem::action(
                 "Copy Relative Path",
                 CopyRelativePath,
             ));
-            menu_entries.push(ContextMenuItem::item("Reveal in Finder", RevealInFinder));
+            menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
             if let Some(clipboard_entry) = self.clipboard_entry {
                 if clipboard_entry.worktree_id() == worktree.id() {
-                    menu_entries.push(ContextMenuItem::item("Paste", Paste));
+                    menu_entries.push(ContextMenuItem::action("Paste", Paste));
                 }
             }
             menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::item("Rename", Rename));
+            menu_entries.push(ContextMenuItem::action("Rename", Rename));
             if !is_root {
-                menu_entries.push(ContextMenuItem::item("Delete", Delete));
+                menu_entries.push(ContextMenuItem::action("Delete", Delete));
             }
         }
 
         self.context_menu.update(cx, |menu, cx| {
-            menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx);
+            menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
         });
 
         cx.notify();
@@ -391,8 +364,7 @@ impl ProjectPanel {
         }
     }
 
-    fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
-        let entry_id = action.0;
+    fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
         if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
             if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
                 match expanded_dir_ids.binary_search(&entry_id) {
@@ -440,13 +412,7 @@ impl ProjectPanel {
             Some(task)
         } else if let Some((_, entry)) = self.selected_entry(cx) {
             if entry.is_file() {
-                self.open_entry(
-                    &Open {
-                        entry_id: entry.id,
-                        change_focus: true,
-                    },
-                    cx,
-                );
+                self.open_entry(entry.id, true, cx);
             }
             None
         } else {
@@ -510,13 +476,7 @@ impl ProjectPanel {
                 }
                 this.update_visible_entries(None, cx);
                 if is_new_entry && !is_dir {
-                    this.open_entry(
-                        &Open {
-                            entry_id: new_entry.id,
-                            change_focus: true,
-                        },
-                        cx,
-                    );
+                    this.open_entry(new_entry.id, true, cx);
                 }
                 cx.notify();
             })?;
@@ -531,10 +491,15 @@ impl ProjectPanel {
         cx.notify();
     }
 
-    fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) {
+    fn open_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        focus_opened_item: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
         cx.emit(Event::OpenedEntry {
-            entry_id: action.entry_id,
-            focus_opened_item: action.change_focus,
+            entry_id,
+            focus_opened_item,
         });
     }
 
@@ -816,11 +781,9 @@ impl ProjectPanel {
 
     fn move_entry(
         &mut self,
-        &MoveProjectEntry {
-            entry_to_move,
-            destination,
-            destination_is_file,
-        }: &MoveProjectEntry,
+        entry_to_move: ProjectEntryId,
+        destination: ProjectEntryId,
+        destination_is_file: bool,
         cx: &mut ViewContext<Self>,
     ) {
         let destination_worktree = self.project.update(cx, |project, cx| {
@@ -1196,34 +1159,29 @@ impl ProjectPanel {
                 cx,
             )
         })
-        .on_click(MouseButton::Left, move |e, _, cx| {
+        .on_click(MouseButton::Left, move |event, this, cx| {
             if !show_editor {
                 if kind == EntryKind::Dir {
-                    cx.dispatch_action(ToggleExpanded(entry_id))
+                    this.toggle_expanded(entry_id, cx);
                 } else {
-                    cx.dispatch_action(Open {
-                        entry_id,
-                        change_focus: e.click_count > 1,
-                    })
+                    this.open_entry(entry_id, event.click_count > 1, cx);
                 }
             }
         })
-        .on_down(MouseButton::Right, move |e, _, cx| {
-            cx.dispatch_action(DeployContextMenu {
-                entry_id,
-                position: e.position,
-            })
+        .on_down(MouseButton::Right, move |event, this, cx| {
+            this.deploy_context_menu(event.position, entry_id, cx);
         })
-        .on_up(MouseButton::Left, move |_, _, cx| {
+        .on_up(MouseButton::Left, move |_, this, cx| {
             if let Some((_, dragged_entry)) = cx
                 .global::<DragAndDrop<Workspace>>()
                 .currently_dragged::<ProjectEntryId>(cx.window_id())
             {
-                cx.dispatch_action(MoveProjectEntry {
-                    entry_to_move: *dragged_entry,
-                    destination: entry_id,
-                    destination_is_file: matches!(details.kind, EntryKind::File(_)),
-                });
+                this.move_entry(
+                    *dragged_entry,
+                    entry_id,
+                    matches!(details.kind, EntryKind::File(_)),
+                    cx,
+                );
             }
         })
         .on_move(move |_, this, cx| {
@@ -1307,14 +1265,11 @@ impl View for ProjectPanel {
                         .with_style(container_style)
                         .expanded()
                     })
-                    .on_down(MouseButton::Right, move |e, _, cx| {
+                    .on_down(MouseButton::Right, move |event, this, cx| {
                         // When deploying the context menu anywhere below the last project entry,
                         // act as if the user clicked the root of the last worktree.
                         if let Some(entry_id) = last_worktree_root_id {
-                            cx.dispatch_action(DeployContextMenu {
-                                entry_id,
-                                position: e.position,
-                            })
+                            this.deploy_context_menu(event.position, entry_id, cx);
                         }
                     }),
                 )
@@ -1895,7 +1850,7 @@ mod tests {
                 let worktree = worktree.read(cx);
                 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
                     let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-                    panel.toggle_expanded(&ToggleExpanded(entry_id), cx);
+                    panel.toggle_expanded(entry_id, cx);
                     return;
                 }
             }

crates/recent_projects/src/recent_projects.rs 🔗

@@ -5,30 +5,30 @@ use gpui::{
     actions,
     anyhow::Result,
     elements::{Flex, ParentElement},
-    AnyElement, AppContext, Element, Task, ViewContext,
+    AnyElement, AppContext, Element, Task, ViewContext, WeakViewHandle,
 };
 use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate, PickerEvent};
 use settings::Settings;
-use std::sync::Arc;
+use std::sync::{Arc, Weak};
 use workspace::{
-    notifications::simple_message_notification::MessageNotification, OpenPaths, Workspace,
+    notifications::simple_message_notification::MessageNotification, AppState, Workspace,
     WorkspaceLocation, WORKSPACE_DB,
 };
 
 actions!(projects, [OpenRecent]);
 
-pub fn init(cx: &mut AppContext) {
-    cx.add_async_action(toggle);
+pub fn init(cx: &mut AppContext, app_state: Weak<AppState>) {
+    cx.add_async_action(
+        move |_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>| {
+            toggle(app_state.clone(), cx)
+        },
+    );
     RecentProjects::init(cx);
 }
 
-fn toggle(
-    _: &mut Workspace,
-    _: &OpenRecent,
-    cx: &mut ViewContext<Workspace>,
-) -> Option<Task<Result<()>>> {
+fn toggle(app_state: Weak<AppState>, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
     Some(cx.spawn(|workspace, mut cx| async move {
         let workspace_locations: Vec<_> = cx
             .background()
@@ -46,9 +46,17 @@ fn toggle(
         workspace.update(&mut cx, |workspace, cx| {
             if !workspace_locations.is_empty() {
                 workspace.toggle_modal(cx, |_, cx| {
+                    let workspace = cx.weak_handle();
                     cx.add_view(|cx| {
-                        RecentProjects::new(RecentProjectsDelegate::new(workspace_locations), cx)
-                            .with_max_size(800., 1200.)
+                        RecentProjects::new(
+                            RecentProjectsDelegate::new(
+                                workspace,
+                                workspace_locations,
+                                app_state.clone(),
+                            ),
+                            cx,
+                        )
+                        .with_max_size(800., 1200.)
                     })
                 });
             } else {
@@ -64,15 +72,23 @@ fn toggle(
 type RecentProjects = Picker<RecentProjectsDelegate>;
 
 struct RecentProjectsDelegate {
+    workspace: WeakViewHandle<Workspace>,
     workspace_locations: Vec<WorkspaceLocation>,
+    app_state: Weak<AppState>,
     selected_match_index: usize,
     matches: Vec<StringMatch>,
 }
 
 impl RecentProjectsDelegate {
-    fn new(workspace_locations: Vec<WorkspaceLocation>) -> Self {
+    fn new(
+        workspace: WeakViewHandle<Workspace>,
+        workspace_locations: Vec<WorkspaceLocation>,
+        app_state: Weak<AppState>,
+    ) -> Self {
         Self {
+            workspace,
             workspace_locations,
+            app_state,
             selected_match_index: 0,
             matches: Default::default(),
         }
@@ -139,11 +155,22 @@ impl PickerDelegate for RecentProjectsDelegate {
     }
 
     fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
-        if let Some(selected_match) = &self.matches.get(self.selected_index()) {
+        if let Some(((selected_match, workspace), app_state)) = self
+            .matches
+            .get(self.selected_index())
+            .zip(self.workspace.upgrade(cx))
+            .zip(self.app_state.upgrade())
+        {
             let workspace_location = &self.workspace_locations[selected_match.candidate_id];
-            cx.dispatch_action(OpenPaths {
-                paths: workspace_location.paths().as_ref().clone(),
-            });
+            workspace
+                .update(cx, |workspace, cx| {
+                    workspace.open_workspace_for_paths(
+                        workspace_location.paths().as_ref().clone(),
+                        app_state,
+                        cx,
+                    )
+                })
+                .detach_and_log_err(cx);
             cx.emit(PickerEvent::Dismiss);
         }
     }

crates/search/src/project_search.rs 🔗

@@ -332,6 +332,11 @@ impl Item for ProjectSearchView {
         Some(Self::new(model, cx))
     }
 
+    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+        self.results_editor
+            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
+    }
+
     fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
         self.results_editor.update(cx, |editor, _| {
             editor.set_nav_history(Some(nav_history));

crates/terminal_view/src/terminal_button.rs 🔗

@@ -1,33 +1,14 @@
+use crate::TerminalView;
 use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     elements::*,
-    impl_internal_actions,
     platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Element, Entity, View, ViewContext, ViewHandle, WeakModelHandle,
-    WeakViewHandle,
+    AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use settings::Settings;
 use std::any::TypeId;
-use terminal::Terminal;
 use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace};
 
-use crate::TerminalView;
-
-#[derive(Clone, PartialEq)]
-pub struct DeployTerminalMenu;
-
-#[derive(Clone, PartialEq)]
-pub struct FocusTerminal {
-    terminal_handle: WeakModelHandle<Terminal>,
-}
-
-impl_internal_actions!(terminal, [FocusTerminal, DeployTerminalMenu]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(TerminalButton::deploy_terminal_menu);
-    cx.add_action(TerminalButton::focus_terminal);
-}
-
 pub struct TerminalButton {
     workspace: WeakViewHandle<Workspace>,
     popup_menu: ViewHandle<ContextMenu>,
@@ -94,9 +75,9 @@ impl View for TerminalButton {
                     }
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
+                .on_click(MouseButton::Left, move |_, this, cx| {
                     if has_terminals {
-                        cx.dispatch_action(DeployTerminalMenu);
+                        this.deploy_terminal_menu(cx);
                     } else {
                         if !active {
                             cx.dispatch_action(FocusDock);
@@ -129,12 +110,8 @@ impl TerminalButton {
         }
     }
 
-    pub fn deploy_terminal_menu(
-        &mut self,
-        _action: &DeployTerminalMenu,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
+    pub fn deploy_terminal_menu(&mut self, cx: &mut ViewContext<Self>) {
+        let mut menu_options = vec![ContextMenuItem::action("New Terminal", NewTerminal)];
 
         if let Some(workspace) = self.workspace.upgrade(cx) {
             let project = workspace.read(cx).project().read(cx);
@@ -146,10 +123,24 @@ impl TerminalButton {
 
             for local_terminal_handle in local_terminal_handles {
                 if let Some(terminal) = local_terminal_handle.upgrade(cx) {
-                    menu_options.push(ContextMenuItem::item(
+                    let workspace = self.workspace.clone();
+                    let local_terminal_handle = local_terminal_handle.clone();
+                    menu_options.push(ContextMenuItem::handler(
                         terminal.read(cx).title(),
-                        FocusTerminal {
-                            terminal_handle: local_terminal_handle.clone(),
+                        move |cx| {
+                            if let Some(workspace) = workspace.upgrade(cx) {
+                                workspace.update(cx, |workspace, cx| {
+                                    let terminal = workspace
+                                        .items_of_type::<TerminalView>(cx)
+                                        .find(|terminal| {
+                                            terminal.read(cx).model().downgrade()
+                                                == local_terminal_handle
+                                        });
+                                    if let Some(terminal) = terminal {
+                                        workspace.activate_item(&terminal, cx);
+                                    }
+                                });
+                            }
                         },
                     ))
                 }
@@ -165,21 +156,6 @@ impl TerminalButton {
             );
         });
     }
-
-    pub fn focus_terminal(&mut self, action: &FocusTerminal, cx: &mut ViewContext<Self>) {
-        if let Some(workspace) = self.workspace.upgrade(cx) {
-            workspace.update(cx, |workspace, cx| {
-                let terminal = workspace
-                    .items_of_type::<TerminalView>(cx)
-                    .find(|terminal| {
-                        terminal.read(cx).model().downgrade() == action.terminal_handle
-                    });
-                if let Some(terminal) = terminal {
-                    workspace.activate_item(&terminal, cx);
-                }
-            });
-        }
-    }
 }
 
 impl StatusItemView for TerminalButton {

crates/terminal_view/src/terminal_element.rs 🔗

@@ -33,7 +33,7 @@ use util::ResultExt;
 use std::{fmt::Debug, ops::RangeInclusive};
 use std::{mem, ops::Range};
 
-use crate::{DeployContextMenu, TerminalView};
+use crate::TerminalView;
 
 ///The information generated during layout that is nescessary for painting
 pub struct LayoutState {
@@ -429,19 +429,20 @@ impl TerminalElement {
                 ),
             )
             // Context menu
-            .on_click(MouseButton::Right, move |e, _: &mut TerminalView, cx| {
-                let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx) {
-                    conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(e.shift))
-                } else {
-                    // If we can't get the model handle, probably can't deploy the context menu
-                    true
-                };
-                if !mouse_mode {
-                    cx.dispatch_action(DeployContextMenu {
-                        position: e.position,
-                    });
-                }
-            })
+            .on_click(
+                MouseButton::Right,
+                move |event, view: &mut TerminalView, cx| {
+                    let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx) {
+                        conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift))
+                    } else {
+                        // If we can't get the model handle, probably can't deploy the context menu
+                        true
+                    };
+                    if !mouse_mode {
+                        view.deploy_context_menu(event.position, cx);
+                    }
+                },
+            )
             .on_move(move |event, _: &mut TerminalView, cx| {
                 if cx.is_parent_view_focused() {
                     if let Some(conn_handle) = connection.upgrade(cx) {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
     actions,
     elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack},
     geometry::vector::Vector2F,
-    impl_actions, impl_internal_actions,
+    impl_actions,
     keymap_matcher::{KeymapContext, Keystroke},
     platform::KeyDownEvent,
     AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext,
@@ -50,11 +50,6 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 #[derive(Clone, Debug, PartialEq)]
 pub struct ScrollTerminal(pub i32);
 
-#[derive(Clone, PartialEq)]
-pub struct DeployContextMenu {
-    pub position: Vector2F,
-}
-
 #[derive(Clone, Default, Deserialize, PartialEq)]
 pub struct SendText(String);
 
@@ -68,8 +63,6 @@ actions!(
 
 impl_actions!(terminal, [SendText, SendKeystroke]);
 
-impl_internal_actions!(project_panel, [DeployContextMenu]);
-
 pub fn init(cx: &mut AppContext) {
     cx.add_action(TerminalView::deploy);
 
@@ -78,7 +71,6 @@ pub fn init(cx: &mut AppContext) {
     //Useful terminal views
     cx.add_action(TerminalView::send_text);
     cx.add_action(TerminalView::send_keystroke);
-    cx.add_action(TerminalView::deploy_context_menu);
     cx.add_action(TerminalView::copy);
     cx.add_action(TerminalView::paste);
     cx.add_action(TerminalView::clear);
@@ -197,14 +189,14 @@ impl TerminalView {
         cx.emit(Event::Wakeup);
     }
 
-    pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
+    pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
         let menu_entries = vec![
-            ContextMenuItem::item("Clear", Clear),
-            ContextMenuItem::item("Close", pane::CloseActiveItem),
+            ContextMenuItem::action("Clear", Clear),
+            ContextMenuItem::action("Close", pane::CloseActiveItem),
         ];
 
         self.context_menu.update(cx, |menu, cx| {
-            menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
+            menu.show(position, AnchorCorner::TopLeft, menu_entries, cx)
         });
 
         cx.notify();

crates/theme/src/ui.rs 🔗

@@ -139,27 +139,11 @@ pub fn keystroke_label<V: View>(
 ) -> Container<V> {
     // FIXME: Put the theme in it's own global so we can
     // query the keystroke style on our own
-    keystroke_label_for(
-        cx.handle().id(),
-        label_text,
-        label_style,
-        keystroke_style,
-        action,
-    )
-}
-
-pub fn keystroke_label_for<V: View>(
-    view_id: usize,
-    label_text: &'static str,
-    label_style: &ContainedText,
-    keystroke_style: &ContainedText,
-    action: Box<dyn Action>,
-) -> Container<V> {
     Flex::row()
         .with_child(Label::new(label_text, label_style.text.clone()).contained())
         .with_child(
             KeystrokeLabel::new(
-                view_id,
+                cx.view_id(),
                 action,
                 keystroke_style.container,
                 keystroke_style.text.clone(),

crates/workspace/src/dock.rs 🔗

@@ -1,13 +1,10 @@
 mod toggle_dock_button;
 
-use serde::Deserialize;
-
 use collections::HashMap;
 use gpui::{
     actions,
     elements::{ChildView, Empty, MouseEventHandler, ParentElement, Side, Stack},
     geometry::vector::Vector2F,
-    impl_internal_actions,
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, Border, Element, SizeConstraint, ViewContext, ViewHandle,
 };
@@ -17,12 +14,6 @@ use theme::Theme;
 use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace};
 pub use toggle_dock_button::ToggleDockButton;
 
-#[derive(PartialEq, Clone, Deserialize)]
-pub struct MoveDock(pub DockAnchor);
-
-#[derive(PartialEq, Clone)]
-pub struct AddDefaultItemToDock;
-
 actions!(
     dock,
     [
@@ -35,16 +26,10 @@ actions!(
         RemoveTabFromDock,
     ]
 );
-impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(Dock::focus_dock);
     cx.add_action(Dock::hide_dock);
-    cx.add_action(
-        |workspace: &mut Workspace, &MoveDock(dock_anchor), cx: &mut ViewContext<Workspace>| {
-            Dock::move_dock(workspace, dock_anchor, true, cx);
-        },
-    );
     cx.add_action(
         |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
             Dock::move_dock(workspace, DockAnchor::Right, true, cx);
@@ -182,21 +167,14 @@ pub struct Dock {
 
 impl Dock {
     pub fn new(
-        workspace_id: usize,
         default_item_factory: DockDefaultItemFactory,
         background_actions: BackgroundActions,
         cx: &mut ViewContext<Workspace>,
     ) -> Self {
         let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
-
-        let pane = cx.add_view(|cx| {
-            Pane::new(
-                workspace_id,
-                Some(position.anchor()),
-                background_actions,
-                cx,
-            )
-        });
+        let workspace = cx.weak_handle();
+        let pane =
+            cx.add_view(|cx| Pane::new(workspace, Some(position.anchor()), background_actions, cx));
         pane.update(cx, |pane, cx| {
             pane.set_active(false, cx);
         });
@@ -426,11 +404,13 @@ mod tests {
     use std::{
         ops::{Deref, DerefMut},
         path::PathBuf,
+        sync::Arc,
     };
 
     use gpui::{AppContext, BorrowWindowContext, TestAppContext, ViewContext, WindowContext};
     use project::{FakeFs, Project};
     use settings::Settings;
+    use theme::ThemeRegistry;
 
     use super::*;
     use crate::{
@@ -441,7 +421,7 @@ mod tests {
         },
         register_deserializable_item,
         sidebar::Sidebar,
-        ItemHandle, Workspace,
+        AppState, ItemHandle, Workspace,
     };
 
     pub fn default_item_factory(
@@ -489,8 +469,17 @@ mod tests {
                 Some(serialized_workspace),
                 0,
                 project.clone(),
-                default_item_factory,
-                || &[],
+                Arc::new(AppState {
+                    languages: project.read(cx).languages().clone(),
+                    themes: ThemeRegistry::new((), cx.font_cache().clone()),
+                    client: project.read(cx).client(),
+                    user_store: project.read(cx).user_store(),
+                    fs: project.read(cx).fs().clone(),
+                    build_window_options: |_, _, _| Default::default(),
+                    initialize_workspace: |_, _, _| {},
+                    dock_default_item_factory: default_item_factory,
+                    background_actions: || &[],
+                }),
                 cx,
             )
         });
@@ -582,7 +571,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
-        let cx = DockTestContext::new(cx).await;
+        let mut cx = DockTestContext::new(cx).await;
 
         cx.move_dock(DockAnchor::Right);
         cx.assert_dock_pane_active();
@@ -620,11 +609,20 @@ mod tests {
             let project = Project::test(fs, [], cx).await;
             let (window_id, workspace) = cx.add_window(|cx| {
                 Workspace::new(
-                    Default::default(),
+                    None,
                     0,
-                    project,
-                    default_item_factory,
-                    || &[],
+                    project.clone(),
+                    Arc::new(AppState {
+                        languages: project.read(cx).languages().clone(),
+                        themes: ThemeRegistry::new((), cx.font_cache().clone()),
+                        client: project.read(cx).client(),
+                        user_store: project.read(cx).user_store(),
+                        fs: project.read(cx).fs().clone(),
+                        build_window_options: |_, _, _| Default::default(),
+                        initialize_workspace: |_, _, _| {},
+                        dock_default_item_factory: default_item_factory,
+                        background_actions: || &[],
+                    }),
                     cx,
                 )
             });
@@ -728,8 +726,8 @@ mod tests {
             })
         }
 
-        pub fn move_dock(&self, anchor: DockAnchor) {
-            self.cx.dispatch_action(self.window_id, MoveDock(anchor));
+        pub fn move_dock(&mut self, anchor: DockAnchor) {
+            self.update_workspace(|workspace, cx| Dock::move_dock(workspace, anchor, true, cx));
         }
 
         pub fn hide_dock(&self) {

crates/workspace/src/dock/toggle_dock_button.rs 🔗

@@ -67,9 +67,17 @@ impl View for ToggleDockButton {
             }
         })
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_up(MouseButton::Left, move |event, _, cx| {
+        .on_up(MouseButton::Left, move |event, this, cx| {
             let drop_index = dock_pane.read(cx).items_len() + 1;
-            handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
+            handle_dropped_item(
+                event,
+                this.workspace.clone(),
+                &dock_pane.downgrade(),
+                drop_index,
+                false,
+                None,
+                cx,
+            );
         });
 
         if dock_position.is_visible() {

crates/workspace/src/item.rs 🔗

@@ -365,7 +365,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                 workspace.update_followers(
                     proto::update_followers::Variant::CreateView(proto::View {
                         id: followed_item
-                            .remote_id(&workspace.client, cx)
+                            .remote_id(&workspace.app_state.client, cx)
                             .map(|id| id.to_proto()),
                         variant: Some(message),
                         leader_id: workspace.leader_for_pane(&pane),
@@ -421,7 +421,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                         proto::update_followers::Variant::UpdateView(
                                             proto::UpdateView {
                                                 id: item
-                                                    .remote_id(&this.client, cx)
+                                                    .remote_id(&this.app_state.client, cx)
                                                     .map(|id| id.to_proto()),
                                                 variant: pending_update.borrow_mut().take(),
                                                 leader_id,

crates/workspace/src/notifications.rs 🔗

@@ -1,9 +1,7 @@
-use std::{any::TypeId, ops::DerefMut};
-
+use crate::{Toast, Workspace};
 use collections::HashSet;
 use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
-
-use crate::Workspace;
+use std::{any::TypeId, ops::DerefMut};
 
 pub fn init(cx: &mut AppContext) {
     cx.set_global(NotificationTracker::new());
@@ -113,6 +111,28 @@ impl Workspace {
         self.dismiss_notification_internal(type_id, id, cx)
     }
 
+    pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
+        self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
+        self.show_notification(toast.id, cx, |cx| {
+            cx.add_view(|_cx| match &toast.click {
+                Some((click_msg, action)) => {
+                    simple_message_notification::MessageNotification::new_boxed_action(
+                        toast.msg.clone(),
+                        action.boxed_clone(),
+                        click_msg.clone(),
+                    )
+                }
+                None => {
+                    simple_message_notification::MessageNotification::new_message(toast.msg.clone())
+                }
+            })
+        })
+    }
+
+    pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
+        self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
+    }
+
     fn dismiss_notification_internal(
         &mut self,
         type_id: TypeId,

crates/workspace/src/pane.rs 🔗

@@ -20,11 +20,12 @@ use gpui::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    impl_actions, impl_internal_actions,
+    impl_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
-    Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle,
-    MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
+    ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    WindowContext,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
@@ -74,14 +75,6 @@ actions!(
     ]
 );
 
-#[derive(Clone, PartialEq)]
-pub struct MoveItem {
-    pub item_id: usize,
-    pub from: WeakViewHandle<Pane>,
-    pub to: WeakViewHandle<Pane>,
-    pub destination_index: usize,
-}
-
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct GoBack {
     #[serde(skip_deserializing)]
@@ -94,36 +87,7 @@ pub struct GoForward {
     pub pane: Option<WeakViewHandle<Pane>>,
 }
 
-#[derive(Clone, PartialEq)]
-pub struct DeploySplitMenu;
-
-#[derive(Clone, PartialEq)]
-pub struct DeployDockMenu;
-
-#[derive(Clone, PartialEq)]
-pub struct DeployNewMenu;
-
-#[derive(Clone, PartialEq)]
-pub struct DeployTabContextMenu {
-    pub position: Vector2F,
-    pub item_id: usize,
-    pub pane: WeakViewHandle<Pane>,
-}
-
 impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
-impl_internal_actions!(
-    pane,
-    [
-        CloseItemById,
-        CloseItemsToTheLeftById,
-        CloseItemsToTheRightById,
-        DeployTabContextMenu,
-        DeploySplitMenu,
-        DeployNewMenu,
-        DeployDockMenu,
-        MoveItem
-    ]
-);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -148,68 +112,10 @@ pub fn init(cx: &mut AppContext) {
     cx.add_async_action(Pane::close_items_to_the_left);
     cx.add_async_action(Pane::close_items_to_the_right);
     cx.add_async_action(Pane::close_all_items);
-    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItemById, cx| {
-        let pane = action.pane.upgrade(cx)?;
-        let task = Pane::close_item_by_id(workspace, pane, action.item_id, cx);
-        Some(cx.foreground().spawn(async move {
-            task.await?;
-            Ok(())
-        }))
-    });
-    cx.add_async_action(
-        |workspace: &mut Workspace, action: &CloseItemsToTheLeftById, cx| {
-            let pane = action.pane.upgrade(cx)?;
-            let task = Pane::close_items_to_the_left_by_id(workspace, pane, action.item_id, cx);
-            Some(cx.foreground().spawn(async move {
-                task.await?;
-                Ok(())
-            }))
-        },
-    );
-    cx.add_async_action(
-        |workspace: &mut Workspace, action: &CloseItemsToTheRightById, cx| {
-            let pane = action.pane.upgrade(cx)?;
-            let task = Pane::close_items_to_the_right_by_id(workspace, pane, action.item_id, cx);
-            Some(cx.foreground().spawn(async move {
-                task.await?;
-                Ok(())
-            }))
-        },
-    );
-    cx.add_action(
-        |workspace,
-         MoveItem {
-             from,
-             to,
-             item_id,
-             destination_index,
-         },
-         cx| {
-            // Get item handle to move
-            let from = if let Some(from) = from.upgrade(cx) {
-                from
-            } else {
-                return;
-            };
-
-            // Add item to new pane at given index
-            let to = if let Some(to) = to.upgrade(cx) {
-                to
-            } else {
-                return;
-            };
-
-            Pane::move_item(workspace, from, to, *item_id, *destination_index, cx)
-        },
-    );
     cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
     cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
     cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
     cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
-    cx.add_action(Pane::deploy_split_menu);
-    cx.add_action(Pane::deploy_dock_menu);
-    cx.add_action(Pane::deploy_new_menu);
-    cx.add_action(Pane::deploy_tab_context_menu);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         Pane::reopen_closed_item(workspace, cx).detach();
     });
@@ -243,7 +149,7 @@ pub struct Pane {
     tab_context_menu: ViewHandle<ContextMenu>,
     docked: Option<DockAnchor>,
     _background_actions: BackgroundActions,
-    _workspace_id: usize,
+    workspace: WeakViewHandle<Workspace>,
 }
 
 pub struct ItemNavHistory {
@@ -315,7 +221,7 @@ impl TabBarContextMenu {
 
 impl Pane {
     pub fn new(
-        workspace_id: usize,
+        workspace: WeakViewHandle<Workspace>,
         docked: Option<DockAnchor>,
         background_actions: BackgroundActions,
         cx: &mut ViewContext<Self>,
@@ -349,7 +255,7 @@ impl Pane {
             tab_context_menu: cx.add_view(ContextMenu::new),
             docked,
             _background_actions: background_actions,
-            _workspace_id: workspace_id,
+            workspace,
         }
     }
 
@@ -1223,16 +1129,16 @@ impl Pane {
         cx.emit(Event::Split(direction));
     }
 
-    fn deploy_split_menu(&mut self, _: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
+    fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
             menu.show(
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
-                    ContextMenuItem::item("Split Right", SplitRight),
-                    ContextMenuItem::item("Split Left", SplitLeft),
-                    ContextMenuItem::item("Split Up", SplitUp),
-                    ContextMenuItem::item("Split Down", SplitDown),
+                    ContextMenuItem::action("Split Right", SplitRight),
+                    ContextMenuItem::action("Split Left", SplitLeft),
+                    ContextMenuItem::action("Split Up", SplitUp),
+                    ContextMenuItem::action("Split Down", SplitDown),
                 ],
                 cx,
             );
@@ -1241,15 +1147,15 @@ impl Pane {
         self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
     }
 
-    fn deploy_dock_menu(&mut self, _: &DeployDockMenu, cx: &mut ViewContext<Self>) {
+    fn deploy_dock_menu(&mut self, cx: &mut ViewContext<Self>) {
         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
             menu.show(
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
-                    ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
-                    ContextMenuItem::item("Anchor Dock Bottom", AnchorDockBottom),
-                    ContextMenuItem::item("Expand Dock", ExpandDock),
+                    ContextMenuItem::action("Anchor Dock Right", AnchorDockRight),
+                    ContextMenuItem::action("Anchor Dock Bottom", AnchorDockBottom),
+                    ContextMenuItem::action("Expand Dock", ExpandDock),
                 ],
                 cx,
             );
@@ -1258,15 +1164,15 @@ impl Pane {
         self.tab_bar_context_menu.kind = TabBarContextMenuKind::Dock;
     }
 
-    fn deploy_new_menu(&mut self, _: &DeployNewMenu, cx: &mut ViewContext<Self>) {
+    fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
             menu.show(
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
-                    ContextMenuItem::item("New File", NewFile),
-                    ContextMenuItem::item("New Terminal", NewTerminal),
-                    ContextMenuItem::item("New Search", NewSearch),
+                    ContextMenuItem::action("New File", NewFile),
+                    ContextMenuItem::action("New Terminal", NewTerminal),
+                    ContextMenuItem::action("New Search", NewSearch),
                 ],
                 cx,
             );
@@ -1277,56 +1183,87 @@ impl Pane {
 
     fn deploy_tab_context_menu(
         &mut self,
-        action: &DeployTabContextMenu,
+        position: Vector2F,
+        target_item_id: usize,
         cx: &mut ViewContext<Self>,
     ) {
-        let target_item_id = action.item_id;
-        let target_pane = action.pane.clone();
         let active_item_id = self.items[self.active_item_index].id();
         let is_active_item = target_item_id == active_item_id;
+        let target_pane = cx.weak_handle();
 
         // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on.  Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
 
         self.tab_context_menu.update(cx, |menu, cx| {
             menu.show(
-                action.position,
+                position,
                 AnchorCorner::TopLeft,
                 if is_active_item {
                     vec![
-                        ContextMenuItem::item("Close Active Item", CloseActiveItem),
-                        ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
-                        ContextMenuItem::item("Close Clean Items", CloseCleanItems),
-                        ContextMenuItem::item("Close Items To The Left", CloseItemsToTheLeft),
-                        ContextMenuItem::item("Close Items To The Right", CloseItemsToTheRight),
-                        ContextMenuItem::item("Close All Items", CloseAllItems),
+                        ContextMenuItem::action("Close Active Item", CloseActiveItem),
+                        ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
+                        ContextMenuItem::action("Close Clean Items", CloseCleanItems),
+                        ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
+                        ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
+                        ContextMenuItem::action("Close All Items", CloseAllItems),
                     ]
                 } else {
                     // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
                     vec![
-                        ContextMenuItem::item(
-                            "Close Inactive Item",
-                            CloseItemById {
-                                item_id: target_item_id,
-                                pane: target_pane.clone(),
-                            },
-                        ),
-                        ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
-                        ContextMenuItem::item("Close Clean Items", CloseCleanItems),
-                        ContextMenuItem::item(
-                            "Close Items To The Left",
-                            CloseItemsToTheLeftById {
-                                item_id: target_item_id,
-                                pane: target_pane.clone(),
-                            },
-                        ),
-                        ContextMenuItem::item(
-                            "Close Items To The Right",
-                            CloseItemsToTheRightById {
-                                item_id: target_item_id,
-                                pane: target_pane.clone(),
-                            },
-                        ),
-                        ContextMenuItem::item("Close All Items", CloseAllItems),
+                        ContextMenuItem::handler("Close Inactive Item", {
+                            let workspace = self.workspace.clone();
+                            let pane = target_pane.clone();
+                            move |cx| {
+                                if let Some((workspace, pane)) =
+                                    workspace.upgrade(cx).zip(pane.upgrade(cx))
+                                {
+                                    workspace.update(cx, |workspace, cx| {
+                                        Self::close_item_by_id(workspace, pane, target_item_id, cx)
+                                            .detach_and_log_err(cx);
+                                    })
+                                }
+                            }
+                        }),
+                        ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
+                        ContextMenuItem::action("Close Clean Items", CloseCleanItems),
+                        ContextMenuItem::handler("Close Items To The Left", {
+                            let workspace = self.workspace.clone();
+                            let pane = target_pane.clone();
+                            move |cx| {
+                                if let Some((workspace, pane)) =
+                                    workspace.upgrade(cx).zip(pane.upgrade(cx))
+                                {
+                                    workspace.update(cx, |workspace, cx| {
+                                        Self::close_items_to_the_left_by_id(
+                                            workspace,
+                                            pane,
+                                            target_item_id,
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
+                                    })
+                                }
+                            }
+                        }),
+                        ContextMenuItem::handler("Close Items To The Right", {
+                            let workspace = self.workspace.clone();
+                            let pane = target_pane.clone();
+                            move |cx| {
+                                if let Some((workspace, pane)) =
+                                    workspace.upgrade(cx).zip(pane.upgrade(cx))
+                                {
+                                    workspace.update(cx, |workspace, cx| {
+                                        Self::close_items_to_the_right_by_id(
+                                            workspace,
+                                            pane,
+                                            target_item_id,
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
+                                    })
+                                }
+                            }
+                        }),
+                        ContextMenuItem::action("Close All Items", CloseAllItems),
                     ]
                 },
                 cx,
@@ -1407,24 +1344,28 @@ impl Pane {
                                     cx.dispatch_action(ActivateItem(ix));
                                 })
                                 .on_click(MouseButton::Middle, {
-                                    let item = item.clone();
-                                    let pane = pane.clone();
-                                    move |_, _, cx| {
-                                        cx.dispatch_action(CloseItemById {
-                                            item_id: item.id(),
-                                            pane: pane.clone(),
-                                        })
+                                    let item_id = item.id();
+                                    move |_, pane, cx| {
+                                        let workspace = pane.workspace.clone();
+                                        let pane = cx.weak_handle();
+                                        cx.window_context().defer(move |cx| {
+                                            if let Some((workspace, pane)) =
+                                                workspace.upgrade(cx).zip(pane.upgrade(cx))
+                                            {
+                                                workspace.update(cx, |workspace, cx| {
+                                                    Self::close_item_by_id(
+                                                        workspace, pane, item_id, cx,
+                                                    )
+                                                    .detach_and_log_err(cx);
+                                                });
+                                            }
+                                        });
                                     }
                                 })
                                 .on_down(
                                     MouseButton::Right,
-                                    move |e, _, cx| {
-                                        let item = item.clone();
-                                        cx.dispatch_action(DeployTabContextMenu {
-                                            position: e.position,
-                                            item_id: item.id(),
-                                            pane: pane.clone(),
-                                        });
+                                    move |event, pane, cx| {
+                                        pane.deploy_tab_context_menu(event.position, item.id(), cx);
                                     },
                                 );
 
@@ -1622,10 +1563,17 @@ impl Pane {
                     .on_click(MouseButton::Left, {
                         let pane = pane.clone();
                         move |_, _, cx| {
-                            cx.dispatch_action(CloseItemById {
-                                item_id,
-                                pane: pane.clone(),
-                            })
+                            let pane = pane.clone();
+                            cx.window_context().defer(move |cx| {
+                                if let Some(pane) = pane.upgrade(cx) {
+                                    if let Some(workspace) = pane.read(cx).workspace.upgrade(cx) {
+                                        workspace.update(cx, |workspace, cx| {
+                                            Self::close_item_by_id(workspace, pane, item_id, cx)
+                                                .detach_and_log_err(cx);
+                                        });
+                                    }
+                                }
+                            });
                         }
                     })
                     .into_any_named("close-tab-icon")
@@ -1654,7 +1602,7 @@ impl Pane {
                 0,
                 "icons/plus_12.svg",
                 cx,
-                DeployNewMenu,
+                |pane, cx| pane.deploy_new_menu(cx),
                 self.tab_bar_context_menu
                     .handle_if_kind(TabBarContextMenuKind::New),
             ))
@@ -1668,7 +1616,7 @@ impl Pane {
                             1,
                             dock_icon,
                             cx,
-                            DeployDockMenu,
+                            |pane, cx| pane.deploy_dock_menu(cx),
                             self.tab_bar_context_menu
                                 .handle_if_kind(TabBarContextMenuKind::Dock),
                         )
@@ -1679,17 +1627,22 @@ impl Pane {
                             2,
                             "icons/split_12.svg",
                             cx,
-                            DeploySplitMenu,
+                            |pane, cx| pane.deploy_split_menu(cx),
                             self.tab_bar_context_menu
                                 .handle_if_kind(TabBarContextMenuKind::Split),
                         )
                     }),
             )
             // Add the close dock button if this pane is a dock
-            .with_children(
-                self.docked
-                    .map(|_| render_tab_bar_button(3, "icons/x_mark_8.svg", cx, HideDock, None)),
-            )
+            .with_children(self.docked.map(|_| {
+                render_tab_bar_button(
+                    3,
+                    "icons/x_mark_8.svg",
+                    cx,
+                    |_, cx| cx.dispatch_action(HideDock),
+                    None,
+                )
+            }))
             .contained()
             .with_style(theme.workspace.tab_bar.pane_button_container)
             .flex(1., false)
@@ -1863,11 +1816,11 @@ impl View for Pane {
     }
 }
 
-fn render_tab_bar_button<A: Action + Clone>(
+fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
     index: usize,
     icon: &'static str,
     cx: &mut ViewContext<Pane>,
-    action: A,
+    on_click: F,
     context_menu: Option<ViewHandle<ContextMenu>>,
 ) -> AnyElement<Pane> {
     enum TabBarButton {}
@@ -1887,9 +1840,7 @@ fn render_tab_bar_button<A: Action + Clone>(
                     .with_height(style.button_width)
             })
             .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, _, cx| {
-                cx.dispatch_action(action.clone());
-            }),
+            .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)),
         )
         .with_children(
             context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -10,10 +10,7 @@ use gpui::{
 use project::ProjectEntryId;
 use settings::Settings;
 
-use crate::{
-    MoveItem, OpenProjectEntryInPane, Pane, SplitDirection, SplitWithItem, SplitWithProjectEntry,
-    Workspace,
-};
+use crate::{Pane, SplitDirection, Workspace};
 
 use super::DraggedItem;
 
@@ -72,9 +69,18 @@ where
             }))
     })
     .on_up(MouseButton::Left, {
-        move |event, _, cx| {
+        move |event, pane, cx| {
+            let workspace = pane.workspace.clone();
             let pane = cx.weak_handle();
-            handle_dropped_item(event, &pane, drop_index, allow_same_pane, split_margin, cx);
+            handle_dropped_item(
+                event,
+                workspace,
+                &pane,
+                drop_index,
+                allow_same_pane,
+                split_margin,
+                cx,
+            );
             cx.notify();
         }
     })
@@ -97,6 +103,7 @@ where
 
 pub fn handle_dropped_item<V: View>(
     event: MouseUp,
+    workspace: WeakViewHandle<Workspace>,
     pane: &WeakViewHandle<Pane>,
     index: usize,
     allow_same_pane: bool,
@@ -126,36 +133,74 @@ pub fn handle_dropped_item<V: View>(
     {
         let pane_to_split = pane.clone();
         match action {
-            Action::Move(from, item_id_to_move) => cx.dispatch_action(SplitWithItem {
-                from,
-                item_id_to_move,
-                pane_to_split,
-                split_direction,
-            }),
-            Action::Open(project_entry) => cx.dispatch_action(SplitWithProjectEntry {
-                pane_to_split,
-                split_direction,
-                project_entry,
-            }),
+            Action::Move(from, item_id_to_move) => {
+                cx.window_context().defer(move |cx| {
+                    if let Some(workspace) = workspace.upgrade(cx) {
+                        workspace.update(cx, |workspace, cx| {
+                            workspace.split_pane_with_item(
+                                pane_to_split,
+                                split_direction,
+                                from,
+                                item_id_to_move,
+                                cx,
+                            );
+                        })
+                    }
+                });
+            }
+            Action::Open(project_entry) => {
+                cx.window_context().defer(move |cx| {
+                    if let Some(workspace) = workspace.upgrade(cx) {
+                        workspace.update(cx, |workspace, cx| {
+                            if let Some(task) = workspace.split_pane_with_project_entry(
+                                pane_to_split,
+                                split_direction,
+                                project_entry,
+                                cx,
+                            ) {
+                                task.detach_and_log_err(cx);
+                            }
+                        })
+                    }
+                });
+            }
         };
     } else {
         match action {
             Action::Move(from, item_id) => {
                 if pane != &from || allow_same_pane {
-                    cx.dispatch_action(MoveItem {
-                        item_id,
-                        from,
-                        to: pane.clone(),
-                        destination_index: index,
-                    })
+                    let pane = pane.clone();
+                    cx.window_context().defer(move |cx| {
+                        if let Some(((workspace, from), to)) = workspace
+                            .upgrade(cx)
+                            .zip(from.upgrade(cx))
+                            .zip(pane.upgrade(cx))
+                        {
+                            workspace.update(cx, |workspace, cx| {
+                                Pane::move_item(workspace, from, to, item_id, index, cx);
+                            })
+                        }
+                    });
                 } else {
                     cx.propagate_event();
                 }
             }
-            Action::Open(project_entry) => cx.dispatch_action(OpenProjectEntryInPane {
-                pane: pane.clone(),
-                project_entry,
-            }),
+            Action::Open(project_entry) => {
+                let pane = pane.clone();
+                cx.window_context().defer(move |cx| {
+                    if let Some(workspace) = workspace.upgrade(cx) {
+                        workspace.update(cx, |workspace, cx| {
+                            if let Some(path) =
+                                workspace.project.read(cx).path_for_entry(project_entry, cx)
+                            {
+                                workspace
+                                    .open_path(path, Some(pane), true, cx)
+                                    .detach_and_log_err(cx);
+                            }
+                        });
+                    }
+                });
+            }
         }
     }
 }

crates/workspace/src/pane_group.rs 🔗

@@ -1,4 +1,6 @@
-use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
+use std::sync::Arc;
+
+use crate::{AppState, FollowerStatesByLeader, Pane, Workspace};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use gpui::{
@@ -70,6 +72,7 @@ impl PaneGroup {
         follower_states: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
+        app_state: &Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
     ) -> AnyElement<Workspace> {
         self.root.render(
@@ -78,6 +81,7 @@ impl PaneGroup {
             follower_states,
             active_call,
             active_pane,
+            app_state,
             cx,
         )
     }
@@ -131,6 +135,7 @@ impl Member {
         follower_states: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
+        app_state: &Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
     ) -> AnyElement<Workspace> {
         enum FollowIntoExternalProject {}
@@ -175,6 +180,7 @@ impl Member {
                             } else {
                                 let leader_user = leader.user.clone();
                                 let leader_user_id = leader.user.id;
+                                let app_state = Arc::downgrade(app_state);
                                 Some(
                                     MouseEventHandler::<FollowIntoExternalProject, _>::new(
                                         pane.id(),
@@ -199,10 +205,15 @@ impl Member {
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
                                     .on_click(MouseButton::Left, move |_, _, cx| {
-                                        cx.dispatch_action(JoinProject {
-                                            project_id: leader_project_id,
-                                            follow_user_id: leader_user_id,
-                                        })
+                                        if let Some(app_state) = app_state.upgrade() {
+                                            crate::join_remote_project(
+                                                leader_project_id,
+                                                leader_user_id,
+                                                app_state,
+                                                cx,
+                                            )
+                                            .detach_and_log_err(cx);
+                                        }
                                     })
                                     .aligned()
                                     .bottom()
@@ -257,6 +268,7 @@ impl Member {
                 follower_states,
                 active_call,
                 active_pane,
+                app_state,
                 cx,
             ),
         }
@@ -360,6 +372,7 @@ impl PaneAxis {
         follower_state: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
+        app_state: &Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
     ) -> AnyElement<Workspace> {
         let last_member_ix = self.members.len() - 1;
@@ -370,8 +383,15 @@ impl PaneAxis {
                     flex = cx.global::<Settings>().active_pane_magnification;
                 }
 
-                let mut member =
-                    member.render(project, theme, follower_state, active_call, active_pane, cx);
+                let mut member = member.render(
+                    project,
+                    theme,
+                    follower_state,
+                    active_call,
+                    active_pane,
+                    app_state,
+                    cx,
+                );
                 if ix < last_member_ix {
                     let mut border = theme.workspace.pane_divider;
                     border.left = false;

crates/workspace/src/workspace.rs 🔗

@@ -25,7 +25,6 @@ use client::{
 use collections::{hash_map, HashMap, HashSet};
 use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
-use fs::{self, Fs};
 use futures::{
     channel::{mpsc, oneshot},
     future::try_join_all,
@@ -38,7 +37,7 @@ use gpui::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    impl_actions, impl_internal_actions,
+    impl_actions,
     keymap_matcher::KeymapContext,
     platform::{
         CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
@@ -135,41 +134,6 @@ pub struct OpenPaths {
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivatePane(pub usize);
 
-#[derive(Clone, PartialEq)]
-pub struct ToggleFollow(pub PeerId);
-
-#[derive(Clone, PartialEq)]
-pub struct JoinProject {
-    pub project_id: u64,
-    pub follow_user_id: u64,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct OpenSharedScreen {
-    pub peer_id: PeerId,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct SplitWithItem {
-    pane_to_split: WeakViewHandle<Pane>,
-    split_direction: SplitDirection,
-    from: WeakViewHandle<Pane>,
-    item_id_to_move: usize,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct SplitWithProjectEntry {
-    pane_to_split: WeakViewHandle<Pane>,
-    split_direction: SplitDirection,
-    project_entry: ProjectEntryId,
-}
-
-#[derive(Clone, PartialEq)]
-pub struct OpenProjectEntryInPane {
-    pane: WeakViewHandle<Pane>,
-    project_entry: ProjectEntryId,
-}
-
 pub struct Toast {
     id: usize,
     msg: Cow<'static, str>,
@@ -220,34 +184,8 @@ impl Clone for Toast {
     }
 }
 
-#[derive(Clone, PartialEq)]
-pub struct DismissToast {
-    id: usize,
-}
-
-impl DismissToast {
-    pub fn new(id: usize) -> Self {
-        DismissToast { id }
-    }
-}
-
 pub type WorkspaceId = i64;
 
-impl_internal_actions!(
-    workspace,
-    [
-        OpenPaths,
-        ToggleFollow,
-        JoinProject,
-        OpenSharedScreen,
-        RemoveWorktreeFromProject,
-        SplitWithItem,
-        SplitWithProjectEntry,
-        OpenProjectEntryInPane,
-        Toast,
-        DismissToast
-    ]
-);
 impl_actions!(workspace, [ActivatePane]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
@@ -255,81 +193,53 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     dock::init(cx);
     notifications::init(cx);
 
-    cx.add_global_action(|_: &Open, cx: &mut AppContext| {
-        let mut paths = cx.prompt_for_paths(PathPromptOptions {
-            files: true,
-            directories: true,
-            multiple: true,
-        });
-
-        cx.spawn(|mut cx| async move {
-            if let Some(paths) = paths.recv().await.flatten() {
-                cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
-            }
-        })
-        .detach();
-    });
-    cx.add_action(|_, _: &Open, cx: &mut ViewContext<Workspace>| {
-        let mut paths = cx.prompt_for_paths(PathPromptOptions {
-            files: true,
-            directories: true,
-            multiple: true,
-        });
-
-        let handle = cx.handle().downgrade();
-        cx.spawn(|_, mut cx| async move {
-            if let Some(paths) = paths.recv().await.flatten() {
-                cx.update(|cx| {
-                    cx.dispatch_action_at(handle.window_id(), handle.id(), OpenPaths { paths })
-                })
-            }
-        })
-        .detach();
-    });
     cx.add_global_action({
         let app_state = Arc::downgrade(&app_state);
-        move |action: &OpenPaths, cx: &mut AppContext| {
+        move |_: &Open, cx: &mut AppContext| {
+            let mut paths = cx.prompt_for_paths(PathPromptOptions {
+                files: true,
+                directories: true,
+                multiple: true,
+            });
+
             if let Some(app_state) = app_state.upgrade() {
-                open_paths(&action.paths, &app_state, None, cx).detach();
+                cx.spawn(move |mut cx| async move {
+                    if let Some(paths) = paths.recv().await.flatten() {
+                        cx.update(|cx| {
+                            open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
+                        });
+                    }
+                })
+                .detach();
             }
         }
     });
-    cx.add_async_action({
+    cx.add_action({
         let app_state = Arc::downgrade(&app_state);
-        move |workspace, action: &OpenPaths, cx: &mut ViewContext<Workspace>| {
-            if !workspace.project().read(cx).is_local() {
-                cx.propagate_action();
-                return None;
-            }
-
-            let app_state = app_state.upgrade()?;
-            let window_id = cx.window_id();
-            let action = action.clone();
-            let is_remote = workspace.project.read(cx).is_remote();
-            let has_worktree = workspace.project.read(cx).worktrees(cx).next().is_some();
-            let has_dirty_items = workspace.items(cx).any(|item| item.is_dirty(cx));
-            let close_task = if is_remote || has_worktree || has_dirty_items {
-                None
-            } else {
-                Some(workspace.prepare_to_close(false, cx))
-            };
+        move |_, _: &Open, cx: &mut ViewContext<Workspace>| {
+            let mut paths = cx.prompt_for_paths(PathPromptOptions {
+                files: true,
+                directories: true,
+                multiple: true,
+            });
 
-            Some(cx.spawn(|_, mut cx| async move {
-                let window_id_to_replace = if let Some(close_task) = close_task {
-                    if !close_task.await? {
-                        return Ok(());
+            if let Some(app_state) = app_state.upgrade() {
+                cx.spawn(|this, mut cx| async move {
+                    if let Some(paths) = paths.recv().await.flatten() {
+                        if let Some(task) = this
+                            .update(&mut cx, |this, cx| {
+                                this.open_workspace_for_paths(paths, app_state, cx)
+                            })
+                            .log_err()
+                        {
+                            task.await.log_err();
+                        }
                     }
-                    Some(window_id)
-                } else {
-                    None
-                };
-                cx.update(|cx| open_paths(&action.paths, &app_state, window_id_to_replace, cx))
-                    .await?;
-                Ok(())
-            }))
+                })
+                .detach();
+            }
         }
     });
-
     cx.add_global_action({
         let app_state = Arc::downgrade(&app_state);
         move |_: &NewWindow, cx: &mut AppContext| {
@@ -347,14 +257,11 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         }
     });
 
-    cx.add_async_action(Workspace::toggle_follow);
     cx.add_async_action(Workspace::follow_next_collaborator);
     cx.add_async_action(Workspace::close);
     cx.add_global_action(Workspace::close_global);
     cx.add_async_action(Workspace::save_all);
-    cx.add_action(Workspace::open_shared_screen);
     cx.add_action(Workspace::add_folder_to_project);
-    cx.add_action(Workspace::remove_folder_from_project);
     cx.add_action(
         |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
             let pane = workspace.active_pane().clone();
@@ -384,30 +291,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     });
     cx.add_action(Workspace::activate_pane_at_index);
 
-    cx.add_action(Workspace::split_pane_with_item);
-    cx.add_async_action(Workspace::split_pane_with_project_entry);
-
-    cx.add_async_action(
-        |workspace: &mut Workspace,
-         OpenProjectEntryInPane {
-             pane,
-             project_entry,
-         }: &_,
-         cx| {
-            workspace
-                .project
-                .read(cx)
-                .path_for_entry(*project_entry, cx)
-                .map(|path| {
-                    let task = workspace.open_path(path, Some(pane.clone()), true, cx);
-                    cx.foreground().spawn(async move {
-                        task.await?;
-                        Ok(())
-                    })
-                })
-        },
-    );
-
     cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
         cx.spawn(|workspace, mut cx| async move {
             let err = install_cli::install_cli(&cx)
@@ -431,24 +314,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         .detach();
     });
 
-    cx.add_action(|workspace: &mut Workspace, alert: &Toast, cx| {
-        workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
-        workspace.show_notification(alert.id, cx, |cx| {
-            cx.add_view(|_cx| match &alert.click {
-                Some((click_msg, action)) => MessageNotification::new_boxed_action(
-                    alert.msg.clone(),
-                    action.boxed_clone(),
-                    click_msg.clone(),
-                ),
-                None => MessageNotification::new_message(alert.msg.clone()),
-            })
-        })
-    });
-
-    cx.add_action(|workspace: &mut Workspace, alert: &DismissToast, cx| {
-        workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
-    });
-
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
     client.add_view_message_handler(Workspace::handle_unfollow);
@@ -617,10 +482,7 @@ pub enum Event {
 
 pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
-    client: Arc<Client>,
-    user_store: ModelHandle<client::UserStore>,
     remote_entity_subscription: Option<client::Subscription>,
-    fs: Arc<dyn Fs>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     left_sidebar: ViewHandle<Sidebar>,
@@ -641,7 +503,7 @@ pub struct Workspace {
     active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
-    background_actions: BackgroundActions,
+    app_state: Arc<AppState>,
     _window_subscriptions: [Subscription; 3],
     _apply_leader_updates: Task<Result<()>>,
     _observe_current_user: Task<Result<()>>,
@@ -671,8 +533,7 @@ impl Workspace {
         serialized_workspace: Option<SerializedWorkspace>,
         workspace_id: WorkspaceId,
         project: ModelHandle<Project>,
-        dock_default_factory: DockDefaultItemFactory,
-        background_actions: BackgroundActions,
+        app_state: Arc<AppState>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
@@ -709,8 +570,8 @@ impl Workspace {
 
         let weak_handle = cx.weak_handle();
 
-        let center_pane =
-            cx.add_view(|cx| Pane::new(weak_handle.id(), None, background_actions, cx));
+        let center_pane = cx
+            .add_view(|cx| Pane::new(weak_handle.clone(), None, app_state.background_actions, cx));
         let pane_id = center_pane.id();
         cx.subscribe(&center_pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -719,18 +580,14 @@ impl Workspace {
         cx.focus(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
         let dock = Dock::new(
-            weak_handle.id(),
-            dock_default_factory,
-            background_actions,
+            app_state.dock_default_item_factory,
+            app_state.background_actions,
             cx,
         );
         let dock_pane = dock.pane().clone();
 
-        let fs = project.read(cx).fs().clone();
-        let user_store = project.read(cx).user_store();
-        let client = project.read(cx).client();
-        let mut current_user = user_store.read(cx).watch_current_user();
-        let mut connection_status = client.status();
+        let mut current_user = app_state.user_store.read(cx).watch_current_user();
+        let mut connection_status = app_state.client.status();
         let _observe_current_user = cx.spawn(|this, mut cx| async move {
             current_user.recv().await;
             connection_status.recv().await;
@@ -823,10 +680,7 @@ impl Workspace {
             status_bar,
             titlebar_item: None,
             notifications: Default::default(),
-            client,
             remote_entity_subscription: None,
-            user_store,
-            fs,
             left_sidebar,
             right_sidebar,
             project: project.clone(),
@@ -836,7 +690,7 @@ impl Workspace {
             window_edited: false,
             active_call,
             database_id: workspace_id,
-            background_actions,
+            app_state,
             _observe_current_user,
             _apply_leader_updates,
             leader_updates_tx,
@@ -925,8 +779,7 @@ impl Workspace {
                         serialized_workspace,
                         workspace_id,
                         project_handle.clone(),
-                        app_state.dock_default_item_factory,
-                        app_state.background_actions,
+                        app_state.clone(),
                         cx,
                     );
                     (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@@ -1036,8 +889,12 @@ impl Workspace {
         &self.status_bar
     }
 
+    pub fn app_state(&self) -> &Arc<AppState> {
+        &self.app_state
+    }
+
     pub fn user_store(&self) -> &ModelHandle<UserStore> {
-        &self.user_store
+        &self.app_state.user_store
     }
 
     pub fn project(&self) -> &ModelHandle<Project> {
@@ -1045,7 +902,7 @@ impl Workspace {
     }
 
     pub fn client(&self) -> &Client {
-        &self.client
+        &self.app_state.client
     }
 
     pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -1243,6 +1100,37 @@ impl Workspace {
         })
     }
 
+    pub fn open_workspace_for_paths(
+        &mut self,
+        paths: Vec<PathBuf>,
+        app_state: Arc<AppState>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let window_id = cx.window_id();
+        let is_remote = self.project.read(cx).is_remote();
+        let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
+        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
+        let close_task = if is_remote || has_worktree || has_dirty_items {
+            None
+        } else {
+            Some(self.prepare_to_close(false, cx))
+        };
+
+        cx.spawn(|_, mut cx| async move {
+            let window_id_to_replace = if let Some(close_task) = close_task {
+                if !close_task.await? {
+                    return Ok(());
+                }
+                Some(window_id)
+            } else {
+                None
+            };
+            cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
+                .await?;
+            Ok(())
+        })
+    }
+
     #[allow(clippy::type_complexity)]
     pub fn open_paths(
         &mut self,
@@ -1250,7 +1138,7 @@ impl Workspace {
         visible: bool,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
-        let fs = self.fs.clone();
+        let fs = self.app_state.fs.clone();
 
         // Sort the paths to ensure we add worktrees for parents before their children.
         abs_paths.sort_unstable();
@@ -1319,15 +1207,6 @@ impl Workspace {
         .detach_and_log_err(cx);
     }
 
-    fn remove_folder_from_project(
-        &mut self,
-        RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.project
-            .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
-    }
-
     fn project_path_for_path(
         project: ModelHandle<Project>,
         abs_path: &Path,
@@ -1561,8 +1440,14 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane =
-            cx.add_view(|cx| Pane::new(self.weak_handle().id(), None, self.background_actions, cx));
+        let pane = cx.add_view(|cx| {
+            Pane::new(
+                self.weak_handle(),
+                None,
+                self.app_state.background_actions,
+                cx,
+            )
+        });
         let pane_id = pane.id();
         cx.subscribe(&pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -1678,10 +1563,8 @@ impl Workspace {
         item
     }
 
-    pub fn open_shared_screen(&mut self, action: &OpenSharedScreen, cx: &mut ViewContext<Self>) {
-        if let Some(shared_screen) =
-            self.shared_screen_for_peer(action.peer_id, &self.active_pane, cx)
-        {
+    pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
+        if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
             let pane = self.active_pane.clone();
             Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
         }
@@ -1755,7 +1638,7 @@ impl Workspace {
             proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
                 id: self.active_item(cx).and_then(|item| {
                     item.to_followable_item_handle(cx)?
-                        .remote_id(&self.client, cx)
+                        .remote_id(&self.app_state.client, cx)
                         .map(|id| id.to_proto())
                 }),
                 leader_id: self.leader_for_pane(&pane),
@@ -1833,35 +1716,37 @@ impl Workspace {
         maybe_pane_handle
     }
 
-    pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
-        let Some(pane_to_split) = action.pane_to_split.upgrade(cx) else { return; };
-        let Some(from) = action.from.upgrade(cx) else { return; };
+    pub fn split_pane_with_item(
+        &mut self,
+        pane_to_split: WeakViewHandle<Pane>,
+        split_direction: SplitDirection,
+        from: WeakViewHandle<Pane>,
+        item_id_to_move: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
+        let Some(from) = from.upgrade(cx) else { return; };
         if &pane_to_split == self.dock_pane() {
             warn!("Can't split dock pane.");
             return;
         }
 
         let new_pane = self.add_pane(cx);
-        Pane::move_item(
-            self,
-            from.clone(),
-            new_pane.clone(),
-            action.item_id_to_move,
-            0,
-            cx,
-        );
+        Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
         self.center
-            .split(&pane_to_split, &new_pane, action.split_direction)
+            .split(&pane_to_split, &new_pane, split_direction)
             .unwrap();
         cx.notify();
     }
 
     pub fn split_pane_with_project_entry(
         &mut self,
-        action: &SplitWithProjectEntry,
+        pane_to_split: WeakViewHandle<Pane>,
+        split_direction: SplitDirection,
+        project_entry: ProjectEntryId,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
-        let pane_to_split = action.pane_to_split.upgrade(cx)?;
+        let pane_to_split = pane_to_split.upgrade(cx)?;
         if &pane_to_split == self.dock_pane() {
             warn!("Can't split dock pane.");
             return None;
@@ -1869,13 +1754,10 @@ impl Workspace {
 
         let new_pane = self.add_pane(cx);
         self.center
-            .split(&pane_to_split, &new_pane, action.split_direction)
+            .split(&pane_to_split, &new_pane, split_direction)
             .unwrap();
 
-        let path = self
-            .project
-            .read(cx)
-            .path_for_entry(action.project_entry, cx)?;
+        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
         let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
         Some(cx.foreground().spawn(async move {
             task.await?;
@@ -1924,8 +1806,11 @@ impl Workspace {
 
     fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
         if let Some(remote_id) = remote_id {
-            self.remote_entity_subscription =
-                Some(self.client.add_view_for_remote_entity(remote_id, cx));
+            self.remote_entity_subscription = Some(
+                self.app_state
+                    .client
+                    .add_view_for_remote_entity(remote_id, cx),
+            );
         } else {
             self.remote_entity_subscription.take();
         }
@@ -1945,10 +1830,9 @@ impl Workspace {
 
     pub fn toggle_follow(
         &mut self,
-        ToggleFollow(leader_id): &ToggleFollow,
+        leader_id: PeerId,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
-        let leader_id = *leader_id;
         let pane = self.active_pane().clone();
 
         if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
@@ -1966,7 +1850,7 @@ impl Workspace {
         cx.notify();
 
         let project_id = self.project.read(cx).remote_id()?;
-        let request = self.client.request(proto::Follow {
+        let request = self.app_state.client.request(proto::Follow {
             project_id,
             leader_id: Some(leader_id),
         });
@@ -2027,7 +1911,7 @@ impl Workspace {
 
         next_leader_id
             .or_else(|| collaborators.keys().copied().next())
-            .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
+            .and_then(|leader_id| self.toggle_follow(leader_id, cx))
     }
 
     pub fn unfollow(
@@ -2045,7 +1929,8 @@ impl Workspace {
                 if states_by_pane.is_empty() {
                     self.follower_states_by_leader.remove(&leader_id);
                     if let Some(project_id) = self.project.read(cx).remote_id() {
-                        self.client
+                        self.app_state
+                            .client
                             .send(proto::Unfollow {
                                 project_id,
                                 leader_id: Some(leader_id),
@@ -2226,7 +2111,7 @@ impl Workspace {
         mut cx: AsyncAppContext,
     ) -> Result<proto::FollowResponse> {
         this.update(&mut cx, |this, cx| {
-            let client = &this.client;
+            let client = &this.app_state.client;
             this.leader_state
                 .followers
                 .insert(envelope.original_sender_id()?);
@@ -2434,7 +2319,8 @@ impl Workspace {
     ) -> Option<()> {
         let project_id = self.project.read(cx).remote_id()?;
         if !self.leader_state.followers.is_empty() {
-            self.client
+            self.app_state
+                .client
                 .send(proto::UpdateFollowers {
                     project_id,
                     follower_ids: self.leader_state.followers.iter().copied().collect(),
@@ -2766,7 +2652,18 @@ impl Workspace {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
-        Self::new(None, 0, project, |_, _| None, || &[], cx)
+        let app_state = Arc::new(AppState {
+            languages: project.read(cx).languages().clone(),
+            themes: ThemeRegistry::new((), cx.font_cache().clone()),
+            client: project.read(cx).client(),
+            user_store: project.read(cx).user_store(),
+            fs: project.read(cx).fs().clone(),
+            build_window_options: |_, _, _| Default::default(),
+            initialize_workspace: |_, _, _| {},
+            dock_default_item_factory: |_, _| None,
+            background_actions: || &[],
+        });
+        Self::new(None, 0, project, app_state, cx)
     }
 }
 
@@ -2852,6 +2749,7 @@ impl View for Workspace {
                                                         &self.follower_states_by_leader,
                                                         self.active_call(),
                                                         self.active_pane(),
+                                                        &self.app_state,
                                                         cx,
                                                     ))
                                                     .flex(1., true),
@@ -3083,6 +2981,87 @@ pub fn open_new(
     })
 }
 
+pub fn join_remote_project(
+    project_id: u64,
+    follow_user_id: u64,
+    app_state: Arc<AppState>,
+    cx: &mut AppContext,
+) -> Task<Result<()>> {
+    cx.spawn(|mut cx| async move {
+        let existing_workspace = cx.update(|cx| {
+            cx.window_ids()
+                .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
+                .find(|workspace| {
+                    workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
+                })
+        });
+
+        let workspace = if let Some(existing_workspace) = existing_workspace {
+            existing_workspace.downgrade()
+        } else {
+            let active_call = cx.read(ActiveCall::global);
+            let room = active_call
+                .read_with(&cx, |call, _| call.room().cloned())
+                .ok_or_else(|| anyhow!("not in a call"))?;
+            let project = room
+                .update(&mut cx, |room, cx| {
+                    room.join_project(
+                        project_id,
+                        app_state.languages.clone(),
+                        app_state.fs.clone(),
+                        cx,
+                    )
+                })
+                .await?;
+
+            let (_, workspace) = cx.add_window(
+                (app_state.build_window_options)(None, None, cx.platform().as_ref()),
+                |cx| {
+                    let mut workspace =
+                        Workspace::new(Default::default(), 0, project, app_state.clone(), cx);
+                    (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
+                    workspace
+                },
+            );
+            workspace.downgrade()
+        };
+
+        cx.activate_window(workspace.window_id());
+        cx.platform().activate(true);
+
+        workspace.update(&mut cx, |workspace, cx| {
+            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+                let follow_peer_id = room
+                    .read(cx)
+                    .remote_participants()
+                    .iter()
+                    .find(|(_, participant)| participant.user.id == follow_user_id)
+                    .map(|(_, p)| p.peer_id)
+                    .or_else(|| {
+                        // If we couldn't follow the given user, follow the host instead.
+                        let collaborator = workspace
+                            .project()
+                            .read(cx)
+                            .collaborators()
+                            .values()
+                            .find(|collaborator| collaborator.replica_id == 0)?;
+                        Some(collaborator.peer_id)
+                    });
+
+                if let Some(follow_peer_id) = follow_peer_id {
+                    if !workspace.is_being_followed(follow_peer_id) {
+                        workspace
+                            .toggle_follow(follow_peer_id, cx)
+                            .map(|follow| follow.detach_and_log_err(cx));
+                    }
+                }
+            }
+        })?;
+
+        anyhow::Ok(())
+    })
+}
+
 fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
     let mut parts = value.split(',');
     let width: usize = parts.next()?.parse().ok()?;
@@ -3109,16 +3088,7 @@ mod tests {
 
         let fs = FakeFs::new(cx.background());
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| None,
-                || &[],
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         // Adding an item with no ambiguity renders the tab without detail.
         let item1 = cx.add_view(&workspace, |_| {
@@ -3182,16 +3152,7 @@ mod tests {
         .await;
 
         let project = Project::test(fs, ["root1".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| None,
-                || &[],
-                cx,
-            )
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let worktree_id = project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         });
@@ -3281,16 +3242,7 @@ mod tests {
         fs.insert_tree("/root", json!({ "one": "" })).await;
 
         let project = Project::test(fs, ["root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| None,
-                || &[],
-                cx,
-            )
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         // When there are no dirty items, there's nothing to do.
         let item1 = cx.add_view(&workspace, |_| TestItem::new());
@@ -3325,9 +3277,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let item1 = cx.add_view(&workspace, |cx| {
             TestItem::new()
@@ -3434,9 +3384,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // Create several workspace items with single project entries, and two
         // workspace items with multiple project entries.
@@ -3543,9 +3491,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let item = cx.add_view(&workspace, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
@@ -3662,9 +3608,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let item = cx.add_view(&workspace, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])

crates/zed/src/main.rs 🔗

@@ -44,7 +44,7 @@ use theme::ThemeRegistry;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::{
     self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
-    OpenPaths, Workspace,
+    Workspace,
 };
 use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
 
@@ -160,7 +160,6 @@ fn main() {
         vim::init(cx);
         terminal_view::init(cx);
         theme_testbench::init(cx);
-        recent_projects::init(cx);
         copilot::init(http.clone(), node_runtime, cx);
 
         cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
@@ -194,12 +193,13 @@ fn main() {
         auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
 
         workspace::init(app_state.clone(), cx);
+        recent_projects::init(cx, Arc::downgrade(&app_state));
 
         journal::init(app_state.clone(), cx);
         language_selector::init(app_state.clone(), cx);
         theme_selector::init(app_state.clone(), cx);
         zed::init(&app_state, cx);
-        collab_ui::init(app_state.clone(), cx);
+        collab_ui::init(&app_state, cx);
         feedback::init(app_state.clone(), cx);
         welcome::init(cx);
 
@@ -212,7 +212,7 @@ fn main() {
                 cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await })
                     .detach()
             } else {
-                cx.dispatch_global_action(OpenPaths { paths });
+                workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx);
             }
         } else {
             if let Ok(Some(connection)) = cli_connections_rx.try_next() {
@@ -267,11 +267,9 @@ fn main() {
 
 async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
     if let Some(location) = workspace::last_opened_workspace_paths().await {
-        cx.update(|cx| {
-            cx.dispatch_global_action(OpenPaths {
-                paths: location.paths().as_ref().clone(),
-            })
-        });
+        cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))
+            .await
+            .log_err();
     } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
         cx.update(|cx| show_welcome_experience(app_state, cx));
     } else {

crates/zed/src/zed.rs 🔗

@@ -31,7 +31,7 @@ use serde::Deserialize;
 use serde_json::to_string_pretty;
 use settings::Settings;
 use std::{borrow::Cow, env, path::Path, str, sync::Arc};
-use terminal_view::terminal_button::{self, TerminalButton};
+use terminal_view::terminal_button::TerminalButton;
 use util::{channel::ReleaseChannel, paths, ResultExt};
 use uuid::Uuid;
 pub use workspace;
@@ -73,7 +73,6 @@ actions!(
 const MIN_FONT_SIZE: f32 = 6.0;
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
-    terminal_button::init(cx);
     cx.add_action(about);
     cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
         cx.platform().hide();
@@ -261,7 +260,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         },
     );
     activity_indicator::init(cx);
-    copilot_button::init(cx);
     lsp_log::init(cx);
     call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
     settings::KeymapFileContent::load_defaults(cx);
@@ -302,8 +300,9 @@ pub fn initialize_workspace(
     cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
     cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
 
-    let collab_titlebar_item =
-        cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
+    let collab_titlebar_item = cx.add_view(|cx| {
+        CollabTitlebarItem::new(&workspace_handle, app_state.user_store.clone(), cx)
+    });
     workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
 
     let project_panel = ProjectPanel::new(workspace.project().clone(), cx);