diff --git a/Cargo.lock b/Cargo.lock index 5957cc406f..454db4c7b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,6 +907,7 @@ dependencies = [ "fuzzy", "gpui", "picker", + "project", "serde_json", "settings", "theme", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 218ddc2dfe..0f2589e31d 100644 --- a/assets/keymaps/default.json +++ b/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" } }, { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index f33441a2b8..aa7af0740d 100644 --- a/crates/collab/src/rpc.rs +++ b/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(¶ms, 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(¶ms, 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 { }) }); - Channel::init(&client); - Project::init(&client); - cx.update(|cx| { - workspace::init(&client, cx); + 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!(), }); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + Channel::init(&client); + Project::init(&client); + cx.update(|cx| workspace::init(app_state.clone(), cx)); + client .authenticate_and_connect(false, &cx.to_async()) .await @@ -6846,23 +6842,7 @@ mod tests { cx: &mut TestAppContext, ) -> ViewHandle { 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( diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index aeaffa3e6f..2a4d27570f 100644 --- a/crates/command_palette/Cargo.toml +++ b/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" diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index f724cc19a6..9f0f396d85 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/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(¶ms.client, cx); + workspace::init(app_state.clone(), cx); init(cx); }); - let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, 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); diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 4f484f0cd6..51cf95f5ec 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/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, collapsed_sections: Vec
, selection: Option, - app_state: Arc, _maintain_contacts: Subscription, } @@ -92,7 +91,7 @@ pub fn init(cx: &mut MutableAppContext) { impl ContactsPanel { pub fn new( - app_state: Arc, + user_store: ModelHandle, workspace: WeakViewHandle, cx: &mut ViewContext, ) -> 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, current_user_id: Option, project_index: usize, - app_state: Arc, 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::().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::().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, ) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 8f2aa6cea2..66d101ac33 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/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(¶ms, 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| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c9e4732f9f..31dc6df357 100644 --- a/crates/editor/src/editor.rs +++ b/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)) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index e85147d7e2..f58c733cc7 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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)); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 826592daa0..a7ff52e19e 100644 --- a/crates/gpui/src/app.rs +++ b/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, }, 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) { - 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 diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 526a9aea40..3f384b5ea5 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/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) { diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index c42fbff907..bd156ed661 100644 --- a/crates/gpui/src/keymap.rs +++ b/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, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index d851e195be..c4b68c0741 100644 --- a/crates/gpui/src/platform.rs +++ b/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 ()>); fn on_menu_command(&self, callback: Box); - fn set_menus(&self, menus: Vec); + fn on_validate_menu_command(&self, callback: Box bool>); + fn on_will_open_menu(&self, callback: Box); + fn set_menus(&self, menus: Vec, matcher: &keymap::Matcher); fn prompt_for_paths( &self, options: PathPromptOptions, diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 651805370c..9d07177b16 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/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 { + 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) -> Option { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index fc1b7e59d8..26cde46c04 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/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>, event: Option bool>>, menu_command: Option>, + validate_menu_command: Option bool>>, + will_open_menu: Option>, open_urls: Option)>>, finish_launching: Option ()>>, menu_actions: Vec>, } impl MacForegroundPlatform { - unsafe fn create_menu_bar(&self, menus: Vec) -> id { + unsafe fn create_menu_bar( + &self, + menus: Vec, + 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) { + fn on_will_open_menu(&self, callback: Box) { + self.0.borrow_mut().will_open_menu = Some(callback); + } + + fn on_validate_menu_command(&self, callback: Box bool>) { + self.0.borrow_mut().validate_menu_command = Some(callback); + } + + fn set_menus(&self, menus: Vec, 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() } diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 8786eff255..30ceec335e 100644 --- a/crates/gpui/src/platform/test.rs +++ b/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) {} - - fn set_menus(&self, _: Vec) {} + fn on_validate_menu_command(&self, _: Box bool>) {} + fn on_will_open_menu(&self, _: Box) {} + fn set_menus(&self, _: Vec, _: &keymap::Matcher) {} fn prompt_for_paths( &self, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e6eb2dcf77..abcd667293 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -497,7 +497,7 @@ impl Project { #[cfg(any(test, feature = "test-support"))] pub async fn test( fs: Arc, - root_paths: impl IntoIterator>, + root_paths: impl IntoIterator, cx: &mut gpui::TestAppContext, ) -> ModelHandle { let languages = Arc::new(LanguageRegistry::test()); @@ -528,6 +528,14 @@ impl Project { &self.languages } + pub fn client(&self) -> Arc { + self.client.clone() + } + + pub fn user_store(&self) -> ModelHandle { + 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 diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 639d7b44d9..443b165a36 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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(¶ms, 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(¶ms, 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); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index f30370a0c0..20da49b1df 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/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 diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 636c055f1c..97c2d3201e 100644 --- a/crates/search/src/project_search.rs +++ b/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) diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index aaac2f34ba..9f445c633a 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/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, @@ -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, cx: &mut MutableAppContext) { Picker::::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) { - let themes = workspace.themes(); + fn toggle( + workspace: &mut Workspace, + themes: Arc, + cx: &mut ViewContext, + ) { workspace.toggle_modal(cx, |_, cx| { let this = cx.add_view(|cx| Self::new(themes, cx)); cx.subscribe(&this, Self::on_event).detach(); diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 4122c46059..f9080e554c 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/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(¶ms, 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) }) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e963ca7e02..97bb8a2bc0 100644 --- a/crates/workspace/src/pane.rs +++ b/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(¶ms, 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(¶ms, 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; diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs index fef7bf2e43..3720d9ec43 100644 --- a/crates/workspace/src/waiting_room.rs +++ b/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) - { - 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 + 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, ) - ) - } - 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(); + { + 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 + ) + ) + } + 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, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b6cb95292a..d12c8a2eea 100644 --- a/crates/workspace/src/workspace.rs +++ b/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); - -#[derive(Clone)] -pub struct OpenNew(pub Arc); - #[derive(Clone)] pub struct OpenPaths { pub paths: Vec, - pub app_state: Arc, } #[derive(Clone)] @@ -102,31 +96,37 @@ pub struct ToggleFollow(pub PeerId); pub struct JoinProject { pub contact: Arc, pub project_index: usize, - pub app_state: Arc, } -impl_internal_actions!( - workspace, - [Open, OpenNew, OpenPaths, ToggleFollow, JoinProject] -); +impl_internal_actions!(workspace, [OpenPaths, ToggleFollow, JoinProject]); -pub fn init(client: &Arc, cx: &mut MutableAppContext) { +pub fn init(app_state: Arc, 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, 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, pub user_store: ModelHandle, pub fs: Arc, - pub channel_list: ModelHandle, pub build_window_options: fn() -> WindowOptions<'static>, - pub build_workspace: - fn(ModelHandle, &Arc, &mut ViewContext) -> Workspace, + pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), } pub trait Item: View { @@ -636,20 +635,9 @@ impl Into for &dyn NotificationHandle { } } -#[derive(Clone)] -pub struct WorkspaceParams { - pub project: ModelHandle, - pub client: Arc, - pub fs: Arc, - pub languages: Arc, - pub themes: Arc, - pub user_store: ModelHandle, - pub channel_list: ModelHandle, -} - -impl WorkspaceParams { +impl AppState { #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &mut MutableAppContext) -> Self { + pub fn test(cx: &mut MutableAppContext) -> Arc { 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, 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, remote_entity_subscription: Option, fs: Arc, - themes: Arc, modal: Option, center: PaneGroup, left_sidebar: ViewHandle, @@ -744,8 +705,8 @@ enum FollowerItem { } impl Workspace { - pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { - cx.observe(¶ms.project, |_, project, cx| { + pub fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { + cx.observe(&project, |_, project, cx| { if project.read(cx).is_read_only() { cx.blur(); } @@ -753,7 +714,7 @@ impl Workspace { }) .detach(); - cx.subscribe(¶ms.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 { - 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, 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); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 54691f2254..8231d2ac3f 100644 --- a/crates/zed/src/main.rs +++ b/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 { diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 3f19dcbdac..c5ea7b7391 100644 --- a/crates/zed/src/menus.rs +++ b/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) -> Vec> { +pub fn menus() -> Vec> { 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) -> Vec> { 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) -> Vec> { 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), + }, ], }, ] diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index d4f24297e4..67622db83f 100644 --- a/crates/zed/src/test.rs +++ b/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 { - 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, - }) -} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bfb75d8874..4f0f8e21cc 100644 --- a/crates/zed/src/zed.rs +++ b/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, cx: &mut gpui::MutableAppContext) { settings::KeymapFileContent::load_defaults(cx); } -pub fn build_workspace( - project: ModelHandle, +pub fn initialize_workspace( + workspace: &mut Workspace, app_state: &Arc, cx: &mut ViewContext, -) -> 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::>(); 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::(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::(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(¶ms, 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(¶ms, 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 app_state = init(cx); + app_state + .fs + .as_fake() + .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(¶ms, 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; + 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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 { + 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 { + Arc::new(language::Language::new( + language::LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )) + } }