Merge pull request #1023 from zed-industries/app-menu-improvements

Max Brunsfeld created

Correctly populate application menus' keystrokes and enabled status

Change summary

Cargo.lock                                    |   1 
assets/keymaps/default.json                   |   5 
crates/collab/src/rpc.rs                      |  54 +--
crates/command_palette/Cargo.toml             |   2 
crates/command_palette/src/command_palette.rs |  10 
crates/contacts_panel/src/contacts_panel.rs   |  43 +--
crates/diagnostics/src/diagnostics.rs         |  45 +--
crates/editor/src/editor.rs                   |   4 
crates/file_finder/src/file_finder.rs         | 118 +++-----
crates/gpui/src/app.rs                        |  38 ++
crates/gpui/src/elements/uniform_list.rs      |   4 
crates/gpui/src/keymap.rs                     |   4 
crates/gpui/src/platform.rs                   |   5 
crates/gpui/src/platform/mac/event.rs         |  30 ++
crates/gpui/src/platform/mac/platform.rs      |  89 +++++-
crates/gpui/src/platform/test.rs              |   7 
crates/project/src/project.rs                 |  52 ++-
crates/project_panel/src/project_panel.rs     |  19 
crates/project_symbols/src/project_symbols.rs |   2 
crates/search/src/project_search.rs           |   2 
crates/theme_selector/src/theme_selector.rs   |  18 +
crates/vim/src/vim_test_context.rs            |  11 
crates/workspace/src/pane.rs                  |  12 
crates/workspace/src/waiting_room.rs          | 151 +++++-----
crates/workspace/src/workspace.rs             | 175 +++++--------
crates/zed/src/main.rs                        |  23 -
crates/zed/src/menus.rs                       | 162 +++++++++++-
crates/zed/src/test.rs                        |  39 ---
crates/zed/src/zed.rs                         | 273 +++++++++-----------
29 files changed, 757 insertions(+), 641 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -907,6 +907,7 @@ dependencies = [
  "fuzzy",
  "gpui",
  "picker",
+ "project",
  "serde_json",
  "settings",
  "theme",

assets/keymaps/default.json πŸ”—

@@ -18,7 +18,10 @@
             "cmd-s": "workspace::Save",
             "cmd-=": "zed::IncreaseBufferFontSize",
             "cmd--": "zed::DecreaseBufferFontSize",
-            "cmd-,": "zed::OpenSettings"
+            "cmd-,": "zed::OpenSettings",
+            "cmd-q": "zed::Quit",
+            "cmd-n": "workspace::OpenNew",
+            "cmd-o": "workspace::Open"
         }
     },
     {

crates/collab/src/rpc.rs πŸ”—

@@ -1630,7 +1630,7 @@ mod tests {
     use gpui::{
         executor::{self, Deterministic},
         geometry::vector::vec2f,
-        ModelHandle, TestAppContext, ViewHandle,
+        ModelHandle, Task, TestAppContext, ViewHandle,
     };
     use language::{
         range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
@@ -1662,7 +1662,7 @@ mod tests {
         time::Duration,
     };
     use theme::ThemeRegistry;
-    use workspace::{Item, SplitDirection, ToggleFollow, Workspace, WorkspaceParams};
+    use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
 
     #[cfg(test)]
     #[ctor::ctor]
@@ -4322,13 +4322,7 @@ mod tests {
 
         // Join the project as client B.
         let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let mut params = cx_b.update(WorkspaceParams::test);
-        params.languages = lang_registry.clone();
-        params.project = project_b.clone();
-        params.client = client_b.client.clone();
-        params.user_store = client_b.user_store.clone();
-
-        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
+        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
                 workspace.open_path((worktree_id, "main.rs"), true, cx)
@@ -4563,13 +4557,7 @@ mod tests {
 
         // Join the worktree as client B.
         let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let mut params = cx_b.update(WorkspaceParams::test);
-        params.languages = lang_registry.clone();
-        params.project = project_b.clone();
-        params.client = client_b.client.clone();
-        params.user_store = client_b.user_store.clone();
-
-        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
+        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
                 workspace.open_path((worktree_id, "one.rs"), true, cx)
@@ -6602,13 +6590,21 @@ mod tests {
                     })
                 });
 
+            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+            let app_state = Arc::new(workspace::AppState {
+                client: client.clone(),
+                user_store: user_store.clone(),
+                languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
+                themes: ThemeRegistry::new((), cx.font_cache()),
+                fs: FakeFs::new(cx.background()),
+                build_window_options: || Default::default(),
+                initialize_workspace: |_, _, _| unimplemented!(),
+            });
+
             Channel::init(&client);
             Project::init(&client);
-            cx.update(|cx| {
-                workspace::init(&client, cx);
-            });
+            cx.update(|cx| workspace::init(app_state.clone(), cx));
 
-            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
             client
                 .authenticate_and_connect(false, &cx.to_async())
                 .await
@@ -6846,23 +6842,7 @@ mod tests {
             cx: &mut TestAppContext,
         ) -> ViewHandle<Workspace> {
             let (window_id, _) = cx.add_window(|_| EmptyView);
-            cx.add_view(window_id, |cx| {
-                let fs = project.read(cx).fs().clone();
-                Workspace::new(
-                    &WorkspaceParams {
-                        fs,
-                        project: project.clone(),
-                        user_store: self.user_store.clone(),
-                        languages: self.language_registry.clone(),
-                        themes: ThemeRegistry::new((), cx.font_cache().clone()),
-                        channel_list: cx.add_model(|cx| {
-                            ChannelList::new(self.user_store.clone(), self.client.clone(), cx)
-                        }),
-                        client: self.client.clone(),
-                    },
-                    cx,
-                )
-            })
+            cx.add_view(window_id, |cx| Workspace::new(project.clone(), cx))
         }
 
         async fn simulate_host(

crates/command_palette/Cargo.toml πŸ”—

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 picker = { path = "../picker" }
+project = { path = "../project" }
 settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
@@ -20,6 +21,7 @@ workspace = { path = "../workspace" }
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
 serde_json = { version = "1.0.64", features = ["preserve_order"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"

crates/command_palette/src/command_palette.rs πŸ”—

@@ -299,7 +299,8 @@ mod tests {
     use super::*;
     use editor::Editor;
     use gpui::TestAppContext;
-    use workspace::{Workspace, WorkspaceParams};
+    use project::Project;
+    use workspace::{AppState, Workspace};
 
     #[test]
     fn test_humanize_action_name() {
@@ -319,15 +320,16 @@ mod tests {
 
     #[gpui::test]
     async fn test_command_palette(cx: &mut TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
+        let app_state = cx.update(AppState::test);
 
         cx.update(|cx| {
             editor::init(cx);
-            workspace::init(&params.client, cx);
+            workspace::init(app_state.clone(), cx);
             init(cx);
         });
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let editor = cx.add_view(window_id, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", cx);

crates/contacts_panel/src/contacts_panel.rs πŸ”—

@@ -23,7 +23,7 @@ use theme::IconButton;
 use workspace::{
     menu::{Confirm, SelectNext, SelectPrev},
     sidebar::SidebarItem,
-    AppState, JoinProject, Workspace,
+    JoinProject, Workspace,
 };
 
 impl_actions!(
@@ -60,7 +60,6 @@ pub struct ContactsPanel {
     filter_editor: ViewHandle<Editor>,
     collapsed_sections: Vec<Section>,
     selection: Option<usize>,
-    app_state: Arc<AppState>,
     _maintain_contacts: Subscription,
 }
 
@@ -92,7 +91,7 @@ pub fn init(cx: &mut MutableAppContext) {
 
 impl ContactsPanel {
     pub fn new(
-        app_state: Arc<AppState>,
+        user_store: ModelHandle<UserStore>,
         workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
@@ -152,8 +151,8 @@ impl ContactsPanel {
             }
         });
 
-        cx.subscribe(&app_state.user_store, {
-            let user_store = app_state.user_store.downgrade();
+        cx.subscribe(&user_store, {
+            let user_store = user_store.downgrade();
             move |_, _, event, cx| {
                 if let Some((workspace, user_store)) =
                     workspace.upgrade(cx).zip(user_store.upgrade(cx))
@@ -175,7 +174,6 @@ impl ContactsPanel {
         let mut this = Self {
             list_state: ListState::new(0, Orientation::Top, 1000., {
                 let this = cx.weak_handle();
-                let app_state = app_state.clone();
                 move |ix, cx| {
                     let this = this.upgrade(cx).unwrap();
                     let this = this.read(cx);
@@ -222,7 +220,6 @@ impl ContactsPanel {
                                 contact.clone(),
                                 current_user_id,
                                 *project_ix,
-                                app_state.clone(),
                                 theme,
                                 is_last_project_for_contact,
                                 is_selected,
@@ -237,10 +234,8 @@ impl ContactsPanel {
             entries: Default::default(),
             match_candidates: Default::default(),
             filter_editor,
-            _maintain_contacts: cx
-                .observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
-            user_store: app_state.user_store.clone(),
-            app_state,
+            _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)),
+            user_store,
         };
         this.update_entries(cx);
         this
@@ -339,7 +334,6 @@ impl ContactsPanel {
         contact: Arc<Contact>,
         current_user_id: Option<u64>,
         project_index: usize,
-        app_state: Arc<AppState>,
         theme: &theme::ContactsPanel,
         is_last_project: bool,
         is_selected: bool,
@@ -444,7 +438,6 @@ impl ContactsPanel {
                 cx.dispatch_global_action(JoinProject {
                     contact: contact.clone(),
                     project_index,
-                    app_state: app_state.clone(),
                 });
             }
         })
@@ -770,7 +763,6 @@ impl ContactsPanel {
                         .dispatch_global_action(JoinProject {
                             contact: contact.clone(),
                             project_index: *project_index,
-                            app_state: self.app_state.clone(),
                         }),
                     _ => {}
                 }
@@ -916,19 +908,20 @@ impl PartialEq for ContactEntry {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use client::{proto, test::FakeServer, ChannelList, Client};
+    use client::{proto, test::FakeServer, Client};
     use gpui::TestAppContext;
     use language::LanguageRegistry;
+    use project::Project;
     use theme::ThemeRegistry;
-    use workspace::WorkspaceParams;
+    use workspace::AppState;
 
     #[gpui::test]
     async fn test_contact_panel(cx: &mut TestAppContext) {
         let (app_state, server) = init(cx).await;
-        let workspace_params = cx.update(WorkspaceParams::test);
-        let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let workspace = cx.add_view(0, |cx| Workspace::new(project, cx));
         let panel = cx.add_view(0, |cx| {
-            ContactsPanel::new(app_state.clone(), workspace.downgrade(), cx)
+            ContactsPanel::new(app_state.user_store.clone(), workspace.downgrade(), cx)
         });
 
         let get_users_request = server.receive::<proto::GetUsers>().await.unwrap();
@@ -1110,13 +1103,6 @@ mod tests {
         let mut client = Client::new(http_client.clone());
         let server = FakeServer::for_client(100, &mut client, &cx).await;
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
-        let channel_list =
-            cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));
-
-        let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
-        server
-            .respond(get_channels.receipt(), Default::default())
-            .await;
 
         (
             Arc::new(AppState {
@@ -1125,9 +1111,8 @@ mod tests {
                 client,
                 user_store: user_store.clone(),
                 fs,
-                channel_list,
-                build_window_options: || unimplemented!(),
-                build_workspace: |_, _, _| unimplemented!(),
+                build_window_options: || Default::default(),
+                initialize_workspace: |_, _, _| {},
             }),
             server,
         )

crates/diagnostics/src/diagnostics.rs πŸ”—

@@ -707,49 +707,42 @@ mod tests {
     use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
     use serde_json::json;
     use unindent::Unindent as _;
-    use workspace::WorkspaceParams;
+    use workspace::AppState;
 
     #[gpui::test]
     async fn test_diagnostics(cx: &mut TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
-        let project = params.project.clone();
-        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
-
-        params
+        let app_state = cx.update(AppState::test);
+        app_state
             .fs
             .as_fake()
             .insert_tree(
                 "/test",
                 json!({
                     "consts.rs": "
-                    const a: i32 = 'a';
-                    const b: i32 = c;
-                "
+                        const a: i32 = 'a';
+                        const b: i32 = c;
+                    "
                     .unindent(),
 
                     "main.rs": "
-                    fn main() {
-                        let x = vec![];
-                        let y = vec![];
-                        a(x);
-                        b(y);
-                        // comment 1
-                        // comment 2
-                        c(y);
-                        d(x);
-                    }
-                "
+                        fn main() {
+                            let x = vec![];
+                            let y = vec![];
+                            a(x);
+                            b(y);
+                            // comment 1
+                            // comment 2
+                            c(y);
+                            d(x);
+                        }
+                    "
                     .unindent(),
                 }),
             )
             .await;
 
-        project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/test", true, cx)
-            })
-            .await
-            .unwrap();
+        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
+        let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx));
 
         // Create some diagnostics
         project.update(cx, |project, cx| {

crates/editor/src/editor.rs πŸ”—

@@ -8849,7 +8849,7 @@ mod tests {
         let fs = FakeFs::new(cx.background().clone());
         fs.insert_file("/file.rs", Default::default()).await;
 
-        let project = Project::test(fs, ["/file.rs"], cx).await;
+        let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
@@ -8971,7 +8971,7 @@ mod tests {
         let fs = FakeFs::new(cx.background().clone());
         fs.insert_file("/file.rs", text).await;
 
-        let project = Project::test(fs, ["/file.rs"], cx).await;
+        let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))

crates/file_finder/src/file_finder.rs πŸ”—

@@ -258,9 +258,10 @@ mod tests {
     use super::*;
     use editor::{Editor, Input};
     use serde_json::json;
-    use std::path::PathBuf;
-    use workspace::menu::{Confirm, SelectNext};
-    use workspace::{Workspace, WorkspaceParams};
+    use workspace::{
+        menu::{Confirm, SelectNext},
+        AppState, Workspace,
+    };
 
     #[ctor::ctor]
     fn init_logger() {
@@ -271,13 +272,13 @@ mod tests {
 
     #[gpui::test]
     async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
-        cx.update(|cx| {
+        let app_state = cx.update(|cx| {
             super::init(cx);
             editor::init(cx);
+            AppState::test(cx)
         });
 
-        let params = cx.update(WorkspaceParams::test);
-        params
+        app_state
             .fs
             .as_fake()
             .insert_tree(
@@ -291,16 +292,8 @@ mod tests {
             )
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         cx.dispatch_action(window_id, Toggle);
 
         let finder = cx.read(|cx| {
@@ -341,32 +334,26 @@ mod tests {
 
     #[gpui::test]
     async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
-        let fs = params.fs.as_fake();
-        fs.insert_tree(
-            "/dir",
-            json!({
-                "hello": "",
-                "goodbye": "",
-                "halogen-light": "",
-                "happiness": "",
-                "height": "",
-                "hi": "",
-                "hiccup": "",
-            }),
-        )
-        .await;
-
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/dir", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+        let app_state = cx.update(AppState::test);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/dir",
+                json!({
+                    "hello": "",
+                    "goodbye": "",
+                    "halogen-light": "",
+                    "happiness": "",
+                    "height": "",
+                    "hi": "",
+                    "hiccup": "",
+                }),
+            )
             .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -406,23 +393,20 @@ mod tests {
 
     #[gpui::test]
     async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
-        params
+        let app_state = cx.update(AppState::test);
+        app_state
             .fs
             .as_fake()
             .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
             .await;
 
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root/the-parent-dir/the-file", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(
+            app_state.fs.clone(),
+            ["/root/the-parent-dir/the-file".as_ref()],
+            cx,
+        )
+        .await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -451,10 +435,12 @@ mod tests {
         finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 0));
     }
 
-    #[gpui::test(retries = 5)]
+    #[gpui::test]
     async fn test_multiple_matches_with_same_relative_path(cx: &mut gpui::TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
-        params
+        cx.foreground().forbid_parking();
+
+        let app_state = cx.update(AppState::test);
+        app_state
             .fs
             .as_fake()
             .insert_tree(
@@ -466,19 +452,13 @@ mod tests {
             )
             .await;
 
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-
-        workspace
-            .update(cx, |workspace, cx| {
-                workspace.open_paths(
-                    vec![PathBuf::from("/root/dir1"), PathBuf::from("/root/dir2")],
-                    cx,
-                )
-            })
-            .await;
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
-
+        let project = Project::test(
+            app_state.fs.clone(),
+            ["/root/dir1".as_ref(), "/root/dir2".as_ref()],
+            cx,
+        )
+        .await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 

crates/gpui/src/app.rs πŸ”—

@@ -154,7 +154,6 @@ pub struct Menu<'a> {
 pub enum MenuItem<'a> {
     Action {
         name: &'a str,
-        keystroke: Option<&'a str>,
         action: Box<dyn Action>,
     },
     Separator,
@@ -193,6 +192,20 @@ impl App {
                 cx.borrow_mut().quit();
             }
         }));
+        foreground_platform.on_will_open_menu(Box::new({
+            let cx = app.0.clone();
+            move || {
+                let mut cx = cx.borrow_mut();
+                cx.keystroke_matcher.clear_pending();
+            }
+        }));
+        foreground_platform.on_validate_menu_command(Box::new({
+            let cx = app.0.clone();
+            move |action| {
+                let cx = cx.borrow_mut();
+                !cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action)
+            }
+        }));
         foreground_platform.on_menu_command(Box::new({
             let cx = app.0.clone();
             move |action| {
@@ -1070,7 +1083,8 @@ impl MutableAppContext {
     }
 
     pub fn set_menus(&mut self, menus: Vec<Menu>) {
-        self.foreground_platform.set_menus(menus);
+        self.foreground_platform
+            .set_menus(menus, &self.keystroke_matcher);
     }
 
     fn prompt(
@@ -1364,6 +1378,26 @@ impl MutableAppContext {
             })
     }
 
+    pub fn is_action_available(&self, action: &dyn Action) -> bool {
+        let action_type = action.as_any().type_id();
+        if let Some(window_id) = self.cx.platform.key_window_id() {
+            if let Some((presenter, _)) = self.presenters_and_platform_windows.get(&window_id) {
+                let dispatch_path = presenter.borrow().dispatch_path(&self.cx);
+                for view_id in dispatch_path {
+                    if let Some(view) = self.views.get(&(window_id, view_id)) {
+                        let view_type = view.as_any().type_id();
+                        if let Some(actions) = self.actions.get(&view_type) {
+                            if actions.contains_key(&action_type) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        self.global_actions.contains_key(&action_type)
+    }
+
     pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) {
         let presenter = self
             .presenters_and_platform_windows

crates/gpui/src/elements/uniform_list.rs πŸ”—

@@ -215,12 +215,12 @@ where
         self.autoscroll(scroll_max, size.y(), item_height);
 
         let start = cmp::min(
-            ((self.scroll_top() - self.padding_top) / item_height) as usize,
+            ((self.scroll_top() - self.padding_top) / item_height.max(1.)) as usize,
             self.item_count,
         );
         let end = cmp::min(
             self.item_count,
-            start + (size.y() / item_height).ceil() as usize + 1,
+            start + (size.y() / item_height.max(1.)).ceil() as usize + 1,
         );
 
         if (start..end).contains(&sample_item_ix) {

crates/gpui/src/keymap.rs πŸ”—

@@ -123,6 +123,10 @@ impl Matcher {
         self.pending.clear();
     }
 
+    pub fn has_pending_keystrokes(&self) -> bool {
+        !self.pending.is_empty()
+    }
+
     pub fn push_keystroke(
         &mut self,
         keystroke: Keystroke,

crates/gpui/src/platform.rs πŸ”—

@@ -14,6 +14,7 @@ use crate::{
         rect::{RectF, RectI},
         vector::Vector2F,
     },
+    keymap,
     text_layout::{LineLayout, RunStyle},
     Action, ClipboardItem, Menu, Scene,
 };
@@ -72,7 +73,9 @@ pub(crate) trait ForegroundPlatform {
     fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
 
     fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
-    fn set_menus(&self, menus: Vec<Menu>);
+    fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
+    fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
+    fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
     fn prompt_for_paths(
         &self,
         options: PathPromptOptions,

crates/gpui/src/platform/mac/event.rs πŸ”—

@@ -8,7 +8,35 @@ use cocoa::{
     base::{id, nil, YES},
     foundation::NSString as _,
 };
-use std::{ffi::CStr, os::raw::c_char};
+use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
+
+pub fn key_to_native(key: &str) -> Cow<str> {
+    use cocoa::appkit::*;
+    let code = match key {
+        "backspace" => 0x7F,
+        "up" => NSUpArrowFunctionKey,
+        "down" => NSDownArrowFunctionKey,
+        "left" => NSLeftArrowFunctionKey,
+        "right" => NSRightArrowFunctionKey,
+        "pageup" => NSPageUpFunctionKey,
+        "pagedown" => NSPageDownFunctionKey,
+        "delete" => NSDeleteFunctionKey,
+        "f1" => NSF1FunctionKey,
+        "f2" => NSF2FunctionKey,
+        "f3" => NSF3FunctionKey,
+        "f4" => NSF4FunctionKey,
+        "f5" => NSF5FunctionKey,
+        "f6" => NSF6FunctionKey,
+        "f7" => NSF7FunctionKey,
+        "f8" => NSF8FunctionKey,
+        "f9" => NSF9FunctionKey,
+        "f10" => NSF10FunctionKey,
+        "f11" => NSF11FunctionKey,
+        "f12" => NSF12FunctionKey,
+        _ => return Cow::Borrowed(key),
+    };
+    Cow::Owned(String::from_utf16(&[code]).unwrap())
+}
 
 impl Event {
     pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {

crates/gpui/src/platform/mac/platform.rs πŸ”—

@@ -1,7 +1,6 @@
-use super::{BoolExt as _, Dispatcher, FontSystem, Window};
+use super::{event::key_to_native, BoolExt as _, Dispatcher, FontSystem, Window};
 use crate::{
-    executor,
-    keymap::Keystroke,
+    executor, keymap,
     platform::{self, CursorStyle},
     Action, ClipboardItem, Event, Menu, MenuItem,
 };
@@ -90,6 +89,14 @@ unsafe fn build_classes() {
             sel!(handleGPUIMenuItem:),
             handle_menu_item as extern "C" fn(&mut Object, Sel, id),
         );
+        decl.add_method(
+            sel!(validateMenuItem:),
+            validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
+        );
+        decl.add_method(
+            sel!(menuWillOpen:),
+            menu_will_open as extern "C" fn(&mut Object, Sel, id),
+        );
         decl.add_method(
             sel!(application:openURLs:),
             open_urls as extern "C" fn(&mut Object, Sel, id, id),
@@ -108,14 +115,22 @@ pub struct MacForegroundPlatformState {
     quit: Option<Box<dyn FnMut()>>,
     event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
     menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+    validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
+    will_open_menu: Option<Box<dyn FnMut()>>,
     open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
     finish_launching: Option<Box<dyn FnOnce() -> ()>>,
     menu_actions: Vec<Box<dyn Action>>,
 }
 
 impl MacForegroundPlatform {
-    unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
+    unsafe fn create_menu_bar(
+        &self,
+        menus: Vec<Menu>,
+        delegate: id,
+        keystroke_matcher: &keymap::Matcher,
+    ) -> id {
         let menu_bar = NSMenu::new(nil).autorelease();
+        menu_bar.setDelegate_(delegate);
         let mut state = self.0.borrow_mut();
 
         state.menu_actions.clear();
@@ -126,6 +141,7 @@ impl MacForegroundPlatform {
             let menu_name = menu_config.name;
 
             menu.setTitle_(ns_string(menu_name));
+            menu.setDelegate_(delegate);
 
             for item_config in menu_config.items {
                 let item;
@@ -134,19 +150,18 @@ impl MacForegroundPlatform {
                     MenuItem::Separator => {
                         item = NSMenuItem::separatorItem(nil);
                     }
-                    MenuItem::Action {
-                        name,
-                        keystroke,
-                        action,
-                    } => {
-                        if let Some(keystroke) = keystroke {
-                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
-                                panic!(
-                                    "Invalid keystroke for menu item {}:{} - {:?}",
-                                    menu_name, name, err
-                                )
-                            });
+                    MenuItem::Action { name, action } => {
+                        let mut keystroke = None;
+                        if let Some(binding) = keystroke_matcher
+                            .bindings_for_action_type(action.as_any().type_id())
+                            .next()
+                        {
+                            if binding.keystrokes().len() == 1 {
+                                keystroke = binding.keystrokes().first()
+                            }
+                        }
 
+                        if let Some(keystroke) = keystroke {
                             let mut mask = NSEventModifierFlags::empty();
                             for (modifier, flag) in &[
                                 (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
@@ -162,7 +177,7 @@ impl MacForegroundPlatform {
                                 .initWithTitle_action_keyEquivalent_(
                                     ns_string(name),
                                     selector("handleGPUIMenuItem:"),
-                                    ns_string(&keystroke.key),
+                                    ns_string(key_to_native(&keystroke.key).as_ref()),
                                 )
                                 .autorelease();
                             item.setKeyEquivalentModifierMask_(mask);
@@ -239,10 +254,18 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         self.0.borrow_mut().menu_command = Some(callback);
     }
 
-    fn set_menus(&self, menus: Vec<Menu>) {
+    fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
+        self.0.borrow_mut().will_open_menu = Some(callback);
+    }
+
+    fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
+        self.0.borrow_mut().validate_menu_command = Some(callback);
+    }
+
+    fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
         unsafe {
             let app: id = msg_send![APP_CLASS, sharedApplication];
-            app.setMainMenu_(self.create_menu_bar(menus));
+            app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), keystroke_matcher));
         }
     }
 
@@ -740,6 +763,34 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
     }
 }
 
+extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
+    unsafe {
+        let mut result = false;
+        let platform = get_foreground_platform(this);
+        let mut platform = platform.0.borrow_mut();
+        if let Some(mut callback) = platform.validate_menu_command.take() {
+            let tag: NSInteger = msg_send![item, tag];
+            let index = tag as usize;
+            if let Some(action) = platform.menu_actions.get(index) {
+                result = callback(action.as_ref());
+            }
+            platform.validate_menu_command = Some(callback);
+        }
+        result
+    }
+}
+
+extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
+    unsafe {
+        let platform = get_foreground_platform(this);
+        let mut platform = platform.0.borrow_mut();
+        if let Some(mut callback) = platform.will_open_menu.take() {
+            callback();
+            platform.will_open_menu = Some(callback);
+        }
+    }
+}
+
 unsafe fn ns_string(string: &str) -> id {
     NSString::alloc(nil).init_str(string).autorelease()
 }

crates/gpui/src/platform/test.rs πŸ”—

@@ -1,7 +1,7 @@
 use super::{AppVersion, CursorStyle, WindowBounds};
 use crate::{
     geometry::vector::{vec2f, Vector2F},
-    Action, ClipboardItem,
+    keymap, Action, ClipboardItem,
 };
 use anyhow::{anyhow, Result};
 use parking_lot::Mutex;
@@ -73,8 +73,9 @@ impl super::ForegroundPlatform for ForegroundPlatform {
     }
 
     fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
-
-    fn set_menus(&self, _: Vec<crate::Menu>) {}
+    fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
+    fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
+    fn set_menus(&self, _: Vec<crate::Menu>, _: &keymap::Matcher) {}
 
     fn prompt_for_paths(
         &self,

crates/project/src/project.rs πŸ”—

@@ -497,7 +497,7 @@ impl Project {
     #[cfg(any(test, feature = "test-support"))]
     pub async fn test(
         fs: Arc<dyn Fs>,
-        root_paths: impl IntoIterator<Item = impl AsRef<Path>>,
+        root_paths: impl IntoIterator<Item = &Path>,
         cx: &mut gpui::TestAppContext,
     ) -> ModelHandle<Project> {
         let languages = Arc::new(LanguageRegistry::test());
@@ -528,6 +528,14 @@ impl Project {
         &self.languages
     }
 
+    pub fn client(&self) -> Arc<Client> {
+        self.client.clone()
+    }
+
+    pub fn user_store(&self) -> ModelHandle<UserStore> {
+        self.user_store.clone()
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn check_invariants(&self, cx: &AppContext) {
         if self.is_local() {
@@ -5294,7 +5302,7 @@ mod tests {
         )
         .unwrap();
 
-        let project = Project::test(Arc::new(RealFs), [root_link_path], cx).await;
+        let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
 
         project.read_with(cx, |project, cx| {
             let tree = project.worktrees(cx).next().unwrap().read(cx);
@@ -5378,7 +5386,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/the-root"], cx).await;
+        let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
         project.update(cx, |project, _| {
             project.languages.add(Arc::new(rust_language));
             project.languages.add(Arc::new(json_language));
@@ -5714,7 +5722,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir/a.rs", "/dir/b.rs"], cx).await;
+        let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
 
         let buffer_a = project
             .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -5825,7 +5833,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let worktree_id =
             project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
@@ -5947,7 +5955,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 
         let buffer = project
@@ -6016,7 +6024,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({ "a.rs": text })).await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 
         let buffer = project
@@ -6285,7 +6293,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({ "a.rs": text })).await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
             .await
@@ -6376,7 +6384,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -6530,7 +6538,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
             .await
@@ -6686,7 +6694,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir/b.rs"], cx).await;
+        let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 
         let buffer = project
@@ -6780,7 +6788,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
@@ -6838,7 +6846,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
@@ -6944,7 +6952,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
             .await
@@ -6973,7 +6981,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir/file1"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
             .await
@@ -6995,7 +7003,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({})).await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let buffer = project.update(cx, |project, cx| {
             project.create_buffer("", None, cx).unwrap()
         });
@@ -7182,7 +7190,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 
         // Spawn multiple tasks to open paths, repeating some paths.
         let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
@@ -7227,7 +7235,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 
         let buffer1 = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
@@ -7359,7 +7367,7 @@ mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
             .await
@@ -7444,7 +7452,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/the-dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
             .await
@@ -7708,7 +7716,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| {
@@ -7827,7 +7835,7 @@ mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         assert_eq!(
             search(&project, SearchQuery::text("TWO", false, true), cx)
                 .await

crates/project_panel/src/project_panel.rs πŸ”—

@@ -913,11 +913,14 @@ mod tests {
     use project::FakeFs;
     use serde_json::json;
     use std::{collections::HashSet, path::Path};
-    use workspace::WorkspaceParams;
 
     #[gpui::test]
     async fn test_visible_list(cx: &mut gpui::TestAppContext) {
         cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            let settings = Settings::test(cx);
+            cx.set_global(settings);
+        });
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -956,9 +959,8 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await;
-        let params = cx.update(WorkspaceParams::test);
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..50, cx),
@@ -1005,6 +1007,10 @@ mod tests {
     #[gpui::test(iterations = 30)]
     async fn test_editing_files(cx: &mut gpui::TestAppContext) {
         cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            let settings = Settings::test(cx);
+            cx.set_global(settings);
+        });
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -1043,9 +1049,8 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await;
-        let params = cx.update(WorkspaceParams::test);
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
 
         select_path(&panel, "root1", cx);

crates/project_symbols/src/project_symbols.rs πŸ”—

@@ -295,7 +295,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
 
         let _buffer = project

crates/search/src/project_search.rs πŸ”—

@@ -848,7 +848,7 @@ mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
         let search_view = cx.add_view(Default::default(), |cx| {
             ProjectSearchView::new(search.clone(), cx)

crates/theme_selector/src/theme_selector.rs πŸ”—

@@ -7,7 +7,7 @@ use picker::{Picker, PickerDelegate};
 use settings::Settings;
 use std::sync::Arc;
 use theme::{Theme, ThemeRegistry};
-use workspace::Workspace;
+use workspace::{AppState, Workspace};
 
 pub struct ThemeSelector {
     registry: Arc<ThemeRegistry>,
@@ -21,9 +21,14 @@ pub struct ThemeSelector {
 
 actions!(theme_selector, [Toggle, Reload]);
 
-pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(ThemeSelector::toggle);
+pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     Picker::<ThemeSelector>::init(cx);
+    cx.add_action({
+        let theme_registry = app_state.themes.clone();
+        move |workspace, _: &Toggle, cx| {
+            ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
+        }
+    });
 }
 
 pub enum Event {
@@ -63,8 +68,11 @@ impl ThemeSelector {
         this
     }
 
-    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-        let themes = workspace.themes();
+    fn toggle(
+        workspace: &mut Workspace,
+        themes: Arc<ThemeRegistry>,
+        cx: &mut ViewContext<Workspace>,
+    ) {
         workspace.toggle_modal(cx, |_, cx| {
             let this = cx.add_view(|cx| Self::new(themes, cx));
             cx.subscribe(&this, Self::on_event).detach();

crates/vim/src/vim_test_context.rs πŸ”—

@@ -7,11 +7,12 @@ use editor::{display_map::ToDisplayPoint, Autoscroll};
 use gpui::{json::json, keymap::Keystroke, ViewHandle};
 use indoc::indoc;
 use language::Selection;
+use project::Project;
 use util::{
     set_eq,
     test::{marked_text, marked_text_ranges_by, SetEqError},
 };
-use workspace::{WorkspaceHandle, WorkspaceParams};
+use workspace::{AppState, WorkspaceHandle};
 
 use crate::{state::Operator, *};
 
@@ -30,7 +31,8 @@ impl<'a> VimTestContext<'a> {
             settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
         });
 
-        let params = cx.update(WorkspaceParams::test);
+        let params = cx.update(AppState::test);
+        let project = Project::test(params.fs.clone(), [], cx).await;
 
         cx.update(|cx| {
             cx.update_global(|settings: &mut Settings, _| {
@@ -44,9 +46,8 @@ impl<'a> VimTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)
             })

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

@@ -920,7 +920,7 @@ impl NavHistory {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::WorkspaceParams;
+    use crate::AppState;
     use gpui::{ModelHandle, TestAppContext, ViewContext};
     use project::Project;
     use std::sync::atomic::AtomicUsize;
@@ -929,8 +929,9 @@ mod tests {
     async fn test_close_items(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
 
-        let params = cx.update(WorkspaceParams::test);
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let app_state = cx.update(AppState::test);
+        let project = Project::test(app_state.fs.clone(), None, cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let item1 = cx.add_view(window_id, |_| {
             let mut item = TestItem::new();
             item.is_dirty = true;
@@ -1019,8 +1020,9 @@ mod tests {
     async fn test_prompting_only_on_last_item_for_entry(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
 
-        let params = cx.update(WorkspaceParams::test);
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let app_state = cx.update(AppState::test);
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let item = cx.add_view(window_id, |_| {
             let mut item = TestItem::new();
             item.is_dirty = true;

crates/workspace/src/waiting_room.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
     sidebar::{Side, ToggleSidebarItem},
-    AppState, ToggleFollow,
+    AppState, ToggleFollow, Workspace,
 };
 use anyhow::Result;
 use client::{proto, Client, Contact};
@@ -77,86 +77,87 @@ impl WaitingRoom {
     ) -> Self {
         let project_id = contact.projects[project_index].id;
         let client = app_state.client.clone();
-        let _join_task = cx.spawn_weak({
-            let contact = contact.clone();
-            |this, mut cx| async move {
-                let project = Project::remote(
-                    project_id,
-                    app_state.client.clone(),
-                    app_state.user_store.clone(),
-                    app_state.languages.clone(),
-                    app_state.fs.clone(),
-                    &mut cx,
-                )
-                .await;
+        let _join_task =
+            cx.spawn_weak({
+                let contact = contact.clone();
+                |this, mut cx| async move {
+                    let project = Project::remote(
+                        project_id,
+                        app_state.client.clone(),
+                        app_state.user_store.clone(),
+                        app_state.languages.clone(),
+                        app_state.fs.clone(),
+                        &mut cx,
+                    )
+                    .await;
 
-                if let Some(this) = this.upgrade(&cx) {
-                    this.update(&mut cx, |this, cx| {
-                        this.waiting = false;
-                        match project {
-                            Ok(project) => {
-                                cx.replace_root_view(|cx| {
-                                    let mut workspace = (app_state.build_workspace)(
-                                        project.clone(),
-                                        &app_state,
-                                        cx,
-                                    );
-                                    workspace.toggle_sidebar_item(
-                                        &ToggleSidebarItem {
-                                            side: Side::Left,
-                                            item_index: 0,
-                                        },
-                                        cx,
-                                    );
-                                    if let Some((host_peer_id, _)) = project
-                                        .read(cx)
-                                        .collaborators()
-                                        .iter()
-                                        .find(|(_, collaborator)| collaborator.replica_id == 0)
-                                    {
-                                        if let Some(follow) = workspace
-                                            .toggle_follow(&ToggleFollow(*host_peer_id), cx)
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| {
+                            this.waiting = false;
+                            match project {
+                                Ok(project) => {
+                                    cx.replace_root_view(|cx| {
+                                        let mut workspace = Workspace::new(project, cx);
+                                        (app_state.initialize_workspace)(
+                                            &mut workspace,
+                                            &app_state,
+                                            cx,
+                                        );
+                                        workspace.toggle_sidebar_item(
+                                            &ToggleSidebarItem {
+                                                side: Side::Left,
+                                                item_index: 0,
+                                            },
+                                            cx,
+                                        );
+                                        if let Some((host_peer_id, _)) =
+                                            workspace.project.read(cx).collaborators().iter().find(
+                                                |(_, collaborator)| collaborator.replica_id == 0,
+                                            )
                                         {
-                                            follow.detach_and_log_err(cx);
+                                            if let Some(follow) = workspace
+                                                .toggle_follow(&ToggleFollow(*host_peer_id), cx)
+                                            {
+                                                follow.detach_and_log_err(cx);
+                                            }
                                         }
-                                    }
-                                    workspace
-                                });
-                            }
-                            Err(error @ _) => {
-                                let login = &contact.user.github_login;
-                                let message = match error {
-                                    project::JoinProjectError::HostDeclined => {
-                                        format!("@{} declined your request.", login)
-                                    }
-                                    project::JoinProjectError::HostClosedProject => {
-                                        format!(
-                                            "@{} closed their copy of {}.",
-                                            login,
-                                            humanize_list(
-                                                &contact.projects[project_index]
-                                                    .worktree_root_names
+                                        workspace
+                                    });
+                                }
+                                Err(error @ _) => {
+                                    let login = &contact.user.github_login;
+                                    let message = match error {
+                                        project::JoinProjectError::HostDeclined => {
+                                            format!("@{} declined your request.", login)
+                                        }
+                                        project::JoinProjectError::HostClosedProject => {
+                                            format!(
+                                                "@{} closed their copy of {}.",
+                                                login,
+                                                humanize_list(
+                                                    &contact.projects[project_index]
+                                                        .worktree_root_names
+                                                )
                                             )
-                                        )
-                                    }
-                                    project::JoinProjectError::HostWentOffline => {
-                                        format!("@{} went offline.", login)
-                                    }
-                                    project::JoinProjectError::Other(error) => {
-                                        log::error!("error joining project: {}", error);
-                                        "An error occurred.".to_string()
-                                    }
-                                };
-                                this.message = message;
-                                cx.notify();
+                                        }
+                                        project::JoinProjectError::HostWentOffline => {
+                                            format!("@{} went offline.", login)
+                                        }
+                                        project::JoinProjectError::Other(error) => {
+                                            log::error!("error joining project: {}", error);
+                                            "An error occurred.".to_string()
+                                        }
+                                    };
+                                    this.message = message;
+                                    cx.notify();
+                                }
                             }
-                        }
-                    })
-                }
+                        })
+                    }
 
-                Ok(())
-            }
-        });
+                    Ok(())
+                }
+            });
 
         Self {
             project_id,

crates/workspace/src/workspace.rs πŸ”—

@@ -9,8 +9,7 @@ mod waiting_room;
 
 use anyhow::{anyhow, Context, Result};
 use client::{
-    proto, Authenticate, ChannelList, Client, Contact, PeerId, Subscription, TypedEnvelope, User,
-    UserStore,
+    proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
 };
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
@@ -75,6 +74,8 @@ type FollowableItemBuilders = HashMap<
 actions!(
     workspace,
     [
+        Open,
+        OpenNew,
         Unfollow,
         Save,
         ActivatePreviousPane,
@@ -83,16 +84,9 @@ actions!(
     ]
 );
 
-#[derive(Clone)]
-pub struct Open(pub Arc<AppState>);
-
-#[derive(Clone)]
-pub struct OpenNew(pub Arc<AppState>);
-
 #[derive(Clone)]
 pub struct OpenPaths {
     pub paths: Vec<PathBuf>,
-    pub app_state: Arc<AppState>,
 }
 
 #[derive(Clone)]
@@ -102,31 +96,37 @@ pub struct ToggleFollow(pub PeerId);
 pub struct JoinProject {
     pub contact: Arc<Contact>,
     pub project_index: usize,
-    pub app_state: Arc<AppState>,
 }
 
-impl_internal_actions!(
-    workspace,
-    [Open, OpenNew, OpenPaths, ToggleFollow, JoinProject]
-);
+impl_internal_actions!(workspace, [OpenPaths, ToggleFollow, JoinProject]);
 
-pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
+pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     pane::init(cx);
 
     cx.add_global_action(open);
-    cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
-        open_paths(&action.paths, &action.app_state, cx).detach();
+    cx.add_global_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |action: &OpenPaths, cx: &mut MutableAppContext| {
+            if let Some(app_state) = app_state.upgrade() {
+                open_paths(&action.paths, &app_state, cx).detach();
+            }
+        }
     });
-    cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
-        open_new(&action.0, cx)
+    cx.add_global_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |_: &OpenNew, cx: &mut MutableAppContext| {
+            if let Some(app_state) = app_state.upgrade() {
+                open_new(&app_state, cx)
+            }
+        }
     });
-    cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
-        join_project(
-            action.contact.clone(),
-            action.project_index,
-            &action.app_state,
-            cx,
-        );
+    cx.add_global_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |action: &JoinProject, cx: &mut MutableAppContext| {
+            if let Some(app_state) = app_state.upgrade() {
+                join_project(action.contact.clone(), action.project_index, &app_state, cx);
+            }
+        }
     });
 
     cx.add_async_action(Workspace::toggle_follow);
@@ -151,6 +151,7 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
         workspace.activate_next_pane(cx)
     });
 
+    let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
     client.add_view_message_handler(Workspace::handle_unfollow);
     client.add_view_message_handler(Workspace::handle_update_followers);
@@ -188,10 +189,8 @@ pub struct AppState {
     pub client: Arc<client::Client>,
     pub user_store: ModelHandle<client::UserStore>,
     pub fs: Arc<dyn fs::Fs>,
-    pub channel_list: ModelHandle<client::ChannelList>,
     pub build_window_options: fn() -> WindowOptions<'static>,
-    pub build_workspace:
-        fn(ModelHandle<Project>, &Arc<AppState>, &mut ViewContext<Workspace>) -> Workspace,
+    pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
 }
 
 pub trait Item: View {
@@ -636,20 +635,9 @@ impl Into<AnyViewHandle> for &dyn NotificationHandle {
     }
 }
 
-#[derive(Clone)]
-pub struct WorkspaceParams {
-    pub project: ModelHandle<Project>,
-    pub client: Arc<Client>,
-    pub fs: Arc<dyn Fs>,
-    pub languages: Arc<LanguageRegistry>,
-    pub themes: Arc<ThemeRegistry>,
-    pub user_store: ModelHandle<UserStore>,
-    pub channel_list: ModelHandle<ChannelList>,
-}
-
-impl WorkspaceParams {
+impl AppState {
     #[cfg(any(test, feature = "test-support"))]
-    pub fn test(cx: &mut MutableAppContext) -> Self {
+    pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
         let settings = Settings::test(cx);
         cx.set_global(settings);
 
@@ -658,42 +646,16 @@ impl WorkspaceParams {
         let http_client = client::test::FakeHttpClient::with_404_response();
         let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
-        let project = Project::local(
-            client.clone(),
-            user_store.clone(),
-            languages.clone(),
-            fs.clone(),
-            cx,
-        );
-        Self {
-            project,
-            channel_list: cx
-                .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
+        let themes = ThemeRegistry::new((), cx.font_cache().clone());
+        Arc::new(Self {
             client,
-            themes: ThemeRegistry::new((), cx.font_cache().clone()),
+            themes,
             fs,
             languages,
             user_store,
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn local(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Self {
-        Self {
-            project: Project::local(
-                app_state.client.clone(),
-                app_state.user_store.clone(),
-                app_state.languages.clone(),
-                app_state.fs.clone(),
-                cx,
-            ),
-            client: app_state.client.clone(),
-            fs: app_state.fs.clone(),
-            themes: app_state.themes.clone(),
-            languages: app_state.languages.clone(),
-            user_store: app_state.user_store.clone(),
-            channel_list: app_state.channel_list.clone(),
-        }
+            initialize_workspace: |_, _, _| {},
+            build_window_options: || Default::default(),
+        })
     }
 }
 
@@ -708,7 +670,6 @@ pub struct Workspace {
     user_store: ModelHandle<client::UserStore>,
     remote_entity_subscription: Option<Subscription>,
     fs: Arc<dyn Fs>,
-    themes: Arc<ThemeRegistry>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     left_sidebar: ViewHandle<Sidebar>,
@@ -744,8 +705,8 @@ enum FollowerItem {
 }
 
 impl Workspace {
-    pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
-        cx.observe(&params.project, |_, project, cx| {
+    pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+        cx.observe(&project, |_, project, cx| {
             if project.read(cx).is_read_only() {
                 cx.blur();
             }
@@ -753,7 +714,7 @@ impl Workspace {
         })
         .detach();
 
-        cx.subscribe(&params.project, move |this, project, event, cx| {
+        cx.subscribe(&project, move |this, project, event, cx| {
             match event {
                 project::Event::RemoteIdChanged(remote_id) => {
                     this.project_remote_id_changed(*remote_id, cx);
@@ -785,8 +746,11 @@ impl Workspace {
         cx.focus(&pane);
         cx.emit(Event::PaneAdded(pane.clone()));
 
-        let mut current_user = params.user_store.read(cx).watch_current_user().clone();
-        let mut connection_status = params.client.status().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().clone();
+        let mut connection_status = client.status().clone();
         let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
             current_user.recv().await;
             connection_status.recv().await;
@@ -826,14 +790,13 @@ impl Workspace {
             active_pane: pane.clone(),
             status_bar,
             notifications: Default::default(),
-            client: params.client.clone(),
+            client,
             remote_entity_subscription: None,
-            user_store: params.user_store.clone(),
-            fs: params.fs.clone(),
-            themes: params.themes.clone(),
+            user_store,
+            fs,
             left_sidebar,
             right_sidebar,
-            project: params.project.clone(),
+            project,
             leader_state: Default::default(),
             follower_states_by_leader: Default::default(),
             last_leaders_by_pane: Default::default(),
@@ -867,10 +830,6 @@ impl Workspace {
         &self.project
     }
 
-    pub fn themes(&self) -> Arc<ThemeRegistry> {
-        self.themes.clone()
-    }
-
     pub fn worktrees<'a>(
         &self,
         cx: &'a AppContext,
@@ -2203,8 +2162,7 @@ impl std::fmt::Debug for OpenPaths {
     }
 }
 
-fn open(action: &Open, cx: &mut MutableAppContext) {
-    let app_state = action.0.clone();
+fn open(_: &Open, cx: &mut MutableAppContext) {
     let mut paths = cx.prompt_for_paths(PathPromptOptions {
         files: true,
         directories: true,
@@ -2212,7 +2170,7 @@ fn open(action: &Open, cx: &mut MutableAppContext) {
     });
     cx.spawn(|mut cx| async move {
         if let Some(paths) = paths.recv().await.flatten() {
-            cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths, app_state }));
+            cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
         }
     })
     .detach();
@@ -2260,14 +2218,17 @@ pub fn open_paths(
                     .contains(&false);
 
             cx.add_window((app_state.build_window_options)(), |cx| {
-                let project = Project::local(
-                    app_state.client.clone(),
-                    app_state.user_store.clone(),
-                    app_state.languages.clone(),
-                    app_state.fs.clone(),
+                let mut workspace = Workspace::new(
+                    Project::local(
+                        app_state.client.clone(),
+                        app_state.user_store.clone(),
+                        app_state.languages.clone(),
+                        app_state.fs.clone(),
+                        cx,
+                    ),
                     cx,
                 );
-                let mut workspace = (app_state.build_workspace)(project, &app_state, cx);
+                (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
                 if contains_directory {
                     workspace.toggle_sidebar_item(
                         &ToggleSidebarItem {
@@ -2313,14 +2274,18 @@ pub fn join_project(
 
 fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
     let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
-        let project = Project::local(
-            app_state.client.clone(),
-            app_state.user_store.clone(),
-            app_state.languages.clone(),
-            app_state.fs.clone(),
+        let mut workspace = Workspace::new(
+            Project::local(
+                app_state.client.clone(),
+                app_state.user_store.clone(),
+                app_state.languages.clone(),
+                app_state.fs.clone(),
+                cx,
+            ),
             cx,
         );
-        (app_state.build_workspace)(project, &app_state, cx)
+        (app_state.initialize_workspace)(&mut workspace, app_state, cx);
+        workspace
     });
-    cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
+    cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew);
 }

crates/zed/src/main.rs πŸ”—

@@ -12,7 +12,7 @@ use cli::{
 use client::{
     self,
     http::{self, HttpClient},
-    ChannelList, UserStore, ZED_SECRET_CLIENT_TOKEN,
+    UserStore, ZED_SECRET_CLIENT_TOKEN,
 };
 use fs::OpenOptions;
 use futures::{
@@ -40,9 +40,9 @@ use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::{ResultExt, TryFutureExt};
 use workspace::{self, AppState, OpenNew, OpenPaths};
 use zed::{
-    self, build_window_options, build_workspace,
+    self, build_window_options,
     fs::RealFs,
-    languages, menus,
+    initialize_workspace, languages, menus,
     settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile},
 };
 
@@ -133,15 +133,12 @@ fn main() {
         let client = client::Client::new(http.clone());
         let mut languages = languages::build_language_registry(login_shell_env_loaded);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
-        let channel_list =
-            cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));
 
         auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
         project::Project::init(&client);
         client::Channel::init(&client);
         client::init(client.clone(), cx);
         command_palette::init(cx);
-        workspace::init(&client, cx);
         editor::init(cx);
         go_to_line::init(cx);
         file_finder::init(cx);
@@ -192,33 +189,33 @@ fn main() {
         let app_state = Arc::new(AppState {
             languages,
             themes,
-            channel_list,
             client: client.clone(),
             user_store,
             fs,
             build_window_options,
-            build_workspace,
+            initialize_workspace,
         });
+        workspace::init(app_state.clone(), cx);
         journal::init(app_state.clone(), cx);
-        theme_selector::init(cx);
+        theme_selector::init(app_state.clone(), cx);
         zed::init(&app_state, cx);
 
-        cx.set_menus(menus::menus(&app_state.clone()));
+        cx.set_menus(menus::menus());
 
         if stdout_is_a_pty() {
             cx.platform().activate(true);
             let paths = collect_path_args();
             if paths.is_empty() {
-                cx.dispatch_global_action(OpenNew(app_state.clone()));
+                cx.dispatch_global_action(OpenNew);
             } else {
-                cx.dispatch_global_action(OpenPaths { paths, app_state });
+                cx.dispatch_global_action(OpenPaths { paths });
             }
         } else {
             if let Ok(Some(connection)) = cli_connections_rx.try_next() {
                 cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
                     .detach();
             } else {
-                cx.dispatch_global_action(OpenNew(app_state.clone()));
+                cx.dispatch_global_action(OpenNew);
             }
             cx.spawn(|cx| async move {
                 while let Some(connection) = cli_connections_rx.next().await {

crates/zed/src/menus.rs πŸ”—

@@ -1,33 +1,27 @@
-use crate::AppState;
 use gpui::{Menu, MenuItem};
-use std::sync::Arc;
 
 #[cfg(target_os = "macos")]
-pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
+pub fn menus() -> Vec<Menu<'static>> {
     vec![
         Menu {
             name: "Zed",
             items: vec![
                 MenuItem::Action {
                     name: "About Zed…",
-                    keystroke: None,
                     action: Box::new(super::About),
                 },
                 MenuItem::Action {
                     name: "Check for Updates",
-                    keystroke: None,
                     action: Box::new(auto_update::Check),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Install CLI",
-                    keystroke: None,
                     action: Box::new(super::InstallCommandLineInterface),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Quit",
-                    keystroke: Some("cmd-q"),
                     action: Box::new(super::Quit),
                 },
             ],
@@ -37,14 +31,20 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
             items: vec![
                 MenuItem::Action {
                     name: "New",
-                    keystroke: Some("cmd-n"),
-                    action: Box::new(workspace::OpenNew(state.clone())),
+                    action: Box::new(workspace::OpenNew),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Open…",
-                    keystroke: Some("cmd-o"),
-                    action: Box::new(workspace::Open(state.clone())),
+                    action: Box::new(workspace::Open),
+                },
+                MenuItem::Action {
+                    name: "Save",
+                    action: Box::new(workspace::Save),
+                },
+                MenuItem::Action {
+                    name: "Close Editor",
+                    action: Box::new(workspace::CloseActiveItem),
                 },
             ],
         },
@@ -53,30 +53,160 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
             items: vec![
                 MenuItem::Action {
                     name: "Undo",
-                    keystroke: Some("cmd-z"),
                     action: Box::new(editor::Undo),
                 },
                 MenuItem::Action {
                     name: "Redo",
-                    keystroke: Some("cmd-Z"),
                     action: Box::new(editor::Redo),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Cut",
-                    keystroke: Some("cmd-x"),
                     action: Box::new(editor::Cut),
                 },
                 MenuItem::Action {
                     name: "Copy",
-                    keystroke: Some("cmd-c"),
                     action: Box::new(editor::Copy),
                 },
                 MenuItem::Action {
                     name: "Paste",
-                    keystroke: Some("cmd-v"),
                     action: Box::new(editor::Paste),
                 },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Find",
+                    action: Box::new(search::buffer_search::Deploy { focus: true }),
+                },
+                MenuItem::Action {
+                    name: "Find In Project",
+                    action: Box::new(search::project_search::Deploy),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Toggle Line Comment",
+                    action: Box::new(editor::ToggleComments),
+                },
+            ],
+        },
+        Menu {
+            name: "Selection",
+            items: vec![
+                MenuItem::Action {
+                    name: "Select All",
+                    action: Box::new(editor::SelectAll),
+                },
+                MenuItem::Action {
+                    name: "Expand Selection",
+                    action: Box::new(editor::SelectLargerSyntaxNode),
+                },
+                MenuItem::Action {
+                    name: "Shrink Selection",
+                    action: Box::new(editor::SelectSmallerSyntaxNode),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Add Cursor Above",
+                    action: Box::new(editor::AddSelectionAbove),
+                },
+                MenuItem::Action {
+                    name: "Add Cursor Below",
+                    action: Box::new(editor::AddSelectionBelow),
+                },
+                MenuItem::Action {
+                    name: "Select Next Occurrence",
+                    action: Box::new(editor::SelectNext {
+                        replace_newest: false,
+                    }),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Move Line Up",
+                    action: Box::new(editor::MoveLineUp),
+                },
+                MenuItem::Action {
+                    name: "Move Line Down",
+                    action: Box::new(editor::MoveLineDown),
+                },
+                MenuItem::Action {
+                    name: "Duplicate Selection",
+                    action: Box::new(editor::DuplicateLine),
+                },
+            ],
+        },
+        Menu {
+            name: "View",
+            items: vec![
+                MenuItem::Action {
+                    name: "Zoom In",
+                    action: Box::new(super::IncreaseBufferFontSize),
+                },
+                MenuItem::Action {
+                    name: "Zoom Out",
+                    action: Box::new(super::DecreaseBufferFontSize),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Project Browser",
+                    action: Box::new(workspace::sidebar::ToggleSidebarItemFocus {
+                        side: workspace::sidebar::Side::Left,
+                        item_index: 0,
+                    }),
+                },
+                MenuItem::Action {
+                    name: "Command Palette",
+                    action: Box::new(command_palette::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Diagnostics",
+                    action: Box::new(diagnostics::Deploy),
+                },
+            ],
+        },
+        Menu {
+            name: "Go",
+            items: vec![
+                MenuItem::Action {
+                    name: "Back",
+                    action: Box::new(workspace::GoBack { pane: None }),
+                },
+                MenuItem::Action {
+                    name: "Forward",
+                    action: Box::new(workspace::GoForward { pane: None }),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Go to File",
+                    action: Box::new(file_finder::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Go to Symbol in Project",
+                    action: Box::new(project_symbols::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Go to Symbol in Editor",
+                    action: Box::new(outline::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Go to Definition",
+                    action: Box::new(editor::GoToDefinition),
+                },
+                MenuItem::Action {
+                    name: "Go to References",
+                    action: Box::new(editor::FindAllReferences),
+                },
+                MenuItem::Action {
+                    name: "Go to Line/Column",
+                    action: Box::new(go_to_line::Toggle),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Next Problem",
+                    action: Box::new(editor::GoToNextDiagnostic),
+                },
+                MenuItem::Action {
+                    name: "Previous Problem",
+                    action: Box::new(editor::GoToPrevDiagnostic),
+                },
             ],
         },
     ]

crates/zed/src/test.rs πŸ”—

@@ -1,13 +1,3 @@
-use crate::{build_window_options, build_workspace, AppState};
-use assets::Assets;
-use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
-use gpui::MutableAppContext;
-use language::LanguageRegistry;
-use project::fs::FakeFs;
-use settings::Settings;
-use std::sync::Arc;
-use theme::ThemeRegistry;
-
 #[cfg(test)]
 #[ctor::ctor]
 fn init_logger() {
@@ -15,32 +5,3 @@ fn init_logger() {
         env_logger::init();
     }
 }
-
-pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
-    let settings = Settings::test(cx);
-    editor::init(cx);
-    cx.set_global(settings);
-    let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
-    let http = FakeHttpClient::with_404_response();
-    let client = Client::new(http.clone());
-    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-    let languages = LanguageRegistry::test();
-    languages.add(Arc::new(language::Language::new(
-        language::LanguageConfig {
-            name: "Rust".into(),
-            path_suffixes: vec!["rs".to_string()],
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    )));
-    Arc::new(AppState {
-        themes,
-        languages: Arc::new(languages),
-        channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
-        client,
-        user_store,
-        fs: FakeFs::new(cx.background().clone()),
-        build_window_options,
-        build_workspace,
-    })
-}

crates/zed/src/zed.rs πŸ”—

@@ -15,7 +15,7 @@ use gpui::{
     actions,
     geometry::vector::vec2f,
     platform::{WindowBounds, WindowOptions},
-    AsyncAppContext, ModelHandle, ViewContext,
+    AsyncAppContext, ViewContext,
 };
 use lazy_static::lazy_static;
 pub use lsp;
@@ -31,7 +31,7 @@ use std::{
 };
 use util::ResultExt;
 pub use workspace;
-use workspace::{AppState, Workspace, WorkspaceParams};
+use workspace::{AppState, Workspace};
 
 actions!(
     zed,
@@ -115,13 +115,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     settings::KeymapFileContent::load_defaults(cx);
 }
 
-pub fn build_workspace(
-    project: ModelHandle<Project>,
+pub fn initialize_workspace(
+    workspace: &mut Workspace,
     app_state: &Arc<AppState>,
     cx: &mut ViewContext<Workspace>,
-) -> Workspace {
+) {
     cx.subscribe(&cx.handle(), {
-        let project = project.clone();
+        let project = workspace.project().clone();
         move |_, _, event, cx| {
             if let workspace::Event::PaneAdded(pane) = event {
                 pane.update(cx, |pane, cx| {
@@ -139,22 +139,12 @@ pub fn build_workspace(
     })
     .detach();
 
-    let workspace_params = WorkspaceParams {
-        project,
-        client: app_state.client.clone(),
-        fs: app_state.fs.clone(),
-        languages: app_state.languages.clone(),
-        themes: app_state.themes.clone(),
-        user_store: app_state.user_store.clone(),
-        channel_list: app_state.channel_list.clone(),
-    };
-    let workspace = Workspace::new(&workspace_params, cx);
-    let project = workspace.project().clone();
+    cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
 
     let theme_names = app_state.themes.list().collect();
     let language_names = app_state.languages.language_names();
 
-    project.update(cx, |project, cx| {
+    workspace.project().update(cx, |project, cx| {
         let action_names = cx.all_action_names().collect::<Vec<_>>();
         project.set_language_server_settings(serde_json::json!({
             "json": {
@@ -172,9 +162,10 @@ pub fn build_workspace(
         }));
     });
 
-    let project_panel = ProjectPanel::new(project, cx);
-    let contact_panel =
-        cx.add_view(|cx| ContactsPanel::new(app_state.clone(), workspace.weak_handle(), cx));
+    let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
+    let contact_panel = cx.add_view(|cx| {
+        ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx)
+    });
 
     workspace.left_sidebar().update(cx, |sidebar, cx| {
         sidebar.add_item("icons/folder-tree-solid-14.svg", project_panel.into(), cx)
@@ -196,8 +187,6 @@ pub fn build_workspace(
         status_bar.add_right_item(cursor_position, cx);
         status_bar.add_right_item(auto_update, cx);
     });
-
-    workspace
 }
 
 pub fn build_window_options() -> WindowOptions<'static> {
@@ -287,14 +276,18 @@ fn open_config_file(
                     workspace.open_paths(vec![path.to_path_buf()], cx)
                 } else {
                     let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
-                        let project = Project::local(
-                            app_state.client.clone(),
-                            app_state.user_store.clone(),
-                            app_state.languages.clone(),
-                            app_state.fs.clone(),
+                        let mut workspace = Workspace::new(
+                            Project::local(
+                                app_state.client.clone(),
+                                app_state.user_store.clone(),
+                                app_state.languages.clone(),
+                                app_state.fs.clone(),
+                                cx,
+                            ),
                             cx,
                         );
-                        (app_state.build_workspace)(project, &app_state, cx)
+                        (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
+                        workspace
                     });
                     workspace.update(cx, |workspace, cx| {
                         workspace.open_paths(vec![path.to_path_buf()], cx)
@@ -313,43 +306,45 @@ mod tests {
     use assets::Assets;
     use editor::{Autoscroll, DisplayPoint, Editor};
     use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
-    use project::{Fs, ProjectPath};
+    use project::ProjectPath;
     use serde_json::json;
     use std::{
         collections::HashSet,
         path::{Path, PathBuf},
     };
-    use test::test_app_state;
     use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
-    use util::test::temp_tree;
     use workspace::{
         open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
     async fn test_open_paths_action(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
-        let dir = temp_tree(json!({
-            "a": {
-                "aa": null,
-                "ab": null,
-            },
-            "b": {
-                "ba": null,
-                "bb": null,
-            },
-            "c": {
-                "ca": null,
-                "cb": null,
-            },
-        }));
+        let app_state = init(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "a": {
+                        "aa": null,
+                        "ab": null,
+                    },
+                    "b": {
+                        "ba": null,
+                        "bb": null,
+                    },
+                    "c": {
+                        "ca": null,
+                        "cb": null,
+                    },
+                }),
+            )
+            .await;
 
         cx.update(|cx| {
             open_paths(
-                &[
-                    dir.path().join("a").to_path_buf(),
-                    dir.path().join("b").to_path_buf(),
-                ],
+                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
                 &app_state,
                 cx,
             )
@@ -357,7 +352,7 @@ mod tests {
         .await;
         assert_eq!(cx.window_ids().len(), 1);
 
-        cx.update(|cx| open_paths(&[dir.path().join("a").to_path_buf()], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
             .await;
         assert_eq!(cx.window_ids().len(), 1);
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
@@ -369,10 +364,7 @@ mod tests {
 
         cx.update(|cx| {
             open_paths(
-                &[
-                    dir.path().join("b").to_path_buf(),
-                    dir.path().join("c").to_path_buf(),
-                ],
+                &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
                 &app_state,
                 cx,
             )
@@ -383,11 +375,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
-        cx.update(|cx| {
-            workspace::init(&app_state.client, cx);
-        });
-        cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
+        let app_state = init(cx);
+        cx.dispatch_global_action(workspace::OpenNew);
         let window_id = *cx.window_ids().first().unwrap();
         let workspace = cx.root_view::<Workspace>(window_id).unwrap();
         let editor = workspace.update(cx, |workspace, cx| {
@@ -414,7 +403,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_entry(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
         app_state
             .fs
             .as_fake()
@@ -429,18 +418,10 @@ mod tests {
                 }),
             )
             .await;
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
 
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
         let file2 = entries[1].clone();
@@ -535,7 +516,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_paths(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
+
         let fs = app_state.fs.as_fake();
         fs.insert_dir("/dir1").await;
         fs.insert_dir("/dir2").await;
@@ -544,17 +526,8 @@ mod tests {
         fs.insert_file("/dir2/b.txt", "".into()).await;
         fs.insert_file("/dir3/c.txt", "".into()).await;
 
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/dir1", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -655,19 +628,15 @@ mod tests {
 
     #[gpui::test]
     async fn test_save_conflicting_item(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
-        let fs = app_state.fs.as_fake();
-        fs.insert_tree("/root", json!({ "a.txt": "" })).await;
-
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
+        let app_state = init(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree("/root", json!({ "a.txt": "" }))
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -687,7 +656,11 @@ mod tests {
                 editor.handle_input(&editor::Input("x".into()), cx)
             })
         });
-        fs.insert_file("/root/a.txt", "changed".to_string()).await;
+        app_state
+            .fs
+            .as_fake()
+            .insert_file("/root/a.txt", "changed".to_string())
+            .await;
         editor
             .condition(&cx, |editor, cx| editor.has_conflict(cx))
             .await;
@@ -704,21 +677,16 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
         app_state.fs.as_fake().insert_dir("/root").await;
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(rust_lang()));
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew);
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -773,18 +741,11 @@ mod tests {
 
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
-        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew);
         workspace
             .update(cx, |workspace, cx| {
                 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
-                workspace.open_path(
-                    ProjectPath {
-                        worktree_id: worktree.read(cx).id(),
-                        path: Path::new("the-new-name.rs").into(),
-                    },
-                    true,
-                    cx,
-                )
+                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
             })
             .await
             .unwrap();
@@ -805,13 +766,15 @@ mod tests {
 
     #[gpui::test]
     async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
         app_state.fs.as_fake().insert_dir("/root").await;
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        project.update(cx, |project, _| project.languages().add(rust_lang()));
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew);
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -842,10 +805,9 @@ mod tests {
 
     #[gpui::test]
     async fn test_pane_actions(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
+        init(cx);
 
-        cx.update(|cx| pane::init(cx));
-        let app_state = cx.update(test_app_state);
+        let app_state = cx.update(AppState::test);
         app_state
             .fs
             .as_fake()
@@ -861,17 +823,9 @@ mod tests {
             )
             .await;
 
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
 
@@ -926,7 +880,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_navigation(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
         app_state
             .fs
             .as_fake()
@@ -941,17 +895,10 @@ mod tests {
                 }),
             )
             .await;
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
         let file2 = entries[1].clone();
@@ -990,7 +937,7 @@ mod tests {
                 editor.newline(&Default::default(), cx);
                 editor.move_down(&Default::default(), cx);
                 editor.move_down(&Default::default(), cx);
-                editor.save(params.project.clone(), cx)
+                editor.save(project.clone(), cx)
             })
             .await
             .unwrap();
@@ -1104,7 +1051,6 @@ mod tests {
             .unwrap();
         app_state
             .fs
-            .as_fake()
             .remove_file(Path::new("/root/a/file2"), Default::default())
             .await
             .unwrap();
@@ -1219,4 +1165,29 @@ mod tests {
         }
         assert!(has_default_theme);
     }
+
+    fn init(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            let mut app_state = AppState::test(cx);
+            let state = Arc::get_mut(&mut app_state).unwrap();
+            state.initialize_workspace = initialize_workspace;
+            state.build_window_options = build_window_options;
+            workspace::init(app_state.clone(), cx);
+            editor::init(cx);
+            pane::init(cx);
+            app_state
+        })
+    }
+
+    fn rust_lang() -> Arc<language::Language> {
+        Arc::new(language::Language::new(
+            language::LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        ))
+    }
 }