diff --git a/Cargo.lock b/Cargo.lock index 6b2e66bff3b6c4306d80fbb0d5bc933323a764e7..1d1712b6f288945a9006a918e9b27052e4ff3fae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4942,6 +4942,7 @@ dependencies = [ "serde_json", "settings", "smol", + "theme", "ui", "util", "workspace", @@ -8481,7 +8482,6 @@ dependencies = [ "fuzzy", "gpui", "language", - "platform_title_bar", "project", "serde_json", "serde_json_lenient", @@ -12371,6 +12371,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" name = "platform_title_bar" version = "0.1.0" dependencies = [ + "feature_flags", "gpui", "settings", "smallvec", @@ -15361,6 +15362,30 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sidebar" +version = "0.1.0" +dependencies = [ + "acp_thread", + "agent_ui", + "db", + "editor", + "feature_flags", + "fs", + "fuzzy", + "gpui", + "picker", + "project", + "recent_projects", + "serde_json", + "settings", + "theme", + "ui", + "ui_input", + "util", + "workspace", +] + [[package]] name = "signal-hook" version = "0.3.18" @@ -17240,6 +17265,7 @@ dependencies = [ "cloud_api_types", "collections", "db", + "feature_flags", "git_ui", "gpui", "http_client", @@ -21127,6 +21153,7 @@ dependencies = [ "settings_profile_selector", "settings_ui", "shellexpand 2.1.2", + "sidebar", "smol", "snippet_provider", "snippets_ui", diff --git a/Cargo.toml b/Cargo.toml index b5397ddf73470a28edfe8ec7867701345ee4449d..ecb469f6d83780db7192a2ac100f4d6993aaba4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ members = [ "crates/schema_generator", "crates/search", "crates/session", + "crates/sidebar", "crates/settings", "crates/settings_content", "crates/settings_json", @@ -396,6 +397,7 @@ rules_library = { path = "crates/rules_library" } scheduler = { path = "crates/scheduler" } search = { path = "crates/search" } session = { path = "crates/session" } +sidebar = { path = "crates/sidebar" } settings = { path = "crates/settings" } settings_content = { path = "crates/settings_content" } settings_json = { path = "crates/settings_json" } @@ -855,6 +857,7 @@ refineable = { codegen-units = 1 } release_channel = { codegen-units = 1 } reqwest_client = { codegen-units = 1 } session = { codegen-units = 1 } +sidebar = { codegen-units = 1 } snippet = { codegen-units = 1 } snippets_ui = { codegen-units = 1 } story = { codegen-units = 1 } diff --git a/assets/icons/workspace_nav_closed.svg b/assets/icons/workspace_nav_closed.svg new file mode 100644 index 0000000000000000000000000000000000000000..ed1fce52d6826a4d10299f331358ff84e4caa973 --- /dev/null +++ b/assets/icons/workspace_nav_closed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/workspace_nav_open.svg b/assets/icons/workspace_nav_open.svg new file mode 100644 index 0000000000000000000000000000000000000000..464b6aac73c2aeaa9463a805aabc4559377bbfd3 --- /dev/null +++ b/assets/icons/workspace_nav_open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 4462ac4429a9f24db7da981f4fc9b44c37605302..bf38edfeb85e07280d7ae817ad56067337c0f149 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -603,6 +603,8 @@ "ctrl-alt-b": "workspace::ToggleRightDock", "ctrl-b": "workspace::ToggleLeftDock", "ctrl-j": "workspace::ToggleBottomDock", + "ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar", + "ctrl-alt-;": "multi_workspace::FocusWorkspaceSidebar", "ctrl-alt-y": "workspace::ToggleAllDocks", "ctrl-alt-0": "workspace::ResetActiveDockSize", // For 0px parameter, uses UI font size value. @@ -662,6 +664,13 @@ "ctrl-w": "workspace::CloseActiveDock", }, }, + { + "context": "WorkspaceSidebar", + "use_key_equivalents": true, + "bindings": { + "ctrl-n": "multi_workspace::NewWorkspaceInWindow", + }, + }, { "context": "Workspace && debugger_running", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 8ca82963523a65cfd483935edd33e1cb00f5cc55..bca1e42d9ceaf96a6da3f6ceaca77ef21cc40ef3 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -664,6 +664,8 @@ "cmd-alt-b": "workspace::ToggleRightDock", "cmd-r": "workspace::ToggleRightDock", "cmd-j": "workspace::ToggleBottomDock", + "cmd-alt-j": "multi_workspace::ToggleWorkspaceSidebar", + "cmd-alt-;": "multi_workspace::FocusWorkspaceSidebar", "alt-cmd-y": "workspace::ToggleAllDocks", // For 0px parameter, uses UI font size value. "ctrl-alt-0": "workspace::ResetActiveDockSize", @@ -723,6 +725,13 @@ // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], }, }, + { + "context": "WorkspaceSidebar", + "use_key_equivalents": true, + "bindings": { + "cmd-n": "multi_workspace::NewWorkspaceInWindow", + }, + }, { "context": "Workspace && debugger_running", "use_key_equivalents": true, diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 41d4a976b0773ab3ae7b4cdb0b7ce271cf303432..0f117a75688e441de7b4dc98c80bf63a05238c7c 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -598,6 +598,8 @@ "ctrl-alt-b": "workspace::ToggleRightDock", "ctrl-b": "workspace::ToggleLeftDock", "ctrl-j": "workspace::ToggleBottomDock", + "ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar", + "ctrl-alt-;": "multi_workspace::FocusWorkspaceSidebar", "ctrl-shift-y": "workspace::ToggleAllDocks", "alt-r": "workspace::ResetActiveDockSize", // For 0px parameter, uses UI font size value. @@ -666,6 +668,13 @@ "f5": "debugger::Continue", }, }, + { + "context": "WorkspaceSidebar", + "use_key_equivalents": true, + "bindings": { + "ctrl-n": "multi_workspace::NewWorkspaceInWindow", + }, + }, { "context": "ApplicationMenu", "use_key_equivalents": true, diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index 904c9a6c7b7e383d09b54f58115be2303ef8754a..f76e64b557e7ee2ec6054bd0fab0afc36b201e2c 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -5,7 +5,7 @@ mod mode_selector; mod model_selector; mod model_selector_popover; mod thread_history; -mod thread_view; +pub(crate) mod thread_view; pub use mode_selector::ModeSelector; pub use model_selector::AcpModelSelector; diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 7db45461d0db7ec994b7a63810d25f79c2f98560..353e1168c8a685bd1822ebe83e7ea2d52733a728 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -419,7 +419,7 @@ mod tests { use serde_json::json; use settings::SettingsStore; use util::path; - use workspace::Workspace; + use workspace::MultiWorkspace; #[gpui::test] async fn test_diff_sync(cx: &mut TestAppContext) { @@ -434,8 +434,9 @@ mod tests { .await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tool_call = acp::ToolCall::new("tool", "Tool call") .status(acp::ToolCallStatus::InProgress) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 7c9966295483d5c0b0b5586b7d020c98db50f25f..af636dfa74949fb4e8095a553607ae6741102294 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -815,8 +815,13 @@ impl MessageEditor { } if self.prompt_capabilities.borrow().image - && let Some(task) = - paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) + && let Some(task) = paste_images_as_context( + self.editor.clone(), + self.mention_set.clone(), + self.workspace.clone(), + window, + cx, + ) { task.detach(); return; @@ -1084,6 +1089,7 @@ impl MessageEditor { let editor = self.editor.clone(); let mention_set = self.mention_set.clone(); + let workspace = self.workspace.clone(); let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions { files: true, @@ -1134,7 +1140,14 @@ impl MessageEditor { images.push(gpui::Image::from_bytes(format, content)); } - crate::mention_set::insert_images_as_context(images, editor, mention_set, cx).await; + crate::mention_set::insert_images_as_context( + images, + editor, + mention_set, + workspace, + cx, + ) + .await; Ok(()) }) .detach_and_log_err(cx); @@ -1450,7 +1463,7 @@ mod tests { use text::Point; use ui::{App, Context, IntoElement, Render, SharedString, Window}; use util::{path, paths::PathStyle, rel_path::rel_path}; - use workspace::{AppState, Item, Workspace}; + use workspace::{AppState, Item, MultiWorkspace}; use crate::acp::{ message_editor::{Mention, MessageEditor, parse_mention_links}, @@ -1558,8 +1571,9 @@ mod tests { fs.insert_tree("/project", json!({"file": ""})).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = None; let history = cx @@ -1673,8 +1687,9 @@ mod tests { // Start with no available commands - simulating Claude which doesn't support slash commands let available_commands = Rc::new(RefCell::new(vec![])); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let history = cx .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx))); let workspace_handle = workspace.downgrade(); @@ -1822,10 +1837,13 @@ mod tests { }); let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); - let mut cx = VisualTestContext::from_window(*window, cx); + let mut cx = VisualTestContext::from_window(window.into(), cx); let thread_store = None; let history = cx @@ -2014,8 +2032,11 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let worktree = project.update(cx, |project, cx| { let mut worktrees = project.worktrees(cx).collect::>(); @@ -2024,7 +2045,7 @@ mod tests { }); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - let mut cx = VisualTestContext::from_window(*window, cx); + let mut cx = VisualTestContext::from_window(window.into(), cx); let paths = vec![ rel_path("a/one.txt"), @@ -2551,8 +2572,9 @@ mod tests { let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let history = cx @@ -2651,8 +2673,9 @@ mod tests { fs.insert_tree("/project", json!({"file": ""})).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let history = cx @@ -2732,8 +2755,9 @@ mod tests { fs.insert_tree("/project", json!({"file": ""})).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = None; let history = cx @@ -2791,8 +2815,9 @@ mod tests { fs.insert_tree("/project", json!({"file": ""})).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = None; let history = cx @@ -2845,8 +2870,9 @@ mod tests { fs.insert_tree("/project", json!({"file": ""})).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let history = cx @@ -2900,8 +2926,9 @@ mod tests { .await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let history = cx @@ -2964,8 +2991,9 @@ mod tests { let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let history = cx @@ -3085,8 +3113,11 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let worktree = project.update(cx, |project, cx| { let mut worktrees = project.worktrees(cx).collect::>(); @@ -3095,7 +3126,7 @@ mod tests { }); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - let mut cx = VisualTestContext::from_window(*window, cx); + let mut cx = VisualTestContext::from_window(window.into(), cx); // Open a regular editor with the created file, and select a portion of // the text that will be used for the selections that are meant to be @@ -3237,10 +3268,13 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); - let mut cx = VisualTestContext::from_window(*window, cx); + let mut cx = VisualTestContext::from_window(window.into(), cx); let thread_store = cx.new(|cx| ThreadStore::new(cx)); let history = cx diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 94fbff72f780ab5f4a1fa00d53a1b068c8505247..dd15ab75113835bc345c8c071382c22fa8d88ba4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -57,7 +57,9 @@ use ui::{ }; use util::defer; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; -use workspace::{CollaboratorId, NewTerminal, Toast, Workspace, notifications::NotificationId}; +use workspace::{ + CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId, +}; use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::assistant::OpenRulesLibrary; @@ -2161,9 +2163,30 @@ impl AcpServerView { self.show_notification(caption, icon, window, cx); } + fn agent_is_visible(&self, window: &Window, cx: &App) -> bool { + if window.is_window_active() { + let workspace_is_foreground = window + .root::() + .flatten() + .and_then(|mw| { + let mw = mw.read(cx); + self.workspace.upgrade().map(|ws| mw.workspace() == &ws) + }) + .unwrap_or(true); + + if workspace_is_foreground { + if let Some(workspace) = self.workspace.upgrade() { + return AgentPanel::is_visible(&workspace, cx); + } + } + } + + false + } + fn play_notification_sound(&self, window: &Window, cx: &mut App) { let settings = AgentSettings::get_global(cx); - if settings.play_sound_when_agent_done && !window.is_window_active() { + if settings.play_sound_when_agent_done && !self.agent_is_visible(window, cx) { Audio::play_sound(Sound::AgentDone, cx); } } @@ -2181,14 +2204,7 @@ impl AcpServerView { let settings = AgentSettings::get_global(cx); - let window_is_inactive = !window.is_window_active(); - let panel_is_hidden = self - .workspace - .upgrade() - .map(|workspace| AgentPanel::is_hidden(&workspace, cx)) - .unwrap_or(true); - - let should_notify = window_is_inactive || panel_is_hidden; + let should_notify = !self.agent_is_visible(window, cx); if !should_notify { return; @@ -2251,19 +2267,22 @@ impl AcpServerView { .push(cx.subscribe_in(&pop_up, window, { |this, _, event, window, cx| match event { AgentNotificationEvent::Accepted => { - let handle = window.window_handle(); + let Some(handle) = window.window_handle().downcast::() + else { + log::error!("root view should be a MultiWorkspace"); + return; + }; cx.activate(true); let workspace_handle = this.workspace.clone(); - // If there are multiple Zed windows, activate the correct one. cx.defer(move |cx| { handle - .update(cx, |_view, window, _cx| { + .update(cx, |multi_workspace, window, cx| { window.activate_window(); - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(_cx, |workspace, cx| { + multi_workspace.activate(workspace.clone(), cx); + workspace.update(cx, |workspace, cx| { workspace.focus_panel::(window, cx); }); } @@ -2288,12 +2307,12 @@ impl AcpServerView { .push({ let pop_up_weak = pop_up.downgrade(); - cx.observe_window_activation(window, move |_, window, cx| { - if window.is_window_active() + cx.observe_window_activation(window, move |this, window, cx| { + if this.agent_is_visible(window, cx) && let Some(pop_up) = pop_up_weak.upgrade() { - pop_up.update(cx, |_, cx| { - cx.emit(AgentNotificationEvent::Dismissed); + pop_up.update(cx, |notification, cx| { + notification.dismiss(cx); }); } }) @@ -2545,6 +2564,7 @@ pub(crate) mod tests { use action_log::ActionLog; use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext}; use agent_client_protocol::SessionId; + use assistant_text_thread::TextThreadStore; use editor::MultiBufferOffset; use fs::FakeFs; use gpui::{EventEmitter, TestAppContext, VisualTestContext}; @@ -2556,7 +2576,9 @@ pub(crate) mod tests { use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; - use workspace::Item; + use workspace::{Item, MultiWorkspace}; + + use crate::agent_panel; use super::*; @@ -2628,8 +2650,9 @@ pub(crate) mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); // Create history without an initial session list - it will be set after connection @@ -2700,8 +2723,9 @@ pub(crate) mod tests { let session = AgentSessionInfo::new(SessionId::new("resume-session")); let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx))); @@ -2747,8 +2771,9 @@ pub(crate) mod tests { ) .await; let project = Project::test(fs, [Path::new("/project")], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let connection = CwdCapturingConnection::new(); let captured_cwd = connection.captured_cwd.clone(); @@ -2798,8 +2823,9 @@ pub(crate) mod tests { ) .await; let project = Project::test(fs, [Path::new("/project")], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let connection = CwdCapturingConnection::new(); let captured_cwd = connection.captured_cwd.clone(); @@ -2849,8 +2875,9 @@ pub(crate) mod tests { ) .await; let project = Project::test(fs, [Path::new("/project")], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let connection = CwdCapturingConnection::new(); let captured_cwd = connection.captured_cwd.clone(); @@ -3011,6 +3038,137 @@ pub(crate) mod tests { ); } + #[gpui::test] + async fn test_notification_when_workspace_is_background_in_multi_workspace( + cx: &mut TestAppContext, + ) { + init_test(cx); + + // Enable multi-workspace feature flag and init globals needed by AgentPanel + let fs = FakeFs::new(cx.executor()); + + cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + ::set_global(fs.clone(), cx); + }); + + let project1 = Project::test(fs.clone(), [], cx).await; + + // Create a MultiWorkspace window with one workspace + let multi_workspace_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx)); + + // Get workspace 1 (the initial workspace) + let workspace1 = multi_workspace_handle + .read_with(cx, |mw, _cx| mw.workspace().clone()) + .unwrap(); + + let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx); + + workspace1.update_in(cx, |workspace, window, cx| { + let text_thread_store = + cx.new(|cx| TextThreadStore::fake(workspace.project().clone(), cx)); + let panel = + cx.new(|cx| crate::AgentPanel::new(workspace, text_thread_store, None, window, cx)); + workspace.add_panel(panel, window, cx); + + // Open the dock and activate the agent panel so it's visible + workspace.focus_panel::(window, cx); + }); + + cx.run_until_parked(); + + cx.read(|cx| { + assert!( + crate::AgentPanel::is_visible(&workspace1, cx), + "AgentPanel should be visible in workspace1's dock" + ); + }); + + // Set up thread view in workspace 1 + let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); + let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx))); + + let agent = StubAgentServer::default_response(); + let thread_view = cx.update(|window, cx| { + cx.new(|cx| { + AcpServerView::new( + Rc::new(agent), + None, + None, + workspace1.downgrade(), + project1.clone(), + Some(thread_store), + None, + history, + window, + cx, + ) + }) + }); + cx.run_until_parked(); + + let message_editor = message_editor(&thread_view, cx); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello", window, cx); + }); + + // Create a second workspace and switch to it. + // This makes workspace1 the "background" workspace. + let project2 = Project::test(fs, [], cx).await; + multi_workspace_handle + .update(cx, |mw, window, cx| { + mw.test_add_workspace(project2, window, cx); + }) + .unwrap(); + + cx.run_until_parked(); + + // Verify workspace1 is no longer the active workspace + multi_workspace_handle + .read_with(cx, |mw, _cx| { + assert_eq!(mw.active_workspace_index(), 1); + assert_ne!(mw.workspace(), &workspace1); + }) + .unwrap(); + + // Window is active, agent panel is visible in workspace1, but workspace1 + // is in the background. The notification should show because the user + // can't actually see the agent panel. + active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + + cx.run_until_parked(); + + assert!( + cx.windows() + .iter() + .any(|window| window.downcast::().is_some()), + "Expected notification when workspace is in background within MultiWorkspace" + ); + + // Also verify: clicking "View Panel" should switch to workspace1. + cx.windows() + .iter() + .find_map(|window| window.downcast::()) + .unwrap() + .update(cx, |window, _, cx| window.accept(cx)) + .unwrap(); + + cx.run_until_parked(); + + multi_workspace_handle + .read_with(cx, |mw, _cx| { + assert_eq!( + mw.workspace(), + &workspace1, + "Expected workspace1 to become the active workspace after accepting notification" + ); + }) + .unwrap(); + } + #[gpui::test] async fn test_notification_respects_never_setting(cx: &mut TestAppContext) { init_test(cx); @@ -3103,8 +3261,9 @@ pub(crate) mod tests { ) -> (Entity, &mut VisualTestContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx))); @@ -3173,18 +3332,18 @@ pub(crate) mod tests { } } - struct StubAgentServer { + pub(crate) struct StubAgentServer { connection: C, } impl StubAgentServer { - fn new(connection: C) -> Self { + pub(crate) fn new(connection: C) -> Self { Self { connection } } } impl StubAgentServer { - fn default_response() -> Self { + pub(crate) fn default_response() -> Self { let conn = StubAgentConnection::new(); conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( acp::ContentChunk::new("Default response".into()), @@ -3580,6 +3739,7 @@ pub(crate) mod tests { cx.set_global(settings_store); theme::init(theme::LoadThemes::JustBase, cx); editor::init(cx); + agent_panel::init(cx); release_channel::init(semver::Version::new(0, 0, 0), cx); prompt_store::init(cx) }); @@ -3614,8 +3774,9 @@ pub(crate) mod tests { ) .await; let project = Project::test(fs, [Path::new("/project")], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx))); diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 09f993577ad6ec9ce27a664cfae5adaaa093c1ff..f3e2d9f8ee361b83ae379d9d1c55a98a0eaace78 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -599,6 +599,7 @@ mod tests { use project::Project; use settings::SettingsStore; use util::path; + use workspace::MultiWorkspace; #[gpui::test] async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) { @@ -815,8 +816,9 @@ mod tests { let fs = FakeFs::new(cx.executor()); cx.update(|cx| ::set_global(fs.clone(), cx)); let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (_, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let _workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); cx } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index efaa670720283054ee1d81f0691ce2e31cfc236c..841121cfa347c0e8b67bf378da76abe1fb47ac39 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1352,10 +1352,10 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } - AcpThreadEvent::Stopped - | AcpThreadEvent::Error - | AcpThreadEvent::LoadError(_) - | AcpThreadEvent::Refusal => { + AcpThreadEvent::Stopped => { + self.update_reviewing_editors(workspace, window, cx); + } + AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::Refusal => { self.update_reviewing_editors(workspace, window, cx); } AcpThreadEvent::TitleUpdated @@ -1734,6 +1734,7 @@ mod tests { use settings::SettingsStore; use std::{path::Path, rc::Rc}; use util::path; + use workspace::MultiWorkspace; #[gpui::test] async fn test_multibuffer_agent_diff(cx: &mut TestAppContext) { @@ -1770,8 +1771,9 @@ mod tests { let action_log = cx.read(|cx| thread.read(cx).action_log().clone()); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let agent_diff = cx.new_window_entity(|window, cx| { AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx) }); @@ -1929,8 +1931,9 @@ mod tests { }) .unwrap(); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // Add the diff toolbar to the active pane let diff_toolbar = cx.new_window_entity(|_, cx| AgentDiffToolbar::new(cx)); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index ccfc0cd7073b08249a9bdc07cf3525f92e689e9a..c57f156db693c5c24a4428994f7db7f32cb351e1 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -67,6 +67,7 @@ use ui::{ use util::ResultExt as _; use workspace::{ CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace, + WorkspaceId, dock::{DockPosition, Panel, PanelEvent}, }; use zed_actions::{ @@ -81,10 +82,50 @@ const AGENT_PANEL_KEY: &str = "agent_panel"; const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; const DEFAULT_THREAD_TITLE: &str = "New Thread"; -#[derive(Serialize, Deserialize, Debug)] +fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option { + let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY); + let key = i64::from(workspace_id).to_string(); + scope + .read(&key) + .log_err() + .flatten() + .and_then(|json| serde_json::from_str::(&json).log_err()) +} + +async fn save_serialized_panel( + workspace_id: workspace::WorkspaceId, + panel: SerializedAgentPanel, +) -> Result<()> { + let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY); + let key = i64::from(workspace_id).to_string(); + scope.write(key, serde_json::to_string(&panel)?).await?; + Ok(()) +} + +/// Migration: reads the original single-panel format stored under the +/// `"agent_panel"` KVP key before per-workspace keying was introduced. +fn read_legacy_serialized_panel() -> Option { + KEY_VALUE_STORE + .read_kvp(AGENT_PANEL_KEY) + .log_err() + .flatten() + .and_then(|json| serde_json::from_str::(&json).log_err()) +} + +#[derive(Serialize, Deserialize, Debug, Clone)] struct SerializedAgentPanel { width: Option, selected_agent: Option, + #[serde(default)] + last_active_thread: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct SerializedActiveThread { + session_id: String, + agent_type: AgentType, + title: Option, + cwd: Option, } pub fn init(cx: &mut App) { @@ -128,7 +169,9 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &NewTextThread, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); - panel.update(cx, |panel, cx| panel.new_text_thread(window, cx)); + panel.update(cx, |panel, cx| { + panel.new_text_thread(window, cx); + }); } }) .register_action(|workspace, action: &NewExternalAgentThread, window, cx| { @@ -413,6 +456,8 @@ impl ActiveView { pub struct AgentPanel { workspace: WeakEntity, + /// Workspace id is used as a database key + workspace_id: Option, user_store: Entity, project: Entity, fs: Arc, @@ -428,6 +473,7 @@ pub struct AgentPanel { focus_handle: FocusHandle, active_view: ActiveView, previous_view: Option, + _active_view_observation: Option, new_thread_menu_handle: PopoverMenuHandle, agent_panel_menu_handle: PopoverMenuHandle, agent_navigation_menu_handle: PopoverMenuHandle, @@ -444,19 +490,39 @@ pub struct AgentPanel { } impl AgentPanel { - fn serialize(&mut self, cx: &mut Context) { + fn serialize(&mut self, cx: &mut App) { + let Some(workspace_id) = self.workspace_id else { + return; + }; + let width = self.width; let selected_agent = self.selected_agent.clone(); + + let last_active_thread = self.active_agent_thread(cx).map(|thread| { + let thread = thread.read(cx); + let title = thread.title(); + SerializedActiveThread { + session_id: thread.session_id().0.to_string(), + agent_type: self.selected_agent.clone(), + title: if title.as_ref() != DEFAULT_THREAD_TITLE { + Some(title.to_string()) + } else { + None + }, + cwd: None, + } + }); + self.pending_serialization = Some(cx.background_spawn(async move { - KEY_VALUE_STORE - .write_kvp( - AGENT_PANEL_KEY.into(), - serde_json::to_string(&SerializedAgentPanel { - width, - selected_agent: Some(selected_agent), - })?, - ) - .await?; + save_serialized_panel( + workspace_id, + SerializedAgentPanel { + width, + selected_agent: Some(selected_agent), + last_active_thread, + }, + ) + .await?; anyhow::Ok(()) })); } @@ -472,16 +538,18 @@ impl AgentPanel { Ok(prompt_store) => prompt_store.await.ok(), Err(_) => None, }; - let serialized_panel = if let Some(panel) = cx - .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) }) - .await - .log_err() - .flatten() - { - serde_json::from_str::(&panel).log_err() - } else { - None - }; + let workspace_id = workspace + .read_with(cx, |workspace, _| workspace.database_id()) + .ok() + .flatten(); + + let serialized_panel = cx + .background_spawn(async move { + workspace_id + .and_then(read_serialized_panel) + .or_else(read_legacy_serialized_panel) + }) + .await; let slash_commands = Arc::new(SlashCommandWorkingSet::default()); let text_thread_store = workspace @@ -500,15 +568,30 @@ impl AgentPanel { let panel = cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx)); - if let Some(serialized_panel) = serialized_panel { + if let Some(serialized_panel) = &serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); - if let Some(selected_agent) = serialized_panel.selected_agent { + if let Some(selected_agent) = serialized_panel.selected_agent.clone() { panel.selected_agent = selected_agent; } cx.notify(); }); } + + if let Some(thread_info) = serialized_panel.and_then(|p| p.last_active_thread) { + let agent_type = thread_info.agent_type.clone(); + let session_info = AgentSessionInfo { + session_id: acp::SessionId::new(thread_info.session_id), + cwd: thread_info.cwd, + title: thread_info.title.map(SharedString::from), + updated_at: None, + meta: None, + }; + panel.update(cx, |panel, cx| { + panel.selected_agent = agent_type; + panel.load_agent_thread(session_info, window, cx); + }); + } panel })?; @@ -516,7 +599,7 @@ impl AgentPanel { }) } - fn new( + pub(crate) fn new( workspace: &Workspace, text_thread_store: Entity, prompt_store: Option>, @@ -528,6 +611,7 @@ impl AgentPanel { let project = workspace.project(); let language_registry = project.read(cx).languages().clone(); let client = workspace.client().clone(); + let workspace_id = workspace.database_id(); let workspace = workspace.weak_handle(); let context_server_registry = @@ -633,6 +717,7 @@ impl AgentPanel { }; let mut panel = Self { + workspace_id, active_view, workspace, user_store, @@ -646,6 +731,7 @@ impl AgentPanel { focus_handle: cx.focus_handle(), context_server_registry, previous_view: None, + _active_view_observation: None, new_thread_menu_handle: PopoverMenuHandle::default(), agent_panel_menu_handle: PopoverMenuHandle::default(), agent_navigation_menu_handle: PopoverMenuHandle::default(), @@ -714,7 +800,7 @@ impl AgentPanel { &self.context_server_registry } - pub fn is_hidden(workspace: &Entity, cx: &App) -> bool { + pub fn is_visible(workspace: &Entity, cx: &App) -> bool { let workspace_read = workspace.read(cx); workspace_read @@ -722,15 +808,13 @@ impl AgentPanel { .map(|panel| { let panel_id = Entity::entity_id(&panel); - let is_visible = workspace_read.all_docks().iter().any(|dock| { + workspace_read.all_docks().iter().any(|dock| { dock.read(cx) .visible_panel() .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id) - }); - - !is_visible + }) }) - .unwrap_or(true) + .unwrap_or(false) } pub(crate) fn active_thread_view(&self) -> Option<&Entity> { @@ -1023,6 +1107,7 @@ impl AgentPanel { ActiveView::Configuration | ActiveView::History { .. } => { if let Some(previous_view) = self.previous_view.take() { self.active_view = previous_view; + cx.emit(AgentPanelEvent::ActiveViewChanged); match &self.active_view { ActiveView::AgentThread { thread_view } => { @@ -1419,7 +1504,7 @@ impl AgentPanel { } } - pub(crate) fn active_agent_thread(&self, cx: &App) -> Option> { + pub fn active_agent_thread(&self, cx: &App) -> Option> { match &self.active_view { ActiveView::AgentThread { thread_view, .. } => thread_view .read(cx) @@ -1475,9 +1560,21 @@ impl AgentPanel { self.active_view = new_view; } + self._active_view_observation = match &self.active_view { + ActiveView::AgentThread { thread_view } => { + Some(cx.observe(thread_view, |this, _, cx| { + cx.emit(AgentPanelEvent::ActiveViewChanged); + this.serialize(cx); + cx.notify(); + })) + } + _ => None, + }; + if focus { self.focus_handle(cx).focus(window, cx); } + cx.emit(AgentPanelEvent::ActiveViewChanged); } fn populate_recently_updated_menu_section( @@ -1750,7 +1847,12 @@ fn agent_panel_dock_position(cx: &App) -> DockPosition { AgentSettings::get_global(cx).dock.into() } +pub enum AgentPanelEvent { + ActiveViewChanged, +} + impl EventEmitter for AgentPanel {} +impl EventEmitter for AgentPanel {} impl Panel for AgentPanel { fn persistent_name() -> &'static str { @@ -3251,7 +3353,8 @@ impl Dismissable for TrialEndUpsell { const KEY: &'static str = "dismissed-trial-end-upsell"; } -#[cfg(feature = "test-support")] +/// Test-only helper methods +#[cfg(any(test, feature = "test-support"))] impl AgentPanel { /// Opens an external thread using an arbitrary AgentServer. /// @@ -3284,3 +3387,196 @@ impl AgentPanel { self.active_thread_view() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::acp::thread_view::tests::{StubAgentServer, init_test}; + use assistant_text_thread::TextThreadStore; + use feature_flags::FeatureFlagAppExt; + use fs::FakeFs; + use gpui::{TestAppContext, VisualTestContext}; + use project::Project; + use workspace::MultiWorkspace; + + #[gpui::test] + async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + }); + + // --- Create a MultiWorkspace window with two workspaces --- + let fs = FakeFs::new(cx.executor()); + let project_a = Project::test(fs.clone(), [], cx).await; + let project_b = Project::test(fs, [], cx).await; + + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); + + let workspace_a = multi_workspace + .read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().clone() + }) + .unwrap(); + + let workspace_b = multi_workspace + .update(cx, |multi_workspace, window, cx| { + multi_workspace.test_add_workspace(project_b.clone(), window, cx) + }) + .unwrap(); + + workspace_a.update(cx, |workspace, _cx| { + workspace.set_random_database_id(); + }); + workspace_b.update(cx, |workspace, _cx| { + workspace.set_random_database_id(); + }); + + let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); + + // --- Set up workspace A: width=300, with an active thread --- + let panel_a = workspace_a.update_in(cx, |workspace, window, cx| { + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx)); + cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)) + }); + + panel_a.update(cx, |panel, _cx| { + panel.width = Some(px(300.0)); + }); + + panel_a.update_in(cx, |panel, window, cx| { + panel.open_external_thread_with_server( + Rc::new(StubAgentServer::default_response()), + window, + cx, + ); + }); + + cx.run_until_parked(); + + panel_a.read_with(cx, |panel, cx| { + assert!( + panel.active_agent_thread(cx).is_some(), + "workspace A should have an active thread after connection" + ); + }); + + let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone()); + + // --- Set up workspace B: ClaudeCode, width=400, no active thread --- + let panel_b = workspace_b.update_in(cx, |workspace, window, cx| { + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx)); + cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)) + }); + + panel_b.update(cx, |panel, _cx| { + panel.width = Some(px(400.0)); + panel.selected_agent = AgentType::ClaudeCode; + }); + + // --- Serialize both panels --- + panel_a.update(cx, |panel, cx| panel.serialize(cx)); + panel_b.update(cx, |panel, cx| panel.serialize(cx)); + cx.run_until_parked(); + + // --- Load fresh panels for each workspace and verify independent state --- + let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap()); + + let async_cx = cx.update(|window, cx| window.to_async(cx)); + let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx) + .await + .expect("panel A load should succeed"); + cx.run_until_parked(); + + let async_cx = cx.update(|window, cx| window.to_async(cx)); + let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx) + .await + .expect("panel B load should succeed"); + cx.run_until_parked(); + + // Workspace A should restore its thread, width, and agent type + loaded_a.read_with(cx, |panel, _cx| { + assert_eq!( + panel.width, + Some(px(300.0)), + "workspace A width should be restored" + ); + assert_eq!( + panel.selected_agent, agent_type_a, + "workspace A agent type should be restored" + ); + assert!( + panel.active_thread_view().is_some(), + "workspace A should have its active thread restored" + ); + }); + + // Workspace B should restore its own width and agent type, with no thread + loaded_b.read_with(cx, |panel, _cx| { + assert_eq!( + panel.width, + Some(px(400.0)), + "workspace B width should be restored" + ); + assert_eq!( + panel.selected_agent, + AgentType::ClaudeCode, + "workspace B agent type should be restored" + ); + assert!( + panel.active_thread_view().is_none(), + "workspace B should have no active thread" + ); + }); + } + + // Simple regression test + #[gpui::test] + async fn test_new_text_thread_action_handler(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + let slash_command_registry = + assistant_slash_command::SlashCommandRegistry::default_global(cx); + slash_command_registry + .register_command(assistant_slash_commands::DefaultSlashCommand, false); + ::set_global(fs.clone(), cx); + }); + + let project = Project::test(fs.clone(), [], cx).await; + + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + let workspace_a = multi_workspace + .read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().clone() + }) + .unwrap(); + + let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); + + workspace_a.update_in(cx, |workspace, window, cx| { + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let panel = + cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)); + workspace.add_panel(panel, window, cx); + }); + + cx.run_until_parked(); + + workspace_a.update_in(cx, |_, window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }); + + cx.run_until_parked(); + } +} diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index aca99810b259107fd3be5bcfc05064ff6158a3c3..d7f003e95b7e3c286b45e3e5272463a32ac1a9b2 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -49,7 +49,7 @@ use std::any::TypeId; use workspace::Workspace; use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal}; -pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate}; +pub use crate::agent_panel::{AgentPanel, AgentPanelEvent, ConcreteAssistantPanelDelegate}; use crate::agent_registry_ui::AgentRegistryPage; pub use crate::inline_assistant::InlineAssistant; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; @@ -422,6 +422,12 @@ fn update_command_palette_filter(cx: &mut App) { filter.hide_action_types(&[TypeId::of::()]); } } + + if agent_v2_enabled { + filter.show_namespace("multi_workspace"); + } else { + filter.hide_namespace("multi_workspace"); + } }); } diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index faa65768b04c75a89c2490b45e58a335fa993a21..b858db698cff07d0d488d92b09a604f65d63e58a 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -2354,7 +2354,7 @@ mod tests { use project::Project; use serde_json::json; use util::{path, rel_path::rel_path}; - use workspace::AppState; + use workspace::{AppState, MultiWorkspace}; let app_state = cx.update(|cx| { let state = AppState::test(cx); @@ -2379,8 +2379,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| workspace::Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 48c597f0431c480ade5810db99c36a890ec65093..2066a7ad886614373b200f4e45dd3bb0034f72a2 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -417,8 +417,13 @@ impl PromptEditor { fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { if inline_assistant_model_supports_images(cx) - && let Some(task) = - paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) + && let Some(task) = paste_images_as_context( + self.editor.clone(), + self.mention_set.clone(), + self.workspace.clone(), + window, + cx, + ) { task.detach(); } @@ -438,7 +443,7 @@ impl PromptEditor { self.mention_set .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); - if let Some(workspace) = window.root::().flatten() { + if let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { let is_via_ssh = workspace.project().read(cx).is_via_remote_server(); diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs index ee796323e28c64fb4162bbb05f6f6f9555a12d38..707e7b45343363b9db440998190e319df1da5b80 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -297,8 +297,9 @@ impl MentionSet { self.mentions.insert(crease_id, (mention_uri, task.clone())); // Notify the user if we failed to load the mentioned context - cx.spawn_in(window, async move |this, cx| { - let result = task.await.notify_async_err(cx); + let workspace = workspace.downgrade(); + cx.spawn(async move |this, mut cx| { + let result = task.await.notify_workspace_async_err(workspace, &mut cx); drop(tx); if result.is_none() { this.update(cx, |this, cx| { @@ -644,6 +645,7 @@ pub(crate) async fn insert_images_as_context( images: Vec, editor: Entity, mention_set: Entity, + workspace: WeakEntity, cx: &mut gpui::AsyncWindowContext, ) { if images.is_empty() { @@ -718,7 +720,11 @@ pub(crate) async fn insert_images_as_context( mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone()) }); - if task.await.notify_async_err(cx).is_none() { + if task + .await + .notify_workspace_async_err(workspace.clone(), cx) + .is_none() + { editor.update(cx, |editor, cx| { editor.edit([(start_anchor..end_anchor, "")], cx); }); @@ -732,11 +738,12 @@ pub(crate) async fn insert_images_as_context( pub(crate) fn paste_images_as_context( editor: Entity, mention_set: Entity, + workspace: WeakEntity, window: &mut Window, cx: &mut App, ) -> Option> { let clipboard = cx.read_from_clipboard()?; - Some(window.spawn(cx, async move |cx| { + Some(window.spawn(cx, async move |mut cx| { use itertools::Itertools; let (mut images, paths) = clipboard .into_entries() @@ -783,7 +790,7 @@ pub(crate) fn paste_images_as_context( }) .ok(); - insert_images_as_context(images, editor, mention_set, cx).await; + insert_images_as_context(images, editor, mention_set, workspace, &mut cx).await; })) } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 447449fe72fee89b0c6775bbbcf8836141efb2b9..2d4ada96e9fa6107b9f77c55b03948e4a00f1013 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -3168,6 +3168,7 @@ mod tests { use text::OffsetRangeExt; use unindent::Unindent; use util::path; + use workspace::MultiWorkspace; #[gpui::test] async fn test_copy_paste_whole_message(cx: &mut TestAppContext) { @@ -3337,25 +3338,27 @@ mod tests { let text_thread = create_text_thread_with_messages(messages, cx); let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - let mut cx = VisualTestContext::from_window(*window, cx); - - let text_thread_editor = window - .update(&mut cx, |_, window, cx| { - cx.new(|cx| { - TextThreadEditor::for_text_thread( - text_thread.clone(), - fs, - workspace.downgrade(), - project, - None, - window, - cx, - ) - }) - }) + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let mut cx = VisualTestContext::from_window(window_handle.into(), cx); + + let weak_workspace = workspace.downgrade(); + let text_thread_editor = workspace.update_in(&mut cx, |_, window, cx| { + cx.new(|cx| { + TextThreadEditor::for_text_thread( + text_thread.clone(), + fs, + weak_workspace, + project, + None, + window, + cx, + ) + }) + }); (text_thread, text_thread_editor, cx) } diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index 34ca0bb32a82aa23d1b954554ce2dfec436bfe1c..371523f129869786f13d1a220747f4d0d944d1e5 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -75,6 +75,16 @@ pub enum AgentNotificationEvent { impl EventEmitter for AgentNotification {} +impl AgentNotification { + pub fn accept(&mut self, cx: &mut Context) { + cx.emit(AgentNotificationEvent::Accepted); + } + + pub fn dismiss(&mut self, cx: &mut Context) { + cx.emit(AgentNotificationEvent::Dismissed); + } +} + impl Render for AgentNotification { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let ui_font = theme::setup_ui_font(window, cx); @@ -174,14 +184,14 @@ impl Render for AgentNotification { .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .full_width() .on_click({ - cx.listener(move |_this, _event, _, cx| { - cx.emit(AgentNotificationEvent::Accepted); + cx.listener(move |this, _event, _, cx| { + this.accept(cx); }) }), ) .child(Button::new("dismiss", "Dismiss").full_width().on_click({ - cx.listener(move |_, _event, _, cx| { - cx.emit(AgentNotificationEvent::Dismissed); + cx.listener(move |this, _event, _, cx| { + this.dismiss(cx); }) })), ) diff --git a/crates/collab/tests/integration/channel_guest_tests.rs b/crates/collab/tests/integration/channel_guest_tests.rs index 0d98af2a188ce18cfab5905e5b464c77101dfa00..85d69914a832c65260014f5f5792eb664879f715 100644 --- a/crates/collab/tests/integration/channel_guest_tests.rs +++ b/crates/collab/tests/integration/channel_guest_tests.rs @@ -34,9 +34,11 @@ async fn test_channel_guests( cx_a.executor().run_until_parked(); // Client B joins channel A as a guest - cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) - .await - .unwrap(); + cx_b.update(|cx| { + workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx) + }) + .await + .unwrap(); // b should be following a in the shared project. // B is a guest, @@ -76,9 +78,11 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test .await; let project_a = client_a.build_test_project(cx_a).await; - cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx)) - .await - .unwrap(); + cx_a.update(|cx| { + workspace::join_channel(channel_id, client_a.app_state.clone(), None, None, cx) + }) + .await + .unwrap(); // Client A shares a project in the channel active_call_a @@ -88,9 +92,11 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test cx_a.run_until_parked(); // Client B joins channel A as a guest - cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) - .await - .unwrap(); + cx_b.update(|cx| { + workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx) + }) + .await + .unwrap(); cx_a.run_until_parked(); // client B opens 1.txt as a guest diff --git a/crates/collab/tests/integration/editor_tests.rs b/crates/collab/tests/integration/editor_tests.rs index 34bed0086b9af8bc2ed39580f4ecda2c6c609338..596d857729da4c7d37881567f5e90013e403d5ca 100644 --- a/crates/collab/tests/integration/editor_tests.rs +++ b/crates/collab/tests/integration/editor_tests.rs @@ -19,7 +19,8 @@ use fs::Fs; use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; use git::repository::repo_path; use gpui::{ - App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext, + App, AppContext as _, Entity, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, + VisualTestContext, }; use indoc::indoc; use language::{FakeLspAdapter, language_settings::language_settings, rust_lang}; @@ -52,7 +53,7 @@ use std::{ use text::Point; use util::{path, rel_path::rel_path, uri}; use workspace::item::Item as _; -use workspace::{CloseIntent, Workspace}; +use workspace::{CloseIntent, MultiWorkspace, Workspace}; #[gpui::test(iterations = 10)] async fn test_host_disconnect( @@ -96,34 +97,46 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); - let workspace_b = cx_b.add_window(|window, cx| { - Workspace::new( - None, - project_b.clone(), - client_b.app_state.clone(), - window, - cx, - ) + let window_b = cx_b.add_window(|window, cx| { + let workspace = cx.new(|cx| { + Workspace::new( + None, + project_b.clone(), + client_b.app_state.clone(), + window, + cx, + ) + }); + MultiWorkspace::new(workspace, cx) }); - let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); - let workspace_b_view = workspace_b.root(cx_b).unwrap(); + let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b); + let workspace_b = window_b + .root(cx_b) + .unwrap() + .read_with(cx_b, |multi_workspace, _| { + multi_workspace.workspace().clone() + }); - let editor_b = workspace_b - .update(cx_b, |workspace, window, cx| { + let editor_b: Entity = workspace_b + .update_in(cx_b, |workspace, window, cx| { workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx) }) - .unwrap() .await .unwrap() .downcast::() .unwrap(); //TODO: focus - assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window))); - editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx)); + assert!( + cx_b.update_window_entity(&editor_b, |editor: &mut Editor, window, _| editor + .is_focused(window)) + ); + editor_b.update_in(cx_b, |editor: &mut Editor, window, cx| { + editor.insert("X", window, cx) + }); cx_b.update(|_, cx| { - assert!(workspace_b_view.read(cx).is_edited()); + assert!(workspace_b.read(cx).is_edited()); }); // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. @@ -141,19 +154,16 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer())); // Ensure client B's edited state is reset and that the whole window is blurred. - workspace_b - .update(cx_b, |workspace, _, cx| { - assert!(workspace.active_modal::(cx).is_some()); - assert!(!workspace.is_edited()); - }) - .unwrap(); + workspace_b.update(cx_b, |workspace, cx| { + assert!(workspace.active_modal::(cx).is_some()); + assert!(!workspace.is_edited()); + }); // Ensure client B is not prompted to save edits when closing window after disconnecting. - let can_close = workspace_b - .update(cx_b, |workspace, window, cx| { + let can_close: bool = workspace_b + .update_in(cx_b, |workspace, window, cx| { workspace.prepare_to_close(CloseIntent::Quit, window, cx) }) - .unwrap() .await .unwrap(); assert!(can_close); diff --git a/crates/collab/tests/integration/following_tests.rs b/crates/collab/tests/integration/following_tests.rs index 295105ecbd9f8663469276fe4d0d197708a4254e..6bdb06a6c5a0ffb95bc75a026a26c4797030f8ce 100644 --- a/crates/collab/tests/integration/following_tests.rs +++ b/crates/collab/tests/integration/following_tests.rs @@ -17,7 +17,7 @@ use serde_json::json; use settings::SettingsStore; use text::{Point, ToPoint}; use util::{path, rel_path::rel_path, test::sample_text}; -use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _}; +use workspace::{CollaboratorId, MultiWorkspace, SplitDirection, Workspace, item::ItemHandle as _}; use super::TestClient; @@ -1555,9 +1555,9 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b); let workspace_b_project_a = window_b_project_a - .downcast::() + .downcast::() .unwrap() - .root(cx_b) + .read_with(cx_b, |mw, _| mw.workspace().clone()) .unwrap(); // assert that b is following a in project a in w.rs @@ -1657,9 +1657,9 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut .unwrap(); let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a); let workspace_a_project_b = window_a_project_b - .downcast::() + .downcast::() .unwrap() - .root(cx_a) + .read_with(cx_a, |mw, _| mw.workspace().clone()) .unwrap(); executor.run_until_parked(); @@ -2144,7 +2144,7 @@ pub(crate) async fn join_channel( client: &TestClient, cx: &mut TestAppContext, ) -> anyhow::Result<()> { - cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx)) + cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, None, cx)) .await } diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs index 1378fcf95c63c883ee8dd424dc10ac67ccd774bd..63cee5886d5096cb0e3fbee3886b90f66c675bfa 100644 --- a/crates/collab/tests/integration/git_tests.rs +++ b/crates/collab/tests/integration/git_tests.rs @@ -3,11 +3,11 @@ use std::path::Path; use call::ActiveCall; use git::status::{FileStatus, StatusCode, TrackedStatus}; use git_ui::project_diff::ProjectDiff; -use gpui::{TestAppContext, VisualTestContext}; +use gpui::{AppContext as _, TestAppContext, VisualTestContext}; use project::ProjectPath; use serde_json::json; use util::{path, rel_path::rel_path}; -use workspace::Workspace; +use workspace::{MultiWorkspace, Workspace}; // use crate::TestServer; @@ -57,17 +57,25 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) cx_b.update(editor::init); cx_b.update(git_ui::init); let project_b = client_b.join_remote_project(project_id, cx_b).await; - let workspace_b = cx_b.add_window(|window, cx| { - Workspace::new( - None, - project_b.clone(), - client_b.app_state.clone(), - window, - cx, - ) + let window_b = cx_b.add_window(|window, cx| { + let workspace = cx.new(|cx| { + Workspace::new( + None, + project_b.clone(), + client_b.app_state.clone(), + window, + cx, + ) + }); + MultiWorkspace::new(workspace, cx) }); - let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); - let workspace_b = workspace_b.root(cx_b).unwrap(); + let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b); + let workspace_b = window_b + .root(cx_b) + .unwrap() + .read_with(cx_b, |multi_workspace, _| { + multi_workspace.workspace().clone() + }); cx_b.update(|window, cx| { window diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index 1f4dd0d353234f61675b5beefd2226c3d684c062..c6daedff803b6f5cada32750f90dd1adca5aeda6 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -8,7 +8,9 @@ use editor::{Editor, EditorMode, MultiBuffer}; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; -use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext}; +use gpui::{ + AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext as _, +}; use http_client::BlockedHttpClient; use language::{ FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, @@ -663,7 +665,7 @@ async fn test_remote_server_debugger( let workspace_window = cx_a .window_handle() - .downcast::() + .downcast::() .unwrap(); let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap(); @@ -671,13 +673,16 @@ async fn test_remote_server_debugger( debug_panel.update(cx_a, |debug_panel, cx| { assert_eq!( debug_panel.active_session().unwrap().read(cx).session(cx), - session + session.clone() ) }); - session.update(cx_a, |session, _| { - assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock")); - }); + session.update( + cx_a, + |session: &mut project::debugger::session::Session, _| { + assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock")); + }, + ); let shutdown_session = workspace.update(cx_a, |workspace, cx| { workspace.project().update(cx, |project, cx| { @@ -772,7 +777,7 @@ async fn test_slow_adapter_startup_retries( let workspace_window = cx_a .window_handle() - .downcast::() + .downcast::() .unwrap(); let count = Arc::new(AtomicUsize::new(0)); @@ -804,7 +809,10 @@ async fn test_slow_adapter_startup_retries( .unwrap(); cx_a.run_until_parked(); - let client = session.update(cx_a, |session, _| session.adapter_client().unwrap()); + let client = session.update( + cx_a, + |session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(), + ); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, diff --git a/crates/collab/tests/integration/test_server.rs b/crates/collab/tests/integration/test_server.rs index a731a8ae1d50234f06806c8aba036abc455d223c..d822a087d96fdc119cc700f2f0e8f79d16b95acf 100644 --- a/crates/collab/tests/integration/test_server.rs +++ b/crates/collab/tests/integration/test_server.rs @@ -45,7 +45,7 @@ use std::{ }, }; use util::path; -use workspace::{Workspace, WorkspaceStore}; +use workspace::{MultiWorkspace, Workspace, WorkspaceStore}; use livekit_client::test::TestServer as LivekitTestServer; @@ -827,7 +827,7 @@ impl TestClient { channel_id: ChannelId, cx: &'a mut TestAppContext, ) -> (Entity, &'a mut VisualTestContext) { - cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx)) + cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, None, cx)) .await .unwrap(); cx.run_until_parked(); @@ -881,10 +881,19 @@ impl TestClient { project: &Entity, cx: &'a mut TestAppContext, ) -> (Entity, &'a mut VisualTestContext) { - cx.add_window_view(|window, cx| { + let app_state = self.app_state.clone(); + let project = project.clone(); + let window = cx.add_window(|window, cx| { window.activate_window(); - Workspace::new(None, project.clone(), self.app_state.clone(), window, cx) - }) + let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx)); + MultiWorkspace::new(workspace, cx) + }); + let cx = VisualTestContext::from_window(*window, cx).into_mut(); + cx.run_until_parked(); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + (workspace, cx) } pub async fn build_test_workspace<'a>( @@ -892,19 +901,33 @@ impl TestClient { cx: &'a mut TestAppContext, ) -> (Entity, &'a mut VisualTestContext) { let project = self.build_test_project(cx).await; - cx.add_window_view(|window, cx| { + let app_state = self.app_state.clone(); + let window = cx.add_window(|window, cx| { window.activate_window(); - Workspace::new(None, project.clone(), self.app_state.clone(), window, cx) - }) + let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx)); + MultiWorkspace::new(workspace, cx) + }); + let cx = VisualTestContext::from_window(*window, cx).into_mut(); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + (workspace, cx) } pub fn active_workspace<'a>( &'a self, cx: &'a mut TestAppContext, ) -> (Entity, &'a mut VisualTestContext) { - let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); + let window = cx.update(|cx| { + cx.active_window() + .unwrap() + .downcast::() + .unwrap() + }); - let entity = window.root(cx).unwrap(); + let entity = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut(); // it might be nice to try and cleanup these at the end of each test. (entity, cx) @@ -915,8 +938,15 @@ pub fn open_channel_notes( channel_id: ChannelId, cx: &mut VisualTestContext, ) -> Task>> { - let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::().unwrap()); - let entity = window.root(cx).unwrap(); + let window = cx.update(|_, cx| { + cx.active_window() + .unwrap() + .downcast::() + .unwrap() + }); + let entity = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); cx.update(|window, cx| ChannelView::open(channel_id, None, entity.clone(), window, cx)) } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 60262951ef916183bdaf72df90ab39f2edd83f27..54bf5f3d22cf756db085b9ef81f30bc7465c1db5 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -36,7 +36,8 @@ use ui::{ }; use util::{ResultExt, TryFutureExt, maybe}; use workspace::{ - CopyRoomId, Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace, + CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, ScreenShare, + ShareProject, Workspace, dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyResultExt}, }; @@ -120,6 +121,7 @@ pub fn init(cx: &mut App) { if let Some(room) = ActiveCall::global(cx).read(cx).room() { let romo_id_fut = room.read(cx).room_id(); + let workspace_handle = cx.weak_entity(); cx.spawn(async move |workspace, cx| { let room_id = romo_id_fut.await.context("Failed to get livekit room")?; workspace.update(cx, |workspace, cx| { @@ -134,7 +136,7 @@ pub fn init(cx: &mut App) { ); }) }) - .detach_and_notify_err(window, cx); + .detach_and_notify_err(workspace_handle, window, cx); } else { workspace.show_error(&"There’s no active call; join one first.", cx); } @@ -2189,12 +2191,13 @@ impl CollabPanel { &["Remove", "Cancel"], cx, ); - cx.spawn_in(window, async move |this, cx| { + let workspace = self.workspace.clone(); + cx.spawn_in(window, async move |this, mut cx| { if answer.await? == 0 { channel_store .update(cx, |channels, _| channels.remove_channel(channel_id)) .await - .notify_async_err(cx); + .notify_workspace_async_err(workspace, &mut cx); this.update_in(cx, |_, window, cx| cx.focus_self(window)) .ok(); } @@ -2223,12 +2226,13 @@ impl CollabPanel { &["Remove", "Cancel"], cx, ); - cx.spawn_in(window, async move |_, cx| { + let workspace = self.workspace.clone(); + cx.spawn_in(window, async move |_, mut cx| { if answer.await? == 0 { user_store .update(cx, |store, cx| store.remove_contact(user_id, cx)) .await - .notify_async_err(cx); + .notify_workspace_async_err(workspace, &mut cx); } anyhow::Ok(()) }) @@ -2279,13 +2283,15 @@ impl CollabPanel { let Some(workspace) = self.workspace.upgrade() else { return; }; - let Some(handle) = window.window_handle().downcast::() else { + + let Some(handle) = window.window_handle().downcast::() else { return; }; workspace::join_channel( channel_id, workspace.read(cx).app_state().clone(), Some(handle), + Some(self.workspace.clone()), cx, ) .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None) @@ -2328,12 +2334,13 @@ impl CollabPanel { .full_width() .on_click(cx.listener(|this, _, window, cx| { let client = this.client.clone(); - cx.spawn_in(window, async move |_, cx| { + let workspace = this.workspace.clone(); + cx.spawn_in(window, async move |_, mut cx| { client - .connect(true, cx) + .connect(true, &mut cx) .await .into_response() - .notify_async_err(cx); + .notify_workspace_async_err(workspace, &mut cx); }) .detach() })), diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index dae7427f9f132cd8f1021ed9d99dd1b17a729a3b..a6fc0193a4b18407c2f4473a0fbea471d91eb9a9 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -723,7 +723,7 @@ mod tests { use language::Point; use project::Project; use settings::KeymapFile; - use workspace::{AppState, Workspace}; + use workspace::{AppState, MultiWorkspace, Workspace}; #[test] fn test_humanize_action_name() { @@ -777,8 +777,9 @@ mod tests { .unwrap(); let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let editor = cx.new_window_entity(|window, cx| { let mut editor = Editor::single_line(window, cx); @@ -848,8 +849,9 @@ mod tests { async fn test_normalized_matches(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let editor = cx.new_window_entity(|window, cx| { let mut editor = Editor::single_line(window, cx); @@ -884,8 +886,9 @@ mod tests { async fn test_go_to_line(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); cx.simulate_keystrokes("cmd-n"); @@ -974,8 +977,9 @@ mod tests { async fn test_history_navigation_basic(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let palette = open_palette_with_history(&workspace, &["backspace", "select all"], cx); @@ -1017,8 +1021,9 @@ mod tests { async fn test_history_mode_exit_on_typing(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let palette = open_palette_with_history(&workspace, &["backspace"], cx); @@ -1041,8 +1046,9 @@ mod tests { async fn test_history_navigation_with_suggestions(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let palette = open_palette_with_history(&workspace, &["editor: close", "editor: open"], cx); @@ -1083,8 +1089,9 @@ mod tests { async fn test_history_prefix_search(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let palette = open_palette_with_history( &workspace, @@ -1136,8 +1143,9 @@ mod tests { async fn test_history_prefix_search_no_matches(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let palette = open_palette_with_history(&workspace, &["open file", "backspace", "select all"], cx); @@ -1158,8 +1166,9 @@ mod tests { async fn test_history_empty_prefix_searches_all(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let palette = open_palette_with_history(&workspace, &["alpha", "beta", "gamma"], cx); diff --git a/crates/copilot_ui/src/sign_in.rs b/crates/copilot_ui/src/sign_in.rs index dd48f95e0af6daeaf2a0a15b7b9595cb4c08aba2..24b1218305474a29ac2d2e7c8e0a212d6d757522 100644 --- a/crates/copilot_ui/src/sign_in.rs +++ b/crates/copilot_ui/src/sign_in.rs @@ -35,7 +35,7 @@ pub fn initiate_sign_out(copilot: Entity, window: &mut Window, cx: &mut cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx)) } Err(err) => cx.update(|window, cx| { - if let Some(workspace) = window.root::().flatten() { + if let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { workspace.show_error(&err, cx); }) @@ -82,7 +82,7 @@ fn open_copilot_code_verification_window(copilot: &Entity, window: &Win fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) { const NOTIFICATION_ID: NotificationId = NotificationId::unique::(); - let Some(workspace) = window.root::().flatten() else { + let Some(workspace) = Workspace::for_window(window, cx) else { return; }; diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 8ea877b35bfaf57bb258e7e179fa5b71f2b518ea..438adcdf44921aa1d2590694608c139e9174d788 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -1,3 +1,4 @@ +use anyhow::Context as _; use gpui::App; use sqlez_macros::sql; use util::ResultExt as _; @@ -13,12 +14,22 @@ pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnect impl Domain for KeyValueStore { const NAME: &str = stringify!(KeyValueStore); - const MIGRATIONS: &[&str] = &[sql!( - CREATE TABLE IF NOT EXISTS kv_store( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ) STRICT; - )]; + const MIGRATIONS: &[&str] = &[ + sql!( + CREATE TABLE IF NOT EXISTS kv_store( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) STRICT; + ), + sql!( + CREATE TABLE IF NOT EXISTS scoped_kv_store( + namespace TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY(namespace, key) + ) STRICT; + ), + ]; } crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); @@ -69,6 +80,64 @@ impl KeyValueStore { DELETE FROM kv_store WHERE key = (?) } } + + pub fn scoped<'a>(&'a self, namespace: &'a str) -> ScopedKeyValueStore<'a> { + ScopedKeyValueStore { + store: self, + namespace, + } + } +} + +pub struct ScopedKeyValueStore<'a> { + store: &'a KeyValueStore, + namespace: &'a str, +} + +impl ScopedKeyValueStore<'_> { + pub fn read(&self, key: &str) -> anyhow::Result> { + self.store.select_row_bound::<(&str, &str), String>( + "SELECT value FROM scoped_kv_store WHERE namespace = (?) AND key = (?)", + )?((self.namespace, key)) + .context("Failed to read from scoped_kv_store") + } + + pub async fn write(&self, key: String, value: String) -> anyhow::Result<()> { + let namespace = self.namespace.to_owned(); + self.store + .write(move |connection| { + connection.exec_bound::<(&str, &str, &str)>( + "INSERT OR REPLACE INTO scoped_kv_store(namespace, key, value) VALUES ((?), (?), (?))", + )?((&namespace, &key, &value)) + .context("Failed to write to scoped_kv_store") + }) + .await + } + + pub async fn delete(&self, key: String) -> anyhow::Result<()> { + let namespace = self.namespace.to_owned(); + self.store + .write(move |connection| { + connection.exec_bound::<(&str, &str)>( + "DELETE FROM scoped_kv_store WHERE namespace = (?) AND key = (?)", + )?((&namespace, &key)) + .context("Failed to delete from scoped_kv_store") + }) + .await + } + + pub async fn delete_all(&self) -> anyhow::Result<()> { + let namespace = self.namespace.to_owned(); + self.store + .write(move |connection| { + connection + .exec_bound::<&str>("DELETE FROM scoped_kv_store WHERE namespace = (?)")?( + &namespace, + ) + .context("Failed to delete_all from scoped_kv_store") + }) + .await + } } #[cfg(test)] @@ -99,6 +168,52 @@ mod tests { db.delete_kvp("key-1".to_string()).await.unwrap(); assert_eq!(db.read_kvp("key-1").unwrap(), None); } + + #[gpui::test] + async fn test_scoped_kvp() { + let db = KeyValueStore::open_test_db("test_scoped_kvp").await; + + let scope_a = db.scoped("namespace-a"); + let scope_b = db.scoped("namespace-b"); + + // Reading a missing key returns None + assert_eq!(scope_a.read("key-1").unwrap(), None); + + // Writing and reading back a key works + scope_a + .write("key-1".to_string(), "value-a1".to_string()) + .await + .unwrap(); + assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string())); + + // Two namespaces with the same key don't collide + scope_b + .write("key-1".to_string(), "value-b1".to_string()) + .await + .unwrap(); + assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string())); + assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string())); + + // delete removes a single key without affecting others in the namespace + scope_a + .write("key-2".to_string(), "value-a2".to_string()) + .await + .unwrap(); + scope_a.delete("key-1".to_string()).await.unwrap(); + assert_eq!(scope_a.read("key-1").unwrap(), None); + assert_eq!(scope_a.read("key-2").unwrap(), Some("value-a2".to_string())); + assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string())); + + // delete_all removes all keys in a namespace without affecting other namespaces + scope_a + .write("key-3".to_string(), "value-a3".to_string()) + .await + .unwrap(); + scope_a.delete_all().await.unwrap(); + assert_eq!(scope_a.read("key-2").unwrap(), None); + assert_eq!(scope_a.read("key-3").unwrap(), None); + assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string())); + } } pub struct GlobalKeyValueStore(ThreadSafeConnection); diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 5aaf3538e86054346d82b1db10aa02d8e5aa34f1..c183f8941c3f30cb43ffaa638eae4e6b387e226d 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -8,7 +8,7 @@ use project::{Project, debugger::session::Session}; use settings::SettingsStore; use task::SharedTaskContext; use terminal_view::terminal_panel::TerminalPanel; -use workspace::Workspace; +use workspace::MultiWorkspace; use crate::{debugger_panel::DebugPanel, session::DebugSession}; @@ -52,14 +52,16 @@ pub fn init_test(cx: &mut gpui::TestAppContext) { pub async fn init_test_workspace( project: &Entity, cx: &mut TestAppContext, -) -> WindowHandle { +) -> WindowHandle { let workspace_handle = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let debugger_panel = workspace_handle - .update(cx, |_, window, cx| { - cx.spawn_in(window, async move |this, cx| { - DebugPanel::load(this, cx).await + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |_workspace, cx| { + cx.spawn_in(window, async move |this, cx| { + DebugPanel::load(this, cx).await + }) }) }) .unwrap() @@ -67,9 +69,10 @@ pub async fn init_test_workspace( .expect("Failed to load debug panel"); let terminal_panel = workspace_handle - .update(cx, |_, window, cx| { - cx.spawn_in(window, async |this, cx| { - TerminalPanel::load(this, cx.clone()).await + .update(cx, |multi, window, cx| { + let weak_workspace = multi.workspace().downgrade(); + cx.spawn_in(window, async move |_, cx| { + TerminalPanel::load(weak_workspace, cx.clone()).await }) }) .unwrap() @@ -77,9 +80,11 @@ pub async fn init_test_workspace( .expect("Failed to load terminal panel"); workspace_handle - .update(cx, |workspace, window, cx| { - workspace.add_panel(debugger_panel, window, cx); - workspace.add_panel(terminal_panel, window, cx); + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.add_panel(debugger_panel, window, cx); + workspace.add_panel(terminal_panel, window, cx); + }); }) .unwrap(); workspace_handle @@ -87,39 +92,45 @@ pub async fn init_test_workspace( #[track_caller] pub fn active_debug_session_panel( - workspace: WindowHandle, + workspace: WindowHandle, cx: &mut TestAppContext, ) -> Entity { workspace - .update(cx, |workspace, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel - .update(cx, |this, _| this.active_session()) - .unwrap() + .update(cx, |multi, _window, cx| { + multi.workspace().update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + debug_panel + .update(cx, |this, _| this.active_session()) + .unwrap() + }) }) .unwrap() } pub fn start_debug_session_with) + 'static>( - workspace: &WindowHandle, + workspace: &WindowHandle, cx: &mut gpui::TestAppContext, config: DebugTaskDefinition, configure: T, ) -> Result> { let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure); - workspace.update(cx, |workspace, window, cx| { - workspace.start_debug_session( - config.to_scenario(), - SharedTaskContext::default(), - None, - None, - window, - cx, - ) + workspace.update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.start_debug_session( + config.to_scenario(), + SharedTaskContext::default(), + None, + None, + window, + cx, + ) + }) })?; cx.run_until_parked(); let session = workspace.read_with(cx, |workspace, cx| { workspace + .workspace() + .read(cx) .panel::(cx) .and_then(|panel| panel.read(cx).active_session()) .map(|session| session.read(cx).running_state().read(cx).session()) @@ -131,7 +142,7 @@ pub fn start_debug_session_with) + 'static>( } pub fn start_debug_session) + 'static>( - workspace: &WindowHandle, + workspace: &WindowHandle, cx: &mut gpui::TestAppContext, configure: T, ) -> Result> { diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index b05ee591f3ac0ca2e138e25928552d93c4426152..4e8839f82f4de69fd1851ef50ff0d55ad09d0aa9 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -60,7 +60,13 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te // assert we didn't show the attach modal workspace .update(cx, |workspace, _window, cx| { - assert!(workspace.active_modal::(cx).is_none()); + assert!( + workspace + .workspace() + .read(cx) + .active_modal::(cx) + .is_none() + ); }) .unwrap(); } @@ -97,9 +103,9 @@ async fn test_show_attach_modal_and_select_process( }); }); let attach_modal = workspace - .update(cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - workspace.toggle_modal(window, cx, |window, cx| { + .update(cx, |multi, window, cx| { + let workspace_handle = multi.workspace().downgrade(); + multi.toggle_modal(window, cx, |window, cx| { AttachModal::with_processes( workspace_handle, vec![ @@ -133,7 +139,7 @@ async fn test_show_attach_modal_and_select_process( ) }); - workspace.active_modal::(cx).unwrap() + multi.active_modal::(cx).unwrap() }) .unwrap(); @@ -208,24 +214,26 @@ async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &m let pick_pid_placeholder = task::VariableName::PickProcessId.template_value(); workspace - .update(cx, |workspace, window, cx| { - workspace.start_debug_session( - DebugTaskDefinition { - adapter: FakeAdapter::ADAPTER_NAME.into(), - label: "attach with picker".into(), - config: json!({ - "request": "attach", - "process_id": pick_pid_placeholder, - }), - tcp_connection: None, - } - .to_scenario(), - SharedTaskContext::default(), - None, - None, - window, - cx, - ) + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.start_debug_session( + DebugTaskDefinition { + adapter: FakeAdapter::ADAPTER_NAME.into(), + label: "attach with picker".into(), + config: json!({ + "request": "attach", + "process_id": pick_pid_placeholder, + }), + tcp_connection: None, + } + .to_scenario(), + SharedTaskContext::default(), + None, + None, + window, + cx, + ); + }) }) .unwrap(); diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 7be2b8798e38108eaa05508002624b98c1595b3f..54c38d8b1cec8d043748338830d643d63479e533 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -145,15 +145,17 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( }; workspace - .update(cx, |workspace, window, cx| { - workspace.start_debug_session( - scenario, - task_context.clone(), - None, - None, - window, - cx, - ) + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + workspace.start_debug_session( + scenario, + task_context.clone(), + None, + None, + window, + cx, + ); + }) }) .unwrap(); @@ -182,8 +184,10 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut let cx = &mut VisualTestContext::from_window(*workspace, cx); workspace - .update(cx, |workspace, window, cx| { - NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); + }); }) .unwrap(); @@ -324,8 +328,10 @@ async fn test_debug_modal_subtitles_with_multiple_worktrees( let cx = &mut VisualTestContext::from_window(*workspace, cx); workspace - .update(cx, |workspace, window, cx| { - NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); + .update(cx, |multi, window, cx| { + multi.workspace().update(cx, |workspace, cx| { + NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); + }); }) .unwrap(); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 372bfa8f5f4eb1da13a59057d077a53a71fa2cea..1f5ac5dea4a19af338feceaa2ee51fd9322fa9a5 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -1113,8 +1113,8 @@ async fn test_stack_frame_filter_persistence( let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); workspace - .update(cx, |workspace, _, _| { - workspace.set_random_database_id(); + .update(cx, |workspace, _, cx| { + workspace.set_random_database_id(cx); }) .unwrap(); @@ -1211,7 +1211,7 @@ async fn test_stack_frame_filter_persistence( cx.run_until_parked(); let workspace_id = workspace - .update(cx, |workspace, _window, _cx| workspace.database_id()) + .update(cx, |workspace, _window, cx| workspace.database_id(cx)) .ok() .flatten() .expect("workspace id has to be some for this test to work properly"); diff --git a/crates/dev_container/Cargo.toml b/crates/dev_container/Cargo.toml index 87a945b97a9e8f3cd3a73a18045960e07405d27c..7b1574da69729a8ff5ddeb5523a8c249779a721b 100644 --- a/crates/dev_container/Cargo.toml +++ b/crates/dev_container/Cargo.toml @@ -24,13 +24,14 @@ worktree.workspace = true workspace.workspace = true [dev-dependencies] -fs.workspace = true +fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } serde_json.workspace = true settings = { workspace = true, features = ["test-support"] } +theme.workspace = true workspace = { workspace = true, features = ["test-support"] } worktree = { workspace = true, features = ["test-support"] } [lints] -workspace = true +workspace = true \ No newline at end of file diff --git a/crates/dev_container/src/devcontainer_api.rs b/crates/dev_container/src/devcontainer_api.rs index 8d79e7a52ffb43463feb7840573ad6b334b6183b..dbd694d686ff92e9593ca1b44e7a72d30ed6c9c9 100644 --- a/crates/dev_container/src/devcontainer_api.rs +++ b/crates/dev_container/src/devcontainer_api.rs @@ -2,19 +2,17 @@ use std::{ collections::{HashMap, HashSet}, fmt::Display, path::{Path, PathBuf}, - sync::Arc, }; -use gpui::AsyncWindowContext; use node_runtime::NodeRuntime; use serde::Deserialize; -use settings::{DevContainerConnection, Settings as _}; +use settings::DevContainerConnection; use smol::{fs, process::Command}; use util::rel_path::RelPath; use workspace::Workspace; use worktree::Snapshot; -use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate}; +use crate::{DevContainerContext, DevContainerFeature, DevContainerTemplate}; /// Represents a discovered devcontainer configuration #[derive(Debug, Clone, PartialEq, Eq)] @@ -67,6 +65,31 @@ pub(crate) struct DevContainerConfigurationOutput { configuration: DevContainerConfiguration, } +pub(crate) struct DevContainerCli { + pub path: PathBuf, + node_runtime_path: Option, +} + +impl DevContainerCli { + fn command(&self, use_podman: bool) -> Command { + let mut command = if let Some(node_runtime_path) = &self.node_runtime_path { + let mut command = util::command::new_smol_command( + node_runtime_path.as_os_str().display().to_string(), + ); + command.arg(self.path.display().to_string()); + command + } else { + util::command::new_smol_command(self.path.display().to_string()) + }; + + if use_podman { + command.arg("--docker-path"); + command.arg("podman"); + } + command + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum DevContainerError { DockerNotAvailable, @@ -107,87 +130,23 @@ impl Display for DevContainerError { } } -pub(crate) async fn read_devcontainer_configuration_for_project( - cx: &mut AsyncWindowContext, - node_runtime: &NodeRuntime, -) -> Result { - let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; - - let Some(directory) = project_directory(cx) else { - return Err(DevContainerError::NotInValidProject); - }; - - devcontainer_read_configuration( - &path_to_devcontainer_cli, - found_in_path, - node_runtime, - &directory, - None, - use_podman(cx), - ) - .await -} - -pub(crate) async fn apply_dev_container_template( - template: &DevContainerTemplate, - options_selected: &HashMap, - features_selected: &HashSet, - cx: &mut AsyncWindowContext, - node_runtime: &NodeRuntime, -) -> Result { - let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; - - let Some(directory) = project_directory(cx) else { - return Err(DevContainerError::NotInValidProject); - }; - - devcontainer_template_apply( - template, - options_selected, - features_selected, - &path_to_devcontainer_cli, - found_in_path, - node_runtime, - &directory, - false, // devcontainer template apply does not use --docker-path option - ) - .await -} - -fn use_podman(cx: &mut AsyncWindowContext) -> bool { - cx.update(|_, cx| DevContainerSettings::get_global(cx).use_podman) - .unwrap_or(false) -} - /// Finds all available devcontainer configurations in the project. /// /// See [`find_configs_in_snapshot`] for the locations that are scanned. -pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec { - let Some(workspace) = cx.window_handle().downcast::() else { - log::debug!("find_devcontainer_configs: No workspace found"); - return Vec::new(); - }; - - let Ok(configs) = workspace.update(cx, |workspace, _, cx| { - let project = workspace.project().read(cx); +pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec { + let project = workspace.project().read(cx); - let worktree = project - .visible_worktrees(cx) - .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); - - let Some(worktree) = worktree else { - log::debug!("find_devcontainer_configs: No worktree found"); - return Vec::new(); - }; + let worktree = project + .visible_worktrees(cx) + .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); - let worktree = worktree.read(cx); - find_configs_in_snapshot(worktree) - }) else { - log::debug!("find_devcontainer_configs: Failed to update workspace"); + let Some(worktree) = worktree else { + log::debug!("find_devcontainer_configs: No worktree found"); return Vec::new(); }; - configs + let worktree = worktree.read(cx); + find_configs_in_snapshot(worktree) } /// Scans a worktree snapshot for devcontainer configurations. @@ -280,60 +239,36 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec } pub async fn start_dev_container_with_config( - cx: &mut AsyncWindowContext, - node_runtime: NodeRuntime, + context: DevContainerContext, config: Option, ) -> Result<(DevContainerConnection, String), DevContainerError> { - let use_podman = use_podman(cx); - check_for_docker(use_podman).await?; - - let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; - - let Some(directory) = project_directory(cx) else { - return Err(DevContainerError::NotInValidProject); - }; + check_for_docker(context.use_podman).await?; + let cli = ensure_devcontainer_cli(&context.node_runtime).await?; + let config_path = config.map(|c| context.project_directory.join(&c.config_path)); - let config_path = config.map(|c| directory.join(&c.config_path)); - - match devcontainer_up( - &path_to_devcontainer_cli, - found_in_path, - &node_runtime, - directory.clone(), - config_path.clone(), - use_podman, - ) - .await - { + match devcontainer_up(&context, &cli, config_path.as_deref()).await { Ok(DevContainerUp { container_id, remote_workspace_folder, remote_user, .. }) => { - let project_name = match devcontainer_read_configuration( - &path_to_devcontainer_cli, - found_in_path, - &node_runtime, - &directory, - config_path.as_ref(), - use_podman, - ) - .await - { - Ok(DevContainerConfigurationOutput { - configuration: - DevContainerConfiguration { - name: Some(project_name), - }, - }) => project_name, - _ => get_backup_project_name(&remote_workspace_folder, &container_id), - }; + let project_name = + match read_devcontainer_configuration(&context, &cli, config_path.as_deref()).await + { + Ok(DevContainerConfigurationOutput { + configuration: + DevContainerConfiguration { + name: Some(project_name), + }, + }) => project_name, + _ => get_backup_project_name(&remote_workspace_folder, &container_id), + }; let connection = DevContainerConnection { name: project_name, - container_id: container_id, - use_podman, + container_id, + use_podman: context.use_podman, remote_user, }; @@ -377,9 +312,9 @@ async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> { } } -async fn ensure_devcontainer_cli( +pub(crate) async fn ensure_devcontainer_cli( node_runtime: &NodeRuntime, -) -> Result<(PathBuf, bool), DevContainerError> { +) -> Result { let mut command = util::command::new_smol_command(&dev_container_cli()); command.arg("--version"); @@ -417,7 +352,10 @@ async fn ensure_devcontainer_cli( Ok(output) => { if output.status.success() { log::info!("Found devcontainer CLI in Data dir"); - return Ok((datadir_cli_path.clone(), false)); + return Ok(DevContainerCli { + path: datadir_cli_path.clone(), + node_runtime_path: Some(node_runtime_path.clone()), + }); } else { log::error!( "Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}", @@ -457,32 +395,29 @@ async fn ensure_devcontainer_cli( ); Err(DevContainerError::DevContainerCliNotAvailable) } else { - Ok((datadir_cli_path, false)) + Ok(DevContainerCli { + path: datadir_cli_path, + node_runtime_path: Some(node_runtime_path), + }) } } else { log::info!("Found devcontainer cli on $PATH, using it"); - Ok((PathBuf::from(&dev_container_cli()), true)) + Ok(DevContainerCli { + path: PathBuf::from(&dev_container_cli()), + node_runtime_path: None, + }) } } async fn devcontainer_up( - path_to_cli: &PathBuf, - found_in_path: bool, - node_runtime: &NodeRuntime, - path: Arc, - config_path: Option, - use_podman: bool, + context: &DevContainerContext, + cli: &DevContainerCli, + config_path: Option<&Path>, ) -> Result { - let Ok(node_runtime_path) = node_runtime.binary_path().await else { - log::error!("Unable to find node runtime path"); - return Err(DevContainerError::NodeRuntimeNotAvailable); - }; - - let mut command = - devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman); + let mut command = cli.command(context.use_podman); command.arg("up"); command.arg("--workspace-folder"); - command.arg(path.display().to_string()); + command.arg(context.project_directory.display().to_string()); if let Some(config) = config_path { command.arg("--config"); @@ -515,24 +450,15 @@ async fn devcontainer_up( } } -async fn devcontainer_read_configuration( - path_to_cli: &PathBuf, - found_in_path: bool, - node_runtime: &NodeRuntime, - path: &Arc, - config_path: Option<&PathBuf>, - use_podman: bool, +pub(crate) async fn read_devcontainer_configuration( + context: &DevContainerContext, + cli: &DevContainerCli, + config_path: Option<&Path>, ) -> Result { - let Ok(node_runtime_path) = node_runtime.binary_path().await else { - log::error!("Unable to find node runtime path"); - return Err(DevContainerError::NodeRuntimeNotAvailable); - }; - - let mut command = - devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman); + let mut command = cli.command(context.use_podman); command.arg("read-configuration"); command.arg("--workspace-folder"); - command.arg(path.display().to_string()); + command.arg(context.project_directory.display().to_string()); if let Some(config) = config_path { command.arg("--config"); @@ -562,23 +488,14 @@ async fn devcontainer_read_configuration( } } -async fn devcontainer_template_apply( +pub(crate) async fn apply_dev_container_template( template: &DevContainerTemplate, template_options: &HashMap, features_selected: &HashSet, - path_to_cli: &PathBuf, - found_in_path: bool, - node_runtime: &NodeRuntime, - path: &Arc, - use_podman: bool, + context: &DevContainerContext, + cli: &DevContainerCli, ) -> Result { - let Ok(node_runtime_path) = node_runtime.binary_path().await else { - log::error!("Unable to find node runtime path"); - return Err(DevContainerError::NodeRuntimeNotAvailable); - }; - - let mut command = - devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman); + let mut command = cli.command(context.use_podman); let Ok(serialized_options) = serde_json::to_string(template_options) else { log::error!("Unable to serialize options for {:?}", template_options); @@ -588,7 +505,7 @@ async fn devcontainer_template_apply( command.arg("templates"); command.arg("apply"); command.arg("--workspace-folder"); - command.arg(path.display().to_string()); + command.arg(context.project_directory.display().to_string()); command.arg("--template-id"); command.arg(format!( "{}/{}", @@ -652,28 +569,6 @@ fn parse_json_from_cli(raw: &str) -> Result Command { - let mut command = if found_in_path { - util::command::new_smol_command(path_to_cli.display().to_string()) - } else { - let mut command = - util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string()); - command.arg(path_to_cli.display().to_string()); - command - }; - - if use_podman { - command.arg("--docker-path"); - command.arg("podman"); - } - command -} - fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) -> String { Path::new(remote_workspace_folder) .file_name() @@ -682,22 +577,6 @@ fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) -> .unwrap_or_else(|| container_id.to_string()) } -fn project_directory(cx: &mut AsyncWindowContext) -> Option> { - let Some(workspace) = cx.window_handle().downcast::() else { - return None; - }; - - match workspace.update(cx, |workspace, _, cx| { - workspace.project().read(cx).active_project_directory(cx) - }) { - Ok(dir) => dir, - Err(e) => { - log::error!("Error getting project directory from workspace: {:?}", e); - None - } - } -} - fn template_features_to_json(features_selected: &HashSet) -> String { let features_map = features_selected .iter() @@ -725,6 +604,9 @@ fn template_features_to_json(features_selected: &HashSet) - mod tests { use std::path::PathBuf; + use crate::devcontainer_api::{ + DevContainerConfig, DevContainerUp, find_configs_in_snapshot, parse_json_from_cli, + }; use fs::FakeFs; use gpui::TestAppContext; use project::Project; @@ -732,10 +614,6 @@ mod tests { use settings::SettingsStore; use util::path; - use crate::devcontainer_api::{ - DevContainerConfig, DevContainerUp, find_configs_in_snapshot, parse_json_from_cli, - }; - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/dev_container/src/lib.rs b/crates/dev_container/src/lib.rs index 735963825428c60d4af856414206905d127f7309..908be691a7ace8d5a7b64e73233f252e2f964a2b 100644 --- a/crates/dev_container/src/lib.rs +++ b/crates/dev_container/src/lib.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use gpui::AppContext; use gpui::Entity; use gpui::Task; @@ -41,7 +43,8 @@ use http_client::{AsyncBody, HttpClient}; mod devcontainer_api; -use devcontainer_api::read_devcontainer_configuration_for_project; +use devcontainer_api::ensure_devcontainer_cli; +use devcontainer_api::read_devcontainer_configuration; use crate::devcontainer_api::DevContainerError; use crate::devcontainer_api::apply_dev_container_template; @@ -51,11 +54,34 @@ pub use devcontainer_api::{ start_dev_container_with_config, }; +pub struct DevContainerContext { + pub project_directory: Arc, + pub use_podman: bool, + pub node_runtime: node_runtime::NodeRuntime, +} + +impl DevContainerContext { + pub fn from_workspace(workspace: &Workspace, cx: &App) -> Option { + let project_directory = workspace.project().read(cx).active_project_directory(cx)?; + let use_podman = DevContainerSettings::get_global(cx).use_podman; + let node_runtime = workspace.app_state().node_runtime.clone(); + Some(Self { + project_directory, + use_podman, + node_runtime, + }) + } +} + #[derive(RegisterSetting)] struct DevContainerSettings { use_podman: bool, } +pub fn use_podman(cx: &App) -> bool { + DevContainerSettings::get_global(cx).use_podman +} + impl Settings for DevContainerSettings { fn from_settings(content: &settings::SettingsContent) -> Self { Self { @@ -1420,22 +1446,41 @@ fn dispatch_apply_templates( cx: &mut Context, ) { cx.spawn_in(window, async move |this, cx| { - if let Some(tree_id) = workspace.update(cx, |workspace, cx| { - let project = workspace.project().clone(); - let worktree = project.read(cx).visible_worktrees(cx).find_map(|tree| { - tree.read(cx) - .root_entry()? - .is_dir() - .then_some(tree.read(cx)) - }); - worktree.map(|w| w.id()) - }) { - let node_runtime = workspace.read_with(cx, |workspace, _| { - workspace.app_state().node_runtime.clone() - }); + let Some((tree_id, context)) = workspace.update(cx, |workspace, cx| { + let worktree = workspace + .project() + .read(cx) + .visible_worktrees(cx) + .find_map(|tree| { + tree.read(cx) + .root_entry()? + .is_dir() + .then_some(tree.read(cx)) + }); + let tree_id = worktree.map(|w| w.id())?; + let context = DevContainerContext::from_workspace(workspace, cx)?; + Some((tree_id, context)) + }) else { + return; + }; + let Ok(cli) = ensure_devcontainer_cli(&context.node_runtime).await else { + this.update_in(cx, |this, window, cx| { + this.accept_message( + DevContainerMessage::FailedToWriteTemplate( + DevContainerError::DevContainerCliNotAvailable, + ), + window, + cx, + ); + }) + .log_err(); + return; + }; + + { if check_for_existing - && read_devcontainer_configuration_for_project(cx, &node_runtime) + && read_devcontainer_configuration(&context, &cli, None) .await .is_ok() { @@ -1454,8 +1499,8 @@ fn dispatch_apply_templates( &template_entry.template, &template_entry.options_selected, &template_entry.features_selected, - cx, - &node_runtime, + &context, + &cli, ) .await { @@ -1497,8 +1542,6 @@ fn dispatch_apply_templates( this.dismiss(&menu::Cancel, window, cx); }) .ok(); - } else { - return; } }) .detach(); diff --git a/crates/diagnostics/src/buffer_diagnostics.rs b/crates/diagnostics/src/buffer_diagnostics.rs index 6360e868d88ddeec677935beeba536d04cbc9131..42139b697d40362578bac4fae6b58d2a1ca10b27 100644 --- a/crates/diagnostics/src/buffer_diagnostics.rs +++ b/crates/diagnostics/src/buffer_diagnostics.rs @@ -904,7 +904,7 @@ impl Render for BufferDiagnosticsEditor { .style(ButtonStyle::Transparent) .tooltip(Tooltip::text("Open File")) .on_click(cx.listener(|buffer_diagnostics, _, window, cx| { - if let Some(workspace) = window.root::().flatten() { + if let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { workspace .open_path( diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index d2504fde4a6bcb828db75f85f01aea2f296bd9dd..06b71a583f5d02a103db69e17d4e2db48c98a415 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -28,6 +28,7 @@ use std::{ }; use unindent::Unindent as _; use util::{RandomCharIter, path, post_inc, rel_path::rel_path}; +use workspace::MultiWorkspace; #[ctor::ctor] fn init_logger() { @@ -68,9 +69,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) { let language_server_id = LanguageServerId(0); let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap(); // Create some diagnostics @@ -344,9 +347,11 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) { let server_id_2 = LanguageServerId(101); let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let diagnostics = window.build_entity(cx, |window, cx| { ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) @@ -453,9 +458,11 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { let server_id_2 = LanguageServerId(101); let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let diagnostics = window.build_entity(cx, |window, cx| { ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) @@ -663,9 +670,11 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let mutated_diagnostics = window.build_entity(cx, |window, cx| { ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) @@ -836,9 +845,11 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let mutated_diagnostics = window.build_entity(cx, |window, cx| { ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) @@ -1389,9 +1400,11 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) { let language_server_id = LanguageServerId(0); let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap(); // Create diagnostics with code fields @@ -1618,8 +1631,8 @@ async fn test_buffer_diagnostics(cx: &mut TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let project_path = project::ProjectPath { worktree_id: project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -1772,8 +1785,8 @@ async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let project_path = project::ProjectPath { worktree_id: project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -1901,8 +1914,8 @@ async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let project_path = project::ProjectPath { worktree_id: project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs index 8835dd5507dc9deccb57ad4f4ba15d8af017bfd3..cd95929e206696cd13942e9e37092ee8d621d0f2 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_button.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs @@ -119,7 +119,7 @@ impl Render for EditPredictionButton { IconButton::new("copilot-error", icon) .icon_size(IconSize::Small) .on_click(cx.listener(move |_, _, window, cx| { - if let Some(workspace) = window.root::().flatten() { + if let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { let copilot = copilot.clone(); workspace.show_toast( diff --git a/crates/editor/src/document_colors.rs b/crates/editor/src/document_colors.rs index 95f64e4f357b9dfb0ec33867a47bca7dcd8252cb..579414c7f91c6b2770951a2439599abc4000b27c 100644 --- a/crates/editor/src/document_colors.rs +++ b/crates/editor/src/document_colors.rs @@ -415,14 +415,14 @@ mod tests { }; use futures::StreamExt; - use gpui::{Rgba, TestAppContext, VisualTestContext}; + use gpui::{Rgba, TestAppContext}; use language::FakeLspAdapter; use languages::rust_lang; use project::{FakeFs, Project}; use serde_json::json; use util::{path, rel_path::rel_path}; use workspace::{ - CloseActiveItem, MoveItemToPaneInDirection, OpenOptions, + CloseActiveItem, MoveItemToPaneInDirection, MultiWorkspace, OpenOptions, item::{Item as _, SaveOptions}, }; @@ -460,9 +460,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -490,7 +490,7 @@ mod tests { ); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/a/first.rs")), OpenOptions::default(), @@ -498,7 +498,6 @@ mod tests { cx, ) }) - .unwrap() .await .unwrap() .downcast::() @@ -579,53 +578,49 @@ mod tests { }); // opening another file in a split should not influence the LSP query counter - workspace - .update(cx, |workspace, window, cx| { - assert_eq!( - workspace.panes().len(), - 1, - "Should have one pane with one editor" - ); - workspace.move_item_to_pane_in_direction( - &MoveItemToPaneInDirection { - direction: workspace::SplitDirection::Right, - focus: false, - clone: true, - }, - window, - cx, - ); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + assert_eq!( + workspace.panes().len(), + 1, + "Should have one pane with one editor" + ); + workspace.move_item_to_pane_in_direction( + &MoveItemToPaneInDirection { + direction: workspace::SplitDirection::Right, + focus: false, + clone: true, + }, + window, + cx, + ); + }); cx.run_until_parked(); - workspace - .update(cx, |workspace, _, cx| { - let panes = workspace.panes(); - assert_eq!(panes.len(), 2, "Should have two panes after splitting"); - for pane in panes { - let editor = pane - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - .expect("Should have opened an editor in each split"); - let editor_file = editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("test deals with singleton buffers") - .read(cx) - .file() - .expect("test buffese should have a file") - .path(); - assert_eq!( - editor_file.as_ref(), - rel_path("first.rs"), - "Both editors should be opened for the same file" - ) - } - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + let panes = workspace.panes(); + assert_eq!(panes.len(), 2, "Should have two panes after splitting"); + for pane in panes { + let editor = pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .expect("Should have opened an editor in each split"); + let editor_file = editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("test deals with singleton buffers") + .read(cx) + .file() + .expect("test buffese should have a file") + .path(); + assert_eq!( + editor_file.as_ref(), + rel_path("first.rs"), + "Both editors should be opened for the same file" + ) + } + }); cx.executor().advance_clock(Duration::from_millis(500)); let save = editor.update_in(cx, |editor, window, cx| { @@ -652,54 +647,44 @@ mod tests { ); drop(editor); - let close = workspace - .update(cx, |workspace, window, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.close_active_item(&CloseActiveItem::default(), window, cx) - }) + let close = workspace.update_in(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem::default(), window, cx) }) - .unwrap(); + }); close.await.unwrap(); - let close = workspace - .update(cx, |workspace, window, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.close_active_item(&CloseActiveItem::default(), window, cx) - }) + let close = workspace.update_in(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem::default(), window, cx) }) - .unwrap(); + }); close.await.unwrap(); assert_eq!( 2, requests_made.load(atomic::Ordering::Acquire), "After saving and closing all editors, no extra requests should be made" ); - workspace - .update(cx, |workspace, _, cx| { - assert!( - workspace.active_item(cx).is_none(), - "Should close all editors" - ) - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + assert!( + workspace.active_item(cx).is_none(), + "Should close all editors" + ) + }); - workspace - .update(cx, |workspace, window, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.navigate_backward(&workspace::GoBack, window, cx); - }) + workspace.update_in(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.navigate_backward(&workspace::GoBack, window, cx); }) - .unwrap(); + }); cx.executor().advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT); cx.run_until_parked(); - let editor = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_item(cx) - .expect("Should have reopened the editor again after navigating back") - .downcast::() - .expect("Should be an editor") - }) - .unwrap(); + let editor = workspace.update_in(cx, |workspace, _, cx| { + workspace + .active_item(cx) + .expect("Should have reopened the editor again after navigating back") + .downcast::() + .expect("Should be an editor") + }); assert_eq!( 2, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2d8f30c6ee28071ec248f571b5045641e0f526a9..bf77305f5eb80b2755907967986e07f1e3a858c2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3114,6 +3114,24 @@ impl Editor { self.workspace.as_ref()?.0.upgrade() } + /// Detaches a task and shows an error notification in the workspace if available, + /// otherwise just logs the error. + pub fn detach_and_notify_err( + &self, + task: Task>, + window: &mut Window, + cx: &mut App, + ) where + E: std::fmt::Debug + std::fmt::Display + 'static, + R: 'static, + { + if let Some(workspace) = self.workspace() { + task.detach_and_notify_err(workspace.downgrade(), window, cx); + } else { + task.detach_and_log_err(cx); + } + } + /// Returns the workspace serialization ID if this editor should be serialized. fn workspace_serialization_id(&self, _cx: &App) -> Option { self.workspace @@ -11481,8 +11499,8 @@ impl Editor { let Some(project) = self.project.clone() else { return; }; - self.reload(project, window, cx) - .detach_and_notify_err(window, cx); + let task = self.reload(project, window, cx); + self.detach_and_notify_err(task, window, cx); } pub fn restore_file( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 997058fbd8c41fd98743fc9a783c97332bcc1ddb..f0e1d601d0454c85b466532a84ebbe7db6b87297 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -67,7 +67,8 @@ use util::{ uri, }; use workspace::{ - CloseActiveItem, CloseAllItems, CloseOtherItems, NavigationEntry, OpenOptions, ViewId, + CloseActiveItem, CloseAllItems, CloseOtherItems, MultiWorkspace, NavigationEntry, OpenOptions, + ViewId, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, register_project_item, }; @@ -854,12 +855,13 @@ async fn test_navigation_history(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace - .update(cx, |workspace, _, _| workspace.active_pane().clone()) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - _ = workspace.update(cx, |_v, window, cx| { + _ = window.update(cx, |_mw, window, cx| { cx.new(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); let mut editor = build_editor(buffer, window, cx); @@ -12293,8 +12295,8 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -12492,8 +12494,8 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -15080,8 +15082,11 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte ..FakeLspAdapter::default() }, ); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let buffer = project .update(cx, |project, cx| { project.open_local_buffer(path!("/a/main.rs"), cx) @@ -15104,27 +15109,23 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte multi_buffer }); - let editor = workspace - .update(cx, |_, window, cx| { - cx.new(|cx| { - Editor::new( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sizing_behavior: SizingBehavior::Default, - }, - multi_buffer.clone(), - Some(project.clone()), - window, - cx, - ) - }) + let editor = workspace.update_in(cx, |_, window, cx| { + cx.new(|cx| { + Editor::new( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sizing_behavior: SizingBehavior::Default, + }, + multi_buffer.clone(), + Some(project.clone()), + window, + cx, + ) }) - .unwrap(); + }); - let pane = workspace - .update(cx, |workspace, _, _| workspace.active_pane().clone()) - .unwrap(); + let pane = workspace.update_in(cx, |workspace, _, _| workspace.active_pane().clone()); pane.update_in(cx, |pane, window, cx| { pane.add_item(Box::new(editor.clone()), true, true, None, window, cx); }); @@ -15483,10 +15484,13 @@ async fn test_completion_can_run_commands(cx: &mut TestAppContext) { ..FakeLspAdapter::default() }, ); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/a/main.rs")), OpenOptions::default(), @@ -15494,7 +15498,6 @@ async fn test_completion_can_run_commands(cx: &mut TestAppContext) { cx, ) }) - .unwrap() .await .unwrap() .downcast::() @@ -16211,15 +16214,17 @@ async fn test_multiline_completion(cx: &mut TestAppContext) { ..FakeLspAdapter::default() }, ); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let worktree_id = workspace - .update(cx, |workspace, _window, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); + let worktree_id = workspace.update_in(cx, |workspace, _window, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + let _buffer = project .update(cx, |project, cx| { project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx) @@ -16227,10 +16232,9 @@ async fn test_multiline_completion(cx: &mut TestAppContext) { .await .unwrap(); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx) }) - .unwrap() .await .unwrap() .downcast::() @@ -17915,12 +17919,13 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace - .update(cx, |workspace, _, _| workspace.active_pane().clone()) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let cx = &mut VisualTestContext::from_window(*window, cx); let leader = pane.update_in(cx, |_, window, cx| { let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite)); @@ -17930,9 +17935,9 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) { // Start following the editor when it has no excerpts. let mut state_message = leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx)); - let workspace_entity = workspace.root(cx).unwrap(); + let workspace_entity = workspace.clone(); let follower_1 = cx - .update_window(*workspace.deref(), |_, window, cx| { + .update_window(*window, |_, window, cx| { Editor::from_state_proto( workspace_entity, ViewId { @@ -18014,9 +18019,9 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) { // Start following separately after it already has excerpts. let mut state_message = leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx)); - let workspace_entity = workspace.root(cx).unwrap(); + let workspace_entity = workspace.clone(); let follower_2 = cx - .update_window(*workspace.deref(), |_, window, cx| { + .update_window(*window, |_, window, cx| { Editor::from_state_proto( workspace_entity, ViewId { @@ -18580,17 +18585,18 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) { }, ); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let cx = &mut VisualTestContext::from_window(*window, cx); - let worktree_id = workspace - .update(cx, |workspace, _, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) + let worktree_id = workspace.update_in(cx, |workspace, _, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() }) - .unwrap(); + }); let buffer = project .update(cx, |project, cx| { @@ -18599,10 +18605,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) { .await .unwrap(); let editor_handle = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx) }) - .unwrap() .await .unwrap() .downcast::() @@ -18749,7 +18754,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon }, ); - let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let _buffer = project .update(cx, |project, cx| { project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx) @@ -20456,8 +20461,11 @@ async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) { ) .await; let project = Project::test(fs, ["/a".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let multi_buffer_editor = cx.new_window_entity(|window, cx| { Editor::new( EditorMode::full(), @@ -20467,30 +20475,29 @@ async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) { cx, ) }); - let multibuffer_item_id = workspace - .update(cx, |workspace, window, cx| { - assert!( - workspace.active_item(cx).is_none(), - "active item should be None before the first item is added" - ); - workspace.add_item_to_active_pane( - Box::new(multi_buffer_editor.clone()), - None, - true, - window, - cx, - ); - let active_item = workspace - .active_item(cx) - .expect("should have an active item after adding the multi buffer"); - assert_eq!( - active_item.buffer_kind(cx), - ItemBufferKind::Multibuffer, - "A multi buffer was expected to active after adding" - ); - active_item.item_id() - }) - .unwrap(); + let multibuffer_item_id = workspace.update_in(cx, |workspace, window, cx| { + assert!( + workspace.active_item(cx).is_none(), + "active item should be None before the first item is added" + ); + workspace.add_item_to_active_pane( + Box::new(multi_buffer_editor.clone()), + None, + true, + window, + cx, + ); + let active_item = workspace + .active_item(cx) + .expect("should have an active item after adding the multi buffer"); + assert_eq!( + active_item.buffer_kind(cx), + ItemBufferKind::Multibuffer, + "A multi buffer was expected to active after adding" + ); + active_item.item_id() + }); + cx.executor().run_until_parked(); multi_buffer_editor.update_in(cx, |editor, window, cx| { @@ -20503,51 +20510,48 @@ async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) { editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); - let first_item_id = workspace - .update(cx, |workspace, window, cx| { - let active_item = workspace - .active_item(cx) - .expect("should have an active item after navigating into the 1st buffer"); - let first_item_id = active_item.item_id(); - assert_ne!( - first_item_id, multibuffer_item_id, - "Should navigate into the 1st buffer and activate it" - ); - assert_eq!( - active_item.buffer_kind(cx), - ItemBufferKind::Singleton, - "New active item should be a singleton buffer" - ); - assert_eq!( - active_item - .act_as::(cx) - .expect("should have navigated into an editor for the 1st buffer") - .read(cx) - .text(cx), - sample_text_1 - ); + let first_item_id = workspace.update_in(cx, |workspace, window, cx| { + let active_item = workspace + .active_item(cx) + .expect("should have an active item after navigating into the 1st buffer"); + let first_item_id = active_item.item_id(); + assert_ne!( + first_item_id, multibuffer_item_id, + "Should navigate into the 1st buffer and activate it" + ); + assert_eq!( + active_item.buffer_kind(cx), + ItemBufferKind::Singleton, + "New active item should be a singleton buffer" + ); + assert_eq!( + active_item + .act_as::(cx) + .expect("should have navigated into an editor for the 1st buffer") + .read(cx) + .text(cx), + sample_text_1 + ); - workspace - .go_back(workspace.active_pane().downgrade(), window, cx) - .detach_and_log_err(cx); + workspace + .go_back(workspace.active_pane().downgrade(), window, cx) + .detach_and_log_err(cx); + + first_item_id + }); - first_item_id - }) - .unwrap(); cx.executor().run_until_parked(); - workspace - .update(cx, |workspace, _, cx| { - let active_item = workspace - .active_item(cx) - .expect("should have an active item after navigating back"); - assert_eq!( - active_item.item_id(), - multibuffer_item_id, - "Should navigate back to the multi buffer" - ); - assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + let active_item = workspace + .active_item(cx) + .expect("should have an active item after navigating back"); + assert_eq!( + active_item.item_id(), + multibuffer_item_id, + "Should navigate back to the multi buffer" + ); + assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer); + }); multi_buffer_editor.update_in(cx, |editor, window, cx| { editor.change_selections( @@ -20559,55 +20563,52 @@ async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) { editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); - let second_item_id = workspace - .update(cx, |workspace, window, cx| { - let active_item = workspace - .active_item(cx) - .expect("should have an active item after navigating into the 2nd buffer"); - let second_item_id = active_item.item_id(); - assert_ne!( - second_item_id, multibuffer_item_id, - "Should navigate away from the multibuffer" - ); - assert_ne!( - second_item_id, first_item_id, - "Should navigate into the 2nd buffer and activate it" - ); - assert_eq!( - active_item.buffer_kind(cx), - ItemBufferKind::Singleton, - "New active item should be a singleton buffer" - ); - assert_eq!( - active_item - .act_as::(cx) - .expect("should have navigated into an editor") - .read(cx) - .text(cx), - sample_text_2 - ); + let second_item_id = workspace.update_in(cx, |workspace, window, cx| { + let active_item = workspace + .active_item(cx) + .expect("should have an active item after navigating into the 2nd buffer"); + let second_item_id = active_item.item_id(); + assert_ne!( + second_item_id, multibuffer_item_id, + "Should navigate away from the multibuffer" + ); + assert_ne!( + second_item_id, first_item_id, + "Should navigate into the 2nd buffer and activate it" + ); + assert_eq!( + active_item.buffer_kind(cx), + ItemBufferKind::Singleton, + "New active item should be a singleton buffer" + ); + assert_eq!( + active_item + .act_as::(cx) + .expect("should have navigated into an editor") + .read(cx) + .text(cx), + sample_text_2 + ); - workspace - .go_back(workspace.active_pane().downgrade(), window, cx) - .detach_and_log_err(cx); + workspace + .go_back(workspace.active_pane().downgrade(), window, cx) + .detach_and_log_err(cx); + + second_item_id + }); - second_item_id - }) - .unwrap(); cx.executor().run_until_parked(); - workspace - .update(cx, |workspace, _, cx| { - let active_item = workspace - .active_item(cx) - .expect("should have an active item after navigating back from the 2nd buffer"); - assert_eq!( - active_item.item_id(), - multibuffer_item_id, - "Should navigate back from the 2nd buffer to the multi buffer" - ); - assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + let active_item = workspace + .active_item(cx) + .expect("should have an active item after navigating back from the 2nd buffer"); + assert_eq!( + active_item.item_id(), + multibuffer_item_id, + "Should navigate back from the 2nd buffer to the multi buffer" + ); + assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer); + }); multi_buffer_editor.update_in(cx, |editor, window, cx| { editor.change_selections( @@ -20619,51 +20620,48 @@ async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) { editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); - workspace - .update(cx, |workspace, window, cx| { - let active_item = workspace - .active_item(cx) - .expect("should have an active item after navigating into the 3rd buffer"); - let third_item_id = active_item.item_id(); - assert_ne!( - third_item_id, multibuffer_item_id, - "Should navigate into the 3rd buffer and activate it" - ); - assert_ne!(third_item_id, first_item_id); - assert_ne!(third_item_id, second_item_id); - assert_eq!( - active_item.buffer_kind(cx), - ItemBufferKind::Singleton, - "New active item should be a singleton buffer" - ); - assert_eq!( - active_item - .act_as::(cx) - .expect("should have navigated into an editor") - .read(cx) - .text(cx), - sample_text_3 - ); + workspace.update_in(cx, |workspace, window, cx| { + let active_item = workspace + .active_item(cx) + .expect("should have an active item after navigating into the 3rd buffer"); + let third_item_id = active_item.item_id(); + assert_ne!( + third_item_id, multibuffer_item_id, + "Should navigate into the 3rd buffer and activate it" + ); + assert_ne!(third_item_id, first_item_id); + assert_ne!(third_item_id, second_item_id); + assert_eq!( + active_item.buffer_kind(cx), + ItemBufferKind::Singleton, + "New active item should be a singleton buffer" + ); + assert_eq!( + active_item + .act_as::(cx) + .expect("should have navigated into an editor") + .read(cx) + .text(cx), + sample_text_3 + ); + + workspace + .go_back(workspace.active_pane().downgrade(), window, cx) + .detach_and_log_err(cx); + }); - workspace - .go_back(workspace.active_pane().downgrade(), window, cx) - .detach_and_log_err(cx); - }) - .unwrap(); cx.executor().run_until_parked(); - workspace - .update(cx, |workspace, _, cx| { - let active_item = workspace - .active_item(cx) - .expect("should have an active item after navigating back from the 3rd buffer"); - assert_eq!( - active_item.item_id(), - multibuffer_item_id, - "Should navigate back from the 3rd buffer to the multi buffer" - ); - assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + let active_item = workspace + .active_item(cx) + .expect("should have an active item after navigating back from the 3rd buffer"); + assert_eq!( + active_item.item_id(), + multibuffer_item_id, + "Should navigate back from the 3rd buffer to the multi buffer" + ); + assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer); + }); } #[gpui::test] @@ -23513,8 +23511,8 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) { fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/a".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); @@ -23586,8 +23584,8 @@ async fn test_folding_buffers(cx: &mut TestAppContext) { ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let worktree = project.update(cx, |project, cx| { let mut worktrees = project.worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -23754,8 +23752,8 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) { ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let worktree = project.update(cx, |project, cx| { let mut worktrees = project.worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -23889,8 +23887,8 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let worktree = project.update(cx, |project, cx| { let mut worktrees = project.worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -24415,8 +24413,8 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( @@ -24427,15 +24425,16 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - let worktree_id = workspace - .update(cx, |workspace, _window, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); + let worktree_id = workspace.update_in(cx, |workspace, _window, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); let buffer = project .update(cx, |project, cx| { @@ -24542,8 +24541,9 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { @@ -24699,8 +24699,8 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( @@ -24711,15 +24711,16 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - let worktree_id = workspace - .update(cx, |workspace, _window, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); + let worktree_id = workspace.update_in(cx, |workspace, _window, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); let buffer = project .update(cx, |project, cx| { @@ -24856,15 +24857,16 @@ async fn test_breakpoint_phantom_indicator_collision_on_toggle(cx: &mut TestAppC ) .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - let worktree_id = workspace - .update(cx, |workspace, _window, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); + let worktree_id = workspace.update_in(cx, |workspace, _window, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); let buffer = project .update(cx, |project, cx| { @@ -25155,8 +25157,11 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(Arc::new(Language::new( @@ -25188,7 +25193,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex ); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/dir/a.ts")), OpenOptions::default(), @@ -25196,7 +25201,6 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex cx, ) }) - .unwrap() .await .unwrap() .downcast::() @@ -25217,7 +25221,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left); drop(buffer_snapshot); let actions = cx - .update_window(*workspace, |_, window, cx| { + .update_window(*window, |_, window, cx| { project.code_actions(&buffer, anchor..anchor, window, cx) }) .unwrap(); @@ -25342,12 +25346,9 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex }); let actions_after_edits = cx - .update_window(*workspace, |_, window, cx| { - project.code_actions(&buffer, anchor..anchor, window, cx) - }) + .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx)) .unwrap() - .await - .unwrap(); + .await; assert_eq!( actions, actions_after_edits, "For the same selection, same code lens actions should be returned" @@ -25362,12 +25363,9 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex }); cx.executor().run_until_parked(); let new_actions = cx - .update_window(*workspace, |_, window, cx| { - project.code_actions(&buffer, anchor..anchor, window, cx) - }) + .update(|window, cx| project.code_actions(&buffer, anchor..anchor, window, cx)) .unwrap() - .await - .unwrap(); + .await; assert_eq!( actions, new_actions, "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now" @@ -25397,8 +25395,9 @@ println!("5"); .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -25667,8 +25666,9 @@ println!("5"); .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -25788,9 +25788,12 @@ async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let cx = &mut VisualTestContext::from_window(*window, cx); let editor = cx.new_window_entity(|window, cx| { Editor::new( EditorMode::full(), @@ -25800,20 +25803,18 @@ async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) { cx, ) }); - workspace - .update(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + }); + editor.update_in(cx, |editor, window, cx| { editor.open_context_menu(&OpenContextMenu, window, cx); assert!(editor.mouse_context_menu.is_some()); }); - workspace - .update(cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx)); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx)); + }); + cx.read(|cx| { assert!(editor.read(cx).mouse_context_menu.is_none()); }); @@ -25887,16 +25888,18 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { }, ); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); - let worktree_id = workspace - .update(cx, |workspace, _window, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) + let worktree_id = workspace.update_in(cx, |workspace, _window, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() }) - .unwrap(); + }); + project .update(cx, |project, cx| { project.open_local_buffer_with_lsp(path!("/file.html"), cx) @@ -25904,10 +25907,9 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { .await .unwrap(); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx) }) - .unwrap() .await .unwrap() .downcast::() @@ -26069,8 +26071,9 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { ..FakeLspAdapter::default() }, ); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -27596,8 +27599,11 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { .await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -27620,7 +27626,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { ); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/a/first.rs")), OpenOptions::default(), @@ -27628,7 +27634,6 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { cx, ) }) - .unwrap() .await .unwrap() .downcast::() @@ -28168,8 +28173,9 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) { .await; let project = Project::test(fs, ["/root1".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = project.update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -28741,8 +28747,8 @@ async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) { .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); let language = rust_lang(); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); @@ -30529,11 +30535,14 @@ async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppCont .await; let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/root/file.txt")), OpenOptions::default(), @@ -30541,7 +30550,6 @@ async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppCont cx, ) }) - .unwrap() .await .unwrap() .downcast::() @@ -30579,11 +30587,14 @@ async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext .await; let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/root/file.txt")), OpenOptions::default(), @@ -30591,7 +30602,6 @@ async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext cx, ) }) - .unwrap() .await .unwrap() .downcast::() @@ -30638,11 +30648,14 @@ async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) .await; let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/root/file.txt")), OpenOptions::default(), @@ -30650,7 +30663,6 @@ async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) cx, ) }) - .unwrap() .await .unwrap() .downcast::() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d1fd99f09217dc736c12c3f8902fc2bdb777e03d..340610faa984d993728416d4b026bd67e2809003 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -99,7 +99,6 @@ use workspace::{ CollaboratorId, ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::{BreadcrumbText, Item, ItemBufferKind}, - notifications::NotifyTaskExt, }; /// Determines what kinds of highlights should be applied to a lines background. @@ -541,21 +540,21 @@ impl EditorElement { register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.format(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.format_selections(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.organize_imports(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } @@ -565,49 +564,49 @@ impl EditorElement { register_action(editor, window, Editor::show_character_palette); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_completion(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_completion_replace(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_completion_insert(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.compose_completion(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_code_action(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.rename(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_rename(action, window, cx) { - task.detach_and_notify_err(window, cx); + editor.detach_and_notify_err(task, window, cx); } else { cx.propagate(); } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 1e8b42988cc7abaa4fb8a55e3580a70566d8046c..b3039a1545f634302e2767f3c0a2073f3a772827 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -719,7 +719,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) { if let Ok(uri) = Url::parse(&link) && uri.scheme() == "file" - && let Some(workspace) = window.root::().flatten() + && let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { let task = workspace.open_abs_path( diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 73247cd3ce398d00cfb3519b9b22f26ac68ca67d..3de25131508cc39edc6cefc500c05fbcc9bb33fb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -2017,6 +2017,7 @@ fn restore_serialized_buffer_contents( mod tests { use crate::editor_tests::init_test; use fs::Fs; + use workspace::MultiWorkspace; use super::*; use fs::MTime; @@ -2071,8 +2072,10 @@ mod tests { // Test case 1: Deserialize with path and contents { let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + MultiWorkspace::test_new(project.clone(), window, cx) + }); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let item_id = 1234 as ItemId; let mtime = fs @@ -2108,8 +2111,10 @@ mod tests { // Test case 2: Deserialize with only path { let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + MultiWorkspace::test_new(project.clone(), window, cx) + }); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); @@ -2146,8 +2151,10 @@ mod tests { project.languages().add(languages::rust_lang()) }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + MultiWorkspace::test_new(project.clone(), window, cx) + }); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); @@ -2182,8 +2189,10 @@ mod tests { // Test case 4: Deserialize with path, content, and old mtime { let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + MultiWorkspace::test_new(project.clone(), window, cx) + }); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); @@ -2212,8 +2221,10 @@ mod tests { // Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer) { let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + MultiWorkspace::test_new(project.clone(), window, cx) + }); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); @@ -2252,8 +2263,10 @@ mod tests { // Create an empty project with no worktrees let project = Project::test(fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + MultiWorkspace::test_new(project.clone(), window, cx) + }); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let item_id = 11000 as ItemId; diff --git a/crates/editor/src/semantic_tokens.rs b/crates/editor/src/semantic_tokens.rs index 24188ed5d84b033e771e4fbe4a1579c57376d828..252d7142a820ebca1cdb16d1bb5180dfbe43c93f 100644 --- a/crates/editor/src/semantic_tokens.rs +++ b/crates/editor/src/semantic_tokens.rs @@ -417,14 +417,12 @@ fn convert_token( #[cfg(test)] mod tests { use std::{ - ops::{Deref as _, Range}, + ops::Range, sync::atomic::{self, AtomicUsize}, }; use futures::StreamExt as _; - use gpui::{ - AppContext as _, Entity, Focusable as _, HighlightStyle, TestAppContext, VisualTestContext, - }; + use gpui::{AppContext as _, Entity, Focusable as _, HighlightStyle, TestAppContext}; use language::{Language, LanguageConfig, LanguageMatcher}; use languages::FakeLspAdapter; use multi_buffer::{ @@ -434,7 +432,7 @@ mod tests { use rope::Point; use serde_json::json; use settings::{LanguageSettingsContent, SemanticTokenRules, SemanticTokens, SettingsStore}; - use workspace::{Workspace, WorkspaceHandle as _}; + use workspace::{MultiWorkspace, WorkspaceHandle as _}; use crate::{ Capability, @@ -854,12 +852,11 @@ mod tests { ) .await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let mut cx = VisualTestContext::from_window(*window.deref(), cx); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); project - .update(&mut cx, |project, cx| { + .update(cx, |project, cx| { project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx) }) .await @@ -869,7 +866,7 @@ mod tests { let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let toml_item = workspace - .update_in(&mut cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_path(toml_file, None, true, window, cx) }) .await @@ -881,7 +878,7 @@ mod tests { .expect("Opened test file wasn't an editor") }); - editor.update_in(&mut cx, |editor, window, cx| { + editor.update_in(cx, |editor, window, cx| { let nav_history = workspace .read(cx) .active_pane() @@ -895,11 +892,11 @@ mod tests { let _toml_server_2 = toml_server_2.next().await.unwrap(); // Trigger semantic tokens. - editor.update_in(&mut cx, |editor, _, cx| { + editor.update_in(cx, |editor, _, cx| { editor.edit([(MultiBufferOffset(0)..MultiBufferOffset(1), "b")], cx); }); cx.executor().advance_clock(Duration::from_millis(200)); - let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); + let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task()); cx.run_until_parked(); task.await; @@ -1074,12 +1071,11 @@ mod tests { ) .await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let mut cx = VisualTestContext::from_window(*window.deref(), cx); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); project - .update(&mut cx, |project, cx| { + .update(cx, |project, cx| { project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx) }) .await @@ -1089,7 +1085,7 @@ mod tests { let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[1].clone()); let rust_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); - let (toml_item, rust_item) = workspace.update_in(&mut cx, |workspace, window, cx| { + let (toml_item, rust_item) = workspace.update_in(cx, |workspace, window, cx| { ( workspace.open_path(toml_file, None, true, window, cx), workspace.open_path(rust_file, None, true, window, cx), @@ -1139,12 +1135,12 @@ mod tests { multibuffer }); - let editor = workspace.update_in(&mut cx, |workspace, window, cx| { + let editor = workspace.update_in(cx, |workspace, window, cx| { let editor = cx.new(|cx| build_editor_with_project(project, multibuffer, window, cx)); workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); editor }); - editor.update_in(&mut cx, |editor, window, cx| { + editor.update_in(cx, |editor, window, cx| { let nav_history = workspace .read(cx) .active_pane() @@ -1159,7 +1155,7 @@ mod tests { // Initial request. cx.executor().advance_clock(Duration::from_millis(200)); - let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); + let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task()); cx.run_until_parked(); task.await; assert_eq!(full_counter_toml.load(atomic::Ordering::Acquire), 1); @@ -1174,8 +1170,8 @@ mod tests { // Get the excerpt id for the TOML excerpt and expand it down by 2 lines. let toml_excerpt_id = - editor.read_with(&cx, |editor, cx| editor.buffer().read(cx).excerpt_ids()[0]); - editor.update_in(&mut cx, |editor, _, cx| { + editor.read_with(cx, |editor, cx| editor.buffer().read(cx).excerpt_ids()[0]); + editor.update_in(cx, |editor, _, cx| { editor.buffer().update(cx, |buffer, cx| { buffer.expand_excerpts([toml_excerpt_id], 2, ExpandExcerptDirection::Down, cx); }); @@ -1183,7 +1179,7 @@ mod tests { // Wait for semantic tokens to be re-fetched after expansion. cx.executor().advance_clock(Duration::from_millis(200)); - let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); + let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task()); cx.run_until_parked(); task.await; @@ -1306,12 +1302,11 @@ mod tests { ) .await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let mut cx = VisualTestContext::from_window(*window.deref(), cx); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); project - .update(&mut cx, |project, cx| { + .update(cx, |project, cx| { project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx) }) .await @@ -1321,7 +1316,7 @@ mod tests { let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let toml_item = workspace - .update_in(&mut cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_path(toml_file, None, true, window, cx) }) .await @@ -1355,10 +1350,10 @@ mod tests { multibuffer }); - let editor = workspace.update_in(&mut cx, |_, window, cx| { + let editor = workspace.update_in(cx, |_, window, cx| { cx.new(|cx| build_editor_with_project(project, multibuffer, window, cx)) }); - editor.update_in(&mut cx, |editor, window, cx| { + editor.update_in(cx, |editor, window, cx| { let nav_history = workspace .read(cx) .active_pane() @@ -1372,7 +1367,7 @@ mod tests { // Initial request. cx.executor().advance_clock(Duration::from_millis(200)); - let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); + let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task()); cx.run_until_parked(); task.await; assert_eq!(full_counter_toml.load(atomic::Ordering::Acquire), 1); @@ -1381,12 +1376,12 @@ mod tests { // // Without debouncing, this grabs semantic tokens 4 times (twice for the // toml editor, and twice for the multibuffer). - editor.update_in(&mut cx, |editor, _, cx| { + editor.update_in(cx, |editor, _, cx| { editor.edit([(MultiBufferOffset(0)..MultiBufferOffset(1), "b")], cx); editor.edit([(MultiBufferOffset(12)..MultiBufferOffset(13), "c")], cx); }); cx.executor().advance_clock(Duration::from_millis(200)); - let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); + let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task()); cx.run_until_parked(); task.await; assert_eq!( diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 73a7f1dc4e5334cd3f681b84a3f90b1b02dca919..02335853734d11edce759ce116fc471b81cfb012 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -2087,7 +2087,7 @@ mod tests { use rand::rngs::StdRng; use settings::{DiffViewStyle, SettingsStore}; use ui::{VisualContext as _, div, px}; - use workspace::Workspace; + use workspace::MultiWorkspace; use crate::SplittableEditor; use crate::display_map::{BlockPlacement, BlockProperties, BlockStyle}; @@ -2105,8 +2105,9 @@ mod tests { crate::init(cx); }); let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let rhs_multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); multibuffer.set_all_diff_hunks_expanded(cx); diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index e372fdbe4ac93325532b96e43f11d501977418d4..d1e5270d6c76e166a33a41df2843138f4b12c411 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -22,7 +22,7 @@ use language::{ use lsp::{notification, request}; use project::Project; use smol::stream::StreamExt; -use workspace::{AppState, Workspace, WorkspaceHandle}; +use workspace::{AppState, MultiWorkspace, Workspace, WorkspaceHandle}; use super::editor_test_context::{AssertionContextManager, EditorTestContext}; @@ -95,7 +95,8 @@ impl EditorLspTestContext { ) .await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let workspace = window.root(cx).unwrap(); @@ -106,12 +107,20 @@ impl EditorLspTestContext { }) .await .unwrap(); - cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) - .await; - let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); + cx.read(|cx| { + workspace + .read(cx) + .workspace() + .read(cx) + .worktree_scans_complete(cx) + }) + .await; + let file = cx.read(|cx| workspace.read(cx).workspace().file_project_paths(cx)[0].clone()); let item = workspace .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path(file, None, true, window, cx) + workspace.workspace().update(cx, |workspace, cx| { + workspace.open_path(file, None, true, window, cx) + }) }) .await .expect("Could not open test file"); @@ -121,6 +130,8 @@ impl EditorLspTestContext { }); editor.update_in(&mut cx, |editor, window, cx| { let nav_history = workspace + .read(cx) + .workspace() .read(cx) .active_pane() .read(cx) @@ -134,6 +145,8 @@ impl EditorLspTestContext { // Ensure the language server is fully registered with the buffer cx.executor().run_until_parked(); + let workspace = cx.read(|cx| workspace.read(cx).workspace().clone()); + Self { cx: EditorTestContext { cx, diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index 17722e1d8452244627f8a80e79fb8ac17704a8fd..b94264879deb87b2880ef0d62ecf08489dfa8655 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -16,6 +16,10 @@ pub struct AgentV2FeatureFlag; impl FeatureFlag for AgentV2FeatureFlag { const NAME: &'static str = "agent-v2"; + + fn enabled_for_staff() -> bool { + true + } } pub struct AcpBetaFeatureFlag; diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 73533c57f156cdfba04ca736eeed5b0d23d2ee8f..71fcfba76363f7d6a3d6d5d37d8f87f3b6a6cdfb 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1566,9 +1566,12 @@ impl PickerDelegate for FileFinderDelegate { .unwrap_or(0) .saturating_sub(1); let finder = self.file_finder.clone(); + let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |_, cx| { - let item = open_task.await.notify_async_err(cx)?; + cx.spawn_in(window, async move |_, mut cx| { + let item = open_task + .await + .notify_workspace_async_err(workspace, &mut cx)?; if let Some(row) = row && let Some(active_editor) = item.downcast::() { diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 1b1421e8b8978a55d89db746b894486888342a65..b477f0671a49e2c28cd08d4ea6c188f2527ff8ae 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -9,7 +9,9 @@ use project::{FS_WATCH_LATENCY, RemoveOptions}; use serde_json::json; use settings::SettingsStore; use util::{path, rel_path::rel_path}; -use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths}; +use workspace::{ + AppState, CloseActiveItem, MultiWorkspace, OpenOptions, ToggleFileFinder, Workspace, open_paths, +}; #[ctor::ctor] fn init_logger() { @@ -1109,7 +1111,9 @@ async fn test_history_items_uniqueness_for_multiple_worktree(cx: &mut TestAppCon ) .await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let (worktree_id1, worktree_id2) = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); (worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) @@ -1207,7 +1211,9 @@ async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) { ) .await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let (_worktree_id1, worktree_id2) = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); (worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) @@ -1282,7 +1288,9 @@ async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppCon ) .await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let (_worktree_id1, worktree_id2) = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); (worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) @@ -1334,7 +1342,9 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); @@ -1423,7 +1433,9 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -1565,7 +1577,9 @@ async fn test_history_match_positions(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); workspace.update_in(cx, |_workspace, window, cx| window.focused(cx)); @@ -1642,7 +1656,9 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) { .detach(); cx.background_executor.run_until_parked(); - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1,); @@ -1741,7 +1757,9 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // generate some history to select from open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; @@ -1797,7 +1815,9 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1,); @@ -1903,7 +1923,9 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // generate some history to select from open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await; open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await; @@ -1957,7 +1979,9 @@ async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppCon .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // Open new buffer open_queried_buffer("1", 1, "1_qw", &workspace, cx).await; @@ -1991,7 +2015,9 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one( .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await; @@ -2099,7 +2125,9 @@ async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppC .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await; @@ -2155,7 +2183,9 @@ async fn test_non_separate_history_items(cx: &mut TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await; @@ -2250,7 +2280,9 @@ async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("2", 1, "2.txt", &workspace, cx).await; @@ -2308,7 +2340,9 @@ async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut .await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await; @@ -2369,7 +2403,9 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // generate some history to select from open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; @@ -2414,7 +2450,9 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); // generate some history to select from + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; @@ -2462,8 +2500,9 @@ async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAp .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // Initial state let picker = open_file_picker(&workspace, cx); @@ -2534,8 +2573,14 @@ async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = cx.add_window({ + let project = project.clone(); + |window, cx| MultiWorkspace::test_new(project, window, cx) + }); + let cx = VisualTestContext::from_window(*window, cx).into_mut(); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); cx.update(|_, cx| { open_paths( @@ -2589,8 +2634,9 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees( .await; let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_1_id = project.update(cx, |project, cx| { let worktree = project.worktrees(cx).last().expect("worktree not found"); worktree.read(cx).id() @@ -2680,7 +2726,9 @@ async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files( ) .await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let (worktree_id1, worktree_id2) = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); (worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) @@ -2804,8 +2852,9 @@ async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpu } let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // Initial state let picker = open_file_picker(&workspace, cx); @@ -2863,8 +2912,9 @@ async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list( .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // Initial state let picker = open_file_picker(&workspace, cx); @@ -2902,7 +2952,9 @@ async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui:: .await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; @@ -2930,7 +2982,9 @@ async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) .await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("2", 1, "2.txt", &workspace, cx).await; @@ -2970,7 +3024,9 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav( .await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("2", 1, "2.txt", &workspace, cx).await; @@ -3026,7 +3082,9 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav( .await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("2", 1, "2.txt", &workspace, cx).await; @@ -3081,7 +3139,9 @@ async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::Test .await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; @@ -3112,7 +3172,9 @@ async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); cx.dispatch_action(ToggleFileFinder::default()); let picker = active_file_picker(&workspace, cx); @@ -3231,7 +3293,9 @@ fn build_find_picker( Entity, &mut VisualTestContext, ) { - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let picker = open_file_picker(&workspace, cx); (picker, workspace, cx) } @@ -3469,7 +3533,9 @@ async fn test_clear_navigation_history(cx: &mut TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); workspace.update_in(cx, |_workspace, window, cx| window.focused(cx)); diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 0e4ea56ccfa1bcbd6534109bc439f35ba4c9b6ec..08290cb88a273d1f3f17da5c08a5b4a402aa74cd 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -1295,6 +1295,7 @@ mod tests { use serde_json::json; use settings::SettingsStore; use util::path; + use workspace::MultiWorkspace; fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { @@ -1347,13 +1348,17 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); - let branch_list = workspace - .update(cx, |workspace, window, cx| { + let branch_list = window_handle + .update(cx, |_multi_workspace, window, cx| { cx.new(|cx| { let mut delegate = BranchListDelegate::new( - workspace.weak_handle(), + workspace.downgrade(), repository, BranchListStyle::Modal, cx, @@ -1380,7 +1385,7 @@ mod tests { }) .unwrap(); - let cx = VisualTestContext::from_window(*workspace, cx); + let cx = VisualTestContext::from_window(window_handle.into(), cx); (branch_list, cx) } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 79f581777485b08952b95f2097f2e7083de35c98..7eee1ce7640784fd37efe69b5f6f92b7cbc438ec 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -774,7 +774,7 @@ impl CommitView { callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?; anyhow::Ok(()) }) - .detach_and_notify_err(window, cx); + .detach_and_notify_err(workspace.weak_handle(), window, cx); } async fn close_commit_view( diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index e985f02d8d06d50076643af74199fe488b05ae70..14e2e96089cabea939967616568fc89fa56e4890 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -6,7 +6,7 @@ use editor::{Editor, EditorEvent, MultiBuffer}; use futures::{FutureExt, select_biased}; use gpui::{ AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, - Focusable, IntoElement, Render, Task, Window, + Focusable, IntoElement, Render, Task, WeakEntity, Window, }; use language::{Buffer, LanguageRegistry}; use project::Project; @@ -40,11 +40,10 @@ impl FileDiffView { pub fn open( old_path: PathBuf, new_path: PathBuf, - workspace: &Workspace, + workspace: WeakEntity, window: &mut Window, cx: &mut App, ) -> Task>> { - let workspace = workspace.weak_handle(); window.spawn(cx, async move |cx| { let project = workspace.update(cx, |workspace, _| workspace.project().clone())?; let old_buffer = project @@ -374,7 +373,7 @@ mod tests { use std::path::PathBuf; use unindent::unindent; use util::path; - use workspace::Workspace; + use workspace::MultiWorkspace; fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { @@ -400,15 +399,16 @@ mod tests { let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let diff_view = workspace .update_in(cx, |workspace, window, cx| { FileDiffView::open( path!("/test/old_file.txt").into(), path!("/test/new_file.txt").into(), - workspace, + workspace.weak_handle(), window, cx, ) @@ -534,15 +534,16 @@ mod tests { let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let diff_view = workspace .update_in(cx, |workspace, window, cx| { FileDiffView::open( PathBuf::from(path!("/test/old_file.txt")), PathBuf::from(path!("/test/new_file.txt")), - workspace, + workspace.weak_handle(), window, cx, ) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index a4d26bc5a5a995f7bbc7af7df1cfef18dfe0b3d8..0afbbaa2c3027d34394b19ae15d609b6279cc2ce 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1274,10 +1274,11 @@ impl GitPanel { }) .ok()?; + let workspace = self.workspace.clone(); cx.spawn_in(window, async move |_, mut cx| { let item = open_task .await - .notify_async_err(&mut cx) + .notify_workspace_async_err(workspace, &mut cx) .ok_or_else(|| anyhow::anyhow!("Failed to open file"))?; if let Some(active_editor) = item.downcast::() { if let Some(diff_task) = @@ -6262,6 +6263,8 @@ mod tests { use util::path; use util::rel_path::rel_path; + use workspace::MultiWorkspace; + use super::*; fn init_test(cx: &mut gpui::TestAppContext) { @@ -6308,9 +6311,12 @@ mod tests { let project = Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); cx.read(|cx| { project @@ -6327,7 +6333,7 @@ mod tests { cx.executor().run_until_parked(); - let panel = workspace.update(cx, GitPanel::new).unwrap(); + let panel = workspace.update_in(cx, GitPanel::new); let handle = cx.update_window_entity(&panel, |panel, _, _| { std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) @@ -6429,9 +6435,12 @@ mod tests { ); let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); cx.read(|cx| { project @@ -6448,7 +6457,7 @@ mod tests { cx.executor().run_until_parked(); - let panel = workspace.update(cx, GitPanel::new).unwrap(); + let panel = workspace.update_in(cx, GitPanel::new); let handle = cx.update_window_entity(&panel, |panel, _, _| { std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) @@ -6621,9 +6630,12 @@ mod tests { ); let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); cx.read(|cx| { project @@ -6640,7 +6652,7 @@ mod tests { cx.executor().run_until_parked(); - let panel = workspace.update(cx, GitPanel::new).unwrap(); + let panel = workspace.update_in(cx, GitPanel::new); let handle = cx.update_window_entity(&panel, |panel, _, _| { std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) @@ -6832,11 +6844,14 @@ mod tests { ); let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); - let panel = workspace.update(cx, GitPanel::new).unwrap(); + let panel = workspace.update_in(cx, GitPanel::new); // Test: User has commit message, enables amend (saves message), then disables (restores message) panel.update(cx, |panel, cx| { @@ -6901,16 +6916,19 @@ mod tests { ); let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); // Wait for the project scanning to finish so that `head_commit(cx)` is // actually set, otherwise no head commit would be available from which // to fetch the latest commit message from. cx.executor().run_until_parked(); - let panel = workspace.update(cx, GitPanel::new).unwrap(); + let panel = workspace.update_in(cx, GitPanel::new); panel.read_with(cx, |panel, cx| { assert!(panel.active_repository.is_some()); assert!(panel.head_commit(cx).is_some()); @@ -6987,10 +7005,13 @@ mod tests { ); let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, GitPanel::new).unwrap(); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); + let panel = workspace.update_in(cx, GitPanel::new); // Enable the `sort_by_path` setting and wait for entries to be updated, // as there should no longer be separators between Tracked and Untracked @@ -7016,7 +7037,7 @@ mod tests { }); cx.run_until_parked(); - let _ = workspace.update(cx, |workspace, _window, cx| { + workspace.update_in(cx, |workspace, _window, cx| { let active_path = workspace .item_of_type::(cx) .expect("ProjectDiff should exist") @@ -7060,9 +7081,12 @@ mod tests { ); let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); cx.read(|cx| { project @@ -7087,7 +7111,7 @@ mod tests { }); }); - let panel = workspace.update(cx, GitPanel::new).unwrap(); + let panel = workspace.update_in(cx, GitPanel::new); let handle = cx.update_window_entity(&panel, |panel, _, _| { std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) @@ -7246,10 +7270,13 @@ mod tests { ); let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, GitPanel::new).unwrap(); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); + let panel = workspace.update_in(cx, GitPanel::new); let handle = cx.update_window_entity(&panel, |panel, _, _| { std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 21a0d41fe099d60436bd80b7f4ee06982735c847..7c460d5f89167409c34fcdf56cced49cb60fc0a1 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -124,6 +124,7 @@ impl ProjectDiff { return; } let workspace = cx.entity(); + let workspace_weak = workspace.downgrade(); window .spawn(cx, async move |cx| { let this = cx @@ -138,7 +139,7 @@ impl ProjectDiff { .ok(); anyhow::Ok(()) }) - .detach_and_notify_err(window, cx); + .detach_and_notify_err(workspace_weak, window, cx); } pub fn deploy_at( @@ -1851,6 +1852,8 @@ mod tests { rel_path::{RelPath, rel_path}, }; + use workspace::MultiWorkspace; + use super::*; #[ctor::ctor] @@ -1898,8 +1901,9 @@ mod tests { &[("foo.txt", "foo\n".into())], ); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let diff = cx.new_window_entity(|window, cx| { ProjectDiff::new(project.clone(), workspace, window, cx) }); @@ -1946,8 +1950,9 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let diff = cx.new_window_entity(|window, cx| { ProjectDiff::new(project.clone(), workspace, window, cx) }); @@ -2016,8 +2021,9 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); fs.set_head_for_repo( path!("/project/.git").as_ref(), &[("foo", "original\n".into())], @@ -2146,8 +2152,9 @@ mod tests { ); let project = Project::test(fs, [Path::new(path!("/a"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); cx.run_until_parked(); @@ -2260,8 +2267,9 @@ mod tests { ); let project = Project::test(fs, [Path::new(path!("/a"))], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); cx.run_until_parked(); @@ -2315,8 +2323,9 @@ mod tests { )], ); let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let diff = cx.new_window_entity(|window, cx| { ProjectDiff::new(project.clone(), workspace, window, cx) }); @@ -2395,8 +2404,9 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let diff = cx.new_window_entity(|window, cx| { ProjectDiff::new(project.clone(), workspace, window, cx) }); @@ -2511,8 +2521,9 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let diff = cx .update(|window, cx| { ProjectDiff::new_with_default_branch(project.clone(), workspace, window, cx) @@ -2608,8 +2619,9 @@ mod tests { let worktree_id = project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); cx.run_until_parked(); let _editor = workspace @@ -2693,8 +2705,9 @@ mod tests { (worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); cx.run_until_parked(); // Select project A via the dropdown override and open the diff. diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index aa8418439353bd9cf3d1b5e9b84a84a53f0074f7..1713de71c2db01f11b03a8c8e8e8eb498bb31b77 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -594,7 +594,7 @@ mod tests { use picker::PickerDelegate; use project::{FakeFs, Project}; use settings::SettingsStore; - use workspace::Workspace; + use workspace::MultiWorkspace; fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { @@ -626,25 +626,27 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let cx = &mut VisualTestContext::from_window(*multi_workspace, cx); + let workspace = multi_workspace + .update(cx, |workspace, _, _| workspace.workspace().clone()) + .unwrap(); let stash_entries = vec![ stash_entry(0, "stash #0", Some("main")), stash_entry(1, "stash #1", Some("develop")), ]; - let stash_list = workspace - .update(cx, |workspace, window, cx| { - let weak_workspace = workspace.weak_handle(); + let stash_list = workspace.update_in(cx, |workspace, window, cx| { + let weak_workspace = workspace.weak_handle(); - workspace.toggle_modal(window, cx, move |window, cx| { - StashList::new(None, weak_workspace, rems(34.), window, cx) - }); + workspace.toggle_modal(window, cx, move |window, cx| { + StashList::new(None, weak_workspace, rems(34.), window, cx) + }); - assert!(workspace.active_modal::(cx).is_some()); - workspace.active_modal::(cx).unwrap() - }) - .unwrap(); + assert!(workspace.active_modal::(cx).is_some()); + workspace.active_modal::(cx).unwrap() + }); cx.run_until_parked(); stash_list.update(cx, |stash_list, cx| { @@ -667,10 +669,8 @@ mod tests { stash_list.handle_show_stash(&Default::default(), window, cx); }); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_modal::(cx).is_none()); - }) - .unwrap(); + workspace.update(cx, |workspace, cx| { + assert!(workspace.active_modal::(cx).is_none()); + }); } } diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index e95eb3c902ef63861dd1a9688aa5ad7e88f3191a..1b1c041da7c8abd246a193708160280e9f9419cc 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -450,6 +450,7 @@ mod tests { use settings::SettingsStore; use unindent::unindent; use util::{path, test::marked_text_ranges}; + use workspace::MultiWorkspace; fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { @@ -675,8 +676,9 @@ mod tests { let project = Project::test(fs, [project_root.as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let buffer = project .update(cx, |project, cx| project.open_local_buffer(file_path, cx)) diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index f65eb80e582e20121d4aab2a6b2471784ade45a5..a14336e0058ea64b8a78deae78d98df9c34d3dd9 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -4,8 +4,8 @@ use fuzzy::StringMatchCandidate; use git::repository::Worktree as GitWorktree; use gpui::{ - Action, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, + Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle, + Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, }; @@ -20,7 +20,7 @@ use remote_connection::{RemoteConnectionModal, connect}; use std::{path::PathBuf, sync::Arc}; use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; -use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr}; +use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr}; actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]); @@ -289,7 +289,6 @@ impl WorktreeListDelegate { }; let branch = worktree_branch.to_string(); - let window_handle = window.window_handle(); let workspace = self.workspace.clone(); cx.spawn_in(window, async move |_, cx| { let Some(paths) = worktree_path.await? else { @@ -355,7 +354,7 @@ impl WorktreeListDelegate { connection_options, vec![new_worktree_path], app_state, - window_handle, + workspace.clone(), replace_current_window, cx, ) @@ -407,13 +406,12 @@ impl WorktreeListDelegate { |e, _, _| Some(e.to_string()), ); } else if let Some(connection_options) = connection_options { - let window_handle = window.window_handle(); cx.spawn_in(window, async move |_, cx| { open_remote_worktree( connection_options, vec![path], app_state, - window_handle, + workspace, replace_current_window, cx, ) @@ -441,15 +439,16 @@ async fn open_remote_worktree( connection_options: RemoteConnectionOptions, paths: Vec, app_state: Arc, - window: gpui::AnyWindowHandle, + workspace: WeakEntity, replace_current_window: bool, - cx: &mut AsyncApp, + cx: &mut AsyncWindowContext, ) -> anyhow::Result<()> { - let workspace_window = window - .downcast::() + let workspace_window = cx + .window_handle() + .downcast::() .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?; - let connect_task = workspace_window.update(cx, |workspace, window, cx| { + let connect_task = workspace.update_in(cx, |workspace, window, cx| { workspace.toggle_modal(window, cx, |window, cx| { RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx) }); @@ -473,17 +472,19 @@ async fn open_remote_worktree( let session = connect_task.await; - workspace_window.update(cx, |workspace, _window, cx| { - if let Some(prompt) = workspace.active_modal::(cx) { - prompt.update(cx, |prompt, cx| prompt.finished(cx)) - } - })?; + workspace + .update_in(cx, |workspace, _window, cx| { + if let Some(prompt) = workspace.active_modal::(cx) { + prompt.update(cx, |prompt, cx| prompt.finished(cx)) + } + }) + .ok(); let Some(Some(session)) = session else { return Ok(()); }; - let new_project: Entity = cx.update(|cx| { + let new_project: Entity = cx.update(|_, cx| { project::Project::remote( session, app_state.client.clone(), @@ -494,29 +495,30 @@ async fn open_remote_worktree( true, cx, ) - }); + })?; let window_to_use = if replace_current_window { workspace_window } else { let workspace_position = cx - .update(|cx| { + .update(|_, cx| { workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx) - }) + })? .await .context("fetching workspace position from db")?; let mut options = - cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx)); + cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?; options.window_bounds = workspace_position.window_bounds; cx.open_window(options, |window, cx| { - cx.new(|cx| { + let workspace = cx.new(|cx| { let mut workspace = Workspace::new(None, new_project.clone(), app_state.clone(), window, cx); workspace.centered_layout = workspace_position.centered_layout; workspace - }) + }); + cx.new(|cx| MultiWorkspace::new(workspace, cx)) })? }; diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 9afe95d9f67be37b59f794a230d6afa07cadfdec..662bf2a98d84ba434da98aeca71791c028f6018c 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -378,7 +378,7 @@ mod tests { use serde_json::json; use std::{num::NonZeroU32, sync::Arc, time::Duration}; use util::{path, rel_path::rel_path}; - use workspace::{AppState, Workspace}; + use workspace::{AppState, MultiWorkspace, Workspace}; #[gpui::test] async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) { @@ -407,8 +407,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -504,8 +505,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); workspace.update_in(cx, |workspace, window, cx| { let cursor_position = cx.new(|_| CursorPosition::new(workspace)); workspace.status_bar().update(cx, |status_bar, cx| { @@ -589,8 +591,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); workspace.update_in(cx, |workspace, window, cx| { let cursor_position = cx.new(|_| CursorPosition::new(workspace)); workspace.status_bar().update(cx, |status_bar, cx| { @@ -667,8 +670,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); workspace.update_in(cx, |workspace, window, cx| { let cursor_position = cx.new(|_| CursorPosition::new(workspace)); workspace.status_bar().update(cx, |status_bar, cx| { @@ -843,8 +847,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -900,8 +905,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -955,8 +961,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index cc416f74fe83ca1f0f966d9b8a453619ada8c2b1..5ebafde1e0f4aee9c2179067cb39de358e05104c 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -265,6 +265,8 @@ pub enum IconName { UserRoundPen, Warning, WholeWord, + WorkspaceNavClosed, + WorkspaceNavOpen, XCircle, XCircleFilled, ZedAgent, diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index b74b20191c2668e59b0ad44d3a8ccce165c5cba7..53d2f74b9c663496da083152ead17d479f5030eb 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -18,7 +18,6 @@ editor.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true -platform_title_bar.workspace = true project.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true diff --git a/crates/inspector_ui/src/inspector.rs b/crates/inspector_ui/src/inspector.rs index be5bb14ff84da92b7e64baa588a20de345c2442f..f1f3ed8d38e4f0947741a0eeb72481e225904929 100644 --- a/crates/inspector_ui/src/inspector.rs +++ b/crates/inspector_ui/src/inspector.rs @@ -1,8 +1,7 @@ use anyhow::{Context as _, anyhow}; use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window}; -use platform_title_bar::PlatformTitleBar; use std::{cell::OnceCell, path::Path, sync::Arc}; -use ui::{Label, Tooltip, prelude::*}; +use ui::{Label, Tooltip, prelude::*, utils::platform_title_bar_height}; use util::{ResultExt as _, command::new_smol_command}; use workspace::AppState; @@ -61,7 +60,7 @@ fn render_inspector( let ui_font = theme::setup_ui_font(window, cx); let colors = cx.theme().colors(); let inspector_id = inspector.active_element_id(); - let toolbar_height = PlatformTitleBar::height(window); + let toolbar_height = platform_title_bar_height(window); v_flex() .size_full() diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index f43949c0051f56559388203e387a540b8c593467..ba97bcf66a77659fb3196ba45ebb3f831452e008 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -118,17 +118,20 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap })? .await?; new_workspace - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![entry_path], - workspace::OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) + .update(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + workspace.open_paths( + vec![entry_path], + workspace::OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + None, + window, + cx, + ) + }) })? .await } else { diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index de22ae01b503fde9aabdd99be5253d7c4e3f1b71..ff3389a4d4a10bc8472d0931d18ffa5be839c631 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -1319,7 +1319,7 @@ impl KeymapEditor { cx.spawn(async move |_, _| { remove_keybinding(to_remove, &fs, keyboard_mapper.as_ref()).await }) - .detach_and_notify_err(window, cx); + .detach_and_notify_err(self.workspace.clone(), window, cx); } fn copy_context_to_clipboard( diff --git a/crates/keymap_editor/src/ui_components/keystroke_input.rs b/crates/keymap_editor/src/ui_components/keystroke_input.rs index 496a8ae7e6359bc169845542a0f05800008a4786..e1f20de587c274a164a96e3b8d7189a3710ff301 100644 --- a/crates/keymap_editor/src/ui_components/keystroke_input.rs +++ b/crates/keymap_editor/src/ui_components/keystroke_input.rs @@ -674,7 +674,7 @@ mod tests { use itertools::Itertools as _; use project::Project; use settings::SettingsStore; - use workspace::Workspace; + use workspace::MultiWorkspace; pub struct KeystrokeInputTestHelper { input: Entity, @@ -1120,9 +1120,9 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = VisualTestContext::from_window(*workspace, cx); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let cx = VisualTestContext::from_window(window_handle.into(), cx); KeystrokeInputTestHelper::new(cx) } diff --git a/crates/miniprofiler_ui/src/miniprofiler_ui.rs b/crates/miniprofiler_ui/src/miniprofiler_ui.rs index cf916c0b5415d9643c9609715d66f77a98ba7222..697027570a46afc550fd4f96d6a204e7e8c23f27 100644 --- a/crates/miniprofiler_ui/src/miniprofiler_ui.rs +++ b/crates/miniprofiler_ui/src/miniprofiler_ui.rs @@ -8,7 +8,7 @@ use std::{ use gpui::{ App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement, ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement, - Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WindowBounds, WindowHandle, + Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds, WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list, }; use util::ResultExt; @@ -22,13 +22,10 @@ use workspace::{ use zed_actions::OpenPerformanceProfiler; pub fn init(startup_time: Instant, cx: &mut App) { - cx.observe_new(move |workspace: &mut workspace::Workspace, _, _| { - workspace.register_action(move |workspace, _: &OpenPerformanceProfiler, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_performance_profiler(startup_time, workspace, window_handle, cx); + cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| { + let workspace_handle = cx.entity().downgrade(); + workspace.register_action(move |_workspace, _: &OpenPerformanceProfiler, window, cx| { + open_performance_profiler(startup_time, workspace_handle.clone(), window, cx); }); }) .detach(); @@ -36,8 +33,8 @@ pub fn init(startup_time: Instant, cx: &mut App) { fn open_performance_profiler( startup_time: Instant, - _workspace: &mut workspace::Workspace, - workspace_handle: WindowHandle, + workspace_handle: WeakEntity, + _window: &mut gpui::Window, cx: &mut App, ) { let existing_window = cx @@ -48,7 +45,7 @@ fn open_performance_profiler( if let Some(existing_window) = existing_window { existing_window .update(cx, |profiler_window, window, _cx| { - profiler_window.workspace = Some(workspace_handle); + profiler_window.workspace = Some(workspace_handle.clone()); window.activate_window(); }) .log_err(); @@ -97,14 +94,14 @@ pub struct ProfilerWindow { include_self_timings: ToggleState, autoscroll: bool, scroll_handle: UniformListScrollHandle, - workspace: Option>, + workspace: Option>, _refresh: Option>, } impl ProfilerWindow { pub fn new( startup_time: Instant, - workspace_handle: Option>, + workspace_handle: Option>, cx: &mut App, ) -> Entity { let entity = cx.new(|cx| ProfilerWindow { @@ -280,7 +277,7 @@ impl Render for ProfilerWindow { Button::new("export-data", "Save") .style(ButtonStyle::Filled) .on_click(cx.listener(|this, _, _window, cx| { - let Some(workspace) = this.workspace else { + let Some(workspace) = this.workspace.as_ref() else { return; }; @@ -297,7 +294,7 @@ impl Render for ProfilerWindow { .log_err() .flatten() .and_then(|p| p.parent().map(|p| p.to_owned())) - .unwrap_or_else(|| PathBuf::default()); + .unwrap_or_else(PathBuf::default); let path = cx.prompt_for_new_path( &active_path, diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 495a55411fc936d476dfa0d443e155d1fa7faecd..866df0e9d91e75b3522f957f54d05db7614c7e5a 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -238,15 +238,16 @@ impl Onboarding { go_to_welcome_page(cx); } - fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) { + fn handle_sign_in(&mut self, _: &SignIn, window: &mut Window, cx: &mut Context) { let client = Client::global(cx); + let workspace = self.workspace.clone(); window - .spawn(cx, async move |cx| { + .spawn(cx, async move |mut cx| { client - .sign_in_with_optional_connect(true, cx) + .sign_in_with_optional_connect(true, &cx) .await - .notify_async_err(cx); + .notify_workspace_async_err(workspace, &mut cx); }) .detach(); } @@ -274,7 +275,7 @@ impl Render for Onboarding { .size_full() .bg(cx.theme().colors().editor_background) .on_action(Self::on_finish) - .on_action(Self::handle_sign_in) + .on_action(cx.listener(Self::handle_sign_in)) .on_action(Self::handle_open_account) .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| { window.focus_next(cx); diff --git a/crates/open_path_prompt/src/open_path_prompt_tests.rs b/crates/open_path_prompt/src/open_path_prompt_tests.rs index d8e96b24b5bcbd76c8d729240d1f1d822f18df79..eba3a3e03be55c210f7b4ebd4fad5abc3842e74b 100644 --- a/crates/open_path_prompt/src/open_path_prompt_tests.rs +++ b/crates/open_path_prompt/src/open_path_prompt_tests.rs @@ -6,7 +6,7 @@ use project::Project; use serde_json::json; use ui::rems; use util::path; -use workspace::{AppState, Workspace}; +use workspace::{AppState, MultiWorkspace}; use crate::OpenPathDelegate; @@ -426,7 +426,9 @@ fn build_open_path_prompt( let (tx, _) = futures::channel::oneshot::channel(); let lister = project::DirectoryLister::Project(project.clone()); - let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); ( workspace.update_in(cx, |_, window, cx| { let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx); diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index f172cf2c44e6a9a388bd37d37f747ee19cf2b65c..6ffb381148edaafcc375c26e6257106246ee58ae 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -20,7 +20,7 @@ use settings::Settings; use theme::{ActiveTheme, ThemeSettings}; use ui::{ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; -use workspace::{DismissDecision, ModalView, Workspace}; +use workspace::{DismissDecision, ModalView}; pub fn init(cx: &mut App) { cx.observe_new(OutlineView::register).detach(); @@ -41,7 +41,7 @@ pub fn toggle( window: &mut Window, cx: &mut App, ) { - let Some(workspace) = window.root::().flatten() else { + let Some(workspace) = editor.read(cx).workspace() else { return; }; if workspace.read(cx).active_modal::(cx).is_some() { @@ -453,7 +453,7 @@ mod tests { use settings::SettingsStore; use smol::stream::StreamExt as _; use util::{path, rel_path::rel_path}; - use workspace::{AppState, Workspace}; + use workspace::{AppState, MultiWorkspace, Workspace}; #[gpui::test] async fn test_outline_view_row_highlights(cx: &mut TestAppContext) { @@ -481,7 +481,9 @@ mod tests { }); let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + let workspace = cx.read(|cx| workspace.read(cx).workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -736,8 +738,9 @@ mod tests { }, ); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = cx.read(|cx| multi_workspace.read(cx).workspace().clone()); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 3405679a96f33cce35011d197d5176bdd4b830e2..aa4b5b2acec9fdafe79fef970547191ae5c17036 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5387,7 +5387,7 @@ mod tests { use serde_json::json; use smol::stream::StreamExt as _; use util::path; - use workspace::{OpenOptions, OpenVisible, ToolbarItemView}; + use workspace::{MultiWorkspace, OpenOptions, OpenVisible, ToolbarItemView}; use super::*; @@ -5402,33 +5402,29 @@ mod tests { populate_with_test_ra_project(&fs, root).await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.set_active(true, window, cx) }); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::default(), + window, + cx, + ) + }); + let search_view = workspace.update_in(cx, |workspace, _window, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view expected to appear after new search event trigger") + }); let query = "param_names_for_lifetime_elision_hints"; perform_project_search(&search_view, query, cx); @@ -5635,33 +5631,29 @@ mod tests { populate_with_test_ra_project(&fs, root).await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.set_active(true, window, cx) }); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::default(), + window, + cx, + ) + }); + let search_view = workspace.update_in(cx, |workspace, _window, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view expected to appear after new search event trigger") + }); let query = "param_names_for_lifetime_elision_hints"; perform_project_search(&search_view, query, cx); @@ -5772,33 +5764,29 @@ mod tests { populate_with_test_ra_project(&fs, root).await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.set_active(true, window, cx) }); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::default(), + window, + cx, + ) + }); + let search_view = workspace.update_in(cx, |workspace, _window, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view expected to appear after new search event trigger") + }); let query = "param_names_for_lifetime_elision_hints"; perform_project_search(&search_view, query, cx); @@ -5998,15 +5986,15 @@ outline: fn hints_lifetimes_named <==== selected" ) .await; let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.set_active(true, window, cx) }); let items = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_paths( vec![PathBuf::from(path!("/root/two"))], OpenOptions { @@ -6018,7 +6006,6 @@ outline: fn hints_lifetimes_named <==== selected" cx, ) }) - .unwrap() .await; assert_eq!(items.len(), 1, "Were opening another worktree directory"); assert!( @@ -6026,26 +6013,22 @@ outline: fn hints_lifetimes_named <==== selected" "Directory should be opened successfully" ); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::default(), + window, + cx, + ) + }); + let search_view = workspace.update_in(cx, |workspace, _window, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view expected to appear after new search event trigger") + }); let query = "aaa"; perform_project_search(&search_view, query, cx); @@ -6183,8 +6166,8 @@ struct OutlineEntryExcerpt { .await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); cx.update(|window, cx| { outline_panel.update(cx, |outline_panel, cx| { @@ -6193,7 +6176,7 @@ struct OutlineEntryExcerpt { }); let _editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/root/src/lib.rs")), OpenOptions { @@ -6204,7 +6187,6 @@ struct OutlineEntryExcerpt { cx, ) }) - .unwrap() .await .expect("Failed to open Rust source file") .downcast::() @@ -6545,33 +6527,29 @@ outline: struct OutlineEntryExcerpt ) .await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.set_active(true, window, cx) }); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::default(), + window, + cx, + ) + }); + let search_view = workspace.update_in(cx, |workspace, _window, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view expected to appear after new search event trigger") + }); let query = "static"; perform_project_search(&search_view, query, cx); @@ -6806,13 +6784,18 @@ outline: struct OutlineEntryExcerpt async fn add_outline_panel( project: &Entity, cx: &mut TestAppContext, - ) -> WindowHandle { - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + ) -> (WindowHandle, Entity) { + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let workspace_weak = workspace.downgrade(); let outline_panel = window .update(cx, |_, window, cx| { - cx.spawn_in(window, async |this, cx| { - OutlinePanel::load(this, cx.clone()).await + cx.spawn_in(window, async move |_this, cx| { + OutlinePanel::load(workspace_weak, cx.clone()).await }) }) .unwrap() @@ -6820,24 +6803,24 @@ outline: struct OutlineEntryExcerpt .expect("Failed to load outline panel"); window - .update(cx, |workspace, window, cx| { - workspace.add_panel(outline_panel, window, cx); + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + workspace.add_panel(outline_panel, window, cx); + }); }) .unwrap(); - window + (window, workspace) } fn outline_panel( - workspace: &WindowHandle, - cx: &mut TestAppContext, + workspace: &Entity, + cx: &mut VisualTestContext, ) -> Entity { - workspace - .update(cx, |workspace, _, cx| { - workspace - .panel::(cx) - .expect("no outline panel") - }) - .unwrap() + workspace.update_in(cx, |workspace, _window, cx| { + workspace + .panel::(cx) + .expect("no outline panel") + }) } fn display_entries( @@ -7196,8 +7179,8 @@ outline: struct OutlineEntryExcerpt let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); outline_panel.update_in(cx, |outline_panel, window, cx| { @@ -7205,7 +7188,7 @@ outline: struct OutlineEntryExcerpt }); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from("/test/src/lib.rs"), OpenOptions { @@ -7216,7 +7199,6 @@ outline: struct OutlineEntryExcerpt cx, ) }) - .unwrap() .await .unwrap(); @@ -7452,8 +7434,8 @@ outline: fn main" let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); outline_panel.update_in(cx, |outline_panel, window, cx| { @@ -7461,7 +7443,7 @@ outline: fn main" }); let _editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from("/test/src/main.rs"), OpenOptions { @@ -7472,7 +7454,6 @@ outline: fn main" cx, ) }) - .unwrap() .await .unwrap(); @@ -7666,8 +7647,8 @@ outline: fn main" let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); outline_panel.update_in(cx, |outline_panel, window, cx| { @@ -7675,7 +7656,7 @@ outline: fn main" }); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from("/test/src/lib.rs"), OpenOptions { @@ -7686,7 +7667,6 @@ outline: fn main" cx, ) }) - .unwrap() .await .unwrap(); @@ -7841,11 +7821,11 @@ outline: fn main" .await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from("/test/foo.txt"), OpenOptions { @@ -7856,22 +7836,19 @@ outline: fn main" cx, ) }) - .unwrap() .await .unwrap() .downcast::() .unwrap(); - let search_bar = workspace - .update(cx, |_, window, cx| { - cx.new(|cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }) + let search_bar = workspace.update_in(cx, |_, window, cx| { + cx.new(|cx| { + let mut search_bar = BufferSearchBar::new(None, window, cx); + search_bar.set_active_pane_item(Some(&editor), window, cx); + search_bar.show(window, cx); + search_bar }) - .unwrap(); + }); let outline_panel = outline_panel(&workspace, cx); @@ -8008,8 +7985,8 @@ search: | Field | Meaning « »|" }, ); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let (window, workspace) = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(window.into(), cx); let outline_panel = outline_panel(&workspace, cx); cx.update(|window, cx| { outline_panel.update(cx, |outline_panel, cx| { @@ -8018,7 +7995,7 @@ search: | Field | Meaning « »|" }); let _editor = workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/root/src/lib.rs")), OpenOptions { @@ -8029,7 +8006,6 @@ search: | Field | Meaning « »|" cx, ) }) - .unwrap() .await .expect("Failed to open Rust source file") .downcast::() diff --git a/crates/platform_title_bar/Cargo.toml b/crates/platform_title_bar/Cargo.toml index a8db1e37f206b90ca1cc18f933d5ab20ff45cdf1..2f1f6d2cd9297136077780aafdc75d22ecf6b845 100644 --- a/crates/platform_title_bar/Cargo.toml +++ b/crates/platform_title_bar/Cargo.toml @@ -13,6 +13,7 @@ path = "src/platform_title_bar.rs" doctest = false [dependencies] +feature_flags.workspace = true gpui.workspace = true settings.workspace = true smallvec.workspace = true diff --git a/crates/platform_title_bar/src/platform_title_bar.rs b/crates/platform_title_bar/src/platform_title_bar.rs index d53e8ae86cdba32b33e6959032667f9748de871e..6f89a5c39137896ee4b1a6cd3b81770fc3382284 100644 --- a/crates/platform_title_bar/src/platform_title_bar.rs +++ b/crates/platform_title_bar/src/platform_title_bar.rs @@ -1,16 +1,21 @@ mod platforms; mod system_window_tabs; +use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; use gpui::{ - AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, - ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, + AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, + MouseButton, ParentElement, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, + px, }; use smallvec::SmallVec; use std::mem; -use ui::prelude::*; +use ui::{ + prelude::*, + utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height}, +}; use crate::{ - platforms::{platform_linux, platform_mac, platform_windows}, + platforms::{platform_linux, platform_windows}, system_window_tabs::SystemWindowTabs, }; @@ -24,6 +29,8 @@ pub struct PlatformTitleBar { children: SmallVec<[AnyElement; 2]>, should_move: bool, system_window_tabs: Entity, + workspace_sidebar_open: bool, + sidebar_has_notifications: bool, } impl PlatformTitleBar { @@ -37,20 +44,11 @@ impl PlatformTitleBar { children: SmallVec::new(), should_move: false, system_window_tabs, + workspace_sidebar_open: false, + sidebar_has_notifications: false, } } - #[cfg(not(target_os = "windows"))] - pub fn height(window: &mut Window) -> Pixels { - (1.75 * window.rem_size()).max(px(34.)) - } - - #[cfg(target_os = "windows")] - pub fn height(_window: &mut Window) -> Pixels { - // todo(windows) instead of hard coded size report the actual size to the Windows platform API - px(32.) - } - pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { if cfg!(any(target_os = "linux", target_os = "freebsd")) { if window.is_window_active() && !self.should_move { @@ -73,17 +71,46 @@ impl PlatformTitleBar { pub fn init(cx: &mut App) { SystemWindowTabs::init(cx); } + + pub fn is_workspace_sidebar_open(&self) -> bool { + self.workspace_sidebar_open + } + + pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context) { + self.workspace_sidebar_open = open; + cx.notify(); + } + + pub fn sidebar_has_notifications(&self) -> bool { + self.sidebar_has_notifications + } + + pub fn set_sidebar_has_notifications( + &mut self, + has_notifications: bool, + cx: &mut Context, + ) { + self.sidebar_has_notifications = has_notifications; + cx.notify(); + } + + pub fn is_multi_workspace_enabled(cx: &App) -> bool { + cx.has_flag::() + } } impl Render for PlatformTitleBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let supported_controls = window.window_controls(); let decorations = window.window_decorations(); - let height = Self::height(window); + let height = platform_title_bar_height(window); let titlebar_color = self.title_bar_color(window, cx); let close_action = Box::new(workspace::CloseWindow); let children = mem::take(&mut self.children); + let is_multiworkspace_sidebar_open = + PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open(); + let title_bar = h_flex() .window_control_area(WindowControlArea::Drag) .w_full() @@ -132,8 +159,10 @@ impl Render for PlatformTitleBar { .map(|this| { if window.is_fullscreen() { this.pl_2() - } else if self.platform_style == PlatformStyle::Mac { - this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING)) + } else if self.platform_style == PlatformStyle::Mac + && !is_multiworkspace_sidebar_open + { + this.pl(px(TRAFFIC_LIGHT_PADDING)) } else { this.pl_2() } @@ -144,9 +173,10 @@ impl Render for PlatformTitleBar { .when(!(tiling.top || tiling.right), |el| { el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) }) - .when(!(tiling.top || tiling.left), |el| { - el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) + .when( + !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open, + |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) // this border is to avoid a transparent gap in the rounded corners .mt(px(-1.)) .mb(px(-1.)) diff --git a/crates/platform_title_bar/src/platforms.rs b/crates/platform_title_bar/src/platforms.rs index 67e87d45ea5d290077af1326e613c6819e0f41dc..26e9c4b4f044eff172d165e3851279fa07c3a269 100644 --- a/crates/platform_title_bar/src/platforms.rs +++ b/crates/platform_title_bar/src/platforms.rs @@ -1,3 +1,2 @@ pub mod platform_linux; -pub mod platform_mac; pub mod platform_windows; diff --git a/crates/platform_title_bar/src/platforms/platform_mac.rs b/crates/platform_title_bar/src/platforms/platform_mac.rs deleted file mode 100644 index 5e8e4e5087054e59f66527915ae97e352a9ff525..0000000000000000000000000000000000000000 --- a/crates/platform_title_bar/src/platforms/platform_mac.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Use pixels here instead of a rem-based size because the macOS traffic -// lights are a static size, and don't scale with the rest of the UI. -// -// Magic number: There is one extra pixel of padding on the left side due to -// the 1px border around the window on macOS apps. -#[cfg(macos_sdk_26)] -pub const TRAFFIC_LIGHT_PADDING: f32 = 78.; - -#[cfg(not(macos_sdk_26))] -pub const TRAFFIC_LIGHT_PADDING: f32 = 71.; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 76dd4abe2f90e285e85ea8778c5ad785e1bbfab5..b1c07a3f94f1317dd5169b68072cc701c3fde548 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -772,7 +772,11 @@ impl ProjectPanel { { match project_panel.confirm_edit(false, window, cx) { Some(task) => { - task.detach_and_notify_err(window, cx); + task.detach_and_notify_err( + project_panel.workspace.clone(), + window, + cx, + ); } None => { project_panel.discard_edit_state(window, cx); @@ -1648,7 +1652,7 @@ impl ProjectPanel { fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { if let Some(task) = self.confirm_edit(true, window, cx) { - task.detach_and_notify_err(window, cx); + task.detach_and_notify_err(self.workspace.clone(), window, cx); } } @@ -3033,20 +3037,25 @@ impl ProjectPanel { } let item_count = paste_tasks.len(); + let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |project_panel, cx| { + cx.spawn_in(window, async move |project_panel, mut cx| { let mut last_succeed = None; for task in paste_tasks { match task { PasteTask::Rename(task) => { - if let Some(CreatedEntry::Included(entry)) = - task.await.notify_async_err(cx) + if let Some(CreatedEntry::Included(entry)) = task + .await + .notify_workspace_async_err(workspace.clone(), &mut cx) { last_succeed = Some(entry); } } PasteTask::Copy(task) => { - if let Some(Some(entry)) = task.await.notify_async_err(cx) { + if let Some(Some(entry)) = task + .await + .notify_workspace_async_err(workspace.clone(), &mut cx) + { last_succeed = Some(entry); } } @@ -3388,7 +3397,7 @@ impl ProjectPanel { if let Some((file_path1, file_path2)) = selected_files { self.workspace .update(cx, |workspace, cx| { - FileDiffView::open(file_path1, file_path2, workspace, window, cx) + FileDiffView::open(file_path1, file_path2, workspace.weak_handle(), window, cx) .detach_and_log_err(cx); }) .ok(); diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 62f1a4906a6c8c51d459b1b593f176250e077956..70defc9ef0d0f501512fde38515309eff241b703 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -1,7 +1,7 @@ use super::*; use collections::HashSet; use editor::MultiBufferOffset; -use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle}; +use gpui::{Empty, Entity, TestAppContext, VisualTestContext}; use menu::Cancel; use pretty_assertions::assert_eq; use project::FakeFs; @@ -10,7 +10,7 @@ use settings::{ProjectPanelAutoOpenSettings, SettingsStore}; use std::path::{Path, PathBuf}; use util::{path, paths::PathStyle, rel_path::rel_path}; use workspace::{ - AppState, ItemHandle, Pane, + AppState, ItemHandle, MultiWorkspace, Pane, Workspace, item::{Item, ProjectItem}, register_project_item, }; @@ -57,9 +57,12 @@ async fn test_visible_list(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), @@ -123,9 +126,12 @@ async fn test_opening_file(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "src/test", cx); @@ -210,9 +216,12 @@ async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), @@ -321,8 +330,11 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { cx, ) .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); ProjectPanelSettings::override_global( @@ -334,7 +346,7 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { cx, ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -404,9 +416,12 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { // Test 2: Single worktree with auto_fold_dirs = true and hide_root = true { let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); ProjectPanelSettings::override_global( @@ -418,7 +433,7 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { cx, ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -505,15 +520,16 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); select_path(&panel, "root1", cx); @@ -831,7 +847,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { ); // Dismiss the rename editor when it loses focus. - workspace.update(cx, |_, window, _| window.blur()).unwrap(); + workspace.update_in(cx, |_, window, _| window.blur()); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -937,15 +953,16 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); select_path(&panel, "root1", cx); @@ -1049,15 +1066,16 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); select_path(&panel, "root1", cx); @@ -1179,9 +1197,12 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update_in(cx, |panel, window, cx| { @@ -1286,9 +1307,12 @@ async fn test_cut_paste(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); select_path_with_mark(&panel, "root/one.txt", cx); @@ -1391,9 +1415,12 @@ async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContex .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); select_path(&panel, "root1/three.txt", cx); @@ -1488,9 +1515,12 @@ async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppConte .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); select_path(&panel, "root1/three.txt", cx); @@ -1611,9 +1641,12 @@ async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); select_path(&panel, "root/a", cx); @@ -1751,9 +1784,12 @@ async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppConte .await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "test/dir1", cx); @@ -1857,9 +1893,12 @@ async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) .await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "test/dir1", cx); @@ -1935,9 +1974,12 @@ async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "src/test", cx); @@ -1982,25 +2024,23 @@ async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { ); ensure_single_file_is_opened(&workspace, "test/second.rs", cx); - workspace - .update(cx, |workspace, window, cx| { - let active_items = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()) - .collect::>(); - assert_eq!(active_items.len(), 1); - let open_editor = active_items - .into_iter() - .next() - .unwrap() - .downcast::() - .expect("Open item should be an editor"); - open_editor.update(cx, |editor, cx| { - editor.set_text("Another text!", window, cx) - }); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + let active_items = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()) + .collect::>(); + assert_eq!(active_items.len(), 1); + let open_editor = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .expect("Open item should be an editor"); + open_editor.update(cx, |editor, cx| { + editor.set_text("Another text!", window, cx) + }); + }); submit_deletion_skipping_prompt(&panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2025,9 +2065,12 @@ async fn test_auto_open_new_file_when_enabled(cx: &mut gpui::TestAppContext) { fs.insert_tree(path!("/root"), json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); @@ -2061,9 +2104,12 @@ async fn test_auto_open_new_file_when_disabled(cx: &mut gpui::TestAppContext) { fs.insert_tree(path!("/root"), json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); @@ -2106,9 +2152,12 @@ async fn test_auto_open_on_paste_when_enabled(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/src", cx); @@ -2152,9 +2201,12 @@ async fn test_auto_open_on_paste_when_disabled(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/src", cx); @@ -2199,9 +2251,12 @@ async fn test_auto_open_on_drop_when_enabled(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); let root_entry = find_project_entry(&panel, "root", cx).unwrap(); @@ -2234,9 +2289,12 @@ async fn test_auto_open_on_drop_when_disabled(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); let root_entry = find_project_entry(&panel, "root", cx).unwrap(); @@ -2270,15 +2328,16 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); select_path(&panel, "src", cx); @@ -2475,15 +2534,16 @@ async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppCon .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); select_path(&panel, "src", cx); @@ -2707,17 +2767,15 @@ async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppCon panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); cx.executor().run_until_parked(); - workspace - .read_with(cx, |this, cx| { - assert!( - this.recent_navigation_history_iter(cx) - .any(|(project_path, abs_path)| { - project_path.path == Arc::from(rel_path("test/fourth.txt")) - && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt"))) - }) - ); - }) - .unwrap(); + workspace.read_with(cx, |this, cx| { + assert!( + this.recent_navigation_history_iter(cx) + .any(|(project_path, abs_path)| { + project_path.path == Arc::from(rel_path("test/fourth.txt")) + && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt"))) + }) + ); + }); } // NOTE: This test is skipped on Windows, because on Windows, @@ -2742,15 +2800,16 @@ async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); select_path(&panel, "src", cx); @@ -2830,17 +2889,15 @@ async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) { panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); cx.executor().run_until_parked(); - workspace - .read_with(cx, |this, cx| { - assert!( - this.recent_navigation_history_iter(cx) - .any(|(project_path, abs_path)| { - project_path.path == Arc::from(rel_path("test/fourth.txt")) - && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt"))) - }) - ); - }) - .unwrap(); + workspace.read_with(cx, |this, cx| { + assert!( + this.recent_navigation_history_iter(cx) + .any(|(project_path, abs_path)| { + project_path.path == Arc::from(rel_path("test/fourth.txt")) + && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt"))) + }) + ); + }); } #[gpui::test] @@ -2916,9 +2973,12 @@ async fn test_select_git_entry(cx: &mut gpui::TestAppContext) { scan2_complete.await; cx.run_until_parked(); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Check initial state @@ -3165,9 +3225,12 @@ async fn test_select_directory(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); @@ -3263,9 +3326,12 @@ async fn test_select_first_last(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -3319,7 +3385,7 @@ async fn test_select_first_last(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); #[rustfmt::skip] @@ -3369,9 +3435,12 @@ async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); @@ -3421,9 +3490,12 @@ async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update_in(cx, |panel, window, cx| { @@ -3476,9 +3548,12 @@ async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppCont cx, ) .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update_in(cx, |panel, window, cx| { @@ -3519,9 +3594,12 @@ async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppCon .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Open project_root/dir_1 to ensure that a nested directory is expanded @@ -3588,9 +3666,12 @@ async fn test_collapse_all_entries_with_invisible_worktree(cx: &mut gpui::TestAp .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); let (_invisible_worktree, _) = project @@ -3628,26 +3709,25 @@ async fn test_new_file_move(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.as_fake().insert_tree(path!("/root"), json!({})).await; let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Make a new buffer with no backing file - workspace - .update(cx, |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + Editor::new_file(workspace, &Default::default(), window, cx) + }); cx.executor().run_until_parked(); // "Save as" the buffer, creating a new backing file for it - let save_task = workspace - .update(cx, |workspace, window, cx| { - workspace.save_active_item(workspace::SaveIntent::Save, window, cx) - }) - .unwrap(); + let save_task = workspace.update_in(cx, |workspace, window, cx| { + workspace.save_active_item(workspace::SaveIntent::Save, window, cx) + }); cx.executor().run_until_parked(); cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new")))); @@ -3674,10 +3754,9 @@ async fn test_new_file_move(cx: &mut gpui::TestAppContext) { ); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.save_active_item(workspace::SaveIntent::Save, window, cx) }) - .unwrap() .await .unwrap(); @@ -3712,9 +3791,12 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root1/dir1", cx); @@ -3793,9 +3875,12 @@ async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { // Test 1: Single worktree, hide_root=true - rename should be blocked { let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -3808,7 +3893,7 @@ async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update(cx, |panel, cx| { @@ -3832,9 +3917,12 @@ async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { // Test 2: Multiple worktrees, hide_root=true - rename should work { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -3847,7 +3935,7 @@ async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); select_path(&panel, "root1", cx); @@ -3891,9 +3979,12 @@ async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) { let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id()); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); cx.update(|window, cx| { @@ -4093,8 +4184,11 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -4107,7 +4201,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c' @@ -4257,8 +4351,11 @@ async fn test_drag_marked_entries_in_folded_directories(cx: &mut gpui::TestAppCo .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -4271,7 +4368,7 @@ async fn test_drag_marked_entries_in_folded_directories(cx: &mut gpui::TestAppCo ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -4349,9 +4446,12 @@ async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppCon .await; let project = Project::test(fs.clone(), ["/root_a".as_ref(), "/root_b".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Case 1: move a file onto a directory in another worktree. @@ -4441,9 +4541,12 @@ async fn test_drag_multiple_entries(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/src", cx); @@ -4539,9 +4642,12 @@ async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -4787,9 +4893,12 @@ async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -4901,9 +5010,12 @@ async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -5092,15 +5204,16 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); select_path(&panel, "root1", cx); @@ -5108,14 +5221,12 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { visible_entries_as_strings(&panel, 0..10, cx), &["v root1 <== selected", " .dockerignore",] ); - workspace - .update(cx, |workspace, _, cx| { - assert!( - workspace.active_item(cx).is_none(), - "Should have no active items in the beginning" - ); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + assert!( + workspace.active_item(cx).is_none(), + "Should have no active items in the beginning" + ); + }); let excluded_file_path = ".git/COMMIT_EDITMSG"; let excluded_dir_path = "excluded_dir"; @@ -5146,28 +5257,26 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { "Should have closed the file name editor" ); }); - workspace - .update(cx, |workspace, _, cx| { - let active_entry_path = workspace - .active_item(cx) - .expect("should have opened and activated the excluded item") - .act_as::(cx) - .expect("should have opened the corresponding project item for the excluded item") - .read(cx) - .path - .clone(); - assert_eq!( - active_entry_path.path.as_ref(), - rel_path(excluded_file_path), - "Should open the excluded file" - ); + workspace.update_in(cx, |workspace, _, cx| { + let active_entry_path = workspace + .active_item(cx) + .expect("should have opened and activated the excluded item") + .act_as::(cx) + .expect("should have opened the corresponding project item for the excluded item") + .read(cx) + .path + .clone(); + assert_eq!( + active_entry_path.path.as_ref(), + rel_path(excluded_file_path), + "Should open the excluded file" + ); - assert!( - workspace.notification_ids().is_empty(), - "Should have no notifications after opening an excluded file" - ); - }) - .unwrap(); + assert!( + workspace.notification_ids().is_empty(), + "Should have no notifications after opening an excluded file" + ); + }); assert!( fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await, "Should have created the excluded file" @@ -5202,18 +5311,16 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { "Should have closed the file name editor" ); }); - workspace - .update(cx, |workspace, _, cx| { - let notifications = workspace.notification_ids(); - assert_eq!( - notifications.len(), - 1, - "Should receive one notification with the error message" - ); - workspace.dismiss_notification(notifications.first().unwrap(), cx); - assert!(workspace.notification_ids().is_empty()); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + let notifications = workspace.notification_ids(); + assert_eq!( + notifications.len(), + 1, + "Should receive one notification with the error message" + ); + workspace.dismiss_notification(notifications.first().unwrap(), cx); + assert!(workspace.notification_ids().is_empty()); + }); select_path(&panel, "root1", cx); panel.update_in(cx, |panel, window, cx| { @@ -5248,18 +5355,16 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { "Should have closed the file name editor" ); }); - workspace - .update(cx, |workspace, _, cx| { - let notifications = workspace.notification_ids(); - assert_eq!( - notifications.len(), - 1, - "Should receive one notification explaining that no directory is actually shown" - ); - workspace.dismiss_notification(notifications.first().unwrap(), cx); - assert!(workspace.notification_ids().is_empty()); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + let notifications = workspace.notification_ids(); + assert_eq!( + notifications.len(), + 1, + "Should receive one notification explaining that no directory is actually shown" + ); + workspace.dismiss_notification(notifications.first().unwrap(), cx); + assert!(workspace.notification_ids().is_empty()); + }); assert!( fs.is_dir(Path::new("/root1/excluded_dir")).await, "Should have created the excluded directory" @@ -5284,15 +5389,16 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); select_path(&panel, "src", cx); @@ -5352,7 +5458,7 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC " > test" ] ); - workspace.update(cx, |_, window, _| window.blur()).unwrap(); + workspace.update_in(cx, |_, window, _| window.blur()); cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -5389,9 +5495,12 @@ async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -5511,8 +5620,11 @@ async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); // Test 1: Auto selection with one gitignored file next to the deleted file cx.update(|_, cx| { @@ -5526,7 +5638,7 @@ async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); select_path(&panel, "root/aa", cx); @@ -5610,8 +5722,11 @@ async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -5624,7 +5739,7 @@ async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Test 1: Visible items should exclude files on gitignore @@ -5684,9 +5799,12 @@ async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -5797,9 +5915,12 @@ async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -5870,9 +5991,12 @@ async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -5945,9 +6069,12 @@ async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Expand all directories for testing @@ -6077,9 +6204,12 @@ async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -6181,9 +6311,12 @@ async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestApp .await; let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root_b/dir1", cx); @@ -6282,8 +6415,11 @@ async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); // Test 1: When auto-fold is enabled cx.update(|_, cx| { @@ -6297,7 +6433,7 @@ async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -6468,12 +6604,15 @@ async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); // Test 1: Basic collapsing { - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -6526,7 +6665,7 @@ async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -6582,7 +6721,7 @@ async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -6654,10 +6793,13 @@ async fn test_collapse_selected_entry_and_children_action(cx: &mut gpui::TestApp .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -6741,10 +6883,13 @@ async fn test_collapse_root_single_worktree(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -6833,10 +6978,13 @@ async fn test_collapse_root_multi_worktree(cx: &mut gpui::TestAppContext) { cx, ) .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root1/dir1", cx); @@ -6935,10 +7083,13 @@ async fn test_collapse_non_root_multi_worktree(cx: &mut gpui::TestAppContext) { cx, ) .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root1/dir1", cx); @@ -7028,10 +7179,13 @@ async fn test_collapse_all_for_root_single_worktree(cx: &mut gpui::TestAppContex .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -7116,10 +7270,13 @@ async fn test_collapse_all_for_root_multi_worktree(cx: &mut gpui::TestAppContext cx, ) .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root1/dir1", cx); @@ -7184,10 +7341,13 @@ async fn test_collapse_all_for_root_noop_on_non_root(cx: &mut gpui::TestAppConte .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/dir1", cx); @@ -7245,16 +7405,17 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); #[rustfmt::skip] @@ -7313,8 +7474,11 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -7327,13 +7491,11 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC ); }); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); #[rustfmt::skip] @@ -7468,16 +7630,17 @@ async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppConte .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.run_until_parked(); #[rustfmt::skip] @@ -7539,9 +7702,12 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update(cx, |panel, cx| { @@ -7613,9 +7779,12 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update(cx, |panel, cx| { @@ -7753,9 +7922,12 @@ async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::T .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update(cx, |panel, cx| { @@ -7844,9 +8016,12 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); panel.update(cx, |panel, cx| { @@ -8003,9 +8178,12 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { // Test 1: Single worktree with hide_root = false { let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -8018,7 +8196,7 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); #[rustfmt::skip] @@ -8037,9 +8215,12 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { // Test 2: Single worktree with hide_root = true { let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); // Set hide_root to true cx.update(|_, cx| { @@ -8053,7 +8234,7 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -8080,9 +8261,12 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { // Test 3: Multiple worktrees with hide_root = true { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); // Set hide_root to true cx.update(|_, cx| { @@ -8096,7 +8280,7 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -8117,9 +8301,12 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { // Test 4: Multiple worktrees with hide_root = false { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -8132,7 +8319,7 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); assert_eq!( @@ -8169,9 +8356,12 @@ async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); let file1_path = "root/file1.txt"; @@ -8184,31 +8374,29 @@ async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { }); cx.executor().run_until_parked(); - workspace - .update(cx, |workspace, _, cx| { - let active_items = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()) - .collect::>(); - assert_eq!(active_items.len(), 1); - let diff_view = active_items - .into_iter() - .next() - .unwrap() - .downcast::() - .expect("Open item should be an FileDiffView"); - assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt"); - assert_eq!( - diff_view.tab_tooltip_text(cx).unwrap(), - format!( - "{} ↔ {}", - rel_path(file1_path).display(PathStyle::local()), - rel_path(file2_path).display(PathStyle::local()) - ) - ); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + let active_items = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()) + .collect::>(); + assert_eq!(active_items.len(), 1); + let diff_view = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .expect("Open item should be an FileDiffView"); + assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt"); + assert_eq!( + diff_view.tab_tooltip_text(cx).unwrap(), + format!( + "{} ↔ {}", + rel_path(file1_path).display(PathStyle::local()), + rel_path(file2_path).display(PathStyle::local()) + ) + ); + }); let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap(); let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap(); @@ -8273,9 +8461,12 @@ async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Test 1: When only one file is selected, there should be no compare option @@ -8371,8 +8562,11 @@ async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -8385,7 +8579,7 @@ async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) { ); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx); @@ -8716,9 +8910,12 @@ async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Default sort mode should be DirectoriesFirst @@ -8753,8 +8950,11 @@ async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); // Switch to Mixed mode cx.update(|_, cx| { @@ -8766,7 +8966,7 @@ async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) { }); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Mixed mode: case-insensitive sorting @@ -8802,8 +9002,11 @@ async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); // Switch to FilesFirst mode cx.update(|_, cx| { @@ -8815,7 +9018,7 @@ async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) { }); }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // FilesFirst mode: files first, then directories (both case-insensitive) @@ -8848,9 +9051,12 @@ async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, ProjectPanel::new); cx.run_until_parked(); // Initially DirectoriesFirst @@ -9033,16 +9239,17 @@ async fn test_preserve_temporary_unfolded_active_index_on_blur_from_context_menu .await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -9216,16 +9423,17 @@ async fn run_create_file_in_folded_path_case( .await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); cx.update(|_, cx| { let settings = *ProjectPanelSettings::get_global(cx); @@ -9380,31 +9588,29 @@ fn set_auto_open_settings( } fn ensure_single_file_is_opened( - window: &WindowHandle, + workspace: &Entity, expected_path: &str, - cx: &mut TestAppContext, + cx: &mut VisualTestContext, ) { - window - .update(cx, |workspace, _, cx| { - let worktrees = workspace.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - let worktree_id = worktrees[0].read(cx).id(); - - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert_eq!( - open_project_paths, - vec![ProjectPath { - worktree_id, - path: Arc::from(rel_path(expected_path)) - }], - "Should have opened file, selected in project panel" - ); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _, cx| { + let worktrees = workspace.worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree_id = worktrees[0].read(cx).id(); + + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert_eq!( + open_project_paths, + vec![ProjectPath { + worktree_id, + path: Arc::from(rel_path(expected_path)) + }], + "Should have opened file, selected in project panel" + ); + }); } fn submit_deletion(panel: &Entity, cx: &mut VisualTestContext) { @@ -9439,24 +9645,22 @@ fn submit_deletion_skipping_prompt(panel: &Entity, cx: &mut Visual cx.executor().run_until_parked(); } -fn ensure_no_open_items_and_panes(workspace: &WindowHandle, cx: &mut VisualTestContext) { +fn ensure_no_open_items_and_panes(workspace: &Entity, cx: &mut VisualTestContext) { assert!( !cx.has_pending_prompt(), "Should have no prompts after deletion operation closes the file" ); - workspace - .read_with(cx, |workspace, cx| { - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert!( - open_project_paths.is_empty(), - "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" - ); - }) - .unwrap(); + workspace.update_in(cx, |workspace, _window, cx| { + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert!( + open_project_paths.is_empty(), + "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" + ); + }); } struct TestProjectItemView { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index c7fb3e6182917c928be56f8f3cc09d7eb88b6e0c..d62935ab3819d2e6857c233a863af434f60f93a3 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -318,6 +318,7 @@ mod tests { use settings::SettingsStore; use std::{path::Path, sync::Arc}; use util::path; + use workspace::MultiWorkspace; #[gpui::test] async fn test_project_symbols(cx: &mut TestAppContext) { @@ -409,8 +410,9 @@ mod tests { }, ); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); // Create the project symbols view. let symbols = cx.new_window_entity(|window, cx| { diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 9ef3250e315d00bf4b6f669b9a5313ea3251a5fe..7b605084d6213ef17ffac83d782bb02bc4213e27 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -23,6 +23,7 @@ db.workspace = true dev_container.workspace = true editor.workspace = true extension_host.workspace = true +fs.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true @@ -66,6 +67,7 @@ language = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } release_channel.workspace = true remote = { workspace = true, features = ["test-support"] } +remote_connection = { workspace = true, features = ["test-support"] } remote_server.workspace = true serde_json.workspace = true settings = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index f45673c38dbad1abab717b3f9f1081a2ffae4bd2..82ff0699054e5614b8078d3223d5e9282e5034b5 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -7,7 +7,9 @@ use ui::{ HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal, ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems, }; -use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr}; +use workspace::{ + ModalView, MultiWorkspace, OpenOptions, Workspace, notifications::DetachAndPromptErr, +}; use crate::open_remote_project; @@ -109,7 +111,7 @@ impl DisconnectedOverlay { return; }; - let Some(window_handle) = window.window_handle().downcast::() else { + let Some(window_handle) = window.window_handle().downcast::() else { return; }; diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 7f67627ad4d6841419292e58b5b41a5fd5ef7d5b..c0a22b43e37a55ac5a3380b1d5e903ea5b06b80e 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -4,7 +4,9 @@ mod remote_connections; mod remote_servers; mod ssh_config; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; + +use fs::Fs; #[cfg(target_os = "windows")] mod wsl_picker; @@ -27,11 +29,11 @@ use picker::{ pub use remote_connections::RemoteSettings; pub use remote_servers::RemoteServerProjects; use settings::Settings; -use std::{path::Path, sync::Arc}; +use std::path::Path; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; use workspace::{ - CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, + HistoryManager, ModalView, MultiWorkspace, OpenOptions, PathList, SerializedWorkspaceLocation, WORKSPACE_DB, Workspace, WorkspaceId, notifications::DetachAndPromptErr, with_active_or_new_workspace, }; @@ -48,9 +50,10 @@ pub struct RecentProjectEntry { pub async fn get_recent_projects( current_workspace_id: Option, limit: Option, + fs: Arc, ) -> Vec { let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk() + .recent_workspaces_on_disk(fs.as_ref()) .await .unwrap_or_default(); @@ -176,7 +179,7 @@ pub fn init(cx: &mut App) { let fs = workspace.project().read(cx).fs().clone(); add_wsl_distro(fs, &open_wsl.distro, cx); let open_options = OpenOptions { - replace_window: window.window_handle().downcast::(), + replace_window: window.window_handle().downcast::(), ..Default::default() }; @@ -232,10 +235,8 @@ pub fn init(cx: &mut App) { cx.on_action(|_: &OpenDevContainer, cx| { with_active_or_new_workspace(cx, move |workspace, window, cx| { - let is_local = workspace.project().read(cx).is_local(); - - cx.spawn_in(window, async move |_, cx| { - if !is_local { + if !workspace.project().read(cx).is_local() { + cx.spawn_in(window, async move |_, cx| { cx.prompt( gpui::PromptLevel::Critical, "Cannot open Dev Container from remote project", @@ -244,21 +245,16 @@ pub fn init(cx: &mut App) { ) .await .ok(); - return; - } - - cx.update(|_, cx| { - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let fs = workspace.project().read(cx).fs().clone(); - let handle = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new_dev_container(fs, window, handle, cx) - }); - }); }) - .log_err(); - }) - .detach(); + .detach(); + return; + } + + let fs = workspace.project().read(cx).fs().clone(); + let handle = cx.entity().downgrade(); + workspace.toggle_modal(window, cx, |window, cx| { + RemoteServerProjects::new_dev_container(fs, window, handle, cx) + }); }); }); @@ -334,6 +330,7 @@ impl ModalView for RecentProjects {} impl RecentProjects { fn new( delegate: RecentProjectsDelegate, + fs: Option>, rem_width: f32, window: &mut Window, cx: &mut Context, @@ -350,8 +347,9 @@ impl RecentProjects { // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap // out workspace locations once the future runs to completion. cx.spawn_in(window, async move |this, cx| { + let Some(fs) = fs else { return }; let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk() + .recent_workspaces_on_disk(fs.as_ref()) .await .log_err() .unwrap_or_default(); @@ -361,7 +359,7 @@ impl RecentProjects { picker.update_matches(picker.query(cx), window, cx) }) }) - .ok() + .ok(); }) .detach(); Self { @@ -379,10 +377,11 @@ impl RecentProjects { cx: &mut Context, ) { let weak = cx.entity().downgrade(); + let fs = Some(workspace.app_state().fs.clone()); workspace.toggle_modal(window, cx, |window, cx| { let delegate = RecentProjectsDelegate::new(weak, create_new_window, true, focus_handle); - Self::new(delegate, 34., window, cx) + Self::new(delegate, fs, 34., window, cx) }) } @@ -393,10 +392,13 @@ impl RecentProjects { window: &mut Window, cx: &mut App, ) -> Entity { + let fs = workspace + .upgrade() + .map(|ws| ws.read(cx).app_state().fs.clone()); cx.new(|cx| { let delegate = RecentProjectsDelegate::new(workspace, create_new_window, true, focus_handle); - let list = Self::new(delegate, 34., window, cx); + let list = Self::new(delegate, fs, 34., window, cx); list.picker.focus_handle(cx).focus(window, cx); list }) @@ -580,27 +582,21 @@ impl PickerDelegate for RecentProjectsDelegate { SerializedWorkspaceLocation::Local => { let paths = candidate_workspace_paths.paths().to_vec(); if replace_current_window { - cx.spawn_in(window, async move |workspace, cx| { - let continue_replacing = workspace - .update_in(cx, |workspace, window, cx| { - workspace.prepare_to_close( - CloseIntent::ReplaceWindow, - window, - cx, - ) - })? - .await?; - if continue_replacing { - workspace - .update_in(cx, |workspace, window, cx| { - workspace - .open_workspace_for_paths(true, paths, window, cx) - })? - .await - } else { - Ok(()) - } - }) + if let Some(handle) = + window.window_handle().downcast::() + { + cx.defer(move |cx| { + if let Some(task) = handle + .update(cx, |multi_workspace, window, cx| { + multi_workspace.open_project(paths, window, cx) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + }); + } + return; } else { workspace.open_workspace_for_paths(false, paths, window, cx) } @@ -609,7 +605,7 @@ impl PickerDelegate for RecentProjectsDelegate { let app_state = workspace.app_state().clone(); let replace_window = if replace_current_window { - window.window_handle().downcast::() + window.window_handle().downcast::() } else { None }; @@ -884,10 +880,18 @@ impl RecentProjectsDelegate { ) { if let Some(selected_match) = self.matches.get(ix) { let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id]; + let fs = self + .workspace + .upgrade() + .map(|ws| ws.read(cx).app_state().fs.clone()); cx.spawn_in(window, async move |this, cx| { - let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; + WORKSPACE_DB + .delete_workspace_by_id(workspace_id) + .await + .log_err(); + let Some(fs) = fs else { return }; let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk() + .recent_workspaces_on_disk(fs.as_ref()) .await .unwrap_or_default(); this.update_in(cx, move |picker, window, cx| { @@ -904,6 +908,7 @@ impl RecentProjectsDelegate { .update(cx, |this, cx| this.delete_history(workspace_id, cx)); } }) + .ok(); }) .detach(); } @@ -951,7 +956,7 @@ mod tests { use super::*; #[gpui::test] - async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { + async fn test_dirty_workspace_survives_when_opening_recent_project(cx: &mut TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { @@ -975,6 +980,11 @@ mod tests { }), ) .await; + app_state + .fs + .as_fake() + .insert_tree(path!("/test/path"), json!({})) + .await; cx.update(|cx| { open_paths( &[PathBuf::from(path!("/dir/main.ts"))], @@ -987,31 +997,40 @@ mod tests { .unwrap(); assert_eq!(cx.update(|cx| cx.windows().len()), 1); - let workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - workspace - .update(cx, |workspace, _, _| assert!(!workspace.is_edited())) + let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + multi_workspace + .update(cx, |multi_workspace, _, cx| { + assert!(!multi_workspace.workspace().read(cx).is_edited()) + }) .unwrap(); - let editor = workspace - .read_with(cx, |workspace, cx| { - workspace + let editor = multi_workspace + .read_with(cx, |multi_workspace, cx| { + multi_workspace + .workspace() + .read(cx) .active_item(cx) .unwrap() .downcast::() .unwrap() }) .unwrap(); - workspace + multi_workspace .update(cx, |_, window, cx| { editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); }) .unwrap(); - workspace - .update(cx, |workspace, _, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project")) + multi_workspace + .update(cx, |multi_workspace, _, cx| { + assert!( + multi_workspace.workspace().read(cx).is_edited(), + "After inserting more text into the editor without saving, we should have a dirty project" + ) + }) .unwrap(); - let recent_projects_picker = open_recent_projects(&workspace, cx); - workspace + let recent_projects_picker = open_recent_projects(&multi_workspace, cx); + multi_workspace .update(cx, |_, _, cx| { recent_projects_picker.update(cx, |picker, cx| { assert_eq!(picker.query(cx), ""); @@ -1035,47 +1054,64 @@ mod tests { !cx.has_pending_prompt(), "Should have no pending prompt on dirty project before opening the new recent project" ); - cx.dispatch_action(*workspace, menu::Confirm); - workspace - .update(cx, |workspace, _, cx| { + let dirty_workspace = multi_workspace + .read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().clone() + }) + .unwrap(); + + cx.dispatch_action(*multi_workspace, menu::Confirm); + cx.run_until_parked(); + + multi_workspace + .update(cx, |multi_workspace, _, cx| { assert!( - workspace.active_modal::(cx).is_none(), + multi_workspace + .workspace() + .read(cx) + .active_modal::(cx) + .is_none(), "Should remove the modal after selecting new recent project" - ) + ); + + assert!( + multi_workspace.workspaces().len() >= 2, + "Should have at least 2 workspaces: the dirty one and the newly opened one" + ); + + assert!( + multi_workspace.workspaces().contains(&dirty_workspace), + "The original dirty workspace should still be present" + ); + + assert!( + dirty_workspace.read(cx).is_edited(), + "The original workspace should still be dirty" + ); }) .unwrap(); - assert!( - cx.has_pending_prompt(), - "Dirty workspace should prompt before opening the new recent project" - ); - cx.simulate_prompt_answer("Cancel"); + assert!( !cx.has_pending_prompt(), - "Should have no pending prompt after cancelling" + "No save prompt in multi-workspace mode — dirty workspace survives in background" ); - workspace - .update(cx, |workspace, _, _| { - assert!( - workspace.is_edited(), - "Should be in the same dirty project after cancelling" - ) - }) - .unwrap(); } fn open_recent_projects( - workspace: &WindowHandle, + multi_workspace: &WindowHandle, cx: &mut TestAppContext, ) -> Entity> { cx.dispatch_action( - (*workspace).into(), + (*multi_workspace).into(), OpenRecent { create_new_window: false, }, ); - workspace - .update(cx, |workspace, _, cx| { - workspace + multi_workspace + .update(cx, |multi_workspace, _, cx| { + multi_workspace + .workspace() + .read(cx) .active_modal::(cx) .unwrap() .read(cx) diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 9d6199786cb6f251a792fa32e8caccd9351d00d3..52304c211a4d38ef1e408093d1fbdc3c8f07c1bf 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -19,7 +19,7 @@ use remote::{ pub use settings::SshConnection; use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection}; use util::paths::PathWithPosition; -use workspace::{AppState, Workspace}; +use workspace::{AppState, MultiWorkspace, Workspace}; pub use remote_connection::{ RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader, @@ -131,8 +131,11 @@ pub async fn open_remote_project( cx: &mut AsyncApp, ) -> Result<()> { let created_new_window = open_options.replace_window.is_none(); - let window = if let Some(window) = open_options.replace_window { - window + let (window, initial_workspace) = if let Some(window) = open_options.replace_window { + let workspace = window.update(cx, |multi_workspace, _, _| { + multi_workspace.workspace().clone() + })?; + (window, workspace) } else { let workspace_position = cx .update(|cx| { @@ -145,7 +148,7 @@ pub async fn open_remote_project( cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx)); options.window_bounds = workspace_position.window_bounds; - cx.open_window(options, |window, cx| { + let window = cx.open_window(options, |window, cx| { let project = project::Project::local( app_state.client.clone(), app_state.node_runtime.clone(), @@ -159,12 +162,17 @@ pub async fn open_remote_project( }, cx, ); - cx.new(|cx| { + let workspace = cx.new(|cx| { let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx); workspace.centered_layout = workspace_position.centered_layout; workspace - }) - })? + }); + cx.new(|cx| MultiWorkspace::new(workspace, cx)) + })?; + let workspace = window.update(cx, |multi_workspace, _, _cx| { + multi_workspace.workspace().clone() + })?; + (window, workspace) }; loop { @@ -172,35 +180,38 @@ pub async fn open_remote_project( let delegate = window.update(cx, { let paths = paths.clone(); let connection_options = connection_options.clone(); - move |workspace, window, cx| { + let initial_workspace = initial_workspace.clone(); + move |_multi_workspace: &mut MultiWorkspace, window, cx| { window.activate_window(); - workspace.hide_modal(window, cx); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteConnectionModal::new(&connection_options, paths, window, cx) - }); - - let ui = workspace - .active_modal::(cx)? - .read(cx) - .prompt - .clone(); - - ui.update(cx, |ui, _cx| { - ui.set_cancellation_tx(cancel_tx); - }); - - Some(Arc::new(RemoteClientDelegate::new( - window.window_handle(), - ui.downgrade(), - if let RemoteConnectionOptions::Ssh(options) = &connection_options { - options - .password - .as_deref() - .and_then(|pw| EncryptedPassword::try_from(pw).ok()) - } else { - None - }, - ))) + initial_workspace.update(cx, |workspace, cx| { + workspace.hide_modal(window, cx); + workspace.toggle_modal(window, cx, |window, cx| { + RemoteConnectionModal::new(&connection_options, paths, window, cx) + }); + + let ui = workspace + .active_modal::(cx)? + .read(cx) + .prompt + .clone(); + + ui.update(cx, |ui, _cx| { + ui.set_cancellation_tx(cancel_tx); + }); + + Some(Arc::new(RemoteClientDelegate::new( + window.window_handle(), + ui.downgrade(), + if let RemoteConnectionOptions::Ssh(options) = &connection_options { + options + .password + .as_deref() + .and_then(|pw| EncryptedPassword::try_from(pw).ok()) + } else { + None + }, + ))) + }) } })?; @@ -209,13 +220,11 @@ pub async fn open_remote_project( let connection = remote::connect(connection_options.clone(), delegate.clone(), cx); let connection = select! { _ = cancel_rx => { - window - .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }) - .ok(); + initial_workspace.update(cx, |workspace, cx| { + if let Some(ui) = workspace.active_modal::(cx) { + ui.update(cx, |modal, cx| modal.finished(cx)) + } + }); break; }, @@ -224,13 +233,11 @@ pub async fn open_remote_project( let remote_connection = match connection { Ok(connection) => connection, Err(e) => { - window - .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }) - .ok(); + initial_workspace.update(cx, |workspace, cx| { + if let Some(ui) = workspace.active_modal::(cx) { + ui.update(cx, |modal, cx| modal.finished(cx)) + } + }); log::error!("Failed to open project: {e:#}"); let response = window .update(cx, |_, window, cx| { @@ -284,13 +291,11 @@ pub async fn open_remote_project( }) .await; - window - .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }) - .ok(); + initial_workspace.update(cx, |workspace, cx| { + if let Some(ui) = workspace.active_modal::(cx) { + ui.update(cx, |modal, cx| modal.finished(cx)) + } + }); match opened_items { Err(e) => { @@ -320,20 +325,20 @@ pub async fn open_remote_project( continue; } - window - .update(cx, |workspace, window, cx| { - if created_new_window { - window.remove_window(); - } - trusted_worktrees::track_worktree_trust( - workspace.project().read(cx).worktree_store(), - None, - None, - None, - cx, - ); - }) - .ok(); + if created_new_window { + window + .update(cx, |_, window, _| window.remove_window()) + .ok(); + } + initial_workspace.update(cx, |workspace, cx| { + trusted_worktrees::track_worktree_trust( + workspace.project().read(cx).worktree_store(), + None, + None, + None, + cx, + ); + }); } Ok(items) => { @@ -366,14 +371,20 @@ pub async fn open_remote_project( break; } + // Register the remote client with extensions. We use `multi_workspace.workspace()` here + // (not `initial_workspace`) because `open_remote_project_inner` activated the new remote + // workspace, so the active workspace is now the one with the remote project. window - .update(cx, |workspace, _, cx| { - if let Some(client) = workspace.project().read(cx).remote_client() { - if let Some(extension_store) = ExtensionStore::try_global(cx) { - extension_store - .update(cx, |store, cx| store.register_remote_client(client, cx)); + .update(cx, |multi_workspace: &mut MultiWorkspace, _, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + if let Some(client) = workspace.project().read(cx).remote_client() { + if let Some(extension_store) = ExtensionStore::try_global(cx) { + extension_store + .update(cx, |store, cx| store.register_remote_client(client, cx)); + } } - } + }); }) .ok(); Ok(()) @@ -500,12 +511,16 @@ mod tests { let windows = cx.update(|cx| cx.windows().len()); assert_eq!(windows, 1, "Should have opened a window"); - let workspace_handle = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + let multi_workspace_handle = + cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - workspace_handle - .update(cx, |workspace, _, cx| { - let project = workspace.project().read(cx); - assert!(project.is_remote(), "Project should be a remote project"); + multi_workspace_handle + .update(cx, |multi_workspace, _, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + assert!(project.is_remote(), "Project should be a remote project"); + }); }) .unwrap(); } diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 5b719940f958ac0e4ecb6e186052e3e09987f80e..921b19686ab49bb4704fa72b376fc8370b8f354b 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -6,7 +6,8 @@ use crate::{ ssh_config::parse_ssh_config_hosts, }; use dev_container::{ - DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config, + DevContainerConfig, DevContainerContext, find_devcontainer_configs, + start_dev_container_with_config, }; use editor::Editor; @@ -51,7 +52,7 @@ use util::{ rel_path::RelPath, }; use workspace::{ - ModalView, OpenLog, OpenOptions, Toast, Workspace, + ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace, notifications::{DetachAndPromptErr, NotificationId}, open_remote_project_with_existing_connection, }; @@ -478,10 +479,11 @@ impl ProjectPicker { .log_err()?; let window = cx .open_window(options, |window, cx| { - cx.new(|cx| { + let workspace = cx.new(|cx| { telemetry::event!("SSH Project Created"); Workspace::new(None, project.clone(), app_state.clone(), window, cx) - }) + }); + cx.new(|cx| MultiWorkspace::new(workspace, cx)) }) .log_err()?; @@ -808,11 +810,18 @@ impl RemoteServerProjects { workspace: WeakEntity, cx: &mut Context, ) -> Self { - let this = Self::new_inner( - Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new( - DevContainerCreationProgress::Creating, - cx, - )), + let configs = workspace + .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx)) + .unwrap_or_default(); + + let initial_mode = if configs.len() > 1 { + DevContainerCreationProgress::SelectingConfig + } else { + DevContainerCreationProgress::Creating + }; + + let mut this = Self::new_inner( + Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(initial_mode, cx)), false, fs, window, @@ -820,35 +829,15 @@ impl RemoteServerProjects { cx, ); - // Spawn a task to scan for configs and then start the container - cx.spawn_in(window, async move |entity, cx| { - let configs = find_devcontainer_configs(cx); - - entity - .update_in(cx, |this, window, cx| { - if configs.len() > 1 { - // Multiple configs found - show selection UI - let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity()); - this.dev_container_picker = Some( - cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)), - ); - - let state = CreateRemoteDevContainer::new( - DevContainerCreationProgress::SelectingConfig, - cx, - ); - this.mode = Mode::CreateRemoteDevContainer(state); - cx.notify(); - } else { - // Single or no config - proceed with opening - let config = configs.into_iter().next(); - this.open_dev_container(config, window, cx); - this.view_in_progress_dev_container(window, cx); - } - }) - .log_err(); - }) - .detach(); + if configs.len() > 1 { + let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity()); + this.dev_container_picker = + Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false))); + } else { + let config = configs.into_iter().next(); + this.open_dev_container(config, window, cx); + this.view_in_progress_dev_container(window, cx); + } this } @@ -1551,7 +1540,9 @@ impl RemoteServerProjects { let replace_window = match (create_new_window, secondary_confirm) { (true, false) | (false, true) => None, - (true, true) | (false, false) => window.window_handle().downcast::(), + (true, true) | (false, false) => { + window.window_handle().downcast::() + } }; cx.spawn_in(window, async move |_, cx| { @@ -1803,25 +1794,25 @@ impl RemoteServerProjects { } fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context) { - cx.spawn_in(window, async move |entity, cx| { - let configs = find_devcontainer_configs(cx); + let configs = self + .workspace + .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx)) + .unwrap_or_default(); - entity - .update_in(cx, |this, window, cx| { - let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity()); - this.dev_container_picker = - Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false))); + if configs.len() > 1 { + let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity()); + self.dev_container_picker = + Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false))); - let state = CreateRemoteDevContainer::new( - DevContainerCreationProgress::SelectingConfig, - cx, - ); - this.mode = Mode::CreateRemoteDevContainer(state); - cx.notify(); - }) - .log_err(); - }) - .detach(); + let state = + CreateRemoteDevContainer::new(DevContainerCreationProgress::SelectingConfig, cx); + self.mode = Mode::CreateRemoteDevContainer(state); + cx.notify(); + } else { + let config = configs.into_iter().next(); + self.open_dev_container(config, window, cx); + self.view_in_progress_dev_container(window, cx); + } } fn open_dev_container( @@ -1830,21 +1821,25 @@ impl RemoteServerProjects { window: &mut Window, cx: &mut Context, ) { - let Some(app_state) = self + let Some((app_state, context)) = self .workspace - .read_with(cx, |workspace, _| workspace.app_state().clone()) + .read_with(cx, |workspace, cx| { + let app_state = workspace.app_state().clone(); + let context = DevContainerContext::from_workspace(workspace, cx)?; + Some((app_state, context)) + }) .log_err() + .flatten() else { + log::error!("No active project directory for Dev Container"); return; }; - let replace_window = window.window_handle().downcast::(); + let replace_window = window.window_handle().downcast::(); cx.spawn_in(window, async move |entity, cx| { let (connection, starting_dir) = - match start_dev_container_with_config(cx, app_state.node_runtime.clone(), config) - .await - { + match start_dev_container_with_config(context, config).await { Ok((c, s)) => (Connection::DevContainer(c), s), Err(e) => { log::error!("Failed to start dev container: {:?}", e); diff --git a/crates/recent_projects/src/wsl_picker.rs b/crates/recent_projects/src/wsl_picker.rs index e386b723fa43777e496565c11b8308f16031d837..7f2a69eb68cb93742d98f438f75f74c95bf3f7d5 100644 --- a/crates/recent_projects/src/wsl_picker.rs +++ b/crates/recent_projects/src/wsl_picker.rs @@ -8,7 +8,7 @@ use ui::{ Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex, }; use util::ResultExt as _; -use workspace::{ModalView, Workspace}; +use workspace::{ModalView, MultiWorkspace}; use crate::open_remote_project; @@ -249,7 +249,7 @@ impl WslOpenModal { false => !secondary, }; let replace_window = match replace_current_window { - true => window.window_handle().downcast::(), + true => window.window_handle().downcast::(), false => None, }; diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 2dfc0abb19b62f0e1401920f07c22c83dc68dfb3..6686b2003abc8222f4044a8c711be86e18d8c116 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -779,8 +779,10 @@ mod tests { let fs = project::FakeFs::new(cx.background_executor.clone()); let project = project::Project::test(fs, [] as [&Path; 0], cx).await; let window = - cx.add_window(|window, cx| workspace::Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).expect("workspace should exist"); + cx.add_window(|window, cx| workspace::MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let weak_workspace = workspace.downgrade(); let visual_cx = gpui::VisualTestContext::from_window(window.into(), cx); (visual_cx, weak_workspace) diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 30a986add52ec935aeb5752d9d2b2fc214d60a84..b3aa0301f204e97e6b1acda2a5cff4479b51c590 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -4,8 +4,8 @@ use editor::{CompletionProvider, SelectionEffects}; use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab}; use gpui::{ App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel, - Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, - actions, point, size, transparent_black, + Subscription, Task, TextStyle, Tiling, TitlebarOptions, WindowBounds, WindowHandle, + WindowOptions, actions, point, size, transparent_black, }; use language::{Buffer, LanguageRegistry, language_settings::SoftWrap}; use language_model::{ @@ -24,7 +24,7 @@ use theme::ThemeSettings; use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*}; use ui_input::ErasedEditor; use util::{ResultExt, TryFutureExt}; -use workspace::{Workspace, WorkspaceSettings, client_side_decorations}; +use workspace::{MultiWorkspace, Workspace, WorkspaceSettings, client_side_decorations}; use zed_actions::assistant::InlineAssist; use prompt_store::*; @@ -968,12 +968,14 @@ impl RulesLibrary { .assist(rule_editor, initial_prompt, window, cx); } else { for window in cx.windows() { - if let Some(workspace) = window.downcast::() { - let panel = workspace - .update(cx, |workspace, window, cx| { + if let Some(multi_workspace) = window.downcast::() { + let panel = multi_workspace + .update(cx, |multi_workspace, window, cx| { window.activate_window(); - self.inline_assist_delegate - .focus_agent_panel(workspace, window, cx) + multi_workspace.workspace().update(cx, |workspace, cx| { + self.inline_assist_delegate + .focus_agent_panel(workspace, window, cx) + }) }) .ok(); if panel == Some(true) { @@ -1427,6 +1429,7 @@ impl Render for RulesLibrary { ), window, cx, + Tiling::default(), ) } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 5c8795a3c429a1fea0b862fe1e604e101d3918be..3c2bfea77aa1f0f409ce78a3a69e612aa30715e2 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2495,7 +2495,6 @@ pub fn perform_project_search( #[cfg(test)] pub mod tests { use std::{ - ops::Deref as _, path::PathBuf, sync::{ Arc, @@ -2516,7 +2515,7 @@ pub mod tests { }; use util::{path, paths::PathStyle, rel_path::rel_path}; use util_macros::perf; - use workspace::DeploySearch; + use workspace::{DeploySearch, MultiWorkspace}; #[perf] #[gpui::test] @@ -2632,8 +2631,11 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); let search_view = cx.add_window(|window, cx| { ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) @@ -2791,14 +2793,16 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let active_item = cx.read(|cx| { workspace .read(cx) - .unwrap() .active_pane() .read(cx) .active_item() @@ -2809,27 +2813,24 @@ pub mod tests { "Expected no search panel to be active" ); - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); + workspace.update_in(cx, move |workspace, window, cx| { + assert_eq!(workspace.panes().len(), 1); + workspace.panes()[0].update(cx, |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::find(), + window, + cx, + ) + }); let Some(search_view) = cx.read(|cx| { workspace .read(cx) - .unwrap() .active_pane() .read(cx) .active_item() @@ -2969,16 +2970,14 @@ pub mod tests { }); }).unwrap(); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::find(), + window, + cx, + ) + }); window.update(cx, |_, window, cx| { search_view.update(cx, |search_view, cx| { assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); @@ -3032,30 +3031,30 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - window - .update(cx, move |workspace, window, cx| { - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); + workspace.update_in(cx, move |workspace, window, cx| { + workspace.panes()[0].update(cx, |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::find(), + window, + cx, + ) + }); let Some(search_view) = cx.read(|cx| { workspace .read(cx) - .unwrap() .active_pane() .read(cx) .active_item() @@ -3153,14 +3152,16 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let active_item = cx.read(|cx| { workspace .read(cx) - .unwrap() .active_pane() .read(cx) .active_item() @@ -3171,22 +3172,19 @@ pub mod tests { "Expected no search panel to be active" ); - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); + workspace.update_in(cx, move |workspace, window, cx| { + assert_eq!(workspace.panes().len(), 1); + workspace.panes()[0].update(cx, |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - }) - .unwrap(); + ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) + }); let Some(search_view) = cx.read(|cx| { workspace .read(cx) - .unwrap() .active_pane() .read(cx) .active_item() @@ -3326,16 +3324,13 @@ pub mod tests { }); }).unwrap(); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) + }); cx.background_executor.run_until_parked(); let Some(search_view_2) = cx.read(|cx| { workspace .read(cx) - .unwrap() .active_pane() .read(cx) .active_item() @@ -3456,8 +3451,11 @@ pub mod tests { let worktree_id = project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let active_item = cx.read(|cx| { @@ -3473,17 +3471,15 @@ pub mod tests { "Expected no search panel to be active" ); - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - }) - .unwrap(); + workspace.update_in(cx, move |workspace, window, cx| { + assert_eq!(workspace.panes().len(), 1); + workspace.panes()[0].update(cx, move |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); + }); - let a_dir_entry = cx.update(|cx| { + let a_dir_entry = cx.update(|_, cx| { workspace .read(cx) .project() @@ -3493,11 +3489,9 @@ pub mod tests { .clone() }); assert!(a_dir_entry.is_dir()); - window - .update(cx, |workspace, window, cx| { - ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx) - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx) + }); let Some(search_view) = cx.read(|cx| { workspace @@ -3576,24 +3570,25 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - window - .update(cx, { - let search_bar = search_bar.clone(); - |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); + workspace.update_in(cx, { + let search_bar = search_bar.clone(); + |workspace, window, cx| { + assert_eq!(workspace.panes().len(), 1); + workspace.panes()[0].update(cx, |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); + ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) + } + }); let search_view = cx.read(|cx| { workspace @@ -3908,21 +3903,22 @@ pub mod tests { this.worktrees(cx).next().unwrap().read(cx).id() }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let panes: Vec<_> = window - .update(cx, |this, _, _| this.panes().to_owned()) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + + let panes: Vec<_> = workspace.update_in(cx, |this, _, _| this.panes().to_owned()); let search_bar_1 = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar_2 = window.build_entity(cx, |_, _| ProjectSearchBar::new()); assert_eq!(panes.len(), 1); let first_pane = panes.first().cloned().unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); - window - .update(cx, |workspace, window, cx| { + assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 0); + workspace + .update_in(cx, |workspace, window, cx| { workspace.open_path( (worktree_id, rel_path("one.rs")), Some(first_pane.downgrade()), @@ -3931,25 +3927,22 @@ pub mod tests { cx, ) }) - .unwrap() .await .unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); + assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 1); // Add a project search item to the first pane - window - .update(cx, { - let search_bar = search_bar_1.clone(); - |workspace, window, cx| { - first_pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); + workspace.update_in(cx, { + let search_bar = search_bar_1.clone(); + |workspace, window, cx| { + first_pane.update(cx, |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); + ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) + } + }); let search_view_1 = cx.read(|cx| { workspace .read(cx) @@ -3958,8 +3951,8 @@ pub mod tests { .expect("Search view expected to appear after new search event trigger") }); - let second_pane = window - .update(cx, |workspace, window, cx| { + let second_pane = workspace + .update_in(cx, |workspace, window, cx| { workspace.split_and_clone( first_pane.clone(), workspace::SplitDirection::Right, @@ -3967,30 +3960,27 @@ pub mod tests { cx, ) }) - .unwrap() .await .unwrap(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); + assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 1); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2); + assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 1); + assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 2); // Add a project search item to the second pane - window - .update(cx, { - let search_bar = search_bar_2.clone(); - let pane = second_pane.clone(); - move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); + workspace.update_in(cx, { + let search_bar = search_bar_2.clone(); + let pane = second_pane.clone(); + move |workspace, window, cx| { + assert_eq!(workspace.panes().len(), 2); + pane.update(cx, |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); + ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) + } + }); let search_view_2 = cx.read(|cx| { workspace @@ -4001,8 +3991,8 @@ pub mod tests { }); cx.run_until_parked(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); + assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 2); + assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 2); let update_search_view = |search_view: &Entity, query: &str, cx: &mut TestAppContext| { @@ -4133,15 +4123,17 @@ pub mod tests { let worktree_id = project.update(cx, |this, cx| { this.worktrees(cx).next().unwrap().read(cx).id() }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let panes: Vec<_> = window - .update(cx, |this, _, _| this.panes().to_owned()) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panes: Vec<_> = workspace.update_in(cx, |this, _, _| this.panes().to_owned()); assert_eq!(panes.len(), 1); let first_pane = panes.first().cloned().unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); - window - .update(cx, |workspace, window, cx| { + assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 0); + workspace + .update_in(cx, |workspace, window, cx| { workspace.open_path( (worktree_id, rel_path("one.rs")), Some(first_pane.downgrade()), @@ -4150,12 +4142,11 @@ pub mod tests { cx, ) }) - .unwrap() .await .unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); - let second_pane = window - .update(cx, |workspace, window, cx| { + assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 1); + let second_pane = workspace + .update_in(cx, |workspace, window, cx| { workspace.split_and_clone( first_pane.clone(), workspace::SplitDirection::Right, @@ -4163,10 +4154,9 @@ pub mod tests { cx, ) }) - .unwrap() .await .unwrap(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); + assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 1); assert!( window .update(cx, |_, window, cx| second_pane @@ -4175,76 +4165,66 @@ pub mod tests { .unwrap() ); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - window - .update(cx, { - let search_bar = search_bar.clone(); - let pane = first_pane.clone(); - move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - pane.update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - } - }) - .unwrap(); + workspace.update_in(cx, { + let search_bar = search_bar.clone(); + let pane = first_pane.clone(); + move |workspace, window, cx| { + assert_eq!(workspace.panes().len(), 2); + pane.update(cx, move |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); + } + }); // Add a project search item to the second pane - window - .update(cx, { - |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - second_pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); + workspace.update_in(cx, { + |workspace, window, cx| { + assert_eq!(workspace.panes().len(), 2); + second_pane.update(cx, |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) + }); - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); + ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) + } + }); cx.run_until_parked(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); + assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 2); + assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 1); // Focus the first pane - window - .update(cx, |workspace, window, cx| { - assert_eq!(workspace.active_pane(), &second_pane); - second_pane.update(cx, |this, cx| { - assert_eq!(this.active_item_index(), 1); - this.activate_previous_item(&Default::default(), window, cx); - assert_eq!(this.active_item_index(), 0); - }); - workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); - }) - .unwrap(); - window - .update(cx, |workspace, _, cx| { - assert_eq!(workspace.active_pane(), &first_pane); - assert_eq!(first_pane.read(cx).items_len(), 1); - assert_eq!(second_pane.read(cx).items_len(), 2); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + assert_eq!(workspace.active_pane(), &second_pane); + second_pane.update(cx, |this, cx| { + assert_eq!(this.active_item_index(), 1); + this.activate_previous_item(&Default::default(), window, cx); + assert_eq!(this.active_item_index(), 0); + }); + workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); + }); + workspace.update_in(cx, |workspace, _, cx| { + assert_eq!(workspace.active_pane(), &first_pane); + assert_eq!(first_pane.read(cx).items_len(), 1); + assert_eq!(second_pane.read(cx).items_len(), 2); + }); // Deploy a new search - cx.dispatch_action(window.into(), DeploySearch::find()); + cx.dispatch_action(DeploySearch::find()); // Both panes should now have a project search in them - window - .update(cx, |workspace, window, cx| { - assert_eq!(workspace.active_pane(), &first_pane); - first_pane.read_with(cx, |this, _| { - assert_eq!(this.active_item_index(), 1); - assert_eq!(this.items_len(), 2); - }); - second_pane.update(cx, |this, cx| { - assert!(!cx.focus_handle().contains_focused(window, cx)); - assert_eq!(this.items_len(), 2); - }); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + assert_eq!(workspace.active_pane(), &first_pane); + first_pane.read_with(cx, |this, _| { + assert_eq!(this.active_item_index(), 1); + assert_eq!(this.items_len(), 2); + }); + second_pane.update(cx, |this, cx| { + assert!(!cx.focus_handle().contains_focused(window, cx)); + assert_eq!(this.items_len(), 2); + }); + }); // Focus the second pane's non-search item window @@ -4256,7 +4236,7 @@ pub mod tests { .unwrap(); // Deploy a new search - cx.dispatch_action(window.into(), DeploySearch::find()); + cx.dispatch_action(DeploySearch::find()); // The project search view should now be focused in the second pane // And the number of items should be unchanged. @@ -4310,8 +4290,11 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let search = cx.new(|cx| ProjectSearch::new(project, cx)); let search_view = cx.add_window(|window, cx| { ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) @@ -4374,9 +4357,12 @@ pub mod tests { let worktree_id = project.update(cx, |this, cx| { this.worktrees(cx).next().unwrap().read(cx).id() }); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - let mut cx = VisualTestContext::from_window(*window.deref(), cx); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let mut cx = VisualTestContext::from_window(window.into(), cx); let editor = workspace .update_in(&mut cx, |workspace, window, cx| { @@ -4398,9 +4384,7 @@ pub mod tests { search_bar }); - let panes: Vec<_> = window - .update(&mut cx, |this, _, _| this.panes().to_owned()) - .unwrap(); + let panes: Vec<_> = workspace.update_in(&mut cx, |this, _, _| this.panes().to_owned()); assert_eq!(panes.len(), 1); let pane = panes.first().cloned().unwrap(); pane.update_in(&mut cx, |pane, window, cx| { @@ -4450,7 +4434,12 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); struct EmptyModalView { focus_handle: gpui::FocusHandle, @@ -4468,34 +4457,28 @@ pub mod tests { } impl workspace::ModalView for EmptyModalView {} - window - .update(cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { - focus_handle: cx.focus_handle(), - }); - assert!(workspace.has_active_modal(window, cx)); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { + focus_handle: cx.focus_handle(), + }); + assert!(workspace.has_active_modal(window, cx)); + }); - cx.dispatch_action(window.into(), Deploy::find()); + cx.dispatch_action(Deploy::find()); - window - .update(cx, |workspace, window, cx| { - assert!(!workspace.has_active_modal(window, cx)); - workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { - focus_handle: cx.focus_handle(), - }); - assert!(workspace.has_active_modal(window, cx)); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + assert!(!workspace.has_active_modal(window, cx)); + workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { + focus_handle: cx.focus_handle(), + }); + assert!(workspace.has_active_modal(window, cx)); + }); - cx.dispatch_action(window.into(), DeploySearch::find()); + cx.dispatch_action(DeploySearch::find()); - window - .update(cx, |workspace, window, cx| { - assert!(!workspace.has_active_modal(window, cx)); - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + assert!(!workspace.has_active_modal(window, cx)); + }); } #[perf] @@ -4562,8 +4545,12 @@ pub mod tests { }, ); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); let search_view = cx.add_window(|window, cx| { ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) @@ -4609,8 +4596,8 @@ pub mod tests { "We did drop the previous buffer when cleared the old project search results, hence another query was made", ); - let singleton_editor = window - .update(cx, |workspace, window, cx| { + let singleton_editor = workspace + .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( PathBuf::from(path!("/dir/main.rs")), workspace::OpenOptions::default(), @@ -4618,7 +4605,6 @@ pub mod tests { cx, ) }) - .unwrap() .await .unwrap() .downcast::() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index ac337fb4c8f53e407178d9ccf1be7e91d89fadcb..d2104492bebf529821f8ad8571fd3fbb8bdbc69e 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -191,7 +191,7 @@ pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) { struct NotifType(); let notification_id = NotificationId::unique::(); - let Some(workspace) = window.root::().flatten() else { + let Some(workspace) = Workspace::for_window(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { diff --git a/crates/session/src/session.rs b/crates/session/src/session.rs index 687c9e20b7a4b92131d77470509a7e5f0b7193ce..de6be034f9732f2c24dd860ebccd0c677d4fc623 100644 --- a/crates/session/src/session.rs +++ b/crates/session/src/session.rs @@ -47,6 +47,15 @@ impl Session { } } + #[cfg(any(test, feature = "test-support"))] + pub fn test_with_old_session(old_session_id: String) -> Self { + Self { + session_id: uuid::Uuid::new_v4().to_string(), + old_session_id: Some(old_session_id), + old_window_ids: None, + } + } + pub fn id(&self) -> &str { &self.session_id } @@ -109,6 +118,11 @@ impl AppSession { self.session.old_session_id.as_deref() } + #[cfg(any(test, feature = "test-support"))] + pub fn replace_session_for_test(&mut self, session: Session) { + self.session = session; + } + pub fn last_session_window_stack(&self) -> Option> { self.session.old_window_ids.clone() } diff --git a/crates/settings_profile_selector/src/settings_profile_selector.rs b/crates/settings_profile_selector/src/settings_profile_selector.rs index 42d714283a4e1ed569bd03a5386ab16988a8014a..7ca91e3767efb6b550af7887e70a0187fed6daad 100644 --- a/crates/settings_profile_selector/src/settings_profile_selector.rs +++ b/crates/settings_profile_selector/src/settings_profile_selector.rs @@ -287,7 +287,7 @@ mod tests { use serde_json::json; use settings::Settings; use theme::{self, ThemeSettings}; - use workspace::{self, AppState}; + use workspace::{self, AppState, MultiWorkspace}; use zed_actions::settings_profile_selector; async fn init_test( @@ -320,8 +320,11 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, ["/test".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let cx = VisualTestContext::from_window(*window, cx).into_mut(); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); cx.update(|_, cx| { assert!(!cx.has_global::()); diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 154cb5ff91a3f120059f966110bad58ce0bc2002..6bc92cb560f23d810739fc3b11826acbd8d8e01f 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -9,8 +9,9 @@ use fuzzy::StringMatchCandidate; use gpui::{ Action, App, AsyncApp, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle, Focusable, Global, KeyContext, ListState, ReadGlobal as _, ScrollHandle, Stateful, - Subscription, Task, TitlebarOptions, UniformListScrollHandle, WeakEntity, Window, WindowBounds, - WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list, + Subscription, Task, Tiling, TitlebarOptions, UniformListScrollHandle, WeakEntity, Window, + WindowBounds, WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, + uniform_list, }; use language::Buffer; @@ -40,7 +41,9 @@ use ui::{ }; use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; -use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations}; +use workspace::{ + AppState, MultiWorkspace, OpenOptions, OpenVisible, Workspace, client_side_decorations, +}; use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt}; use crate::components::{ @@ -394,7 +397,7 @@ pub fn init(cx: &mut App) { |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| { let window_handle = window .window_handle() - .downcast::() + .downcast::() .expect("Workspaces are root Windows"); open_settings_editor(workspace, Some(&path), false, window_handle, cx); }, @@ -402,14 +405,14 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenSettings, window, cx| { let window_handle = window .window_handle() - .downcast::() + .downcast::() .expect("Workspaces are root Windows"); open_settings_editor(workspace, None, false, window_handle, cx); }) .register_action(|workspace, _: &OpenProjectSettings, window, cx| { let window_handle = window .window_handle() - .downcast::() + .downcast::() .expect("Workspaces are root Windows"); open_settings_editor(workspace, None, true, window_handle, cx); }); @@ -549,7 +552,7 @@ pub fn open_settings_editor( _workspace: &mut Workspace, path: Option<&str>, open_project_settings: bool, - workspace_handle: WindowHandle, + workspace_handle: WindowHandle, cx: &mut App, ) { telemetry::event!("Settings Viewed"); @@ -717,7 +720,7 @@ fn active_language_mut() -> Option>, - original_window: Option>, + original_window: Option>, files: Vec<(SettingsUiFile, FocusHandle)>, worktree_root_dirs: HashMap, current_file: SettingsUiFile, @@ -1450,7 +1453,7 @@ impl SettingsUiFile { impl SettingsWindow { fn new( - original_window: Option>, + original_window: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -1521,34 +1524,21 @@ impl SettingsWindow { .detach(); if let Some(app_state) = AppState::global(cx).upgrade() { - for project in app_state + let workspaces: Vec> = app_state .workspace_store .read(cx) .workspaces() - .iter() - .filter_map(|space| { - space - .read(cx) - .ok() - .map(|workspace| workspace.project().clone()) - }) - .collect::>() - { + .filter_map(|weak| weak.upgrade()) + .collect(); + + for workspace in workspaces { + let project = workspace.read(cx).project().clone(); cx.observe_release_in(&project, window, |this, _, window, cx| { this.fetch_files(window, cx) }) .detach(); cx.subscribe_in(&project, window, Self::handle_project_event) .detach(); - } - - for workspace in app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .filter_map(|space| space.entity(cx).ok()) - { cx.observe_release_in(&workspace, window, |this, _, window, cx| { this.fetch_files(window, cx) }) @@ -3324,56 +3314,19 @@ impl SettingsWindow { return; }; original_window - .update(cx, |workspace, window, cx| { - workspace - .with_local_or_wsl_workspace(window, cx, |workspace, window, cx| { - let project = workspace.project().clone(); - - cx.spawn_in(window, async move |workspace, cx| { - let (config_dir, settings_file) = - project.update(cx, |project, cx| { - ( - project.try_windows_path_to_wsl( - paths::config_dir().as_path(), - cx, - ), - project.try_windows_path_to_wsl( - paths::settings_file().as_path(), - cx, - ), - ) - }); - let config_dir = config_dir.await?; - let settings_file = settings_file.await?; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(&config_dir, false, cx) - }) - .await - .ok(); - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_paths( - vec![settings_file], - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - None, - window, - cx, - ) - })? - .await; - - workspace.update_in(cx, |_, window, cx| { - window.activate_window(); - cx.notify(); - }) - }) - .detach(); - }) - .detach(); + .update(cx, |multi_workspace, window, cx| { + multi_workspace + .workspace() + .clone() + .update(cx, |workspace, cx| { + workspace + .with_local_or_wsl_workspace( + window, + cx, + open_user_settings_in_workspace, + ) + .detach(); + }); }) .ok(); @@ -3385,22 +3338,22 @@ impl SettingsWindow { return; }; - let Some((worktree, corresponding_workspace)) = app_state + let Some((workspace_window, worktree, corresponding_workspace)) = app_state .workspace_store .read(cx) - .workspaces() - .iter() - .find_map(|workspace| { + .workspaces_with_windows() + .filter_map(|(window_handle, weak)| { + let workspace = weak.upgrade()?; + let window = window_handle.downcast::()?; + Some((window, workspace)) + }) + .find_map(|(window, workspace): (_, Entity)| { workspace - .read_with(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .worktree_for_id(*worktree_id, cx) - }) - .ok() - .flatten() - .zip(Some(*workspace)) + .read(cx) + .project() + .read(cx) + .worktree_for_id(*worktree_id, cx) + .map(|worktree| (window, worktree, workspace)) }) else { log::error!( @@ -3428,14 +3381,15 @@ impl SettingsWindow { // TODO: move zed::open_local_file() APIs to this crate, and // re-implement the "initial_contents" behavior - corresponding_workspace + let workspace_weak = corresponding_workspace.downgrade(); + workspace_window .update(cx, |_, window, cx| { - cx.spawn_in(window, async move |workspace, cx| { + cx.spawn_in(window, async move |_, cx| { if let Some(create_task) = create_task { create_task.await.ok()?; }; - workspace + workspace_weak .update_in(cx, |workspace, window, cx| { workspace.open_path( (worktree_id, settings_path.clone()), @@ -3449,7 +3403,7 @@ impl SettingsWindow { .await .log_err()?; - workspace + workspace_weak .update_in(cx, |_, window, cx| { window.activate_window(); cx.notify(); @@ -3753,12 +3707,13 @@ impl Render for SettingsWindow { ), window, cx, + Tiling::default(), ) } } fn all_projects( - window: Option<&WindowHandle>, + window: Option<&WindowHandle>, cx: &App, ) -> impl Iterator> { let mut seen_project_ids = std::collections::HashSet::new(); @@ -3769,10 +3724,19 @@ fn all_projects( .workspace_store .read(cx) .workspaces() - .iter() - .filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone())) + .filter_map(|weak| weak.upgrade()) + .map(|workspace: Entity| workspace.read(cx).project().clone()) .chain( - window.and_then(|workspace| Some(workspace.read(cx).ok()?.project().clone())), + window + .and_then(|handle| handle.read(cx).ok()) + .into_iter() + .flat_map(|multi_workspace| { + multi_workspace + .workspaces() + .iter() + .map(|workspace| workspace.read(cx).project().clone()) + .collect::>() + }), ) .filter(move |project| seen_project_ids.insert(project.entity_id())) }) @@ -3780,6 +3744,51 @@ fn all_projects( .flatten() } +fn open_user_settings_in_workspace( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, +) { + let project = workspace.project().clone(); + + cx.spawn_in(window, async move |workspace, cx| { + let (config_dir, settings_file) = project.update(cx, |project, cx| { + ( + project.try_windows_path_to_wsl(paths::config_dir().as_path(), cx), + project.try_windows_path_to_wsl(paths::settings_file().as_path(), cx), + ) + }); + let config_dir = config_dir.await?; + let settings_file = settings_file.await?; + project + .update(cx, |project, cx| { + project.find_or_create_worktree(&config_dir, false, cx) + }) + .await + .ok(); + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_paths( + vec![settings_file], + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + None, + window, + cx, + ) + })? + .await; + + workspace.update_in(cx, |_, window, cx| { + window.activate_window(); + cx.notify(); + }) + }) + .detach(); +} + fn update_settings_file( file: SettingsUiFile, file_name: Option<&'static str>, @@ -4762,29 +4771,33 @@ pub mod test { .await .expect("Failed to create worktree_c"); - let (_workspace1, cx) = cx.add_window_view(|window, cx| { - Workspace::new( - Default::default(), - project1.clone(), - app_state.clone(), - window, - cx, - ) + let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| { + let workspace = cx.new(|cx| { + Workspace::new( + Default::default(), + project1.clone(), + app_state.clone(), + window, + cx, + ) + }); + MultiWorkspace::new(workspace, cx) }); - let _workspace1_handle = cx.window_handle().downcast::().unwrap(); - - let (_workspace2, cx) = cx.add_window_view(|window, cx| { - Workspace::new( - Default::default(), - project2.clone(), - app_state.clone(), - window, - cx, - ) + let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| { + let workspace = cx.new(|cx| { + Workspace::new( + Default::default(), + project2.clone(), + app_state.clone(), + window, + cx, + ) + }); + MultiWorkspace::new(workspace, cx) }); - let workspace2_handle = cx.window_handle().downcast::().unwrap(); + let workspace2_handle = cx.window_handle().downcast::().unwrap(); cx.run_until_parked(); @@ -4903,17 +4916,20 @@ pub mod test { .await .expect("Failed to create worktree_a"); - let (_workspace1, cx) = cx.add_window_view(|window, cx| { - Workspace::new( - Default::default(), - project1.clone(), - app_state.clone(), - window, - cx, - ) + let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| { + let workspace = cx.new(|cx| { + Workspace::new( + Default::default(), + project1.clone(), + app_state.clone(), + window, + cx, + ) + }); + MultiWorkspace::new(workspace, cx) }); - let workspace1_handle = cx.window_handle().downcast::().unwrap(); + let workspace1_handle = cx.window_handle().downcast::().unwrap(); cx.run_until_parked(); @@ -4950,14 +4966,17 @@ pub mod test { .await .expect("Failed to create worktree_b"); - let (_workspace2, cx) = cx.add_window_view(|window, cx| { - Workspace::new( - Default::default(), - project2.clone(), - app_state.clone(), - window, - cx, - ) + let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| { + let workspace = cx.new(|cx| { + Workspace::new( + Default::default(), + project2.clone(), + app_state.clone(), + window, + cx, + ) + }); + MultiWorkspace::new(workspace, cx) }); cx.run_until_parked(); diff --git a/crates/sidebar/Cargo.toml b/crates/sidebar/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..da4f29da8208540a483049687bae5a9715b2c710 --- /dev/null +++ b/crates/sidebar/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "sidebar" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/sidebar.rs" + +[features] +default = [] +test-support = [] + +[dependencies] +acp_thread.workspace = true +agent_ui.workspace = true +db.workspace = true +fs.workspace = true +fuzzy.workspace = true +serde_json.workspace = true +gpui.workspace = true +picker.workspace = true +project.workspace = true +recent_projects.workspace = true +theme.workspace = true +ui.workspace = true +ui_input.workspace = true +util.workspace = true +workspace.workspace = true + +[dev-dependencies] +editor.workspace = true +feature_flags.workspace = true +fs = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +recent_projects = { workspace = true, features = ["test-support"] } +settings = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/sidebar/LICENSE-GPL b/crates/sidebar/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/sidebar/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs new file mode 100644 index 0000000000000000000000000000000000000000..2fb58a7f66ac0d08a5bf42f8635930174e9bfcef --- /dev/null +++ b/crates/sidebar/src/sidebar.rs @@ -0,0 +1,1283 @@ +use acp_thread::ThreadStatus; +use agent_ui::{AgentPanel, AgentPanelEvent}; +use db::kvp::KEY_VALUE_STORE; +use fs::Fs; +use fuzzy::StringMatchCandidate; +use gpui::{ + App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, SharedString, + Subscription, Task, Window, px, +}; +use picker::{Picker, PickerDelegate}; +use project::Event as ProjectEvent; +use recent_projects::{RecentProjectEntry, get_recent_projects}; + +use std::collections::{HashMap, HashSet}; + +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use theme::ActiveTheme; +use ui::utils::TRAFFIC_LIGHT_PADDING; +use ui::{Divider, KeyBinding, ListItem, Tab, ThreadItem, Tooltip, prelude::*}; +use ui_input::ErasedEditor; +use util::ResultExt as _; +use workspace::{ + FocusWorkspaceSidebar, MultiWorkspace, NewWorkspaceInWindow, Sidebar as WorkspaceSidebar, + SidebarEvent, ToggleWorkspaceSidebar, Workspace, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AgentThreadStatus { + Running, + Completed, +} + +#[derive(Clone, Debug)] +struct AgentThreadInfo { + title: SharedString, + status: AgentThreadStatus, +} + +const LAST_THREAD_TITLES_KEY: &str = "sidebar-last-thread-titles"; + +const DEFAULT_WIDTH: Pixels = px(320.0); +const MIN_WIDTH: Pixels = px(200.0); +const MAX_WIDTH: Pixels = px(800.0); +const MAX_MATCHES: usize = 100; + +#[derive(Clone)] +struct WorkspaceThreadEntry { + index: usize, + worktree_label: SharedString, + full_path: SharedString, + thread_info: Option, +} + +impl WorkspaceThreadEntry { + fn new( + index: usize, + workspace: &Entity, + persisted_titles: &HashMap, + cx: &App, + ) -> Self { + let workspace_ref = workspace.read(cx); + + let worktrees: Vec<_> = workspace_ref + .worktrees(cx) + .filter(|worktree| worktree.read(cx).is_visible()) + .map(|worktree| worktree.read(cx).abs_path()) + .collect(); + + let worktree_names: Vec = worktrees + .iter() + .filter_map(|path| { + path.file_name() + .map(|name| name.to_string_lossy().to_string()) + }) + .collect(); + + let worktree_label: SharedString = if worktree_names.is_empty() { + format!("Workspace {}", index + 1).into() + } else { + worktree_names.join(", ").into() + }; + + let full_path: SharedString = worktrees + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>() + .join("\n") + .into(); + + let thread_info = Self::thread_info(workspace, cx).or_else(|| { + if worktrees.is_empty() { + return None; + } + let path_key = sorted_paths_key(&worktrees); + let title = persisted_titles.get(&path_key)?; + Some(AgentThreadInfo { + title: SharedString::from(title.clone()), + status: AgentThreadStatus::Completed, + }) + }); + + Self { + index, + worktree_label, + full_path, + thread_info, + } + } + + fn thread_info(workspace: &Entity, cx: &App) -> Option { + let agent_panel = workspace.read(cx).panel::(cx)?; + let thread = agent_panel.read(cx).active_agent_thread(cx)?; + let thread_ref = thread.read(cx); + let title = thread_ref.title(); + let status = match thread_ref.status() { + ThreadStatus::Generating => AgentThreadStatus::Running, + ThreadStatus::Idle => AgentThreadStatus::Completed, + }; + Some(AgentThreadInfo { title, status }) + } +} + +#[derive(Clone)] +enum SidebarEntry { + Separator(SharedString), + WorkspaceThread(WorkspaceThreadEntry), + RecentProject(RecentProjectEntry), +} + +impl SidebarEntry { + fn searchable_text(&self) -> &str { + match self { + SidebarEntry::Separator(_) => "", + SidebarEntry::WorkspaceThread(entry) => entry.worktree_label.as_ref(), + SidebarEntry::RecentProject(entry) => entry.name.as_ref(), + } + } +} + +#[derive(Clone)] +struct SidebarMatch { + entry: SidebarEntry, + positions: Vec, +} + +struct WorkspacePickerDelegate { + multi_workspace: Entity, + entries: Vec, + active_workspace_index: usize, + workspace_thread_count: usize, + /// All recent projects including what's filtered out of entries + /// used to add unopened projects to entries on rebuild + recent_projects: Vec, + recent_project_thread_titles: HashMap, + matches: Vec, + selected_index: usize, + query: String, + hovered_thread_item: Option, + notified_workspaces: HashSet, +} + +impl WorkspacePickerDelegate { + fn new(multi_workspace: Entity) -> Self { + Self { + multi_workspace, + entries: Vec::new(), + active_workspace_index: 0, + workspace_thread_count: 0, + recent_projects: Vec::new(), + recent_project_thread_titles: HashMap::new(), + matches: Vec::new(), + selected_index: 0, + query: String::new(), + hovered_thread_item: None, + notified_workspaces: HashSet::new(), + } + } + + fn set_entries( + &mut self, + workspace_threads: Vec, + active_workspace_index: usize, + cx: &App, + ) { + if let Some(hovered_index) = self.hovered_thread_item { + let still_exists = workspace_threads + .iter() + .any(|thread| thread.index == hovered_index); + if !still_exists { + self.hovered_thread_item = None; + } + } + + let old_statuses: HashMap = self + .entries + .iter() + .filter_map(|entry| match entry { + SidebarEntry::WorkspaceThread(thread) => thread + .thread_info + .as_ref() + .map(|info| (thread.index, info.status.clone())), + _ => None, + }) + .collect(); + + for thread in &workspace_threads { + if let Some(info) = &thread.thread_info { + if info.status == AgentThreadStatus::Completed + && thread.index != active_workspace_index + { + if old_statuses.get(&thread.index) == Some(&AgentThreadStatus::Running) { + self.notified_workspaces.insert(thread.index); + } + } + } + } + + if self.active_workspace_index != active_workspace_index { + self.notified_workspaces.remove(&active_workspace_index); + } + self.active_workspace_index = active_workspace_index; + self.workspace_thread_count = workspace_threads.len(); + self.rebuild_entries(workspace_threads, cx); + } + + fn set_recent_projects(&mut self, recent_projects: Vec, cx: &App) { + self.recent_project_thread_titles.clear(); + if let Some(map) = read_thread_title_map() { + for entry in &recent_projects { + let path_key = sorted_paths_key(&entry.paths); + if let Some(title) = map.get(&path_key) { + self.recent_project_thread_titles + .insert(entry.full_path.clone(), title.clone().into()); + } + } + } + + self.recent_projects = recent_projects; + + let workspace_threads: Vec = self + .entries + .iter() + .filter_map(|entry| match entry { + SidebarEntry::WorkspaceThread(thread) => Some(thread.clone()), + _ => None, + }) + .collect(); + self.rebuild_entries(workspace_threads, cx); + } + + fn open_workspace_path_sets(&self, cx: &App) -> Vec>> { + self.multi_workspace + .read(cx) + .workspaces() + .iter() + .map(|workspace| { + let mut paths = workspace.read(cx).root_paths(cx); + paths.sort(); + paths + }) + .collect() + } + + fn rebuild_entries(&mut self, workspace_threads: Vec, cx: &App) { + let open_path_sets = self.open_workspace_path_sets(cx); + + self.entries.clear(); + + if !workspace_threads.is_empty() { + self.entries + .push(SidebarEntry::Separator("Active Workspaces".into())); + for thread in workspace_threads { + self.entries.push(SidebarEntry::WorkspaceThread(thread)); + } + } + + let recent: Vec<_> = self + .recent_projects + .iter() + .filter(|project| { + let mut project_paths: Vec<&Path> = + project.paths.iter().map(|p| p.as_path()).collect(); + project_paths.sort(); + !open_path_sets.iter().any(|open_paths| { + open_paths.len() == project_paths.len() + && open_paths + .iter() + .zip(&project_paths) + .all(|(a, b)| a.as_ref() == *b) + }) + }) + .cloned() + .collect(); + + if !recent.is_empty() { + self.entries + .push(SidebarEntry::Separator("Recent Projects".into())); + for project in recent { + self.entries.push(SidebarEntry::RecentProject(project)); + } + } + } + + fn open_recent_project(paths: Vec, window: &mut Window, cx: &mut App) { + let Some(handle) = window.window_handle().downcast::() else { + return; + }; + + cx.defer(move |cx| { + if let Some(task) = handle + .update(cx, |multi_workspace, window, cx| { + multi_workspace.open_project(paths, window, cx) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + }); + } +} + +impl PickerDelegate for WorkspacePickerDelegate { + type ListItem = AnyElement; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + self.selected_index = ix; + } + + fn can_select( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) -> bool { + match self.matches.get(ix) { + Some(SidebarMatch { + entry: SidebarEntry::Separator(_), + .. + }) => false, + _ => true, + } + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Search…".into() + } + + fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { + if self.query.is_empty() { + None + } else { + Some("No threads match your search.".into()) + } + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + let query_changed = self.query != query; + self.query = query.clone(); + if query_changed { + self.hovered_thread_item = None; + } + let entries = self.entries.clone(); + + if query.is_empty() { + self.matches = entries + .into_iter() + .map(|entry| SidebarMatch { + entry, + positions: Vec::new(), + }) + .collect(); + + let separator_offset = if self.workspace_thread_count > 0 { + 1 + } else { + 0 + }; + self.selected_index = (self.active_workspace_index + separator_offset) + .min(self.matches.len().saturating_sub(1)); + return Task::ready(()); + } + + let executor = cx.background_executor().clone(); + cx.spawn_in(window, async move |picker, cx| { + let matches = cx + .background_spawn(async move { + let data_entries: Vec<(usize, &SidebarEntry)> = entries + .iter() + .enumerate() + .filter(|(_, entry)| !matches!(entry, SidebarEntry::Separator(_))) + .collect(); + + let candidates: Vec = data_entries + .iter() + .enumerate() + .map(|(candidate_index, (_, entry))| { + StringMatchCandidate::new(candidate_index, entry.searchable_text()) + }) + .collect(); + + let search_matches = fuzzy::match_strings( + &candidates, + &query, + false, + true, + MAX_MATCHES, + &Default::default(), + executor, + ) + .await; + + let mut workspace_matches = Vec::new(); + let mut project_matches = Vec::new(); + + for search_match in search_matches { + let (original_index, _) = data_entries[search_match.candidate_id]; + let entry = entries[original_index].clone(); + let sidebar_match = SidebarMatch { + positions: search_match.positions, + entry: entry.clone(), + }; + match entry { + SidebarEntry::WorkspaceThread(_) => { + workspace_matches.push(sidebar_match) + } + SidebarEntry::RecentProject(_) => project_matches.push(sidebar_match), + SidebarEntry::Separator(_) => {} + } + } + + let mut result = Vec::new(); + if !workspace_matches.is_empty() { + result.push(SidebarMatch { + entry: SidebarEntry::Separator("Active Workspaces".into()), + positions: Vec::new(), + }); + result.extend(workspace_matches); + } + if !project_matches.is_empty() { + result.push(SidebarMatch { + entry: SidebarEntry::Separator("Recent Projects".into()), + positions: Vec::new(), + }); + result.extend(project_matches); + } + result + }) + .await; + + picker + .update_in(cx, |picker, _window, _cx| { + picker.delegate.matches = matches; + if picker.delegate.matches.is_empty() { + picker.delegate.selected_index = 0; + } else { + let first_selectable = picker + .delegate + .matches + .iter() + .position(|m| !matches!(m.entry, SidebarEntry::Separator(_))) + .unwrap_or(0); + picker.delegate.selected_index = first_selectable; + } + }) + .log_err(); + }) + } + + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { + let Some(selected_match) = self.matches.get(self.selected_index) else { + return; + }; + + match &selected_match.entry { + SidebarEntry::Separator(_) => {} + SidebarEntry::WorkspaceThread(thread_entry) => { + let target_index = thread_entry.index; + self.multi_workspace.update(cx, |multi_workspace, cx| { + multi_workspace.activate_index(target_index, window, cx); + }); + } + SidebarEntry::RecentProject(project_entry) => { + let paths = project_entry.paths.clone(); + Self::open_recent_project(paths, window, cx); + } + } + } + + fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} + + fn render_match( + &self, + index: usize, + selected: bool, + _window: &mut Window, + cx: &mut Context>, + ) -> Option { + let match_entry = self.matches.get(index)?; + let SidebarMatch { entry, positions } = match_entry; + + match entry { + SidebarEntry::Separator(title) => Some( + div() + .px_0p5() + .when(index > 0, |this| this.mt_1().child(Divider::horizontal())) + .child( + ListItem::new("section_header").selectable(false).child( + Label::new(title.clone()) + .size(LabelSize::XSmall) + .color(Color::Muted) + .when(index > 0, |this| this.mt_1p5()) + .mb_1(), + ), + ) + .into_any_element(), + ), + SidebarEntry::WorkspaceThread(thread_entry) => { + let worktree_label = thread_entry.worktree_label.clone(); + let full_path = thread_entry.full_path.clone(); + let thread_info = thread_entry.thread_info.clone(); + let workspace_index = thread_entry.index; + let multi_workspace = self.multi_workspace.clone(); + let workspace_count = self.multi_workspace.read(cx).workspaces().len(); + let is_hovered = self.hovered_thread_item == Some(workspace_index); + + let remove_btn = IconButton::new( + format!("remove-workspace-{}", workspace_index), + IconName::Close, + ) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text("Remove Workspace")) + .on_click({ + let multi_workspace = multi_workspace; + move |_, window, cx| { + multi_workspace.update(cx, |mw, cx| { + mw.remove_workspace(workspace_index, window, cx); + }); + } + }); + + let has_notification = self.notified_workspaces.contains(&workspace_index); + let thread_subtitle = thread_info.as_ref().map(|info| info.title.clone()); + let running = matches!( + thread_info, + Some(AgentThreadInfo { + status: AgentThreadStatus::Running, + .. + }) + ); + + Some( + ThreadItem::new( + ("workspace-item", thread_entry.index), + thread_subtitle.unwrap_or("New Thread".into()), + ) + .icon(IconName::Folder) + .running(running) + .generation_done(has_notification) + .selected(selected) + .worktree(worktree_label.clone()) + .worktree_highlight_positions(positions.clone()) + .when(workspace_count > 1, |item| item.action_slot(remove_btn)) + .hovered(is_hovered) + .on_hover(cx.listener(move |picker, is_hovered, _window, cx| { + let mut changed = false; + if *is_hovered { + if picker.delegate.hovered_thread_item != Some(workspace_index) { + picker.delegate.hovered_thread_item = Some(workspace_index); + changed = true; + } + } else if picker.delegate.hovered_thread_item == Some(workspace_index) { + picker.delegate.hovered_thread_item = None; + changed = true; + } + if changed { + cx.notify(); + } + })) + .when(!full_path.is_empty(), |this| { + this.tooltip(move |_, cx| { + Tooltip::with_meta(worktree_label.clone(), None, full_path.clone(), cx) + }) + }) + .into_any_element(), + ) + } + SidebarEntry::RecentProject(project_entry) => { + let name = project_entry.name.clone(); + let full_path = project_entry.full_path.clone(); + let item_id: SharedString = + format!("recent-project-{:?}", project_entry.workspace_id).into(); + + Some( + ThreadItem::new(item_id, name.clone()) + .icon(IconName::Folder) + .selected(selected) + .highlight_positions(positions.clone()) + .tooltip(move |_, cx| { + Tooltip::with_meta(name.clone(), None, full_path.clone(), cx) + }) + .into_any_element(), + ) + } + } + } + + fn render_editor( + &self, + editor: &Arc, + window: &mut Window, + cx: &mut Context>, + ) -> Div { + h_flex() + .h(Tab::container_height(cx)) + .w_full() + .px_2() + .gap_2() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + Icon::new(IconName::MagnifyingGlass) + .color(Color::Muted) + .size(IconSize::Small), + ) + .child(editor.render(window, cx)) + } +} + +pub struct Sidebar { + multi_workspace: Entity, + width: Pixels, + picker: Entity>, + _subscription: Subscription, + _project_subscriptions: Vec, + _agent_panel_subscriptions: Vec, + _thread_subscriptions: Vec, + #[cfg(any(test, feature = "test-support"))] + test_thread_infos: HashMap, + #[cfg(any(test, feature = "test-support"))] + test_recent_project_thread_titles: HashMap, + _fetch_recent_projects: Task<()>, +} + +impl EventEmitter for Sidebar {} + +impl Sidebar { + pub fn new( + multi_workspace: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let delegate = WorkspacePickerDelegate::new(multi_workspace.clone()); + let picker = cx.new(|cx| { + Picker::list(delegate, window, cx) + .max_height(None) + .show_scrollbar(true) + .modal(false) + }); + + let subscription = cx.observe_in( + &multi_workspace, + window, + |this, multi_workspace, window, cx| { + this.queue_refresh(multi_workspace, window, cx); + }, + ); + + let fetch_recent_projects = { + let picker = picker.downgrade(); + let fs = ::global(cx); + cx.spawn_in(window, async move |_this, cx| { + let projects = get_recent_projects(None, None, fs).await; + + cx.update(|window, cx| { + if let Some(picker) = picker.upgrade() { + picker.update(cx, |picker, cx| { + picker.delegate.set_recent_projects(projects, cx); + let query = picker.query(cx); + picker.update_matches(query, window, cx); + }); + } + }) + .log_err(); + }) + }; + + let mut this = Self { + multi_workspace, + width: DEFAULT_WIDTH, + picker, + _subscription: subscription, + _project_subscriptions: Vec::new(), + _agent_panel_subscriptions: Vec::new(), + _thread_subscriptions: Vec::new(), + #[cfg(any(test, feature = "test-support"))] + test_thread_infos: HashMap::new(), + #[cfg(any(test, feature = "test-support"))] + test_recent_project_thread_titles: HashMap::new(), + _fetch_recent_projects: fetch_recent_projects, + }; + this.queue_refresh(this.multi_workspace.clone(), window, cx); + this + } + + fn subscribe_to_projects( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Vec { + let projects: Vec<_> = self + .multi_workspace + .read(cx) + .workspaces() + .iter() + .map(|w| w.read(cx).project().clone()) + .collect(); + + projects + .iter() + .map(|project| { + cx.subscribe_in( + project, + window, + |this, _project, event, window, cx| match event { + ProjectEvent::WorktreeAdded(_) + | ProjectEvent::WorktreeRemoved(_) + | ProjectEvent::WorktreeOrderChanged => { + this.queue_refresh(this.multi_workspace.clone(), window, cx); + } + _ => {} + }, + ) + }) + .collect() + } + + fn build_workspace_thread_entries( + &self, + multi_workspace: &MultiWorkspace, + cx: &App, + ) -> (Vec, usize) { + let persisted_titles = read_thread_title_map().unwrap_or_default(); + + #[allow(unused_mut)] + let mut entries: Vec = multi_workspace + .workspaces() + .iter() + .enumerate() + .map(|(index, workspace)| { + WorkspaceThreadEntry::new(index, workspace, &persisted_titles, cx) + }) + .collect(); + + #[cfg(any(test, feature = "test-support"))] + for (index, info) in &self.test_thread_infos { + if let Some(entry) = entries.get_mut(*index) { + entry.thread_info = Some(info.clone()); + } + } + + (entries, multi_workspace.active_workspace_index()) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn set_test_recent_projects( + &self, + projects: Vec, + cx: &mut Context, + ) { + self.picker.update(cx, |picker, _cx| { + picker.delegate.recent_projects = projects; + }); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn set_test_thread_info( + &mut self, + index: usize, + title: SharedString, + status: AgentThreadStatus, + ) { + self.test_thread_infos + .insert(index, AgentThreadInfo { title, status }); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn set_test_recent_project_thread_title( + &mut self, + full_path: SharedString, + title: SharedString, + cx: &mut Context, + ) { + self.test_recent_project_thread_titles + .insert(full_path.clone(), title.clone()); + self.picker.update(cx, |picker, _cx| { + picker + .delegate + .recent_project_thread_titles + .insert(full_path, title); + }); + } + + fn subscribe_to_agent_panels( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Vec { + let workspaces: Vec<_> = self.multi_workspace.read(cx).workspaces().to_vec(); + + workspaces + .iter() + .map(|workspace| { + if let Some(agent_panel) = workspace.read(cx).panel::(cx) { + cx.subscribe_in( + &agent_panel, + window, + |this, _, _event: &AgentPanelEvent, window, cx| { + this.queue_refresh(this.multi_workspace.clone(), window, cx); + }, + ) + } else { + // Panel hasn't loaded yet — observe the workspace so we + // re-subscribe once the panel appears on its dock. + cx.observe_in(workspace, window, |this, _, window, cx| { + this.queue_refresh(this.multi_workspace.clone(), window, cx); + }) + } + }) + .collect() + } + + fn subscribe_to_threads( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Vec { + let workspaces: Vec<_> = self.multi_workspace.read(cx).workspaces().to_vec(); + + workspaces + .iter() + .filter_map(|workspace| { + let agent_panel = workspace.read(cx).panel::(cx)?; + let thread = agent_panel.read(cx).active_agent_thread(cx)?; + Some(cx.observe_in(&thread, window, |this, _, window, cx| { + this.queue_refresh(this.multi_workspace.clone(), window, cx); + })) + }) + .collect() + } + + fn persist_thread_titles( + &self, + entries: &[WorkspaceThreadEntry], + multi_workspace: &Entity, + cx: &mut Context, + ) { + let mut map = read_thread_title_map().unwrap_or_default(); + let workspaces = multi_workspace.read(cx).workspaces().to_vec(); + let mut changed = false; + + for (workspace, entry) in workspaces.iter().zip(entries.iter()) { + if let Some(ref info) = entry.thread_info { + let paths: Vec<_> = workspace + .read(cx) + .worktrees(cx) + .map(|wt| wt.read(cx).abs_path()) + .collect(); + if paths.is_empty() { + continue; + } + let path_key = sorted_paths_key(&paths); + let title = info.title.to_string(); + if map.get(&path_key) != Some(&title) { + map.insert(path_key, title); + changed = true; + } + } + } + + if changed { + if let Some(json) = serde_json::to_string(&map).log_err() { + cx.background_spawn(async move { + KEY_VALUE_STORE + .write_kvp(LAST_THREAD_TITLES_KEY.into(), json) + .await + .log_err(); + }) + .detach(); + } + } + } + + fn queue_refresh( + &mut self, + multi_workspace: Entity, + window: &mut Window, + cx: &mut Context, + ) { + cx.defer_in(window, move |this, window, cx| { + this._project_subscriptions = this.subscribe_to_projects(window, cx); + this._agent_panel_subscriptions = this.subscribe_to_agent_panels(window, cx); + this._thread_subscriptions = this.subscribe_to_threads(window, cx); + let (entries, active_index) = multi_workspace.read_with(cx, |multi_workspace, cx| { + this.build_workspace_thread_entries(multi_workspace, cx) + }); + + this.persist_thread_titles(&entries, &multi_workspace, cx); + + let had_notifications = !this.picker.read(cx).delegate.notified_workspaces.is_empty(); + this.picker.update(cx, |picker, cx| { + picker.delegate.set_entries(entries, active_index, cx); + let query = picker.query(cx); + picker.update_matches(query, window, cx); + }); + let has_notifications = !this.picker.read(cx).delegate.notified_workspaces.is_empty(); + if had_notifications != has_notifications { + multi_workspace.update(cx, |_, cx| cx.notify()); + } + }); + } +} + +impl WorkspaceSidebar for Sidebar { + fn width(&self, _cx: &App) -> Pixels { + self.width + } + + fn set_width(&mut self, width: Option, cx: &mut Context) { + self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH); + cx.notify(); + } + + fn has_notifications(&self, cx: &App) -> bool { + !self.picker.read(cx).delegate.notified_workspaces.is_empty() + } +} + +impl Focusable for Sidebar { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.picker.read(cx).focus_handle(cx) + } +} + +fn sorted_paths_key>(paths: &[P]) -> String { + let mut sorted: Vec = paths + .iter() + .map(|p| p.as_ref().to_string_lossy().to_string()) + .collect(); + sorted.sort(); + sorted.join("\n") +} + +fn read_thread_title_map() -> Option> { + let json = KEY_VALUE_STORE + .read_kvp(LAST_THREAD_TITLES_KEY) + .log_err() + .flatten()?; + serde_json::from_str(&json).log_err() +} + +impl Render for Sidebar { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let titlebar_height = ui::utils::platform_title_bar_height(window); + let ui_font = theme::setup_ui_font(window, cx); + let is_focused = self.focus_handle(cx).is_focused(window); + + let focus_tooltip_label = if is_focused { + "Focus Workspace" + } else { + "Focus Sidebar" + }; + + v_flex() + .id("workspace-sidebar") + .key_context("WorkspaceSidebar") + .font(ui_font) + .h_full() + .w(self.width) + .bg(cx.theme().colors().surface_background) + .border_r_1() + .border_color(cx.theme().colors().border) + .child( + h_flex() + .flex_none() + .h(titlebar_height) + .w_full() + .mt_px() + .pb_px() + .pr_1() + .when(cfg!(target_os = "macos"), |this| { + this.pl(px(TRAFFIC_LIGHT_PADDING)) + }) + .when(cfg!(not(target_os = "macos")), |this| this.pl_2()) + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border) + .child({ + let focus_handle = cx.focus_handle(); + IconButton::new("close-sidebar", IconName::WorkspaceNavOpen) + .icon_size(IconSize::Small) + .tooltip(Tooltip::element(move |_, cx| { + v_flex() + .gap_1() + .child( + h_flex() + .gap_2() + .justify_between() + .child(Label::new("Close Sidebar")) + .child(KeyBinding::for_action_in( + &ToggleWorkspaceSidebar, + &focus_handle, + cx, + )), + ) + .child( + h_flex() + .pt_1() + .gap_2() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .justify_between() + .child(Label::new(focus_tooltip_label)) + .child(KeyBinding::for_action_in( + &FocusWorkspaceSidebar, + &focus_handle, + cx, + )), + ) + .into_any_element() + })) + .on_click(cx.listener(|_this, _, _window, cx| { + cx.emit(SidebarEvent::Close); + })) + }) + .child( + IconButton::new("new-workspace", IconName::Plus) + .icon_size(IconSize::Small) + .tooltip(|_window, cx| { + Tooltip::for_action("New Workspace", &NewWorkspaceInWindow, cx) + }) + .on_click(cx.listener(|this, _, window, cx| { + this.multi_workspace.update(cx, |multi_workspace, cx| { + multi_workspace.create_workspace(window, cx); + }); + })), + ), + ) + .child(self.picker.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use feature_flags::FeatureFlagAppExt as _; + use fs::FakeFs; + use gpui::TestAppContext; + use settings::SettingsStore; + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); + editor::init(cx); + cx.update_flags(false, vec!["agent-v2".into()]); + }); + } + + fn set_thread_info_and_refresh( + sidebar: &Entity, + multi_workspace: &Entity, + index: usize, + title: &str, + status: AgentThreadStatus, + cx: &mut gpui::VisualTestContext, + ) { + sidebar.update_in(cx, |s, _window, _cx| { + s.set_test_thread_info(index, SharedString::from(title.to_string()), status.clone()); + }); + multi_workspace.update_in(cx, |_, _window, cx| cx.notify()); + cx.run_until_parked(); + } + + fn has_notifications(sidebar: &Entity, cx: &mut gpui::VisualTestContext) -> bool { + sidebar.read_with(cx, |s, cx| s.has_notifications(cx)) + } + + #[gpui::test] + async fn test_notification_on_running_to_completed_transition(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + cx.update(|cx| ::set_global(fs.clone(), cx)); + let project = project::Project::test(fs, [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + + let sidebar = multi_workspace.update_in(cx, |_mw, window, cx| { + let mw_handle = cx.entity(); + cx.new(|cx| Sidebar::new(mw_handle, window, cx)) + }); + multi_workspace.update_in(cx, |mw, window, cx| { + mw.register_sidebar(sidebar.clone(), window, cx); + }); + cx.run_until_parked(); + + // Create a second workspace and switch to it so workspace 0 is background. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.create_workspace(window, cx); + }); + cx.run_until_parked(); + multi_workspace.update_in(cx, |mw, window, cx| { + mw.activate_index(1, window, cx); + }); + cx.run_until_parked(); + + assert!( + !has_notifications(&sidebar, cx), + "should have no notifications initially" + ); + + set_thread_info_and_refresh( + &sidebar, + &multi_workspace, + 0, + "Test Thread", + AgentThreadStatus::Running, + cx, + ); + + assert!( + !has_notifications(&sidebar, cx), + "Running status alone should not create a notification" + ); + + set_thread_info_and_refresh( + &sidebar, + &multi_workspace, + 0, + "Test Thread", + AgentThreadStatus::Completed, + cx, + ); + + assert!( + has_notifications(&sidebar, cx), + "Running → Completed transition should create a notification" + ); + } + + #[gpui::test] + async fn test_no_notification_for_active_workspace(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + cx.update(|cx| ::set_global(fs.clone(), cx)); + let project = project::Project::test(fs, [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + + let sidebar = multi_workspace.update_in(cx, |_mw, window, cx| { + let mw_handle = cx.entity(); + cx.new(|cx| Sidebar::new(mw_handle, window, cx)) + }); + multi_workspace.update_in(cx, |mw, window, cx| { + mw.register_sidebar(sidebar.clone(), window, cx); + }); + cx.run_until_parked(); + + // Workspace 0 is the active workspace — thread completes while + // the user is already looking at it. + set_thread_info_and_refresh( + &sidebar, + &multi_workspace, + 0, + "Test Thread", + AgentThreadStatus::Running, + cx, + ); + set_thread_info_and_refresh( + &sidebar, + &multi_workspace, + 0, + "Test Thread", + AgentThreadStatus::Completed, + cx, + ); + + assert!( + !has_notifications(&sidebar, cx), + "should not notify for the workspace the user is already looking at" + ); + } + + #[gpui::test] + async fn test_notification_cleared_on_workspace_activation(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + cx.update(|cx| ::set_global(fs.clone(), cx)); + let project = project::Project::test(fs, [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + + let sidebar = multi_workspace.update_in(cx, |_mw, window, cx| { + let mw_handle = cx.entity(); + cx.new(|cx| Sidebar::new(mw_handle, window, cx)) + }); + multi_workspace.update_in(cx, |mw, window, cx| { + mw.register_sidebar(sidebar.clone(), window, cx); + }); + cx.run_until_parked(); + + // Create a second workspace so we can switch away and back. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.create_workspace(window, cx); + }); + cx.run_until_parked(); + + // Switch to workspace 1 so workspace 0 becomes a background workspace. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.activate_index(1, window, cx); + }); + cx.run_until_parked(); + + // Thread on workspace 0 transitions Running → Completed while + // the user is looking at workspace 1. + set_thread_info_and_refresh( + &sidebar, + &multi_workspace, + 0, + "Test Thread", + AgentThreadStatus::Running, + cx, + ); + set_thread_info_and_refresh( + &sidebar, + &multi_workspace, + 0, + "Test Thread", + AgentThreadStatus::Completed, + cx, + ); + + assert!( + has_notifications(&sidebar, cx), + "background workspace completion should create a notification" + ); + + // Switching back to workspace 0 should clear the notification. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.activate_index(0, window, cx); + }); + cx.run_until_parked(); + + assert!( + !has_notifications(&sidebar, cx), + "notification should be cleared when workspace becomes active" + ); + } +} diff --git a/crates/tab_switcher/src/tab_switcher_tests.rs b/crates/tab_switcher/src/tab_switcher_tests.rs index a55dfb6cb7326fae327ab6e7de39cf9c62ad4427..e1e3f138252e4dc41aa67d9d5b848eac773d5f4f 100644 --- a/crates/tab_switcher/src/tab_switcher_tests.rs +++ b/crates/tab_switcher/src/tab_switcher_tests.rs @@ -5,7 +5,7 @@ use menu::SelectPrevious; use project::{Project, ProjectPath}; use serde_json::json; use util::{path, rel_path::rel_path}; -use workspace::{ActivatePreviousItem, AppState, Workspace, item::test::TestItem}; +use workspace::{ActivatePreviousItem, AppState, MultiWorkspace, Workspace, item::test::TestItem}; #[ctor::ctor] fn init_logger() { @@ -33,8 +33,9 @@ async fn test_open_with_prev_tab_selected_and_cycle_on_toggle_action( .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tab_1 = open_buffer("1.txt", &workspace, cx).await; let tab_2 = open_buffer("2.txt", &workspace, cx).await; @@ -89,8 +90,9 @@ async fn test_open_with_last_tab_selected(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tab_1 = open_buffer("1.txt", &workspace, cx).await; let tab_2 = open_buffer("2.txt", &workspace, cx).await; @@ -123,8 +125,9 @@ async fn test_open_item_on_modifiers_release(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tab_1 = open_buffer("1.txt", &workspace, cx).await; let tab_2 = open_buffer("2.txt", &workspace, cx).await; @@ -151,8 +154,9 @@ async fn test_open_on_empty_pane(cx: &mut gpui::TestAppContext) { app_state.fs.as_fake().insert_tree("/root", json!({})).await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); cx.simulate_modifiers_change(Modifiers::control()); let tab_switcher = open_tab_switcher(false, &workspace, cx); @@ -174,8 +178,9 @@ async fn test_open_with_single_item(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tab = open_buffer("1.txt", &workspace, cx).await; @@ -204,8 +209,9 @@ async fn test_close_selected_item(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tab_1 = open_buffer("1.txt", &workspace, cx).await; let tab_3 = open_buffer("3.txt", &workspace, cx).await; @@ -369,8 +375,9 @@ async fn test_open_in_active_pane_deduplicates_files_by_path(cx: &mut gpui::Test .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_buffer("1.txt", &workspace, cx).await; open_buffer("2.txt", &workspace, cx).await; @@ -406,8 +413,9 @@ async fn test_open_in_active_pane_clones_files_to_current_pane(cx: &mut gpui::Te .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_buffer("1.txt", &workspace, cx).await; @@ -453,8 +461,9 @@ async fn test_open_in_active_pane_clones_files_to_current_pane(cx: &mut gpui::Te async fn test_open_in_active_pane_moves_terminals_to_current_pane(cx: &mut gpui::TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let test_item = cx.new(|cx| TestItem::new(cx).with_label("terminal")); workspace.update_in(cx, |workspace, window, cx| { @@ -506,8 +515,9 @@ async fn test_open_in_active_pane_closes_file_in_all_panes(cx: &mut gpui::TestAp .await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); open_buffer("1.txt", &workspace, cx).await; diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index f554253a0ef436cad985b5a7cbbb486cf8acbca8..6b4fc21ef3ede0482c9eb3ac6b8dd9c000b7f7d4 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -754,7 +754,7 @@ mod tests { use serde_json::json; use task::TaskTemplates; use util::path; - use workspace::{CloseInactiveTabsAndPanes, OpenOptions, OpenVisible}; + use workspace::{CloseInactiveTabsAndPanes, MultiWorkspace, OpenOptions, OpenVisible}; use crate::{modal::Spawn, tests::init_test}; @@ -787,8 +787,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tasks_picker = open_spawn_tasks(&workspace, cx); assert_eq!( @@ -960,8 +961,9 @@ mod tests { .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let tasks_picker = open_spawn_tasks(&workspace, cx); assert_eq!( @@ -1115,8 +1117,9 @@ mod tests { ))), )); }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let _ts_file_1 = workspace .update_in(cx, |workspace, window, cx| { diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 35c8a2ee220c6dba3732ca0f323bc50eb592ce19..29e6a9de7fab9b5421fe38fee0fd24fd43b12ccc 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -400,7 +400,7 @@ mod tests { use task::{TaskContext, TaskVariables, VariableName}; use ui::VisualContext; use util::{path, rel_path::rel_path}; - use workspace::{AppState, Workspace}; + use workspace::{AppState, MultiWorkspace}; use crate::task_contexts; @@ -474,8 +474,9 @@ mod tests { let worktree_id = project.update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let buffer1 = workspace .update(cx, |this, cx| { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 292028f7077b99eb1bc47542c1cfb507fc42ef69..80926f17f0ce5a4cd464bfe3bf71e5576495d407 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1847,6 +1847,7 @@ mod tests { use pretty_assertions::assert_eq; use project::FakeFs; use settings::SettingsStore; + use workspace::MultiWorkspace; #[test] fn test_prepare_empty_task() { @@ -1878,13 +1879,14 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) + let terminal_panel = window_handle + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + cx.new(|cx| TerminalPanel::new(workspace, window, cx)) + }) }) .unwrap(); @@ -1963,13 +1965,14 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) + let terminal_panel = window_handle + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + cx.new(|cx| TerminalPanel::new(workspace, window, cx)) + }) }) .unwrap(); @@ -2006,13 +2009,14 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) + let terminal_panel = window_handle + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + cx.new(|cx| TerminalPanel::new(workspace, window, cx)) + }) }) .unwrap(); diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs index 8adad4ebf91f4557465afb2e8659594f05f3b716..e3384323062aaac2cff39d9904688dcf0edba718 100644 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ b/crates/terminal_view/src/terminal_path_like_target.rs @@ -523,7 +523,7 @@ mod tests { terminal_settings::{AlternateScroll, CursorShape}, }; use util::path; - use workspace::AppState; + use workspace::{AppState, MultiWorkspace}; async fn init_test( app_cx: &mut TestAppContext, @@ -552,8 +552,9 @@ mod tests { ) .await; - let (workspace, _cx) = - app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (multi_workspace, cx) = app_cx + .add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); let terminal = app_cx.new(|cx| { TerminalBuilder::new_display_only( diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 50f9ac03ddde19566c60cb10b4624af88f6e422b..24fe59da9043d73d602e08d26e1fd65cc3b45667 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1843,7 +1843,7 @@ mod tests { use std::path::Path; use util::paths::PathStyle; use util::rel_path::RelPath; - use workspace::AppState; + use workspace::{AppState, MultiWorkspace}; // Working directory calculation tests @@ -2020,9 +2020,10 @@ mod tests { }); let project = Project::test(params.fs.clone(), [], cx).await; - let workspace = cx - .add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)) - .root(cx) + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); (project, workspace) diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index db735590716f0dbcf184155c9c6ab0860a80615a..0c93afd63c30c019a016d2215cc252b3776945ab 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -38,6 +38,7 @@ chrono.workspace = true client.workspace = true cloud_api_types.workspace = true db.workspace = true +feature_flags.workspace = true git_ui.workspace = true gpui = { workspace = true, features = ["screen-capture"] } menu.workspace = true diff --git a/crates/title_bar/src/project_dropdown.rs b/crates/title_bar/src/project_dropdown.rs index a0927918c7493c1da711fcab3fa0af546bc4a0e5..1f4c6376c4fb6d3f366fdf8c4008c347004a763f 100644 --- a/crates/title_bar/src/project_dropdown.rs +++ b/crates/title_bar/src/project_dropdown.rs @@ -11,7 +11,8 @@ use project::{Project, Worktree, git_store::Repository}; use recent_projects::{RecentProjectEntry, delete_recent_project, get_recent_projects}; use settings::WorktreeId; use ui::{ContextMenu, DocumentationAside, DocumentationSide, Tooltip, prelude::*}; -use workspace::{CloseIntent, Workspace}; +use util::ResultExt as _; +use workspace::{MultiWorkspace, Workspace}; actions!(project_dropdown, [RemoveSelectedFolder]); @@ -66,8 +67,12 @@ impl ProjectDropdown { let recent_projects_for_fetch = recent_projects.clone(); let menu_shell_for_fetch = menu_shell.clone(); let workspace_for_fetch = workspace.clone(); + let fs = workspace + .upgrade() + .map(|ws| ws.read(cx).app_state().fs.clone()); cx.spawn_in(window, async move |_this, cx| { + let Some(fs) = fs else { return }; let current_workspace_id = cx .update(|_, cx| { workspace_for_fetch @@ -77,7 +82,7 @@ impl ProjectDropdown { .ok() .flatten(); - let projects = get_recent_projects(current_workspace_id, None).await; + let projects = get_recent_projects(current_workspace_id, None, fs).await; cx.update(|window, cx| { *recent_projects_for_fetch.borrow_mut() = projects; @@ -88,7 +93,7 @@ impl ProjectDropdown { }); } }) - .ok() + .ok(); }) .detach(); @@ -396,36 +401,31 @@ impl ProjectDropdown { window: &mut Window, cx: &mut App, ) { - let Some(workspace) = workspace.upgrade() else { - return; - }; + if create_new_window { + let Some(workspace) = workspace.upgrade() else { + return; + }; + workspace.update(cx, |workspace, cx| { + workspace + .open_workspace_for_paths(false, paths, window, cx) + .detach_and_log_err(cx); + }); + } else { + let Some(handle) = window.window_handle().downcast::() else { + return; + }; - workspace.update(cx, |workspace, cx| { - if create_new_window { - workspace.open_workspace_for_paths(false, paths, window, cx) - } else { - cx.spawn_in(window, { - let paths = paths.clone(); - async move |workspace, cx| { - let continue_replacing = workspace - .update_in(cx, |workspace, window, cx| { - workspace.prepare_to_close(CloseIntent::ReplaceWindow, window, cx) - })? - .await?; - if continue_replacing { - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_workspace_for_paths(true, paths, window, cx) - })? - .await - } else { - Ok(()) - } - } - }) - } - .detach_and_log_err(cx); - }); + cx.defer(move |cx| { + if let Some(task) = handle + .update(cx, |multi_workspace, window, cx| { + multi_workspace.open_project(paths, window, cx) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + }); + } } /// Get all projects sorted alphabetically with their branch info. diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ff14c6fa25bfa3b52bfdd34433548431a042bc2b..7d5cdd6f122040790004e3b31bb57903ec9e4a68 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -23,6 +23,7 @@ use auto_update::AutoUpdateStatus; use call::ActiveCall; use client::{Client, UserStore, zed_urls}; use cloud_api_types::Plan; +use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; use gpui::{ Action, AnyElement, App, Context, Corner, Element, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, MouseButton, ParentElement, Render, @@ -39,11 +40,14 @@ use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; use ui::{ Avatar, ButtonLike, Chip, ContextMenu, IconWithIndicator, Indicator, PopoverMenu, - PopoverMenuHandle, TintColor, Tooltip, prelude::*, + PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height, }; use update_version::UpdateVersion; use util::ResultExt; -use workspace::{SwitchProject, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt}; +use workspace::{ + MultiWorkspace, SwitchProject, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace, + notifications::NotifyResultExt, +}; use zed_actions::OpenRemote; pub use onboarding_banner::restore_banner; @@ -95,8 +99,10 @@ pub fn init(cx: &mut App) { .titlebar_item() .and_then(|item| item.downcast::().ok()) { - titlebar.update(cx, |titlebar, cx| { - titlebar.show_project_dropdown(window, cx); + window.defer(cx, move |window, cx| { + titlebar.update(cx, |titlebar, cx| { + titlebar.show_project_dropdown(window, cx); + }) }); } }); @@ -174,11 +180,12 @@ impl Render for TitleBar { children.push( h_flex() - .gap_1() + .gap_0p5() .map(|title_bar| { let mut render_project_items = title_bar_settings.show_branch_name || title_bar_settings.show_project_items; title_bar + .children(self.render_workspace_sidebar_toggle(window, cx)) .when_some( self.application_menu.clone().filter(|_| !show_menus), |title_bar, menu| { @@ -249,7 +256,7 @@ impl Render for TitleBar { ); }); - let height = PlatformTitleBar::height(window); + let height = platform_title_bar_height(window); let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| { platform_titlebar.title_bar_color(window, cx) }); @@ -358,6 +365,48 @@ impl TitleBar { let update_version = cx.new(|cx| UpdateVersion::new(cx)); let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); + // Set up observer to sync sidebar state from MultiWorkspace to PlatformTitleBar. + { + let platform_titlebar = platform_titlebar.clone(); + let window_handle = window.window_handle(); + cx.spawn(async move |this: WeakEntity, cx| { + let Some(multi_workspace_handle) = window_handle.downcast::() + else { + return; + }; + + let _ = cx.update(|cx| { + let Ok(multi_workspace) = multi_workspace_handle.entity(cx) else { + return; + }; + + let is_open = multi_workspace.read(cx).is_sidebar_open(); + let has_notifications = multi_workspace.read(cx).sidebar_has_notifications(cx); + platform_titlebar.update(cx, |titlebar, cx| { + titlebar.set_workspace_sidebar_open(is_open, cx); + titlebar.set_sidebar_has_notifications(has_notifications, cx); + }); + + let platform_titlebar = platform_titlebar.clone(); + let subscription = cx.observe(&multi_workspace, move |mw, cx| { + let is_open = mw.read(cx).is_sidebar_open(); + let has_notifications = mw.read(cx).sidebar_has_notifications(cx); + platform_titlebar.update(cx, |titlebar, cx| { + titlebar.set_workspace_sidebar_open(is_open, cx); + titlebar.set_sidebar_has_notifications(has_notifications, cx); + }); + }); + + if let Some(this) = this.upgrade() { + this.update(cx, |this, _| { + this._subscriptions.push(subscription); + }); + } + }); + }) + .detach(); + } + Self { platform_titlebar, application_menu, @@ -652,6 +701,41 @@ impl TitleBar { ) } + fn render_workspace_sidebar_toggle( + &self, + _window: &mut Window, + cx: &mut Context, + ) -> Option { + if !cx.has_flag::() { + return None; + } + + let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open(); + + if is_sidebar_open { + return None; + } + + let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications(); + + Some( + IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed) + .icon_size(IconSize::Small) + .when(has_notifications, |button| { + button + .indicator(Indicator::dot().color(Color::Accent)) + .indicator_border_color(Some(cx.theme().colors().title_bar_background)) + }) + .tooltip(move |_, cx| { + Tooltip::for_action("Open Workspace Sidebar", &ToggleWorkspaceSidebar, cx) + }) + .on_click(|_, window, cx| { + window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); + }) + .into_any_element(), + ) + } + pub fn render_project_name(&self, cx: &mut Context) -> impl IntoElement { let workspace = self.workspace.clone(); @@ -936,16 +1020,18 @@ impl TitleBar { pub fn render_sign_in_button(&mut self, _: &mut Context) -> Button { let client = self.client.clone(); + let workspace = self.workspace.clone(); Button::new("sign_in", "Sign In") .label_size(LabelSize::Small) .on_click(move |_, window, cx| { let client = client.clone(); + let workspace = workspace.clone(); window - .spawn(cx, async move |cx| { + .spawn(cx, async move |mut cx| { client .sign_in_with_optional_connect(true, cx) .await - .notify_async_err(cx); + .notify_workspace_async_err(workspace, &mut cx); }) .detach(); }) diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 60e966e3429e6b72743fd9fdc957b0f2581ca4b7..a6642c11283425075bbea703a681f1bffcb47f5d 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -37,7 +37,6 @@ mod stack; mod sticky_items; mod tab; mod tab_bar; -mod thread_item; mod toggle; mod tooltip; mod tree_view_item; @@ -84,7 +83,6 @@ pub use stack::*; pub use sticky_items::*; pub use tab::*; pub use tab_bar::*; -pub use thread_item::*; pub use toggle::*; pub use tooltip::*; pub use tree_view_item::*; diff --git a/crates/ui/src/components/ai.rs b/crates/ui/src/components/ai.rs index e36361b7b06559c1442b86acf26b6694bb950d82..a31db264e985b3adbca26b9e8d3fb2bdca306dcb 100644 --- a/crates/ui/src/components/ai.rs +++ b/crates/ui/src/components/ai.rs @@ -1,3 +1,5 @@ mod configured_api_card; +mod thread_item; pub use configured_api_card::*; +pub use thread_item::*; diff --git a/crates/ui/src/components/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs similarity index 50% rename from crates/ui/src/components/thread_item.rs rename to crates/ui/src/components/ai/thread_item.rs index a4f6a8a53348d78563900c2a53b30e95588c2aac..bc7ecd3df68f76e7c7583d6ad567833d4544a4f0 100644 --- a/crates/ui/src/components/thread_item.rs +++ b/crates/ui/src/components/ai/thread_item.rs @@ -1,7 +1,9 @@ use crate::{ - Chip, DecoratedIcon, DiffStat, IconDecoration, IconDecorationKind, SpinnerLabel, prelude::*, + DecoratedIcon, DiffStat, HighlightedLabel, IconDecoration, IconDecorationKind, SpinnerLabel, + prelude::*, }; -use gpui::{ClickEvent, SharedString}; + +use gpui::{AnyView, ClickEvent, SharedString}; #[derive(IntoElement, RegisterComponent)] pub struct ThreadItem { @@ -12,10 +14,16 @@ pub struct ThreadItem { running: bool, generation_done: bool, selected: bool, + hovered: bool, added: Option, removed: Option, worktree: Option, + highlight_positions: Vec, + worktree_highlight_positions: Vec, on_click: Option>, + on_hover: Box, + action_slot: Option, + tooltip: Option AnyView + 'static>>, } impl ThreadItem { @@ -28,10 +36,16 @@ impl ThreadItem { running: false, generation_done: false, selected: false, + hovered: false, added: None, removed: None, worktree: None, + highlight_positions: Vec::new(), + worktree_highlight_positions: Vec::new(), on_click: None, + on_hover: Box::new(|_, _, _| {}), + action_slot: None, + tooltip: None, } } @@ -75,6 +89,21 @@ impl ThreadItem { self } + pub fn highlight_positions(mut self, positions: Vec) -> Self { + self.highlight_positions = positions; + self + } + + pub fn worktree_highlight_positions(mut self, positions: Vec) -> Self { + self.worktree_highlight_positions = positions; + self + } + + pub fn hovered(mut self, hovered: bool) -> Self { + self.hovered = hovered; + self + } + pub fn on_click( mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, @@ -82,17 +111,40 @@ impl ThreadItem { self.on_click = Some(Box::new(handler)); self } + + pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + self.on_hover = Box::new(on_hover); + self + } + + pub fn action_slot(mut self, element: impl IntoElement) -> Self { + self.action_slot = Some(element.into_any_element()); + self + } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Box::new(tooltip)); + self + } } impl RenderOnce for ThreadItem { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let clr = cx.theme().colors(); + // let dot_separator = || { + // Label::new("•") + // .size(LabelSize::Small) + // .color(Color::Muted) + // .alpha(0.5) + // }; + let icon_container = || h_flex().size_4().justify_center(); let agent_icon = Icon::new(self.icon) .color(Color::Muted) .size(IconSize::Small); let icon = if self.generation_done { - DecoratedIcon::new( + icon_container().child(DecoratedIcon::new( agent_icon, Some( IconDecoration::new( @@ -106,65 +158,120 @@ impl RenderOnce for ThreadItem { y: px(-2.), }), ), - ) - .into_any_element() + )) } else { - agent_icon.into_any_element() + icon_container().child(agent_icon) }; - let has_no_changes = self.added.is_none() && self.removed.is_none(); + let running_or_action = self.running || (self.hovered && self.action_slot.is_some()); + + // let has_no_changes = self.added.is_none() && self.removed.is_none(); + + let title = self.title; + let highlight_positions = self.highlight_positions; + let title_label = if highlight_positions.is_empty() { + Label::new(title).truncate().into_any_element() + } else { + HighlightedLabel::new(title, highlight_positions) + .truncate() + .into_any_element() + }; v_flex() .id(self.id.clone()) .cursor_pointer() - .p_2() - .when(self.selected, |this| { - this.bg(cx.theme().colors().element_active) + .map(|this| { + if self.worktree.is_some() { + this.p_2() + } else { + this.px_2().py_1() + } }) - .hover(|s| s.bg(cx.theme().colors().element_hover)) + .when(self.selected, |s| s.bg(clr.element_active)) + .hover(|s| s.bg(clr.element_hover)) + .on_hover(self.on_hover) .child( h_flex() + .min_w_0() .w_full() - .gap_1p5() - .child(icon) - .child(Label::new(self.title).truncate()) - .when(self.running, |this| { - this.child(icon_container().child(SpinnerLabel::new().color(Color::Accent))) - }), - ) - .child( - h_flex() - .gap_1p5() - .child(icon_container()) // Icon Spacing - .when_some(self.worktree, |this, name| { - this.child(Chip::new(name).label_size(LabelSize::XSmall)) - }) + .gap_2() + .justify_between() .child( - Label::new(self.timestamp) - .size(LabelSize::Small) - .color(Color::Muted), + h_flex() + .id("content") + .min_w_0() + .flex_1() + .gap_1p5() + .child(icon) + .child(title_label) + .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)), ) - .child( - Label::new("•") - .size(LabelSize::Small) - .color(Color::Muted) - .alpha(0.5), - ) - .when(has_no_changes, |this| { + .when(running_or_action, |this| { this.child( - Label::new("No Changes") - .size(LabelSize::Small) - .color(Color::Muted), + h_flex() + .gap_1() + .when(self.running, |this| { + this.child( + icon_container() + .child(SpinnerLabel::new().color(Color::Accent)), + ) + }) + .when(self.hovered, |this| { + this.when_some(self.action_slot, |this, slot| this.child(slot)) + }), ) - }) - .when(self.added.is_some() || self.removed.is_some(), |this| { - this.child(DiffStat::new( - self.id, - self.added.unwrap_or(0), - self.removed.unwrap_or(0), - )) }), ) + .when_some(self.worktree, |this, worktree| { + let worktree_highlight_positions = self.worktree_highlight_positions; + let worktree_label = if worktree_highlight_positions.is_empty() { + Label::new(worktree) + .size(LabelSize::Small) + .color(Color::Muted) + .truncate_start() + .into_any_element() + } else { + HighlightedLabel::new(worktree, worktree_highlight_positions) + .size(LabelSize::Small) + .color(Color::Muted) + .into_any_element() + }; + + this.child( + h_flex() + .min_w_0() + .gap_1p5() + .child(icon_container()) // Icon Spacing + .child(worktree_label) + // TODO: Uncomment the elements below when we're ready to expose this data + // .child(dot_separator()) + // .child( + // Label::new(self.timestamp) + // .size(LabelSize::Small) + // .color(Color::Muted), + // ) + // .child( + // Label::new("•") + // .size(LabelSize::Small) + // .color(Color::Muted) + // .alpha(0.5), + // ) + // .when(has_no_changes, |this| { + // this.child( + // Label::new("No Changes") + // .size(LabelSize::Small) + // .color(Color::Muted), + // ) + // }) + .when(self.added.is_some() || self.removed.is_some(), |this| { + this.child(DiffStat::new( + self.id, + self.added.unwrap_or(0), + self.removed.unwrap_or(0), + )) + }), + ) + }) .when_some(self.on_click, |this, on_click| this.on_click(on_click)) } } diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index cd7d8eb497328baed356692e1d88d0286568d344..b73915162f9e6be937af7323e95fb9d6a82d6c52 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -5,6 +5,7 @@ use theme::ActiveTheme; mod apca_contrast; mod color_contrast; +mod constants; mod corner_solver; mod format_distance; mod search_input; @@ -12,6 +13,7 @@ mod with_rem_size; pub use apca_contrast::*; pub use color_contrast::*; +pub use constants::*; pub use corner_solver::{CornerSolver, inner_corner_radius}; pub use format_distance::*; pub use search_input::*; diff --git a/crates/ui/src/utils/constants.rs b/crates/ui/src/utils/constants.rs new file mode 100644 index 0000000000000000000000000000000000000000..823155889f7b4c370ea7998ec7f09340f94ef2a5 --- /dev/null +++ b/crates/ui/src/utils/constants.rs @@ -0,0 +1,27 @@ +use gpui::{Pixels, Window, px}; + +// Use pixels here instead of a rem-based size because the macOS traffic +// lights are a static size, and don't scale with the rest of the UI. +// +// Magic number: There is one extra pixel of padding on the left side due to +// the 1px border around the window on macOS apps. +#[cfg(macos_sdk_26)] +pub const TRAFFIC_LIGHT_PADDING: f32 = 78.; + +#[cfg(not(macos_sdk_26))] +pub const TRAFFIC_LIGHT_PADDING: f32 = 71.; + +/// Returns the platform-appropriate title bar height. +/// +/// On Windows, this returns a fixed height of 32px. +/// On other platforms, it scales with the window's rem size (1.75x) with a minimum of 34px. +#[cfg(not(target_os = "windows"))] +pub fn platform_title_bar_height(window: &Window) -> Pixels { + (1.75 * window.rem_size()).max(px(34.)) +} + +#[cfg(target_os = "windows")] +pub fn platform_title_bar_height(_window: &Window) -> Pixels { + // todo(windows) instead of hard coded size report the actual size to the Windows platform API + px(32.) +} diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 423c3b387b197edd2d8e86398b09157fdcb7711a..ae0461339d2f7fccb1ddc8f7a0e40fafa727483b 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -318,7 +318,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { } }); Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { @@ -327,7 +327,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { @@ -346,7 +346,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { @@ -398,7 +398,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { if action.filename.is_empty() { if whole_buffer { - if let Some(workspace) = vim.workspace(window) { + if let Some(workspace) = vim.workspace(window, cx) { workspace.update(cx, |workspace, cx| { workspace .save_active_item( @@ -472,7 +472,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { return; } if action.filename.is_empty() { - if let Some(workspace) = vim.workspace(window) { + if let Some(workspace) = vim.workspace(window, cx) { workspace.update(cx, |workspace, cx| { workspace .save_active_item( @@ -549,7 +549,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; @@ -647,7 +647,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| { vim.update_editor(cx, |vim, editor, cx| { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; let Some(project) = editor.project().cloned() else { @@ -814,7 +814,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { } }; - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; let task = workspace.update(cx, |workspace, cx| { @@ -855,7 +855,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; let count = Vim::take_count(cx).unwrap_or(1); @@ -888,7 +888,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { anyhow::Ok(()) }); if let Some(e @ Err(_)) = result { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { @@ -932,7 +932,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { let range = match result { None => return, Some(e @ Err(_)) => { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { @@ -2132,7 +2132,7 @@ impl OnMatchingLines { let range = match result { None => return, Some(e @ Err(_)) => { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { @@ -2149,7 +2149,7 @@ impl OnMatchingLines { let mut regexes = match Regex::new(&self.search) { Ok(regex) => vec![(regex, !self.invert)], e @ Err(_) => { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { @@ -2347,7 +2347,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - let Some(workspace) = self.workspace(window) else { + let Some(workspace) = self.workspace(window, cx) else { return; }; let command = self.update_editor(cx, |_, editor, cx| { @@ -2396,7 +2396,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - let Some(workspace) = self.workspace(window) else { + let Some(workspace) = self.workspace(window, cx) else { return; }; let command = self.update_editor(cx, |_, editor, cx| { @@ -2448,7 +2448,7 @@ impl ShellExec { } pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context) { - let Some(workspace) = vim.workspace(window) else { + let Some(workspace) = vim.workspace(window, cx) else { return; }; diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 1b39faf1444294c3509cce1f13095e20c204b6a7..4ce03c0a218dd56e64a0f17db007bf331508f54f 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -874,7 +874,7 @@ mod test { use serde_json::json; use settings::SettingsStore; use util::path; - use workspace::DeploySearch; + use workspace::{DeploySearch, MultiWorkspace}; use crate::{VimAddon, state::Mode, test::VimTestContext}; @@ -1739,8 +1739,11 @@ mod test { .await; let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx)); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); cx.update(|cx| { VimTestContext::init_keybindings(true, cx); @@ -1749,24 +1752,20 @@ mod test { }) }); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx) - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx) + }); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view should be active") - }) - .unwrap(); + let search_view = workspace.update_in(cx, |workspace, _, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view should be active") + }); project_search::perform_project_search(&search_view, "File A", cx); diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index a4d85e87b24fa6e2753f0dbcfcbb43be9488f41a..48cf8739b725f64e1dd5930b23e046e92fd72392 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -81,7 +81,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - let Some(workspace) = self.workspace(window) else { + let Some(workspace) = self.workspace(window, cx) else { return; }; workspace.update(cx, |workspace, cx| { @@ -133,7 +133,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - let Some(workspace) = self.workspace(window) else { + let Some(workspace) = self.workspace(window, cx) else { return; }; let task = workspace.update(cx, |workspace, cx| { @@ -272,7 +272,7 @@ impl Vim { window: &mut Window, cx: &mut App, ) { - let Some(workspace) = self.workspace(window) else { + let Some(workspace) = self.workspace(window, cx) else { return; }; if name == "`" { @@ -324,7 +324,7 @@ impl Vim { return Some(Mark::Local(anchors)); } VimGlobals::update_global(cx, |globals, cx| { - let workspace_id = self.workspace(window)?.entity_id(); + let workspace_id = self.workspace(window, cx)?.entity_id(); globals .marks .get_mut(&workspace_id)? @@ -339,7 +339,7 @@ impl Vim { window: &mut Window, cx: &mut App, ) { - let Some(workspace) = self.workspace(window) else { + let Some(workspace) = self.workspace(window, cx) else { return; }; if name == "`" || name == "'" { diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 5b480a86d846ff719d8784f619be861db9e44c9f..8a4bfc241d1b0c62b17464bfb1dd5076015ac638 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -112,7 +112,7 @@ impl Replayer { let this = self.clone(); window.defer(cx, move |window, cx| { this.next(window, cx); - let Some(Some(workspace)) = window.root::() else { + let Some(workspace) = Workspace::for_window(window, cx) else { return; }; let Some(editor) = workspace @@ -165,7 +165,7 @@ impl Replayer { text, utf16_range_to_replace, } => { - let Some(Some(workspace)) = window.root::() else { + let Some(workspace) = Workspace::for_window(window, cx) else { return; }; let Some(editor) = workspace diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index c11784d163e18451129656aa92d23dba568bd723..248f43c08192182cb266dbfc43a5a769f87429cd 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -555,7 +555,7 @@ impl Vim { let replacement = action.replacement.clone(); let Some(((pane, workspace), editor)) = self .pane(window, cx) - .zip(self.workspace(window)) + .zip(self.workspace(window, cx)) .zip(self.editor()) else { return; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 1075a1144355083bd410b3aee4d015031f946a4e..9546c822ef68f1515745e67a4ec82fca684a6a94 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -36,7 +36,7 @@ use ui::{ use util::ResultExt; use util::rel_path::RelPath; use workspace::searchable::Direction; -use workspace::{Workspace, WorkspaceDb, WorkspaceId}; +use workspace::{MultiWorkspace, Workspace, WorkspaceDb, WorkspaceId}; #[derive(Clone, Copy, Default, Debug, PartialEq, Serialize, Deserialize)] pub enum Mode { @@ -731,12 +731,16 @@ impl VimGlobals { }); GlobalCommandPaletteInterceptor::set(cx, command_interceptor); for window in cx.windows() { - if let Some(workspace) = window.downcast::() { - workspace - .update(cx, |workspace, _, cx| { - Vim::update_globals(cx, |globals, cx| { - globals.register_workspace(workspace, cx) - }); + if let Some(multi_workspace) = window.downcast::() { + multi_workspace + .update(cx, |multi_workspace, _, cx| { + for workspace in multi_workspace.workspaces() { + workspace.update(cx, |workspace, cx| { + Vim::update_globals(cx, |globals, cx| { + globals.register_workspace(workspace, cx) + }); + }); + } }) .ok(); } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index b5151886d7b1c39cac491a5f888225263f06b7b9..0e191fc3cc70f8b17407885b1a8a504299a259cb 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -29,7 +29,7 @@ use project::FakeFs; use search::BufferSearchBar; use search::{ProjectSearchView, project_search}; use serde_json::json; -use workspace::DeploySearch; +use workspace::{DeploySearch, MultiWorkspace}; use crate::{PushSneak, PushSneakBackward, VimAddon, insert::NormalBefore, motion, state::Mode}; @@ -2674,31 +2674,30 @@ async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) .await; let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx)); + let window_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); cx.update(|cx| { VimTestContext::init_keybindings(true, cx); }); - let cx = &mut VisualTestContext::from_window(*workspace, cx); + let cx = &mut VisualTestContext::from_window(window_handle.into(), cx); - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx) - }) - .unwrap(); + workspace.update_in(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx) + }); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view should be active") - }) - .unwrap(); + let search_view = workspace.update_in(cx, |workspace, _, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view should be active") + }); project_search::perform_project_search(&search_view, "File A", cx); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 8e21b2b7a795a20947d5697c034a2bb6ee425f55..1100db4585e286a4e100b9d62b8be552471c2095 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1003,12 +1003,12 @@ impl Vim { self.editor.upgrade() } - pub fn workspace(&self, window: &mut Window) -> Option> { - window.root::().flatten() + pub fn workspace(&self, window: &Window, cx: &App) -> Option> { + Workspace::for_window(window, cx) } - pub fn pane(&self, window: &mut Window, cx: &mut Context) -> Option> { - self.workspace(window) + pub fn pane(&self, window: &Window, cx: &Context) -> Option> { + self.workspace(window, cx) .map(|workspace| workspace.read(cx).focused_pane(window, cx)) } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 6f4aced4259acdc986e2a3f14aee191fe497c23b..b8296d13af4275b6eef8fccc654be5c813a9ef61 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -79,6 +79,7 @@ db = { workspace = true, features = ["test-support"] } fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } +remote = { workspace = true, features = ["test-support"] } session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs index ee5dc550b2fba6d449cb87a0da3ca8b909da1970..ebb4792ef0d6a49c05e2d98f166f7e8260a2ae0a 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -1,5 +1,6 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; +use fs::Fs; use gpui::{AppContext, Entity, Global, MenuItem}; use smallvec::SmallVec; use ui::{App, Context}; @@ -9,10 +10,10 @@ use crate::{ NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, }; -pub fn init(cx: &mut App) { +pub fn init(fs: Arc, cx: &mut App) { let manager = cx.new(|_| HistoryManager::new()); HistoryManager::set_global(manager.clone(), cx); - HistoryManager::init(manager, cx); + HistoryManager::init(manager, fs, cx); } pub struct HistoryManager { @@ -38,10 +39,10 @@ impl HistoryManager { } } - fn init(this: Entity, cx: &App) { + fn init(this: Entity, fs: Arc, cx: &App) { cx.spawn(async move |cx| { let recent_folders = WORKSPACE_DB - .recent_workspaces_on_disk() + .recent_workspaces_on_disk(fs.as_ref()) .await .unwrap_or_default() .into_iter() diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs new file mode 100644 index 0000000000000000000000000000000000000000..ffa1b07a735558df86fe3b4bb4007ad6647a45a8 --- /dev/null +++ b/crates/workspace/src/multi_workspace.rs @@ -0,0 +1,584 @@ +use anyhow::Result; +use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; +use gpui::{ + AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, + ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, actions, + deferred, px, +}; +use project::Project; +use std::path::PathBuf; +use ui::prelude::*; + +const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0); + +use crate::{ + DockPosition, Item, ModalView, Panel, Workspace, WorkspaceId, client_side_decorations, +}; + +actions!( + multi_workspace, + [ + /// Creates a new workspace within the current window. + NewWorkspaceInWindow, + /// Switches to the next workspace within the current window. + NextWorkspaceInWindow, + /// Switches to the previous workspace within the current window. + PreviousWorkspaceInWindow, + /// Toggles the workspace switcher sidebar. + ToggleWorkspaceSidebar, + /// Moves focus to or from the workspace sidebar without closing it. + FocusWorkspaceSidebar, + ] +); + +pub enum SidebarEvent { + Open, + Close, +} + +pub trait Sidebar: EventEmitter + Focusable + Render + Sized { + fn width(&self, cx: &App) -> Pixels; + fn set_width(&mut self, width: Option, cx: &mut Context); + fn has_notifications(&self, cx: &App) -> bool; +} + +pub trait SidebarHandle: 'static + Send + Sync { + fn width(&self, cx: &App) -> Pixels; + fn set_width(&self, width: Option, cx: &mut App); + fn focus_handle(&self, cx: &App) -> FocusHandle; + fn focus(&self, window: &mut Window, cx: &mut App); + fn has_notifications(&self, cx: &App) -> bool; + fn to_any(&self) -> AnyView; + fn entity_id(&self) -> EntityId; +} + +#[derive(Clone)] +pub struct DraggedSidebar; + +impl Render for DraggedSidebar { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + gpui::Empty + } +} + +impl SidebarHandle for Entity { + fn width(&self, cx: &App) -> Pixels { + self.read(cx).width(cx) + } + + fn set_width(&self, width: Option, cx: &mut App) { + self.update(cx, |this, cx| this.set_width(width, cx)) + } + + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.read(cx).focus_handle(cx) + } + + fn focus(&self, window: &mut Window, cx: &mut App) { + let handle = self.read(cx).focus_handle(cx); + window.focus(&handle, cx); + } + + fn has_notifications(&self, cx: &App) -> bool { + self.read(cx).has_notifications(cx) + } + + fn to_any(&self) -> AnyView { + self.clone().into() + } + + fn entity_id(&self) -> EntityId { + Entity::entity_id(self) + } +} + +pub struct MultiWorkspace { + workspaces: Vec>, + active_workspace_index: usize, + sidebar: Option>, + sidebar_open: bool, + _sidebar_subscription: Option, +} + +impl MultiWorkspace { + pub fn new(workspace: Entity, _cx: &mut Context) -> Self { + Self { + workspaces: vec![workspace], + active_workspace_index: 0, + sidebar: None, + sidebar_open: false, + _sidebar_subscription: None, + } + } + + pub fn register_sidebar( + &mut self, + sidebar: Entity, + window: &mut Window, + cx: &mut Context, + ) { + let subscription = + cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event { + SidebarEvent::Open => this.toggle_sidebar(window, cx), + SidebarEvent::Close => { + this.close_sidebar(window, cx); + } + }); + self.sidebar = Some(Box::new(sidebar)); + self._sidebar_subscription = Some(subscription); + } + + pub fn sidebar(&self) -> Option<&dyn SidebarHandle> { + self.sidebar.as_deref() + } + + pub fn sidebar_open(&self) -> bool { + self.sidebar_open && self.sidebar.is_some() + } + + pub fn sidebar_has_notifications(&self, cx: &App) -> bool { + self.sidebar + .as_ref() + .map_or(false, |s| s.has_notifications(cx)) + } + + pub(crate) fn multi_workspace_enabled(&self, cx: &App) -> bool { + cx.has_flag::() + } + + pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context) { + if !self.multi_workspace_enabled(cx) { + return; + } + + if self.sidebar_open { + self.close_sidebar(window, cx); + } else { + self.open_sidebar(window, cx); + if let Some(sidebar) = &self.sidebar { + sidebar.focus(window, cx); + } + } + } + + pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context) { + if !self.multi_workspace_enabled(cx) { + return; + } + + if self.sidebar_open { + let sidebar_is_focused = self + .sidebar + .as_ref() + .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx)); + + if sidebar_is_focused { + let pane = self.workspace().read(cx).active_pane().clone(); + let pane_focus = pane.read(cx).focus_handle(cx); + window.focus(&pane_focus, cx); + } else if let Some(sidebar) = &self.sidebar { + sidebar.focus(window, cx); + } + } else { + self.open_sidebar(window, cx); + if let Some(sidebar) = &self.sidebar { + sidebar.focus(window, cx); + } + } + } + + pub fn open_sidebar(&mut self, window: &mut Window, cx: &mut Context) { + self.sidebar_open = true; + for workspace in &self.workspaces { + workspace.update(cx, |workspace, cx| { + workspace.set_workspace_sidebar_open(true, cx); + }); + } + self.serialize(window, cx); + cx.notify(); + } + + fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context) { + self.sidebar_open = false; + for workspace in &self.workspaces { + workspace.update(cx, |workspace, cx| { + workspace.set_workspace_sidebar_open(false, cx); + }); + } + let pane = self.workspace().read(cx).active_pane().clone(); + let pane_focus = pane.read(cx).focus_handle(cx); + window.focus(&pane_focus, cx); + self.serialize(window, cx); + cx.notify(); + } + + pub fn is_sidebar_open(&self) -> bool { + self.sidebar_open + } + + pub fn workspace(&self) -> &Entity { + &self.workspaces[self.active_workspace_index] + } + + pub fn workspaces(&self) -> &[Entity] { + &self.workspaces + } + + pub fn active_workspace_index(&self) -> usize { + self.active_workspace_index + } + + pub fn activate(&mut self, workspace: Entity, cx: &mut Context) { + if !self.multi_workspace_enabled(cx) { + self.workspaces[0] = workspace; + self.active_workspace_index = 0; + cx.notify(); + return; + } + + let index = self.add_workspace(workspace, cx); + if self.active_workspace_index != index { + self.active_workspace_index = index; + cx.notify(); + } + } + + /// Adds a workspace to this window without changing which workspace is active. + /// Returns the index of the workspace (existing or newly inserted). + pub fn add_workspace(&mut self, workspace: Entity, cx: &mut Context) -> usize { + if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) { + index + } else { + if self.sidebar_open { + workspace.update(cx, |workspace, cx| { + workspace.set_workspace_sidebar_open(true, cx); + }); + } + self.workspaces.push(workspace); + cx.notify(); + self.workspaces.len() - 1 + } + } + + pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + debug_assert!( + index < self.workspaces.len(), + "workspace index out of bounds" + ); + self.active_workspace_index = index; + self.serialize(window, cx); + self.focus_active_workspace(window, cx); + cx.notify(); + } + + pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context) { + if self.workspaces.len() > 1 { + let next_index = (self.active_workspace_index + 1) % self.workspaces.len(); + self.activate_index(next_index, window, cx); + } + } + + pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context) { + if self.workspaces.len() > 1 { + let prev_index = if self.active_workspace_index == 0 { + self.workspaces.len() - 1 + } else { + self.active_workspace_index - 1 + }; + self.activate_index(prev_index, window, cx); + } + } + + fn serialize(&self, window: &mut Window, cx: &mut App) { + let window_id = window.window_handle().window_id(); + let state = crate::persistence::model::MultiWorkspaceState { + active_workspace_id: self.workspace().read(cx).database_id(), + sidebar_open: self.sidebar_open, + }; + cx.background_spawn(async move { + crate::persistence::write_multi_workspace_state(window_id, state).await; + }) + .detach(); + } + + fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) { + let pane = self.workspace().read(cx).active_pane().clone(); + let focus_handle = pane.read(cx).focus_handle(cx); + window.focus(&focus_handle, cx); + } + + pub fn panel(&self, cx: &App) -> Option> { + self.workspace().read(cx).panel::(cx) + } + + pub fn active_modal(&self, cx: &App) -> Option> { + self.workspace().read(cx).active_modal::(cx) + } + + pub fn add_panel( + &mut self, + panel: Entity, + window: &mut Window, + cx: &mut Context, + ) { + self.workspace().update(cx, |workspace, cx| { + workspace.add_panel(panel, window, cx); + }); + } + + pub fn focus_panel( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option> { + self.workspace() + .update(cx, |workspace, cx| workspace.focus_panel::(window, cx)) + } + + pub fn toggle_modal( + &mut self, + window: &mut Window, + cx: &mut Context, + build: B, + ) where + B: FnOnce(&mut Window, &mut gpui::Context) -> V, + { + self.workspace().update(cx, |workspace, cx| { + workspace.toggle_modal(window, cx, build); + }); + } + + pub fn toggle_dock( + &mut self, + dock_side: DockPosition, + window: &mut Window, + cx: &mut Context, + ) { + self.workspace().update(cx, |workspace, cx| { + workspace.toggle_dock(dock_side, window, cx); + }); + } + + pub fn active_item_as(&self, cx: &App) -> Option> { + self.workspace().read(cx).active_item_as::(cx) + } + + pub fn items_of_type<'a, T: Item>( + &'a self, + cx: &'a App, + ) -> impl 'a + Iterator> { + self.workspace().read(cx).items_of_type::(cx) + } + + pub fn database_id(&self, cx: &App) -> Option { + self.workspace().read(cx).database_id() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn set_random_database_id(&mut self, cx: &mut Context) { + self.workspace().update(cx, |workspace, _cx| { + workspace.set_random_database_id(); + }); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn test_new(project: Entity, window: &mut Window, cx: &mut Context) -> Self { + let workspace = cx.new(|cx| Workspace::test_new(project, window, cx)); + Self::new(workspace, cx) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn test_add_workspace( + &mut self, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let workspace = cx.new(|cx| Workspace::test_new(project, window, cx)); + self.activate(workspace.clone(), cx); + workspace + } + + pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context) { + if !self.multi_workspace_enabled(cx) { + return; + } + let app_state = self.workspace().read(cx).app_state().clone(); + let project = Project::local( + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + None, + project::LocalProjectFlags::default(), + cx, + ); + let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx)); + self.activate(new_workspace, cx); + self.focus_active_workspace(window, cx); + } + + pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + if self.workspaces.len() <= 1 || index >= self.workspaces.len() { + return; + } + + self.workspaces.remove(index); + + if self.active_workspace_index >= self.workspaces.len() { + self.active_workspace_index = self.workspaces.len() - 1; + } else if self.active_workspace_index > index { + self.active_workspace_index -= 1; + } + + self.focus_active_workspace(window, cx); + cx.notify(); + } + + pub fn open_project( + &mut self, + paths: Vec, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let workspace = self.workspace().clone(); + + if self.multi_workspace_enabled(cx) { + workspace.update(cx, |workspace, cx| { + workspace.open_workspace_for_paths(true, paths, window, cx) + }) + } else { + cx.spawn_in(window, async move |_this, cx| { + let should_continue = workspace + .update_in(cx, |workspace, window, cx| { + workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx) + })? + .await?; + if should_continue { + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_workspace_for_paths(true, paths, window, cx) + })? + .await + } else { + Ok(()) + } + }) + } + } +} + +impl Render for MultiWorkspace { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let multi_workspace_enabled = self.multi_workspace_enabled(cx); + + let sidebar: Option = if multi_workspace_enabled && self.sidebar_open { + self.sidebar.as_ref().map(|sidebar_handle| { + let weak = cx.weak_entity(); + + let sidebar_width = sidebar_handle.width(cx); + let resize_handle = deferred( + div() + .id("sidebar-resize-handle") + .absolute() + .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.) + .top(px(0.)) + .h_full() + .w(SIDEBAR_RESIZE_HANDLE_SIZE) + .cursor_col_resize() + .on_drag(DraggedSidebar, |dragged, _, _, cx| { + cx.stop_propagation(); + cx.new(|_| dragged.clone()) + }) + .on_mouse_down(MouseButton::Left, |_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up(MouseButton::Left, move |event, _, cx| { + if event.click_count == 2 { + weak.update(cx, |this, cx| { + if let Some(sidebar) = this.sidebar.as_mut() { + sidebar.set_width(None, cx); + } + }) + .ok(); + cx.stop_propagation(); + } + }) + .occlude(), + ); + + div() + .id("sidebar-container") + .relative() + .h_full() + .w(sidebar_width) + .flex_shrink_0() + .child(sidebar_handle.to_any()) + .child(resize_handle) + .into_any_element() + }) + } else { + None + }; + + client_side_decorations( + h_flex() + .key_context("Workspace") + .size_full() + .on_action( + cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| { + this.create_workspace(window, cx); + }), + ) + .on_action( + cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| { + this.activate_next_workspace(window, cx); + }), + ) + .on_action(cx.listener( + |this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| { + this.activate_previous_workspace(window, cx); + }, + )) + .on_action(cx.listener( + |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| { + this.toggle_sidebar(window, cx); + }, + )) + .on_action( + cx.listener(|this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| { + this.focus_sidebar(window, cx); + }), + ) + .when( + self.sidebar_open() && self.multi_workspace_enabled(cx), + |this| { + this.on_drag_move(cx.listener( + |this: &mut Self, e: &DragMoveEvent, _window, cx| { + if let Some(sidebar) = &this.sidebar { + let new_width = e.event.position.x; + sidebar.set_width(Some(new_width), cx); + } + }, + )) + .children(sidebar) + }, + ) + .child( + div() + .flex() + .flex_1() + .size_full() + .overflow_hidden() + .child(self.workspace().clone()), + ), + window, + cx, + Tiling { + left: multi_workspace_enabled && self.sidebar_open, + ..Tiling::default() + }, + ) + } +} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 10437743df39b22722638357976d5a8d6224eaf8..84f479b77e4f0274e0775353d3a7cd5579768f1c 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1,9 +1,9 @@ -use crate::{SuppressNotification, Toast, Workspace}; +use crate::{MultiWorkspace, SuppressNotification, Toast, Workspace}; use anyhow::Context as _; use gpui::{ - AnyEntity, AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, Context, + AnyEntity, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, - Task, TextStyleRefinement, UnderlineStyle, svg, + Task, TextStyleRefinement, UnderlineStyle, WeakEntity, svg, }; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; @@ -1037,14 +1037,18 @@ pub fn show_app_notification( .insert(id.clone(), build_notification.clone()); for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { - workspace_window - .update(cx, |workspace, _window, cx| { - workspace.show_notification_without_handling_dismiss_events( - &id, - cx, - |cx| build_notification(cx), - ); + if let Some(multi_workspace) = window.downcast::() { + multi_workspace + .update(cx, |multi_workspace, _window, cx| { + for workspace in multi_workspace.workspaces() { + workspace.update(cx, |workspace, cx| { + workspace.show_notification_without_handling_dismiss_events( + &id, + cx, + |cx| build_notification(cx), + ); + }); + } }) .ok(); // Doesn't matter if the windows are dropped } @@ -1058,11 +1062,15 @@ pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) { cx.defer(move |cx| { GLOBAL_APP_NOTIFICATIONS.lock().remove(&id); for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { + if let Some(multi_workspace) = window.downcast::() { let id = id.clone(); - workspace_window - .update(cx, |workspace, _window, cx| { - workspace.dismiss_notification(&id, cx) + multi_workspace + .update(cx, |multi_workspace, _window, cx| { + for workspace in multi_workspace.workspaces() { + workspace.update(cx, |workspace, cx| { + workspace.dismiss_notification(&id, cx) + }); + } }) .ok(); } @@ -1076,7 +1084,11 @@ pub trait NotifyResultExt { fn notify_err(self, workspace: &mut Workspace, cx: &mut Context) -> Option; - fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option; + fn notify_workspace_async_err( + self, + workspace: WeakEntity, + cx: &mut AsyncApp, + ) -> Option; /// Notifies the active workspace if there is one, otherwise notifies all workspaces. fn notify_app_err(self, cx: &mut App) -> Option; @@ -1099,17 +1111,18 @@ where } } - fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option { + fn notify_workspace_async_err( + self, + workspace: WeakEntity, + cx: &mut AsyncApp, + ) -> Option { match self { Ok(value) => Some(value), Err(err) => { log::error!("{err:?}"); - cx.update_root(|view, _, cx| { - if let Ok(workspace) = view.downcast::() { - workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) - } - }) - .ok(); + workspace + .update(cx, |workspace, cx| workspace.show_error(&err, cx)) + .ok(); None } } @@ -1137,7 +1150,12 @@ where } pub trait NotifyTaskExt { - fn detach_and_notify_err(self, window: &mut Window, cx: &mut App); + fn detach_and_notify_err( + self, + workspace: WeakEntity, + window: &mut Window, + cx: &mut App, + ); } impl NotifyTaskExt for Task> @@ -1145,9 +1163,16 @@ where E: std::fmt::Debug + std::fmt::Display + Sized + 'static, R: 'static, { - fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) { + fn detach_and_notify_err( + self, + workspace: WeakEntity, + window: &mut Window, + cx: &mut App, + ) { window - .spawn(cx, async move |cx| self.await.notify_async_err(cx)) + .spawn(cx, async move |mut cx| { + self.await.notify_workspace_async_err(workspace, &mut cx) + }) .detach(); } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index be5c1efbdf7b158e3b8f80828fb75eb4a2307d61..c3931e3665d13dd7c0b5875e6bfa42ace8624467 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3889,9 +3889,10 @@ impl Pane { .path_for_entry(project_entry_id, cx) { let load_path_task = workspace.load_path(project_path.clone(), window, cx); - cx.spawn_in(window, async move |workspace, cx| { - if let Some((project_entry_id, build_item)) = - load_path_task.await.notify_async_err(cx) + cx.spawn_in(window, async move |workspace, mut cx| { + if let Some((project_entry_id, build_item)) = load_path_task + .await + .notify_workspace_async_err(workspace.clone(), &mut cx) { let (to_pane, new_item_handle) = workspace .update_in(cx, |workspace, window, cx| { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 08ea880fd573b608400613d54bfec47b3984f260..785f3fd9f32aa3bb8d12d6d4dd87cfb6bfe3a1e7 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -8,6 +8,8 @@ use std::{ sync::Arc, }; +use fs::Fs; + use anyhow::{Context as _, Result, bail}; use collections::{HashMap, HashSet, IndexSet}; use db::{ @@ -48,7 +50,7 @@ use model::{ SerializedPaneGroup, SerializedWorkspace, }; -use self::model::{DockStructure, SerializedWorkspaceLocation}; +use self::model::{DockStructure, SerializedWorkspaceLocation, SessionWorkspace}; // https://www.sqlite.org/limits.html // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, @@ -281,6 +283,64 @@ impl From for WindowBounds { } } +fn multi_workspace_states() -> db::kvp::ScopedKeyValueStore<'static> { + KEY_VALUE_STORE.scoped("multi_workspace_state") +} + +fn read_multi_workspace_state(window_id: WindowId) -> model::MultiWorkspaceState { + multi_workspace_states() + .read(&window_id.as_u64().to_string()) + .log_err() + .flatten() + .and_then(|json| serde_json::from_str(&json).ok()) + .unwrap_or_default() +} + +pub async fn write_multi_workspace_state(window_id: WindowId, state: model::MultiWorkspaceState) { + if let Ok(json_str) = serde_json::to_string(&state) { + multi_workspace_states() + .write(window_id.as_u64().to_string(), json_str) + .await + .log_err(); + } +} + +pub fn read_serialized_multi_workspaces( + session_workspaces: Vec, +) -> Vec { + let mut window_groups: Vec> = Vec::new(); + let mut window_id_to_group: HashMap = HashMap::default(); + + for session_workspace in session_workspaces { + match session_workspace.window_id { + Some(window_id) => { + let group_index = *window_id_to_group.entry(window_id).or_insert_with(|| { + window_groups.push(Vec::new()); + window_groups.len() - 1 + }); + window_groups[group_index].push(session_workspace); + } + None => { + window_groups.push(vec![session_workspace]); + } + } + } + + window_groups + .into_iter() + .map(|group| { + let window_id = group.first().and_then(|sw| sw.window_id); + let state = window_id + .map(read_multi_workspace_state) + .unwrap_or_default(); + model::SerializedMultiWorkspace { + workspaces: group, + state, + } + }) + .collect() +} + const DEFAULT_DOCK_STATE_KEY: &str = "default_dock_state"; pub fn read_default_dock_state() -> Option { @@ -1708,10 +1768,26 @@ impl WorkspaceDb { } } + async fn all_paths_exist_with_a_directory(paths: &[PathBuf], fs: &dyn Fs) -> bool { + let mut any_dir = false; + for path in paths { + match fs.metadata(path).await.ok().flatten() { + None => return false, + Some(meta) => { + if meta.is_dir { + any_dir = true; + } + } + } + } + any_dir + } + // Returns the recent locations which are still valid on disk and deletes ones which no longer // exist. pub async fn recent_workspaces_on_disk( &self, + fs: &dyn Fs, ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); @@ -1744,11 +1820,8 @@ impl WorkspaceDb { // If a local workspace points to WSL, this check will cause us to wait for the // WSL VM and file server to boot up. This can block for many seconds. // Supported scenarios use remote workspaces. - if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) { - // Only show directories in recent projects - if paths.paths().iter().any(|path| path.is_dir()) { - result.push((id, SerializedWorkspaceLocation::Local, paths)); - } + if !has_wsl_path && Self::all_paths_exist_with_a_directory(paths.paths(), fs).await { + result.push((id, SerializedWorkspaceLocation::Local, paths)); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } @@ -1760,65 +1833,67 @@ impl WorkspaceDb { pub async fn last_workspace( &self, + fs: &dyn Fs, ) -> Result> { - Ok(self.recent_workspaces_on_disk().await?.into_iter().next()) + Ok(self.recent_workspaces_on_disk(fs).await?.into_iter().next()) } // Returns the locations of the workspaces that were still opened when the last // session was closed (i.e. when Zed was quit). // If `last_session_window_order` is provided, the returned locations are ordered // according to that. - pub fn last_session_workspace_locations( + pub async fn last_session_workspace_locations( &self, last_session_id: &str, last_session_window_stack: Option>, - ) -> Result> { + fs: &dyn Fs, + ) -> Result> { let mut workspaces = Vec::new(); for (workspace_id, paths, window_id, remote_connection_id) in self.session_workspaces(last_session_id.to_owned())? { + let window_id = window_id.map(WindowId::from); + if let Some(remote_connection_id) = remote_connection_id { - workspaces.push(( + workspaces.push(SessionWorkspace { workspace_id, - SerializedWorkspaceLocation::Remote( + location: SerializedWorkspaceLocation::Remote( self.remote_connection(remote_connection_id)?, ), paths, - window_id.map(WindowId::from), - )); + window_id, + }); } else if paths.is_empty() { // Empty workspace with items (drafts, files) - include for restoration - workspaces.push(( - workspace_id, - SerializedWorkspaceLocation::Local, - paths, - window_id.map(WindowId::from), - )); - } else if paths.paths().iter().all(|path| path.exists()) - && paths.paths().iter().any(|path| path.is_dir()) - { - workspaces.push(( + workspaces.push(SessionWorkspace { workspace_id, - SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local, paths, - window_id.map(WindowId::from), - )); + window_id, + }); + } else { + if Self::all_paths_exist_with_a_directory(paths.paths(), fs).await { + workspaces.push(SessionWorkspace { + workspace_id, + location: SerializedWorkspaceLocation::Local, + paths, + window_id, + }); + } } } if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, _, _, window_id)| { - window_id + workspaces.sort_by_key(|workspace| { + workspace + .window_id .and_then(|id| stack.iter().position(|&order_id| order_id == id)) .unwrap_or(usize::MAX) }); } - Ok(workspaces - .into_iter() - .map(|(workspace_id, location, paths, _)| (workspace_id, location, paths)) - .collect::>()) + Ok(workspaces) } fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { @@ -2272,11 +2347,12 @@ pub fn delete_unloaded_items( mod tests { use super::*; use crate::persistence::model::{ - SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, + SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, SessionWorkspace, }; use gpui; use pretty_assertions::assert_eq; use remote::SshConnectionOptions; + use serde_json::json; use std::{thread, time::Duration}; #[gpui::test] @@ -3040,12 +3116,18 @@ mod tests { } #[gpui::test] - async fn test_last_session_workspace_locations() { + async fn test_last_session_workspace_locations(cx: &mut gpui::TestAppContext) { let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap(); let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap(); let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap(); let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap(); + let fs = fs::FakeFs::new(cx.executor()); + fs.insert_tree(dir1.path(), json!({})).await; + fs.insert_tree(dir2.path(), json!({})).await; + fs.insert_tree(dir3.path(), json!({})).await; + fs.insert_tree(dir4.path(), json!({})).await; + let db = WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await; @@ -3088,47 +3170,55 @@ mod tests { ])); let locations = db - .last_session_workspace_locations("one-session", stack) + .last_session_workspace_locations("one-session", stack, fs.as_ref()) + .await .unwrap(); assert_eq!( locations, [ - ( - WorkspaceId(4), - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path()]) - ), - ( - WorkspaceId(3), - SerializedWorkspaceLocation::Local, - PathList::new(&[dir3.path()]) - ), - ( - WorkspaceId(2), - SerializedWorkspaceLocation::Local, - PathList::new(&[dir2.path()]) - ), - ( - WorkspaceId(1), - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path()]) - ), - ( - WorkspaceId(5), - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path(), dir2.path(), dir3.path()]) - ), - ( - WorkspaceId(6), - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path(), dir3.path(), dir2.path()]) - ), + SessionWorkspace { + workspace_id: WorkspaceId(4), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&[dir4.path()]), + window_id: Some(WindowId::from(2u64)), + }, + SessionWorkspace { + workspace_id: WorkspaceId(3), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&[dir3.path()]), + window_id: Some(WindowId::from(8u64)), + }, + SessionWorkspace { + workspace_id: WorkspaceId(2), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&[dir2.path()]), + window_id: Some(WindowId::from(5u64)), + }, + SessionWorkspace { + workspace_id: WorkspaceId(1), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&[dir1.path()]), + window_id: Some(WindowId::from(9u64)), + }, + SessionWorkspace { + workspace_id: WorkspaceId(5), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&[dir1.path(), dir2.path(), dir3.path()]), + window_id: Some(WindowId::from(3u64)), + }, + SessionWorkspace { + workspace_id: WorkspaceId(6), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&[dir4.path(), dir3.path(), dir2.path()]), + window_id: Some(WindowId::from(4u64)), + }, ] ); } #[gpui::test] - async fn test_last_session_workspace_locations_remote() { + async fn test_last_session_workspace_locations_remote(cx: &mut gpui::TestAppContext) { + let fs = fs::FakeFs::new(cx.executor()); let db = WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote") .await; @@ -3190,40 +3280,45 @@ mod tests { ])); let have = db - .last_session_workspace_locations("one-session", stack) + .last_session_workspace_locations("one-session", stack, fs.as_ref()) + .await .unwrap(); assert_eq!(have.len(), 4); assert_eq!( have[0], - ( - WorkspaceId(4), - SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), - PathList::default() - ) + SessionWorkspace { + workspace_id: WorkspaceId(4), + location: SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), + paths: PathList::default(), + window_id: Some(WindowId::from(2u64)), + } ); assert_eq!( have[1], - ( - WorkspaceId(3), - SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), - PathList::default() - ) + SessionWorkspace { + workspace_id: WorkspaceId(3), + location: SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), + paths: PathList::default(), + window_id: Some(WindowId::from(8u64)), + } ); assert_eq!( have[2], - ( - WorkspaceId(2), - SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), - PathList::default() - ) + SessionWorkspace { + workspace_id: WorkspaceId(2), + location: SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), + paths: PathList::default(), + window_id: Some(WindowId::from(5u64)), + } ); assert_eq!( have[3], - ( - WorkspaceId(1), - SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), - PathList::default() - ) + SessionWorkspace { + workspace_id: WorkspaceId(1), + location: SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), + paths: PathList::default(), + window_id: Some(WindowId::from(9u64)), + } ); } @@ -3555,4 +3650,192 @@ mod tests { assert!(retrieved.display.is_some()); assert_eq!(retrieved.display.unwrap(), display_uuid); } + + #[gpui::test] + async fn test_last_session_workspace_locations_groups_by_window_id( + cx: &mut gpui::TestAppContext, + ) { + let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap(); + let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap(); + let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap(); + let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap(); + let dir5 = tempfile::TempDir::with_prefix("dir5").unwrap(); + + let fs = fs::FakeFs::new(cx.executor()); + fs.insert_tree(dir1.path(), json!({})).await; + fs.insert_tree(dir2.path(), json!({})).await; + fs.insert_tree(dir3.path(), json!({})).await; + fs.insert_tree(dir4.path(), json!({})).await; + fs.insert_tree(dir5.path(), json!({})).await; + + let db = + WorkspaceDb::open_test_db("test_last_session_workspace_locations_groups_by_window_id") + .await; + + // Simulate two MultiWorkspace windows each containing two workspaces, + // plus one single-workspace window: + // Window 10: workspace 1, workspace 2 + // Window 20: workspace 3, workspace 4 + // Window 30: workspace 5 (only one) + // + // On session restore, the caller should be able to group these by + // window_id to reconstruct the MultiWorkspace windows. + let workspaces_data: Vec<(i64, &Path, u64)> = vec![ + (1, dir1.path(), 10), + (2, dir2.path(), 10), + (3, dir3.path(), 20), + (4, dir4.path(), 20), + (5, dir5.path(), 30), + ]; + + for (id, dir, window_id) in &workspaces_data { + db.save_workspace(SerializedWorkspace { + id: WorkspaceId(*id), + paths: PathList::new(&[*dir]), + location: SerializedWorkspaceLocation::Local, + center_group: Default::default(), + window_bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + centered_layout: false, + session_id: Some("test-session".to_owned()), + breakpoints: Default::default(), + window_id: Some(*window_id), + user_toolchains: Default::default(), + }) + .await; + } + + let locations = db + .last_session_workspace_locations("test-session", None, fs.as_ref()) + .await + .unwrap(); + + // All 5 workspaces should be returned with their window_ids. + assert_eq!(locations.len(), 5); + + // Every entry should have a window_id so the caller can group them. + for session_workspace in &locations { + assert!( + session_workspace.window_id.is_some(), + "workspace {:?} missing window_id", + session_workspace.workspace_id + ); + } + + // Group by window_id, simulating what the restoration code should do. + let mut by_window: HashMap> = HashMap::default(); + for session_workspace in &locations { + if let Some(window_id) = session_workspace.window_id { + by_window + .entry(window_id) + .or_default() + .push(session_workspace.workspace_id); + } + } + + // Should produce 3 windows, not 5. + assert_eq!( + by_window.len(), + 3, + "Expected 3 window groups, got {}: {:?}", + by_window.len(), + by_window + ); + + // Window 10 should contain workspaces 1 and 2. + let window_10 = by_window.get(&WindowId::from(10u64)).unwrap(); + assert_eq!(window_10.len(), 2); + assert!(window_10.contains(&WorkspaceId(1))); + assert!(window_10.contains(&WorkspaceId(2))); + + // Window 20 should contain workspaces 3 and 4. + let window_20 = by_window.get(&WindowId::from(20u64)).unwrap(); + assert_eq!(window_20.len(), 2); + assert!(window_20.contains(&WorkspaceId(3))); + assert!(window_20.contains(&WorkspaceId(4))); + + // Window 30 should contain only workspace 5. + let window_30 = by_window.get(&WindowId::from(30u64)).unwrap(); + assert_eq!(window_30.len(), 1); + assert!(window_30.contains(&WorkspaceId(5))); + } + + #[gpui::test] + async fn test_read_serialized_multi_workspaces_with_state() { + use crate::persistence::model::MultiWorkspaceState; + + // Write multi-workspace state for two windows via the scoped KVP. + let window_10 = WindowId::from(10u64); + let window_20 = WindowId::from(20u64); + + write_multi_workspace_state( + window_10, + MultiWorkspaceState { + active_workspace_id: Some(WorkspaceId(2)), + sidebar_open: true, + }, + ) + .await; + + write_multi_workspace_state( + window_20, + MultiWorkspaceState { + active_workspace_id: Some(WorkspaceId(3)), + sidebar_open: false, + }, + ) + .await; + + // Build session workspaces: two in window 10, one in window 20, one with no window. + let session_workspaces = vec![ + SessionWorkspace { + workspace_id: WorkspaceId(1), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&["/a"]), + window_id: Some(window_10), + }, + SessionWorkspace { + workspace_id: WorkspaceId(2), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&["/b"]), + window_id: Some(window_10), + }, + SessionWorkspace { + workspace_id: WorkspaceId(3), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&["/c"]), + window_id: Some(window_20), + }, + SessionWorkspace { + workspace_id: WorkspaceId(4), + location: SerializedWorkspaceLocation::Local, + paths: PathList::new(&["/d"]), + window_id: None, + }, + ]; + + let results = read_serialized_multi_workspaces(session_workspaces); + + // Should produce 3 groups: window 10, window 20, and the orphan. + assert_eq!(results.len(), 3); + + // Window 10 group: 2 workspaces, active_workspace_id = 2, sidebar open. + let group_10 = &results[0]; + assert_eq!(group_10.workspaces.len(), 2); + assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2))); + assert_eq!(group_10.state.sidebar_open, true); + + // Window 20 group: 1 workspace, active_workspace_id = 3, sidebar closed. + let group_20 = &results[1]; + assert_eq!(group_20.workspaces.len(), 1); + assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3))); + assert_eq!(group_20.state.sidebar_open, false); + + // Orphan group: no window_id, so state is default. + let group_none = &results[2]; + assert_eq!(group_none.workspaces.len(), 1); + assert_eq!(group_none.state.active_workspace_id, None); + assert_eq!(group_none.state.sidebar_open, false); + } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 417896c584a1906f5d2f712a864cb2807c69af0a..cdb646ec3b8248bdd0b5784424ed7b8df8ac0ee8 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -10,7 +10,7 @@ use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use gpui::{AsyncWindowContext, Entity, WeakEntity}; +use gpui::{AsyncWindowContext, Entity, WeakEntity, WindowId}; use language::{Toolchain, ToolchainScope}; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; @@ -49,6 +49,32 @@ impl SerializedWorkspaceLocation { } } +/// A workspace entry from a previous session, containing all the info needed +/// to restore it including which window it belonged to (for MultiWorkspace grouping). +#[derive(Debug, PartialEq, Clone)] +pub struct SessionWorkspace { + pub workspace_id: WorkspaceId, + pub location: SerializedWorkspaceLocation, + pub paths: PathList, + pub window_id: Option, +} + +/// Per-window state for a MultiWorkspace, persisted to KVP. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct MultiWorkspaceState { + pub active_workspace_id: Option, + pub sidebar_open: bool, +} + +/// The serialized state of a single MultiWorkspace window from a previous session: +/// all workspaces that shared the window, which one was active, and whether the +/// sidebar was open. +#[derive(Debug, Clone)] +pub struct SerializedMultiWorkspace { + pub workspaces: Vec, + pub state: MultiWorkspaceState, +} + #[derive(Debug, PartialEq, Clone)] pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 9087cbba42b054c1b247bdf3d9402688de4b7add..5e0b8a7f6eabbd652f1f429342a837aa0b43e6d2 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -34,6 +34,7 @@ pub struct StatusBar { right_items: Vec>, active_pane: Entity, _observe_active_pane: Subscription, + workspace_sidebar_open: bool, } impl Render for StatusBar { @@ -51,9 +52,10 @@ impl Render for StatusBar { .when(!(tiling.bottom || tiling.right), |el| { el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) }) - .when(!(tiling.bottom || tiling.left), |el| { - el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }) + .when( + !(tiling.bottom || tiling.left) && !self.workspace_sidebar_open, + |el| el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING), + ) // This border is to avoid a transparent gap in the rounded corners .mb(px(-1.)) .border_b(px(1.0)) @@ -89,11 +91,17 @@ impl StatusBar { _observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| { this.update_active_pane_item(window, cx) }), + workspace_sidebar_open: false, }; this.update_active_pane_item(window, cx); this } + pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context) { + self.workspace_sidebar_open = open; + cx.notify(); + } + pub fn add_left_item(&mut self, item: Entity, window: &mut Window, cx: &mut Context) where T: 'static + StatusItemView, diff --git a/crates/workspace/src/welcome.rs b/crates/workspace/src/welcome.rs index 071bf0826798d382329c9f6e3ced86388e4c0b3e..301f7884dac909f01db1baa2b883253dd7ee3890 100644 --- a/crates/workspace/src/welcome.rs +++ b/crates/workspace/src/welcome.rs @@ -114,7 +114,9 @@ impl RenderOnce for SectionButton { .size(rems_from_px(12.)), ), ) - .on_click(move |_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) + .on_click(move |_, window, cx| { + self.focus_handle.dispatch_action(&*self.action, window, cx) + }) } } @@ -225,9 +227,13 @@ impl WelcomePage { .detach(); if fallback_to_recent_projects { + let fs = workspace + .upgrade() + .map(|ws| ws.read(cx).app_state().fs.clone()); cx.spawn_in(window, async move |this: WeakEntity, cx| { + let Some(fs) = fs else { return }; let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk() + .recent_workspaces_on_disk(fs.as_ref()) .await .log_err() .unwrap_or_default(); @@ -267,21 +273,18 @@ impl WelcomePage { ) { if let Some(recent_workspaces) = &self.recent_workspaces { if let Some((_workspace_id, location, paths)) = recent_workspaces.get(action.index) { - let paths = paths.clone(); - let location = location.clone(); let is_local = matches!(location, SerializedWorkspaceLocation::Local); - let workspace = self.workspace.clone(); if is_local { + let paths = paths.clone(); let paths = paths.paths().to_vec(); - cx.spawn_in(window, async move |_, cx| { - let _ = workspace.update_in(cx, |workspace, window, cx| { + self.workspace + .update(cx, |workspace, cx| { workspace .open_workspace_for_paths(true, paths, window, cx) - .detach(); - }); - }) - .detach(); + .detach_and_log_err(cx); + }) + .log_err(); } else { use zed_actions::OpenRecent; window.dispatch_action(OpenRecent::default().boxed_clone(), cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5c85061740b444dcb4082ff4d89d11a9d90cbe44..8ba34888d68533b3714a61b0adefb2f21819c512 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3,6 +3,7 @@ pub mod history_manager; pub mod invalid_item_view; pub mod item; mod modal_layer; +mod multi_workspace; pub mod notifications; pub mod pane; pub mod pane_group; @@ -22,6 +23,11 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; +pub use multi_workspace::{ + DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, NewWorkspaceInWindow, + NextWorkspaceInWindow, PreviousWorkspaceInWindow, Sidebar, SidebarEvent, SidebarHandle, + ToggleWorkspaceSidebar, +}; pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -71,7 +77,8 @@ pub use pane_group::{ use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedWorkspaceLocation}, + model::{ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace}, + read_serialized_multi_workspaces, }; use postage::stream::Stream; use project::{ @@ -562,9 +569,27 @@ pub struct OpenTerminal { pub local: bool, } -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, + Copy, + Debug, + Default, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + serde::Serialize, + serde::Deserialize, +)] pub struct WorkspaceId(i64); +impl WorkspaceId { + pub fn from_i64(value: i64) -> Self { + Self(value) + } +} + impl StaticColumnCount for WorkspaceId {} impl Bind for WorkspaceId { fn bind(&self, statement: &Statement, start_index: i32) -> Result { @@ -599,11 +624,14 @@ fn prompt_and_open_paths(app_state: Arc, options: PathPromptOptions, c cx.update(|cx| { if let Some(workspace_window) = cx .active_window() - .and_then(|window| window.downcast::()) + .and_then(|window| window.downcast::()) { workspace_window - .update(cx, |workspace, _, cx| { - workspace.show_portal_error(err.to_string(), cx); + .update(cx, |multi_workspace, _, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + workspace.show_portal_error(err.to_string(), cx); + }); }) .ok(); } @@ -618,7 +646,7 @@ pub fn init(app_state: Arc, cx: &mut App) { component::init(); theme_preview::init(cx); toast_layer::init(cx); - history_manager::init(cx); + history_manager::init(app_state.fs.clone(), cx); cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)) .on_action(|_: &Reload, cx| reload(cx)) @@ -969,7 +997,7 @@ struct GlobalAppState(Weak); impl Global for GlobalAppState {} pub struct WorkspaceStore { - workspaces: HashSet>, + workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity)>, client: Arc, _subscriptions: Vec, } @@ -1456,9 +1484,11 @@ impl Workspace { cx.emit(Event::PaneAdded(center_pane.clone())); - let window_handle = window.window_handle().downcast::().unwrap(); + let any_window_handle = window.window_handle(); app_state.workspace_store.update(cx, |store, _| { - store.workspaces.insert(window_handle); + store + .workspaces + .insert((any_window_handle, weak_handle.clone())); }); let mut current_user = app_state.user_store.read(cx).watch_current_user(); @@ -1583,10 +1613,13 @@ impl Workspace { GlobalTheme::reload_theme(cx); GlobalTheme::reload_icon_theme(cx); }), - cx.on_release(move |this, cx| { - this.app_state.workspace_store.update(cx, move |store, _| { - store.workspaces.remove(&window_handle); - }) + cx.on_release({ + let weak_handle = weak_handle.clone(); + move |this, cx| { + this.app_state.workspace_store.update(cx, move |store, _| { + store.workspaces.retain(|(_, weak)| weak != &weak_handle); + }) + } }), ]; @@ -1660,13 +1693,13 @@ impl Workspace { pub fn new_local( abs_paths: Vec, app_state: Arc, - requesting_window: Option>, + requesting_window: Option>, env: Option>, init: Option) + Send>>, cx: &mut App, ) -> Task< anyhow::Result<( - WindowHandle, + WindowHandle, Vec>>>, )>, > { @@ -1764,71 +1797,23 @@ impl Workspace { }); } - let window = if let Some(window) = requesting_window { - let centered_layout = serialized_workspace - .as_ref() - .map(|w| w.centered_layout) - .unwrap_or(false); - - cx.update_window(window.into(), |_, window, cx| { - window.replace_root(cx, |window, cx| { - let mut workspace = Workspace::new( - Some(workspace_id), - project_handle.clone(), - app_state.clone(), - window, - cx, - ); - - workspace.centered_layout = centered_layout; - - // Call init callback to add items before window renders - if let Some(init) = init { - init(&mut workspace, window, cx); - } + let (window, workspace): (WindowHandle, Entity) = + if let Some(window) = requesting_window { + let centered_layout = serialized_workspace + .as_ref() + .map(|w| w.centered_layout) + .unwrap_or(false); - workspace - }); - })?; - window - } else { - let window_bounds_override = window_bounds_env_override(); - - let (window_bounds, display) = if let Some(bounds) = window_bounds_override { - (Some(WindowBounds::Windowed(bounds)), None) - } else if let Some(workspace) = serialized_workspace.as_ref() - && let Some(display) = workspace.display - && let Some(bounds) = workspace.window_bounds.as_ref() - { - // Reopening an existing workspace - restore its saved bounds - (Some(bounds.0), Some(display)) - } else if let Some((display, bounds)) = persistence::read_default_window_bounds() { - // New or empty workspace - use the last known window bounds - (Some(bounds), Some(display)) - } else { - // New window - let GPUI's default_bounds() handle cascading - (None, None) - }; - - // Use the serialized workspace to construct the new window - let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx)); - options.window_bounds = window_bounds; - let centered_layout = serialized_workspace - .as_ref() - .map(|w| w.centered_layout) - .unwrap_or(false); - cx.open_window(options, { - let app_state = app_state.clone(); - let project_handle = project_handle.clone(); - move |window, cx| { - cx.new(|cx| { + let workspace = window.update(cx, |multi_workspace, window, cx| { + let workspace = cx.new(|cx| { let mut workspace = Workspace::new( Some(workspace_id), - project_handle, - app_state, + project_handle.clone(), + app_state.clone(), window, cx, ); + workspace.centered_layout = centered_layout; // Call init callback to add items before window renders @@ -1837,10 +1822,69 @@ impl Workspace { } workspace - }) - } - })? - }; + }); + multi_workspace.activate(workspace.clone(), cx); + workspace + })?; + (window, workspace) + } else { + let window_bounds_override = window_bounds_env_override(); + + let (window_bounds, display) = if let Some(bounds) = window_bounds_override { + (Some(WindowBounds::Windowed(bounds)), None) + } else if let Some(workspace) = serialized_workspace.as_ref() + && let Some(display) = workspace.display + && let Some(bounds) = workspace.window_bounds.as_ref() + { + // Reopening an existing workspace - restore its saved bounds + (Some(bounds.0), Some(display)) + } else if let Some((display, bounds)) = + persistence::read_default_window_bounds() + { + // New or empty workspace - use the last known window bounds + (Some(bounds), Some(display)) + } else { + // New window - let GPUI's default_bounds() handle cascading + (None, None) + }; + + // Use the serialized workspace to construct the new window + let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx)); + options.window_bounds = window_bounds; + let centered_layout = serialized_workspace + .as_ref() + .map(|w| w.centered_layout) + .unwrap_or(false); + let window = cx.open_window(options, { + let app_state = app_state.clone(); + let project_handle = project_handle.clone(); + move |window, cx| { + let workspace = cx.new(|cx| { + let mut workspace = Workspace::new( + Some(workspace_id), + project_handle, + app_state, + window, + cx, + ); + workspace.centered_layout = centered_layout; + + // Call init callback to add items before window renders + if let Some(init) = init { + init(&mut workspace, window, cx); + } + + workspace + }); + cx.new(|cx| MultiWorkspace::new(workspace, cx)) + } + })?; + let workspace = + window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| { + multi_workspace.workspace().clone() + })?; + (window, workspace) + }; notify_if_database_failed(window, cx); // Check if this is an empty workspace (no paths to open) @@ -1853,8 +1897,10 @@ impl Workspace { .unwrap_or(false); let opened_items = window - .update(cx, |_workspace, window, cx| { - open_items(serialized_workspace, project_paths, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |_workspace: &mut Workspace, cx| { + open_items(serialized_workspace, project_paths, window, cx) + }) })? .await .unwrap_or_default(); @@ -1866,29 +1912,30 @@ impl Workspace { if is_empty_workspace && !serialized_workspace_has_paths { if let Some(default_docks) = persistence::read_default_dock_state() { window - .update(cx, |workspace, window, cx| { - for (dock, serialized_dock) in [ - (&mut workspace.right_dock, default_docks.right), - (&mut workspace.left_dock, default_docks.left), - (&mut workspace.bottom_dock, default_docks.bottom), - ] - .iter_mut() - { - dock.update(cx, |dock, cx| { - dock.serialized_dock = Some(serialized_dock.clone()); - dock.restore_state(window, cx); - }); - } - cx.notify(); + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + for (dock, serialized_dock) in [ + (&workspace.right_dock, &default_docks.right), + (&workspace.left_dock, &default_docks.left), + (&workspace.bottom_dock, &default_docks.bottom), + ] { + dock.update(cx, |dock, cx| { + dock.serialized_dock = Some(serialized_dock.clone()); + dock.restore_state(window, cx); + }); + } + cx.notify(); + }); }) .log_err(); } } window - .update(cx, |workspace, window, cx| { - window.activate_window(); - workspace.update_history(cx); + .update(cx, |_, _window, cx| { + workspace.update(cx, |this: &mut Workspace, cx| { + this.update_history(cx); + }); }) .log_err(); Ok((window, opened_items)) @@ -1985,6 +2032,12 @@ impl Workspace { &self.status_bar } + pub fn set_workspace_sidebar_open(&self, open: bool, cx: &mut App) { + self.status_bar.update(cx, |status_bar, cx| { + status_bar.set_workspace_sidebar_open(open, cx); + }); + } + pub fn status_bar_visible(&self, cx: &App) -> bool { StatusBarSettings::get_global(cx).show } @@ -2494,8 +2547,11 @@ impl Workspace { let env = self.project.read(cx).cli_environment(cx); let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx); cx.spawn_in(window, async move |_vh, cx| { - let (workspace, _) = task.await?; - workspace.update(cx, callback) + let (multi_workspace_window, _) = task.await?; + multi_workspace_window.update(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| callback(workspace, window, cx)) + }) }) } } @@ -2521,8 +2577,11 @@ impl Workspace { let env = self.project.read(cx).cli_environment(cx); let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx); cx.spawn_in(window, async move |_vh, cx| { - let (workspace, _) = task.await?; - workspace.update(cx, callback) + let (multi_workspace_window, _) = task.await?; + multi_workspace_window.update(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| callback(workspace, window, cx)) + }) }) } } @@ -2624,7 +2683,7 @@ impl Workspace { let workspace_count = cx.update(|_window, cx| { cx.windows() .iter() - .filter(|window| window.downcast::().is_some()) + .filter(|window| window.downcast::().is_some()) .count() })?; @@ -2637,10 +2696,12 @@ impl Workspace { let remaining_workspaces = cx.update(|_window, cx| { cx.windows() .iter() - .filter_map(|window| window.downcast::()) - .filter_map(|workspace| { - workspace - .update(cx, |workspace, _, _| workspace.removing) + .filter_map(|window| window.downcast::()) + .filter_map(|multi_workspace| { + multi_workspace + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().read(cx).removing + }) .ok() }) .filter(|removing| !removing) @@ -2676,13 +2737,18 @@ impl Workspace { } if close_intent == CloseIntent::ReplaceWindow { _ = active_call.update(cx, |this, cx| { - let workspace = cx + let multi_workspace = cx .windows() .iter() - .filter_map(|window| window.downcast::()) + .filter_map(|window| window.downcast::()) .next() .unwrap(); - let project = workspace.read(cx)?.project.clone(); + let project = multi_workspace + .read(cx)? + .workspace() + .read(cx) + .project + .clone(); if project.read(cx).is_shared() { this.unshare_project(project, cx)?; } @@ -2890,7 +2956,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task> { - let window_handle = window.window_handle().downcast::(); + let window_handle = window.window_handle().downcast::(); let is_remote = self.project.read(cx).is_via_collab(); let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); @@ -5075,21 +5141,27 @@ impl Workspace { self.update_window_edited(window, cx); return; } - if let Some(window_handle) = window.window_handle().downcast::() { - let s = item.on_release( - cx, - Box::new(move |cx| { - window_handle - .update(cx, |this, window, cx| { - this.dirty_items.remove(&item_id); - this.update_window_edited(window, cx) + + let workspace = self.weak_handle(); + let Some(window_handle) = window.window_handle().downcast::() else { + return; + }; + let on_release_callback = Box::new(move |cx: &mut App| { + window_handle + .update(cx, |_, window, cx| { + workspace + .update(cx, |workspace, cx| { + workspace.dirty_items.remove(&item_id); + workspace.update_window_edited(window, cx) }) .ok(); - }), - ); - self.dirty_items.insert(item_id, s); - self.update_window_edited(window, cx); - } + }) + .ok(); + }); + + let s = item.on_release(cx, on_release_callback); + self.dirty_items.insert(item_id, s); + self.update_window_edited(window, cx); } fn render_notifications(&self, _window: &mut Window, _cx: &mut Context) -> Option
{ @@ -6523,7 +6595,11 @@ impl Workspace { } #[cfg(any(test, feature = "test-support"))] - pub fn test_new(project: Entity, window: &mut Window, cx: &mut Context) -> Self { + pub(crate) fn test_new( + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { use node_runtime::NodeRuntime; use session::Session; @@ -6667,8 +6743,11 @@ impl Workspace { ) } - pub fn for_window(window: &mut Window, _: &mut App) -> Option> { - window.root().flatten() + pub fn for_window(window: &Window, cx: &App) -> Option> { + window + .root::() + .flatten() + .map(|multi_workspace| multi_workspace.read(cx).workspace().clone()) } pub fn zoomed_item(&self) -> Option<&AnyWeakView> { @@ -7043,27 +7122,30 @@ enum ActivateInDirectionTarget { Dock(Entity), } -fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncApp) { - workspace - .update(cx, |workspace, _, cx| { - if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { - struct DatabaseFailedNotification; - - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| { - cx.new(|cx| { - MessageNotification::new("Failed to load the database file.", cx) - .primary_message("File an Issue") - .primary_icon(IconName::Plus) - .primary_on_click(|window, cx| { - window.dispatch_action(Box::new(FileBugReport), cx) - }) - }) - }, - ); - } +fn notify_if_database_failed(window: WindowHandle, cx: &mut AsyncApp) { + window + .update(cx, |multi_workspace, _, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { + struct DatabaseFailedNotification; + + workspace.show_notification( + NotificationId::unique::(), + cx, + |cx| { + cx.new(|cx| { + MessageNotification::new("Failed to load the database file.", cx) + .primary_message("File an Issue") + .primary_icon(IconName::Plus) + .primary_on_click(|window, cx| { + window.dispatch_action(Box::new(FileBugReport), cx) + }) + }) + }, + ); + } + }); }) .log_err(); } @@ -7217,15 +7299,14 @@ impl Render for Workspace { .collect::>(); let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout; - client_side_decorations( - self.actions(div(), window, cx) - .key_context(context) - .relative() - .size_full() - .flex() - .flex_col() - .font(ui_font) - .gap_0() + self.actions(div(), window, cx) + .key_context(context) + .relative() + .size_full() + .flex() + .flex_col() + .font(ui_font) + .gap_0() .justify_start() .items_start() .text_color(colors.text) @@ -7708,10 +7789,7 @@ impl Render for Workspace { }) .child(self.modal_layer.clone()) .child(self.toast_layer.clone()), - ), - window, - cx, - ) + ) } } @@ -7756,16 +7834,22 @@ impl WorkspaceStore { }; let mut response = proto::FollowResponse::default(); - this.workspaces.retain(|workspace| { - workspace - .update(cx, |workspace, window, cx| { - let handler_response = - workspace.handle_follow(follower.project_id, window, cx); - if let Some(active_view) = handler_response.active_view - && workspace.project.read(cx).remote_id() == follower.project_id - { - response.active_view = Some(active_view) - } + + this.workspaces.retain(|(window_handle, weak_workspace)| { + let Some(workspace) = weak_workspace.upgrade() else { + return false; + }; + window_handle + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + let handler_response = + workspace.handle_follow(follower.project_id, window, cx); + if let Some(active_view) = handler_response.active_view + && workspace.project.read(cx).remote_id() == follower.project_id + { + response.active_view = Some(active_view) + } + }); }) .is_ok() }); @@ -7783,14 +7867,24 @@ impl WorkspaceStore { let update = envelope.payload; this.update(&mut cx, |this, cx| { - this.workspaces.retain(|workspace| { - workspace - .update(cx, |workspace, window, cx| { - let project_id = workspace.project.read(cx).remote_id(); - if update.project_id != project_id && update.project_id.is_some() { - return; - } - workspace.handle_update_followers(leader_id, update.clone(), window, cx); + this.workspaces.retain(|(window_handle, weak_workspace)| { + let Some(workspace) = weak_workspace.upgrade() else { + return false; + }; + window_handle + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + let project_id = workspace.project.read(cx).remote_id(); + if update.project_id != project_id && update.project_id.is_some() { + return; + } + workspace.handle_update_followers( + leader_id, + update.clone(), + window, + cx, + ); + }); }) .is_ok() }); @@ -7798,8 +7892,14 @@ impl WorkspaceStore { }) } - pub fn workspaces(&self) -> &HashSet> { - &self.workspaces + pub fn workspaces(&self) -> impl Iterator> { + self.workspaces.iter().map(|(_, weak)| weak) + } + + pub fn workspaces_with_windows( + &self, + ) -> impl Iterator)> { + self.workspaces.iter().map(|(window, weak)| (*window, weak)) } } @@ -7851,19 +7951,119 @@ impl WorkspaceHandle for Entity { } } -pub async fn last_opened_workspace_location() --> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> { - DB.last_workspace().await.log_err().flatten() +pub async fn last_opened_workspace_location( + fs: &dyn fs::Fs, +) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> { + DB.last_workspace(fs).await.log_err().flatten() } -pub fn last_session_workspace_locations( +pub async fn last_session_workspace_locations( last_session_id: &str, last_session_window_stack: Option>, -) -> Option> { - DB.last_session_workspace_locations(last_session_id, last_session_window_stack) + fs: &dyn fs::Fs, +) -> Option> { + DB.last_session_workspace_locations(last_session_id, last_session_window_stack, fs) + .await .log_err() } +pub async fn restore_multiworkspace( + multi_workspace: SerializedMultiWorkspace, + app_state: Arc, + cx: &mut AsyncApp, +) -> anyhow::Result> { + let SerializedMultiWorkspace { workspaces, state } = multi_workspace; + let mut group_iter = workspaces.into_iter(); + let first = group_iter + .next() + .context("window group must not be empty")?; + + let window_handle = if first.paths.is_empty() { + cx.update(|cx| open_workspace_by_id(first.workspace_id, app_state.clone(), None, cx)) + .await? + } else { + let (window, _items) = cx + .update(|cx| { + Workspace::new_local( + first.paths.paths().to_vec(), + app_state.clone(), + None, + None, + None, + cx, + ) + }) + .await?; + window + }; + + for session_workspace in group_iter { + if session_workspace.paths.is_empty() { + cx.update(|cx| { + open_workspace_by_id( + session_workspace.workspace_id, + app_state.clone(), + Some(window_handle), + cx, + ) + }) + .await?; + } else { + cx.update(|cx| { + Workspace::new_local( + session_workspace.paths.paths().to_vec(), + app_state.clone(), + Some(window_handle), + None, + None, + cx, + ) + }) + .await?; + } + } + + if let Some(target_id) = state.active_workspace_id { + window_handle + .update(cx, |multi_workspace, window, cx| { + let target_index = multi_workspace + .workspaces() + .iter() + .position(|ws| ws.read(cx).database_id() == Some(target_id)); + if let Some(index) = target_index { + multi_workspace.activate_index(index, window, cx); + } else if !multi_workspace.workspaces().is_empty() { + multi_workspace.activate_index(0, window, cx); + } + }) + .ok(); + } else { + window_handle + .update(cx, |multi_workspace, window, cx| { + if !multi_workspace.workspaces().is_empty() { + multi_workspace.activate_index(0, window, cx); + } + }) + .ok(); + } + + if state.sidebar_open { + window_handle + .update(cx, |multi_workspace, window, cx| { + multi_workspace.open_sidebar(window, cx); + }) + .ok(); + } + + window_handle + .update(cx, |_, window, _cx| { + window.activate_window(); + }) + .ok(); + + Ok(window_handle) +} + actions!( collab, [ @@ -7903,7 +8103,8 @@ actions!( async fn join_channel_internal( channel_id: ChannelId, app_state: &Arc, - requesting_window: Option>, + requesting_window: Option>, + requesting_workspace: Option>, active_call: &Entity, cx: &mut AsyncApp, ) -> Result { @@ -7939,8 +8140,8 @@ async fn join_channel_internal( } if should_prompt { - if let Some(workspace) = requesting_window { - let answer = workspace + if let Some(multi_workspace) = requesting_window { + let answer = multi_workspace .update(cx, |_, window, cx| { window.prompt( PromptLevel::Warning, @@ -8009,9 +8210,9 @@ async fn join_channel_internal( // If you are the first to join a channel, see if you should share your project. if room.remote_participants().is_empty() && !room.local_participant_is_guest() - && let Some(workspace) = requesting_window + && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade()) { - let project = workspace.update(cx, |workspace, _, cx| { + let project = workspace.update(cx, |workspace, cx| { let project = workspace.project.read(cx); if !CallSettings::get_global(cx).share_on_join { @@ -8030,7 +8231,7 @@ async fn join_channel_internal( None } }); - if let Ok(Some(project)) = project { + if let Some(project) = project { return Some(cx.spawn(async move |room, cx| { room.update(cx, |room, cx| room.share_project(project, cx))? .await?; @@ -8051,14 +8252,21 @@ async fn join_channel_internal( pub fn join_channel( channel_id: ChannelId, app_state: Arc, - requesting_window: Option>, + requesting_window: Option>, + requesting_workspace: Option>, cx: &mut App, ) -> Task> { let active_call = ActiveCall::global(cx); cx.spawn(async move |cx| { - let result = - join_channel_internal(channel_id, &app_state, requesting_window, &active_call, cx) - .await; + let result = join_channel_internal( + channel_id, + &app_state, + requesting_window, + requesting_workspace, + &active_call, + cx, + ) + .await; // join channel succeeded, and opened a window if matches!(result, Ok(true)) { @@ -8082,6 +8290,12 @@ pub fn join_channel( }) .await?; + window_handle + .update(cx, |_, window, _cx| { + window.activate_window(); + }) + .ok(); + if result.is_ok() { cx.update(|cx| { cx.dispatch_action(&OpenChannelNotes); @@ -8136,10 +8350,10 @@ pub fn join_channel( }) } -pub async fn get_any_active_workspace( +pub async fn get_any_active_multi_workspace( app_state: Arc, mut cx: AsyncApp, -) -> anyhow::Result> { +) -> anyhow::Result> { // find an existing workspace to focus and show call controls let active_window = activate_any_workspace_window(&mut cx); if active_window.is_none() { @@ -8149,17 +8363,17 @@ pub async fn get_any_active_workspace( activate_any_workspace_window(&mut cx).context("could not open zed") } -fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option> { +fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option> { cx.update(|cx| { if let Some(workspace_window) = cx .active_window() - .and_then(|window| window.downcast::()) + .and_then(|window| window.downcast::()) { return Some(workspace_window); } for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { + if let Some(workspace_window) = window.downcast::() { workspace_window .update(cx, |_, window, _| window.activate_window()) .ok(); @@ -8170,14 +8384,17 @@ fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option Vec> { +pub fn local_workspace_windows(cx: &App) -> Vec> { cx.windows() .into_iter() - .filter_map(|window| window.downcast::()) - .filter(|workspace| { - workspace - .read(cx) - .is_ok_and(|workspace| workspace.project.read(cx).is_local()) + .filter_map(|window| window.downcast::()) + .filter(|multi_workspace| { + multi_workspace.read(cx).is_ok_and(|multi_workspace| { + multi_workspace + .workspaces() + .iter() + .any(|workspace| workspace.read(cx).project.read(cx).is_local()) + }) }) .collect() } @@ -8188,7 +8405,7 @@ pub struct OpenOptions { pub focus: Option, pub open_new_workspace: Option, pub prefer_focused_window: bool, - pub replace_window: Option>, + pub replace_window: Option>, pub env: Option>, } @@ -8196,8 +8413,9 @@ pub struct OpenOptions { pub fn open_workspace_by_id( workspace_id: WorkspaceId, app_state: Arc, + requesting_window: Option>, cx: &mut App, -) -> Task>> { +) -> Task>> { let project_handle = Project::local( app_state.client.clone(), app_state.node_runtime.clone(), @@ -8217,52 +8435,87 @@ pub fn open_workspace_by_id( .workspace_for_id(workspace_id) .with_context(|| format!("Workspace {workspace_id:?} not found"))?; - let window_bounds_override = window_bounds_env_override(); - - let (window_bounds, display) = if let Some(bounds) = window_bounds_override { - (Some(WindowBounds::Windowed(bounds)), None) - } else if let Some(display) = serialized_workspace.display - && let Some(bounds) = serialized_workspace.window_bounds.as_ref() - { - (Some(bounds.0), Some(display)) - } else if let Some((display, bounds)) = persistence::read_default_window_bounds() { - (Some(bounds), Some(display)) - } else { - (None, None) - }; - - let options = cx.update(|cx| { - let mut options = (app_state.build_window_options)(display, cx); - options.window_bounds = window_bounds; - options - }); let centered_layout = serialized_workspace.centered_layout; - let window = cx.open_window(options, { - let app_state = app_state.clone(); - let project_handle = project_handle.clone(); - move |window, cx| { - cx.new(|cx| { - let mut workspace = - Workspace::new(Some(workspace_id), project_handle, app_state, window, cx); + let (window, workspace) = if let Some(window) = requesting_window { + let workspace = window.update(cx, |multi_workspace, window, cx| { + let workspace = cx.new(|cx| { + let mut workspace = Workspace::new( + Some(workspace_id), + project_handle.clone(), + app_state.clone(), + window, + cx, + ); workspace.centered_layout = centered_layout; workspace - }) - } - })?; + }); + multi_workspace.add_workspace(workspace.clone(), cx); + workspace + })?; + (window, workspace) + } else { + let window_bounds_override = window_bounds_env_override(); + + let (window_bounds, display) = if let Some(bounds) = window_bounds_override { + (Some(WindowBounds::Windowed(bounds)), None) + } else if let Some(display) = serialized_workspace.display + && let Some(bounds) = serialized_workspace.window_bounds.as_ref() + { + (Some(bounds.0), Some(display)) + } else if let Some((display, bounds)) = persistence::read_default_window_bounds() { + (Some(bounds), Some(display)) + } else { + (None, None) + }; + + let options = cx.update(|cx| { + let mut options = (app_state.build_window_options)(display, cx); + options.window_bounds = window_bounds; + options + }); + + let window = cx.open_window(options, { + let app_state = app_state.clone(); + let project_handle = project_handle.clone(); + move |window, cx| { + let workspace = cx.new(|cx| { + let mut workspace = Workspace::new( + Some(workspace_id), + project_handle, + app_state, + window, + cx, + ); + workspace.centered_layout = centered_layout; + workspace + }); + cx.new(|cx| MultiWorkspace::new(workspace, cx)) + } + })?; + + let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| { + multi_workspace.workspace().clone() + })?; + + (window, workspace) + }; notify_if_database_failed(window, cx); // Restore items from the serialized workspace window - .update(cx, |_workspace, window, cx| { - open_items(Some(serialized_workspace), vec![], window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |_workspace, cx| { + open_items(Some(serialized_workspace), vec![], window, cx) + }) })? .await?; - window.update(cx, |workspace, window, cx| { - window.activate_window(); - workspace.serialize_workspace(window, cx); + window.update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.serialize_workspace(window, cx); + }); })?; Ok(window) @@ -8277,12 +8530,12 @@ pub fn open_paths( cx: &mut App, ) -> Task< anyhow::Result<( - WindowHandle, + WindowHandle, Vec>>>, )>, > { let abs_paths = abs_paths.to_vec(); - let mut existing = None; + let mut existing: Option<(WindowHandle, Entity)> = None; let mut best_match = None; let mut open_visible = OpenVisible::All; #[cfg(target_os = "windows")] @@ -8301,20 +8554,22 @@ pub fn open_paths( cx.update(|cx| { for window in local_workspace_windows(cx) { - if let Ok(workspace) = window.read(cx) { - let m = workspace.project.read(cx).visibility_for_paths( - &abs_paths, - &all_metadatas, - open_options.open_new_workspace == None, - cx, - ); - if m > best_match { - existing = Some(window); - best_match = m; - } else if best_match.is_none() - && open_options.open_new_workspace == Some(false) - { - existing = Some(window) + if let Ok(multi_workspace) = window.read(cx) { + for workspace in multi_workspace.workspaces() { + let m = workspace.read(cx).project.read(cx).visibility_for_paths( + &abs_paths, + &all_metadatas, + open_options.open_new_workspace == None, + cx, + ); + if m > best_match { + existing = Some((window, workspace.clone())); + best_match = m; + } else if best_match.is_none() + && open_options.open_new_workspace == Some(false) + { + existing = Some((window, workspace.clone())) + } } } } @@ -8327,95 +8582,118 @@ pub fn open_paths( cx.update(|cx| { if let Some(window) = cx .active_window() - .and_then(|window| window.downcast::()) - && let Ok(workspace) = window.read(cx) + .and_then(|window| window.downcast::()) + && let Ok(multi_workspace) = window.read(cx) { - let project = workspace.project().read(cx); + let active_workspace = multi_workspace.workspace().clone(); + let project = active_workspace.read(cx).project().read(cx); if project.is_local() && !project.is_via_collab() { - existing = Some(window); + existing = Some((window, active_workspace)); open_visible = OpenVisible::None; return; } } - for window in local_workspace_windows(cx) { - if let Ok(workspace) = window.read(cx) { - let project = workspace.project().read(cx); - if project.is_via_collab() { - continue; + 'outer: for window in local_workspace_windows(cx) { + if let Ok(multi_workspace) = window.read(cx) { + for workspace in multi_workspace.workspaces() { + let project = workspace.read(cx).project().read(cx); + if project.is_via_collab() { + continue; + } + existing = Some((window, workspace.clone())); + open_visible = OpenVisible::None; + break 'outer; } - existing = Some(window); - open_visible = OpenVisible::None; - break; } } }); } } - let result = if let Some(existing) = existing { + let result = if let Some((existing, target_workspace)) = existing { let open_task = existing - .update(cx, |workspace, window, cx| { + .update(cx, |multi_workspace, window, cx| { window.activate_window(); - workspace.open_paths( - abs_paths, - OpenOptions { - visible: Some(open_visible), - ..Default::default() - }, - None, - window, - cx, - ) + multi_workspace.activate(target_workspace.clone(), cx); + target_workspace.update(cx, |workspace, cx| { + workspace.open_paths( + abs_paths, + OpenOptions { + visible: Some(open_visible), + ..Default::default() + }, + None, + window, + cx, + ) + }) })? .await; - _ = existing.update(cx, |workspace, _, cx| { - for item in open_task.iter().flatten() { - if let Err(e) = item { - workspace.show_error(&e, cx); + _ = existing.update(cx, |multi_workspace, _, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + for item in open_task.iter().flatten() { + if let Err(e) = item { + workspace.show_error(&e, cx); + } } - } + }); }); Ok((existing, open_task)) } else { - cx.update(move |cx| { - Workspace::new_local( - abs_paths, - app_state.clone(), - open_options.replace_window, - open_options.env, - None, - cx, - ) - }) - .await + let result = cx + .update(move |cx| { + Workspace::new_local( + abs_paths, + app_state.clone(), + open_options.replace_window, + open_options.env, + None, + cx, + ) + }) + .await; + + if let Ok((ref window_handle, _)) = result { + window_handle + .update(cx, |_, window, _cx| { + window.activate_window(); + }) + .log_err(); + } + + result }; #[cfg(target_os = "windows")] if let Some(util::paths::WslPath{distro, path}) = wsl_path - && let Ok((workspace, _)) = &result + && let Ok((multi_workspace_window, _)) = &result { - workspace - .update(cx, move |workspace, _window, cx| { + multi_workspace_window + .update(cx, move |multi_workspace, _window, cx| { struct OpenInWsl; - workspace.show_notification(NotificationId::unique::(), cx, move |cx| { - let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy()); - let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote"); - cx.new(move |cx| { - MessageNotification::new(msg, cx) - .primary_message("Open in WSL") - .primary_icon(IconName::FolderOpen) - .primary_on_click(move |window, cx| { - window.dispatch_action(Box::new(remote::OpenWslPath { - distro: remote::WslConnectionOptions { - distro_name: distro.clone(), - user: None, - }, - paths: vec![path.clone().into()], - }), cx) - }) - }) + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + workspace.show_notification(NotificationId::unique::(), cx, move |cx| { + let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy()); + let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote"); + cx.new(move |cx| { + MessageNotification::new(msg, cx) + .primary_message("Open in WSL") + .primary_icon(IconName::FolderOpen) + .primary_on_click(move |window, cx| { + window.dispatch_action(Box::new(remote::OpenWslPath { + distro: remote::WslConnectionOptions { + distro_name: distro.clone(), + user: None, + }, + paths: vec![path.clone().into()], + }), cx) + }) + }) + }); }); }) .unwrap(); @@ -8438,9 +8716,13 @@ pub fn open_new( Some(Box::new(init)), cx, ); - cx.spawn(async move |_cx| { - let (_workspace, _opened_paths) = task.await?; - // Init callback is called synchronously during workspace creation + cx.spawn(async move |cx| { + let (window, _opened_paths) = task.await?; + window + .update(cx, |_, window, _cx| { + window.activate_window(); + }) + .ok(); Ok(()) }) } @@ -8492,7 +8774,7 @@ pub fn create_and_open_local_file( } pub fn open_remote_project_with_new_connection( - window: WindowHandle, + window: WindowHandle, remote_connection: Arc, cancel_rx: oneshot::Receiver<()>, delegate: Arc, @@ -8552,7 +8834,7 @@ pub fn open_remote_project_with_existing_connection( project: Entity, paths: Vec, app_state: Arc, - window: WindowHandle, + window: WindowHandle, cx: &mut AsyncApp, ) -> Task>>>> { cx.spawn(async move |cx| { @@ -8578,7 +8860,7 @@ async fn open_remote_project_inner( workspace_id: WorkspaceId, serialized_workspace: Option, app_state: Arc, - window: WindowHandle, + window: WindowHandle, cx: &mut AsyncApp, ) -> Result>>> { let toolchains = DB.toolchains(workspace_id).await?; @@ -8623,21 +8905,10 @@ async fn open_remote_project_inner( return Err(project_path_errors.pop().context("no paths given")?); } - if let Some(detach_session_task) = window - .update(cx, |_workspace, window, cx| { - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx)) - }) - }) - .ok() - { - detach_session_task.await.ok(); - } - - cx.update_window(window.into(), |_, window, cx| { - window.replace_root(cx, |window, cx| { - telemetry::event!("SSH Project Opened"); + let workspace = window.update(cx, |multi_workspace, window, cx| { + telemetry::event!("SSH Project Opened"); + let new_workspace = cx.new(|cx| { let mut workspace = Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); workspace.update_history(cx); @@ -8648,16 +8919,21 @@ async fn open_remote_project_inner( workspace }); + + multi_workspace.activate(new_workspace.clone(), cx); + new_workspace })?; let items = window .update(cx, |_, window, cx| { window.activate_window(); - open_items(serialized_workspace, project_paths_to_open, window, cx) + workspace.update(cx, |_workspace, cx| { + open_items(serialized_workspace, project_paths_to_open, window, cx) + }) })? .await?; - window.update(cx, |workspace, _, cx| { + workspace.update(cx, |workspace, cx| { for error in project_path_errors { if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist { if let Some(path) = error.error_tag("path") { @@ -8667,7 +8943,7 @@ async fn open_remote_project_inner( workspace.show_error(&error, cx) } } - })?; + }); Ok(items.into_iter().map(|item| item?.ok()).collect()) } @@ -8705,24 +8981,37 @@ pub fn join_in_room_project( ) -> Task> { let windows = cx.windows(); cx.spawn(async move |cx| { - let existing_workspace = windows.into_iter().find_map(|window_handle| { + let existing_window_and_workspace: Option<( + WindowHandle, + Entity, + )> = windows.into_iter().find_map(|window_handle| { window_handle - .downcast::() + .downcast::() .and_then(|window_handle| { window_handle - .update(cx, |workspace, _window, cx| { - if workspace.project().read(cx).remote_id() == Some(project_id) { - Some(window_handle) - } else { - None + .update(cx, |multi_workspace, _window, cx| { + for workspace in multi_workspace.workspaces() { + if workspace.read(cx).project().read(cx).remote_id() + == Some(project_id) + { + return Some((window_handle, workspace.clone())); + } } + None }) .unwrap_or(None) }) }); - let workspace = if let Some(existing_workspace) = existing_workspace { - existing_workspace + let multi_workspace_window = if let Some((existing_window, target_workspace)) = + existing_window_and_workspace + { + existing_window + .update(cx, |multi_workspace, _, cx| { + multi_workspace.activate(target_workspace, cx); + }) + .ok(); + existing_window } else { let active_call = cx.update(|cx| ActiveCall::global(cx)); let room = active_call @@ -8744,39 +9033,44 @@ pub fn join_in_room_project( let mut options = (app_state.build_window_options)(None, cx); options.window_bounds = window_bounds_override.map(WindowBounds::Windowed); cx.open_window(options, |window, cx| { - cx.new(|cx| { + let workspace = cx.new(|cx| { Workspace::new(Default::default(), project, app_state.clone(), window, cx) - }) + }); + cx.new(|cx| MultiWorkspace::new(workspace, cx)) }) })? }; - workspace.update(cx, |workspace, window, cx| { + multi_workspace_window.update(cx, |multi_workspace, window, cx| { cx.activate(true); window.activate_window(); - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - let follow_peer_id = room - .read(cx) - .remote_participants() - .iter() - .find(|(_, participant)| participant.user.id == follow_user_id) - .map(|(_, p)| p.peer_id) - .or_else(|| { - // If we couldn't follow the given user, follow the host instead. - let collaborator = workspace - .project() - .read(cx) - .collaborators() - .values() - .find(|collaborator| collaborator.is_host)?; - Some(collaborator.peer_id) - }); + // We set the active workspace above, so this is the correct workspace. + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + let follow_peer_id = room + .read(cx) + .remote_participants() + .iter() + .find(|(_, participant)| participant.user.id == follow_user_id) + .map(|(_, p)| p.peer_id) + .or_else(|| { + // If we couldn't follow the given user, follow the host instead. + let collaborator = workspace + .project() + .read(cx) + .collaborators() + .values() + .find(|collaborator| collaborator.is_host)?; + Some(collaborator.peer_id) + }); - if let Some(follow_peer_id) = follow_peer_id { - workspace.follow(follow_peer_id, window, cx); + if let Some(follow_peer_id) = follow_peer_id { + workspace.follow(follow_peer_id, window, cx); + } } - } + }); })?; anyhow::Ok(()) @@ -8788,7 +9082,7 @@ pub fn reload(cx: &mut App) { let mut workspace_windows = cx .windows() .into_iter() - .filter_map(|window| window.downcast::()) + .filter_map(|window| window.downcast::()) .collect::>(); // If multiple windows have unsaved changes, and need a save prompt, @@ -8820,8 +9114,11 @@ pub fn reload(cx: &mut App) { // If the user cancels any save prompt, then keep the app open. for window in workspace_windows { - if let Ok(should_close) = window.update(cx, |workspace, window, cx| { - workspace.prepare_to_close(CloseIntent::Quit, window, cx) + if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| { + workspace.prepare_to_close(CloseIntent::Quit, window, cx) + }) }) && !should_close.await? { return anyhow::Ok(()); @@ -8847,14 +9144,28 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { Some(size(px(width as f32), px(height as f32))) } -/// Add client-side decorations (rounded corners, shadows, resize handling) when appropriate. +/// Add client-side decorations (rounded corners, shadows, resize handling) when +/// appropriate. +/// +/// The `border_radius_tiling` parameter allows overriding which corners get +/// rounded, independently of the actual window tiling state. This is used +/// specifically for the workspace switcher sidebar: when the sidebar is open, +/// we want square corners on the left (so the sidebar appears flush with the +/// window edge) but we still need the shadow padding for proper visual +/// appearance. Unlike actual window tiling, this only affects border radius - +/// not padding or shadows. pub fn client_side_decorations( element: impl IntoElement, window: &mut Window, cx: &mut App, + border_radius_tiling: Tiling, ) -> Stateful
{ const BORDER_SIZE: Pixels = px(1.0); let decorations = window.window_decorations(); + let tiling = match decorations { + Decorations::Server => Tiling::default(), + Decorations::Client { tiling } => tiling, + }; match decorations { Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW), @@ -8869,19 +9180,35 @@ pub fn client_side_decorations( .bg(transparent_black()) .map(|div| match decorations { Decorations::Server => div, - Decorations::Client { tiling, .. } => div - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) + Decorations::Client { .. } => div + .when( + !(tiling.top + || tiling.right + || border_radius_tiling.top + || border_radius_tiling.right), + |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) + .when( + !(tiling.top + || tiling.left + || border_radius_tiling.top + || border_radius_tiling.left), + |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) + .when( + !(tiling.bottom + || tiling.right + || border_radius_tiling.bottom + || border_radius_tiling.right), + |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) + .when( + !(tiling.bottom + || tiling.left + || border_radius_tiling.bottom + || border_radius_tiling.left), + |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) .when(!tiling.top, |div| { div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW) }) @@ -8934,20 +9261,36 @@ pub fn client_side_decorations( .cursor(CursorStyle::Arrow) .map(|div| match decorations { Decorations::Server => div, - Decorations::Client { tiling } => div + Decorations::Client { .. } => div .border_color(cx.theme().colors().border) - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) + .when( + !(tiling.top + || tiling.right + || border_radius_tiling.top + || border_radius_tiling.right), + |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) + .when( + !(tiling.top + || tiling.left + || border_radius_tiling.top + || border_radius_tiling.left), + |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) + .when( + !(tiling.bottom + || tiling.right + || border_radius_tiling.bottom + || border_radius_tiling.right), + |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) + .when( + !(tiling.bottom + || tiling.left + || border_radius_tiling.bottom + || border_radius_tiling.left), + |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) .when(!tiling.top, |div| div.border_t(BORDER_SIZE)) .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE)) .when(!tiling.left, |div| div.border_l(BORDER_SIZE)) @@ -9294,11 +9637,17 @@ pub fn with_active_or_new_workspace( cx: &mut App, f: impl FnOnce(&mut Workspace, &mut Window, &mut Context) + Send + 'static, ) { - match cx.active_window().and_then(|w| w.downcast::()) { - Some(workspace) => { + match cx + .active_window() + .and_then(|w| w.downcast::()) + { + Some(multi_workspace) => { cx.defer(move |cx| { - workspace - .update(cx, |workspace, window, cx| f(workspace, window, cx)) + multi_workspace + .update(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace().clone(); + workspace.update(cx, |workspace, cx| f(workspace, window, cx)); + }) .log_err(); }); } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2f9c5b32c60478fc00a668083cf0814258372cca..924352c46a5655813a11f7bff160f093fc94a540 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -49,6 +49,7 @@ visual-tests = [ "language_model/test-support", "fs/test-support", "recent_projects/test-support", + "sidebar/test-support", "title_bar/test-support", ] @@ -187,6 +188,7 @@ settings.workspace = true settings_profile_selector.workspace = true settings_ui.workspace = true shellexpand.workspace = true +sidebar.workspace = true smol.workspace = true snippet_provider.workspace = true snippets_ui.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9a2cea33882b1a9d8434bf0f3e2cb1dba8471007..6dc23fa4c585fa58502c794d8d2480ce6a1b278d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -54,8 +54,8 @@ use theme::{ActiveTheme, GlobalTheme, ThemeRegistry}; use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceId, - WorkspaceSettings, WorkspaceStore, notifications::NotificationId, + AppState, MultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace, Toast, + WorkspaceSettings, WorkspaceStore, notifications::NotificationId, restore_multiworkspace, }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, @@ -511,15 +511,13 @@ fn main() { let workspace_store = workspace_store.clone(); Arc::new(move |cx: &mut App| { workspace_store.update(cx, |workspace_store, cx| { - workspace_store + Ok(workspace_store .workspaces() - .iter() - .map(|workspace| { - workspace.update(cx, |workspace, _, cx| { - workspace.project().read(cx).lsp_store() - }) + .filter_map(|weak| weak.upgrade()) + .map(|workspace: gpui::Entity| { + workspace.read(cx).project().read(cx).lsp_store() }) - .collect() + .collect()) }) }) }), @@ -849,7 +847,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut OpenRequestKind::Extension { extension_id } => { cx.spawn(async move |cx| { let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; + workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?; workspace.update(cx, |_, window, cx| { window.dispatch_action( Box::new(zed_actions::Extensions { @@ -864,31 +862,40 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut } OpenRequestKind::AgentPanel { initial_prompt } => { cx.spawn(async move |cx| { - let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - workspace.update(cx, |workspace, window, cx| { - if let Some(panel) = workspace.focus_panel::(window, cx) { - panel.update(cx, |panel, cx| { - panel.new_external_thread_with_text(initial_prompt, window, cx); - }); - } + let multi_workspace = + workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?; + + multi_workspace.update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + if let Some(panel) = workspace.focus_panel::(window, cx) { + panel.update(cx, |panel, cx| { + panel.new_external_thread_with_text(initial_prompt, window, cx); + }); + } + }); }) }) .detach_and_log_err(cx); } OpenRequestKind::SharedAgentThread { session_id } => { cx.spawn(async move |cx| { + let multi_workspace = + workspace::get_any_active_multi_workspace(app_state.clone(), cx.clone()) + .await?; + let workspace = - workspace::get_any_active_workspace(app_state.clone(), cx.clone()).await?; + multi_workspace.read_with(cx, |mw, _| mw.workspace().clone())?; let (client, thread_store) = - workspace.update(cx, |workspace, _window, cx| { - let client = workspace.project().read(cx).client(); - let thread_store: Option> = workspace - .panel::(cx) - .map(|panel| panel.read(cx).thread_store().clone()); - (client, thread_store) - })?; + multi_workspace.update(cx, |_, _window, cx| { + workspace.update(cx, |workspace, cx| { + let client = workspace.project().read(cx).client(); + let thread_store: Option> = workspace + .panel::(cx) + .map(|panel| panel.read(cx).thread_store().clone()); + anyhow::Ok((client, thread_store)) + }) + })??; let Some(thread_store): Option> = thread_store else { anyhow::bail!("Agent panel not available"); @@ -921,25 +928,27 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut meta: None, }; - workspace.update(cx, |workspace, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.open_thread(thread_metadata, window, cx); - }); - panel.focus_handle(cx).focus(window, cx); - } - })?; + let sharer_username = response.sharer_username.clone(); - workspace.update(cx, |workspace, _window, cx| { - struct ImportedThreadToast; - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - format!("Imported shared thread from {}", response.sharer_username), - ) - .autohide(), - cx, - ); + multi_workspace.update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.open_thread(thread_metadata, window, cx); + }); + panel.focus_handle(cx).focus(window, cx); + } + + struct ImportedThreadToast; + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + format!("Imported shared thread from {}", sharer_username), + ) + .autohide(), + cx, + ); + }); })?; anyhow::Ok(()) @@ -1014,7 +1023,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut // [ languages $(language) tab_size] cx.spawn(async move |cx| { let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; + workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?; workspace.update(cx, |_, window, cx| match setting_path { None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx), @@ -1076,23 +1085,29 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut .await?; workspace - .update(cx, |workspace, window, cx| { - let Some(repo) = workspace.project().read(cx).active_repository(cx) - else { - log::error!("no active repository found for commit view"); - return Err(anyhow::anyhow!("no active repository found")); - }; - - git_ui::commit_view::CommitView::open( - sha, - repo.downgrade(), - workspace.weak_handle(), - None, - None, - window, - cx, - ); - Ok(()) + .update(cx, |multi_workspace, window, cx| { + multi_workspace + .workspace() + .clone() + .update(cx, |workspace, cx| { + let Some(repo) = + workspace.project().read(cx).active_repository(cx) + else { + log::error!("no active repository found for commit view"); + return Err(anyhow::anyhow!("no active repository found")); + }; + + git_ui::commit_view::CommitView::open( + sha, + repo.downgrade(), + workspace.weak_handle(), + None, + None, + window, + cx, + ); + Ok(()) + }) }) .log_err(); @@ -1162,6 +1177,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut client::ChannelId(channel_id), app_state.clone(), None, + None, cx, ) }) @@ -1169,8 +1185,9 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut } let workspace_window = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - let workspace = workspace_window.entity(cx)?; + workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?; + + let workspace = workspace_window.read_with(cx, |mw, _| mw.workspace().clone())?; let mut promises = Vec::new(); for (channel_id, heading) in request.open_channel_notes { @@ -1260,78 +1277,53 @@ async fn installation_id() -> Result { Ok(IdType::New(installation_id)) } -async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp) -> Result<()> { - if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { - let use_system_window_tabs = - cx.update(|cx| WorkspaceSettings::get_global(cx).use_system_window_tabs); +pub(crate) async fn restore_or_create_workspace( + app_state: Arc, + cx: &mut AsyncApp, +) -> Result<()> { + if let Some((multi_workspaces, remote_workspaces)) = restorable_workspaces(cx, &app_state).await + { let mut results: Vec> = Vec::new(); let mut tasks = Vec::new(); - for (index, (workspace_id, location, paths)) in locations.into_iter().enumerate() { - match location { - SerializedWorkspaceLocation::Local if paths.is_empty() => { - // Restore empty workspace by ID (has items like drafts but no folders) - let app_state = app_state.clone(); - let task = cx.spawn(async move |cx| { - let open_task = cx.update(|cx| { - workspace::open_workspace_by_id(workspace_id, app_state, cx) - }); - open_task.await.map(|_| ()) - }); + let mut local_results = Vec::new(); + for multi_workspace in multi_workspaces { + local_results + .push(restore_multiworkspace(multi_workspace, app_state.clone(), cx).await); + } - if use_system_window_tabs && index == 0 { - results.push(task.await); - } else { - tasks.push(task); - } - } - SerializedWorkspaceLocation::Local => { - let app_state = app_state.clone(); - let task = cx.spawn(async move |cx| { - let open_task = cx.update(|cx| { - workspace::open_paths( - &paths.paths(), - app_state, - workspace::OpenOptions::default(), - cx, - ) - }); - open_task.await.map(|_| ()) - }); - - // If we're using system window tabs and this is the first workspace, - // wait for it to finish so that the other windows can be added as tabs. - if use_system_window_tabs && index == 0 { - results.push(task.await); - } else { - tasks.push(task); - } - } - SerializedWorkspaceLocation::Remote(mut connection_options) => { - let app_state = app_state.clone(); - if let RemoteConnectionOptions::Ssh(options) = &mut connection_options { - cx.update(|cx| { - RemoteSettings::get_global(cx) - .fill_connection_options_from_settings(options) - }); - } - let task = cx.spawn(async move |cx| { - recent_projects::open_remote_project( - connection_options, - paths.paths().into_iter().map(PathBuf::from).collect(), - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await - .map_err(|e| anyhow::anyhow!(e)) - }); - tasks.push(task); - } + for result in local_results { + results.push(result.map(|_| ())); + } + + for session_workspace in remote_workspaces { + let app_state = app_state.clone(); + let SerializedWorkspaceLocation::Remote(mut connection_options) = + session_workspace.location + else { + continue; + }; + let paths = session_workspace.paths; + if let RemoteConnectionOptions::Ssh(options) = &mut connection_options { + cx.update(|cx| { + RemoteSettings::get_global(cx).fill_connection_options_from_settings(options) + }); } + let task = cx.spawn(async move |cx| { + recent_projects::open_remote_project( + connection_options, + paths.paths().iter().map(PathBuf::from).collect(), + app_state, + workspace::OpenOptions::default(), + cx, + ) + .await + .map_err(|e| anyhow::anyhow!(e)) + }); + tasks.push(task); } - // Wait for all workspaces to open concurrently + // Wait for all window groups and remote workspaces to open concurrently results.extend(future::join_all(tasks).await); // Show notifications for any errors that occurred @@ -1356,12 +1348,16 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp // Try to find an active workspace to show the toast let toast_shown = cx.update(|cx| { if let Some(window) = cx.active_window() - && let Some(workspace) = window.downcast::() + && let Some(multi_workspace) = window.downcast::() { - workspace - .update(cx, |workspace, _, cx| { - workspace - .show_toast(Toast::new(NotificationId::unique::<()>(), message), cx) + multi_workspace + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new(NotificationId::unique::<()>(), message), + cx, + ) + }); }) .ok(); return true; @@ -1402,10 +1398,25 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp Ok(()) } +async fn restorable_workspaces( + cx: &mut AsyncApp, + app_state: &Arc, +) -> Option<( + Vec, + Vec, +)> { + let locations = restorable_workspace_locations(cx, app_state).await?; + let (remote_workspaces, local_workspaces) = locations + .into_iter() + .partition(|sw| matches!(sw.location, SerializedWorkspaceLocation::Remote(_))); + let multi_workspaces = workspace::read_serialized_multi_workspaces(local_workspaces); + Some((multi_workspaces, remote_workspaces)) +} + pub(crate) async fn restorable_workspace_locations( cx: &mut AsyncApp, app_state: &Arc, -) -> Option> { +) -> Option> { let mut restore_behavior = cx.update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup); let session_handle = app_state.session.clone(); @@ -1429,9 +1440,16 @@ pub(crate) async fn restorable_workspace_locations( match restore_behavior { workspace::RestoreOnStartupBehavior::LastWorkspace => { - workspace::last_opened_workspace_location() + workspace::last_opened_workspace_location(app_state.fs.as_ref()) .await - .map(|location| vec![location]) + .map(|(workspace_id, location, paths)| { + vec![SessionWorkspace { + workspace_id, + location, + paths, + window_id: None, + }] + }) } workspace::RestoreOnStartupBehavior::LastSession => { if let Some(last_session_id) = last_session_id { @@ -1440,7 +1458,9 @@ pub(crate) async fn restorable_workspace_locations( let mut locations = workspace::last_session_workspace_locations( &last_session_id, last_session_window_stack, + app_state.fs.as_ref(), ) + .await .filter(|locations| !locations.is_empty()); // Since last_session_window_order returns the windows ordered front-to-back diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index 9b9e8dab38a5738f9b7d51c14ad5e7c66f0e9c0e..5de8cac8e14bfa31d32361ab77765b721fab26e6 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -59,6 +59,7 @@ use { }, image::RgbaImage, project_panel::ProjectPanel, + recent_projects::RecentProjectEntry, settings::{NotifyWhenAgentWaiting, Settings as _}, settings_ui::SettingsWindow, std::{ @@ -69,7 +70,7 @@ use { time::Duration, }, util::ResultExt as _, - workspace::{AppState, Workspace}, + workspace::{AppState, MultiWorkspace, Workspace, WorkspaceId}, zed_actions::OpenSettingsAt, }; @@ -444,7 +445,24 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()> } } - // Run Test 3: Agent Thread View tests + // Run Test 3: Multi-workspace sidebar visual tests + println!("\n--- Test 3: multi_workspace_sidebar ---"); + match run_multi_workspace_sidebar_visual_tests(app_state.clone(), &mut cx, update_baseline) { + Ok(TestResult::Passed) => { + println!("✓ multi_workspace_sidebar: PASSED"); + passed += 1; + } + Ok(TestResult::BaselineUpdated(_)) => { + println!("✓ multi_workspace_sidebar: Baselines updated"); + updated += 1; + } + Err(e) => { + eprintln!("✗ multi_workspace_sidebar: FAILED - {}", e); + failed += 1; + } + } + + // Run Test 4: Agent Thread View tests #[cfg(feature = "visual-tests")] { println!("\n--- Test 3: agent_thread_with_image (collapsed + expanded) ---"); @@ -2454,3 +2472,300 @@ fn run_tool_permissions_visual_tests( // Return success - we're just capturing screenshots, not comparing baselines Ok(TestResult::Passed) } + +#[cfg(target_os = "macos")] +fn run_multi_workspace_sidebar_visual_tests( + app_state: Arc, + cx: &mut VisualTestAppContext, + update_baseline: bool, +) -> Result { + // Create temporary directories to act as worktrees for active workspaces + let temp_dir = tempfile::tempdir()?; + let temp_path = temp_dir.keep(); + let canonical_temp = temp_path.canonicalize()?; + + let workspace1_dir = canonical_temp.join("private-test-remote"); + let workspace2_dir = canonical_temp.join("zed"); + std::fs::create_dir_all(&workspace1_dir)?; + std::fs::create_dir_all(&workspace2_dir)?; + + // Create directories for recent projects (they must exist on disk for display) + let recent1_dir = canonical_temp.join("tiny-project"); + let recent2_dir = canonical_temp.join("font-kit"); + let recent3_dir = canonical_temp.join("ideas"); + let recent4_dir = canonical_temp.join("tmp"); + std::fs::create_dir_all(&recent1_dir)?; + std::fs::create_dir_all(&recent2_dir)?; + std::fs::create_dir_all(&recent3_dir)?; + std::fs::create_dir_all(&recent4_dir)?; + + // Enable the agent-v2 feature flag so multi-workspace is active + cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + }); + + // Create both projects upfront so we can build both workspaces during + // window creation, before the MultiWorkspace entity exists. + // This avoids a re-entrant read panic that occurs when Workspace::new + // tries to access the window root (MultiWorkspace) while it's being updated. + let project1 = cx.update(|cx| { + project::Project::local( + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + None, + project::LocalProjectFlags { + init_worktree_trust: false, + ..Default::default() + }, + cx, + ) + }); + + let project2 = cx.update(|cx| { + project::Project::local( + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + None, + project::LocalProjectFlags { + init_worktree_trust: false, + ..Default::default() + }, + cx, + ) + }); + + let window_size = size(px(1280.0), px(800.0)); + let bounds = Bounds { + origin: point(px(0.0), px(0.0)), + size: window_size, + }; + + // Open a MultiWorkspace window with both workspaces created at construction time + let multi_workspace_window: WindowHandle = cx + .update(|cx| { + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + focus: false, + show: false, + ..Default::default() + }, + |window, cx| { + let workspace1 = cx.new(|cx| { + Workspace::new(None, project1.clone(), app_state.clone(), window, cx) + }); + let workspace2 = cx.new(|cx| { + Workspace::new(None, project2.clone(), app_state.clone(), window, cx) + }); + cx.new(|cx| { + let mut multi_workspace = MultiWorkspace::new(workspace1, cx); + multi_workspace.activate(workspace2, cx); + multi_workspace + }) + }, + ) + }) + .context("Failed to open MultiWorkspace window")?; + + cx.run_until_parked(); + + // Add worktree to workspace 1 (index 0) so it shows as "private-test-remote" + let add_worktree1_task = multi_workspace_window + .update(cx, |multi_workspace, _window, cx| { + let workspace1 = &multi_workspace.workspaces()[0]; + let project = workspace1.read(cx).project().clone(); + project.update(cx, |project, cx| { + project.find_or_create_worktree(&workspace1_dir, true, cx) + }) + }) + .context("Failed to start adding worktree 1")?; + + cx.background_executor.allow_parking(); + cx.foreground_executor + .block_test(add_worktree1_task) + .context("Failed to add worktree 1")?; + cx.background_executor.forbid_parking(); + + cx.run_until_parked(); + + // Add worktree to workspace 2 (index 1) so it shows as "zed" + let add_worktree2_task = multi_workspace_window + .update(cx, |multi_workspace, _window, cx| { + let workspace2 = &multi_workspace.workspaces()[1]; + let project = workspace2.read(cx).project().clone(); + project.update(cx, |project, cx| { + project.find_or_create_worktree(&workspace2_dir, true, cx) + }) + }) + .context("Failed to start adding worktree 2")?; + + cx.background_executor.allow_parking(); + cx.foreground_executor + .block_test(add_worktree2_task) + .context("Failed to add worktree 2")?; + cx.background_executor.forbid_parking(); + + cx.run_until_parked(); + + // Switch to workspace 1 so it's highlighted as active (index 0) + multi_workspace_window + .update(cx, |multi_workspace, window, cx| { + multi_workspace.activate_index(0, window, cx); + }) + .context("Failed to activate workspace 1")?; + + cx.run_until_parked(); + + // Create the sidebar and register it on the MultiWorkspace + let sidebar = multi_workspace_window + .update(cx, |_multi_workspace, window, cx| { + let multi_workspace_handle = cx.entity(); + cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx)) + }) + .context("Failed to create sidebar")?; + + multi_workspace_window + .update(cx, |multi_workspace, window, cx| { + multi_workspace.register_sidebar(sidebar.clone(), window, cx); + }) + .context("Failed to register sidebar")?; + + cx.run_until_parked(); + + // Inject recent project entries into the sidebar. + // We update the sidebar entity directly (not through the MultiWorkspace window update) + // to avoid a re-entrant read panic: rebuild_entries reads MultiWorkspace, so we can't + // be inside a MultiWorkspace update when that happens. + cx.update(|cx| { + sidebar.update(cx, |sidebar, cx| { + let recent_projects = vec![ + RecentProjectEntry { + name: "tiny-project".into(), + full_path: recent1_dir.to_string_lossy().to_string().into(), + paths: vec![recent1_dir.clone()], + workspace_id: WorkspaceId::default(), + }, + RecentProjectEntry { + name: "font-kit".into(), + full_path: recent2_dir.to_string_lossy().to_string().into(), + paths: vec![recent2_dir.clone()], + workspace_id: WorkspaceId::default(), + }, + RecentProjectEntry { + name: "ideas".into(), + full_path: recent3_dir.to_string_lossy().to_string().into(), + paths: vec![recent3_dir.clone()], + workspace_id: WorkspaceId::default(), + }, + RecentProjectEntry { + name: "tmp".into(), + full_path: recent4_dir.to_string_lossy().to_string().into(), + paths: vec![recent4_dir.clone()], + workspace_id: WorkspaceId::default(), + }, + ]; + sidebar.set_test_recent_projects(recent_projects, cx); + }); + }); + + // Set thread info directly on the sidebar for visual testing + cx.update(|cx| { + sidebar.update(cx, |sidebar, _cx| { + sidebar.set_test_thread_info( + 0, + "Refine thread view scrolling behavior".into(), + sidebar::AgentThreadStatus::Completed, + ); + sidebar.set_test_thread_info( + 1, + "Add line numbers option to FileEditBlock".into(), + sidebar::AgentThreadStatus::Running, + ); + }); + }); + + // Set last-worked-on thread titles on some recent projects for visual testing + cx.update(|cx| { + sidebar.update(cx, |sidebar, cx| { + sidebar.set_test_recent_project_thread_title( + recent1_dir.to_string_lossy().to_string().into(), + "Fix flaky test in CI pipeline".into(), + cx, + ); + sidebar.set_test_recent_project_thread_title( + recent2_dir.to_string_lossy().to_string().into(), + "Upgrade font rendering engine".into(), + cx, + ); + }); + }); + + cx.run_until_parked(); + + // Open the sidebar + multi_workspace_window + .update(cx, |multi_workspace, window, cx| { + multi_workspace.toggle_sidebar(window, cx); + }) + .context("Failed to toggle sidebar")?; + + // Let rendering settle + for _ in 0..10 { + cx.advance_clock(Duration::from_millis(100)); + cx.run_until_parked(); + } + + // Refresh the window + cx.update_window(multi_workspace_window.into(), |_, window, _cx| { + window.refresh(); + })?; + + cx.run_until_parked(); + + // Capture: sidebar open with active workspaces and recent projects + let test_result = run_visual_test( + "multi_workspace_sidebar_open", + multi_workspace_window.into(), + cx, + update_baseline, + )?; + + // Clean up worktrees + multi_workspace_window + .update(cx, |multi_workspace, _window, cx| { + for workspace in multi_workspace.workspaces() { + let project = workspace.read(cx).project().clone(); + project.update(cx, |project, cx| { + let worktree_ids: Vec<_> = + project.worktrees(cx).map(|wt| wt.read(cx).id()).collect(); + for id in worktree_ids { + project.remove_worktree(id, cx); + } + }); + } + }) + .log_err(); + + cx.run_until_parked(); + + // Close the window + cx.update_window(multi_workspace_window.into(), |_, window, _cx| { + window.remove_window(); + }) + .log_err(); + + cx.run_until_parked(); + + for _ in 0..15 { + cx.advance_clock(Duration::from_millis(100)); + cx.run_until_parked(); + } + + Ok(test_result) +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ef74f355e44088bed0f23d87d1b54468a06a7c30..49229ddeb1181f1b52655319f4dcfef570186898 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -68,6 +68,7 @@ use settings::{ initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, update_settings_file, }; +use sidebar::Sidebar; use std::time::Duration; use std::{ borrow::Cow, @@ -88,9 +89,9 @@ use workspace::notifications::{ }; use workspace::utility_pane::utility_slot_for_dock_position; use workspace::{ - AppState, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, WorkspaceSettings, - create_and_open_local_file, notifications::simple_message_notification::MessageNotification, - open_new, + AppState, MultiWorkspace, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, + WorkspaceSettings, create_and_open_local_file, + notifications::simple_message_notification::MessageNotification, open_new, }; use workspace::{ CloseIntent, CloseProject, CloseWindow, NotificationFrame, RestoreBanner, @@ -370,6 +371,16 @@ pub fn initialize_workspace( }) .detach(); + cx.observe_new(|multi_workspace: &mut MultiWorkspace, window, cx| { + let Some(window) = window else { + return; + }; + let multi_workspace_handle = cx.entity(); + let sidebar = cx.new(|cx| Sidebar::new(multi_workspace_handle, window, cx)); + multi_workspace.register_sidebar(sidebar, window, cx); + }) + .detach(); + cx.observe_new(move |workspace: &mut Workspace, window, cx| { let Some(window) = window else { return; @@ -1102,7 +1113,7 @@ fn register_actions( .register_action({ let app_state = Arc::downgrade(&app_state); move |_, _: &CloseProject, window, cx| { - let Some(window_handle) = window.window_handle().downcast::() else { + let Some(window_handle) = window.window_handle().downcast::() else { return; }; if let Some(app_state) = app_state.upgrade() { @@ -1198,6 +1209,7 @@ fn initialize_pane( window: &mut Window, cx: &mut Context, ) { + let workspace_handle = cx.weak_entity(); pane.update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let multibuffer_hint = cx.new(|_| MultibufferHint::new()); @@ -1230,11 +1242,12 @@ fn initialize_pane( toolbar.add_item(telemetry_log_item, window, cx); let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new()); toolbar.add_item(syntax_tree_item, window, cx); + let migration_banner = + cx.new(|inner_cx| MigrationBanner::new(workspace_handle.clone(), inner_cx)); + toolbar.add_item(migration_banner, window, cx); let highlights_tree_item = cx.new(|_| language_tools::HighlightsTreeToolbarItemView::new()); toolbar.add_item(highlights_tree_item, window, cx); - let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx)); - toolbar.add_item(migration_banner, window, cx); let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx)); toolbar.add_item(project_diff_toolbar, window, cx); let branch_diff_toolbar = cx.new(BranchDiffToolbar::new); @@ -1309,10 +1322,10 @@ fn quit(_: &Quit, cx: &mut App) { let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; cx.spawn(async move |cx| { - let mut workspace_windows: Vec> = cx.update(|cx| { + let mut workspace_windows: Vec> = cx.update(|cx| { cx.windows() .into_iter() - .filter_map(|window| window.downcast::()) + .filter_map(|window| window.downcast::()) .collect::>() }); @@ -1322,8 +1335,8 @@ fn quit(_: &Quit, cx: &mut App) { workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false)); }); - if should_confirm && let Some(workspace) = workspace_windows.first() { - let answer = workspace + if should_confirm && let Some(multi_workspace) = workspace_windows.first() { + let answer = multi_workspace .update(cx, |_, window, cx| { window.prompt( PromptLevel::Info, @@ -1347,14 +1360,30 @@ fn quit(_: &Quit, cx: &mut App) { // If the user cancels any save prompt, then keep the app open. for window in workspace_windows { - if let Some(should_close) = window - .update(cx, |workspace, window, cx| { - workspace.prepare_to_close(CloseIntent::Quit, window, cx) + let workspaces = window + .update(cx, |multi_workspace, _, _| { + multi_workspace.workspaces().to_vec() }) - .log_err() - { - if !should_close.await? { - return Ok(()); + .log_err(); + + let Some(workspaces) = workspaces else { + continue; + }; + + for workspace in workspaces { + if let Some(should_close) = window + .update(cx, |multi_workspace, window, cx| { + multi_workspace.activate(workspace.clone(), cx); + window.activate_window(); + workspace.update(cx, |workspace, cx| { + workspace.prepare_to_close(CloseIntent::Quit, window, cx) + }) + }) + .log_err() + { + if !should_close.await? { + return Ok(()); + } } } } @@ -2306,6 +2335,7 @@ mod tests { use settings::{SaturatingBool, SettingsStore, watch_config_file}; use std::{ path::{Path, PathBuf}, + sync::Arc, time::Duration, }; use theme::ThemeRegistry; @@ -2313,6 +2343,7 @@ mod tests { path, rel_path::{RelPath, rel_path}, }; + use workspace::MultiWorkspace; use workspace::{ NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection, WorkspaceHandle, @@ -2348,10 +2379,12 @@ mod tests { .unwrap(); assert_eq!(cx.read(|cx| cx.windows().len()), 1); - let workspace = cx.windows()[0].downcast::().unwrap(); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()) + let multi_workspace = cx.windows()[0].downcast::().unwrap(); + multi_workspace + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + assert!(workspace.active_item_as::(cx).is_some()) + }); }) .unwrap(); } @@ -2359,6 +2392,10 @@ mod tests { #[gpui::test] async fn test_open_paths_action(cx: &mut TestAppContext) { let app_state = init_test(cx); + cx.update(|cx| { + use feature_flags::FeatureFlagAppExt as _; + cx.update_flags(false, vec!["agent-v2".to_string()]); + }); app_state .fs .as_fake() @@ -2412,21 +2449,23 @@ mod tests { .await .unwrap(); assert_eq!(cx.read(|cx| cx.windows().len()), 1); - let workspace_1 = cx - .read(|cx| cx.windows()[0].downcast::()) + let multi_workspace_1 = cx + .read(|cx| cx.windows()[0].downcast::()) .unwrap(); cx.run_until_parked(); - workspace_1 - .update(cx, |workspace, window, cx| { - assert_eq!(workspace.worktrees(cx).count(), 2); - assert!(workspace.left_dock().read(cx).is_open()); - assert!( - workspace - .active_pane() - .read(cx) - .focus_handle(cx) - .is_focused(window) - ); + multi_workspace_1 + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + assert_eq!(workspace.worktrees(cx).count(), 2); + assert!(workspace.left_dock().read(cx).is_open()); + assert!( + workspace + .active_pane() + .read(cx) + .focus_handle(cx) + .is_focused(window) + ); + }); }) .unwrap(); @@ -2444,7 +2483,7 @@ mod tests { // Replace existing windows let window = cx - .update(|cx| cx.windows()[0].downcast::()) + .update(|cx| cx.windows()[0].downcast::()) .unwrap(); cx.update(|cx| { open_paths( @@ -2461,11 +2500,12 @@ mod tests { .unwrap(); cx.background_executor.run_until_parked(); assert_eq!(cx.read(|cx| cx.windows().len()), 2); - let workspace_1 = cx - .update(|cx| cx.windows()[0].downcast::()) + let multi_workspace_1 = cx + .update(|cx| cx.windows()[0].downcast::()) .unwrap(); - workspace_1 - .update(cx, |workspace, window, cx| { + multi_workspace_1 + .update(cx, |multi_workspace, window, cx| { + let workspace = multi_workspace.workspace().read(cx); assert_eq!( workspace .worktrees(cx) @@ -2637,17 +2677,21 @@ mod tests { assert_eq!(cx.update(|cx| cx.windows().len()), 1); // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { - cx.update(|cx| window.read(cx).unwrap().is_edited()) + let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { + cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).is_edited()) }; let pane = window - .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .read_with(cx, |multi_workspace, cx| { + multi_workspace.workspace().read(cx).active_pane().clone() + }) .unwrap(); let editor = window - .read_with(cx, |workspace, cx| { - workspace + .read_with(cx, |multi_workspace, cx| { + multi_workspace + .workspace() + .read(cx) .active_item(cx) .unwrap() .downcast::() @@ -2720,22 +2764,26 @@ mod tests { executor.run_until_parked(); window - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + let editor = workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap(); - editor.update(cx, |editor, cx| { - assert_eq!(editor.text(cx), "hey"); + editor.update(cx, |editor, cx| { + assert_eq!(editor.text(cx), "hey"); + }); }); }) .unwrap(); let editor = window - .read_with(cx, |workspace, cx| { - workspace + .read_with(cx, |multi_workspace, cx| { + multi_workspace + .workspace() + .read(cx) .active_item(cx) .unwrap() .downcast::() @@ -2788,15 +2836,17 @@ mod tests { assert_eq!(cx.update(|cx| cx.windows().len()), 1); // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { - cx.update(|cx| window.read(cx).unwrap().is_edited()) + let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { + cx.update(|cx| window.read(cx).unwrap().workspace().read(cx).is_edited()) }; let editor = window - .read_with(cx, |workspace, cx| { - workspace + .read_with(cx, |multi_workspace, cx| { + multi_workspace + .workspace() + .read(cx) .active_item(cx) .unwrap() .downcast::() @@ -2843,22 +2893,27 @@ mod tests { cx.run_until_parked(); // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); + let window = cx.update(|cx| { + cx.active_window() + .unwrap() + .downcast::() + .unwrap() + }); assert!(window_is_edited(window, cx)); window - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); - editor.update(cx, |editor, cx| { - assert_eq!(editor.text(cx), "EDIThey"); - assert!(editor.is_dirty(cx)); + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + let editor = workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap(); + editor.update(cx, |editor, cx| { + assert_eq!(editor.text(cx), "EDIThey"); + assert!(editor.is_dirty(cx)); + }); }); - - editor }) .unwrap(); } @@ -2880,36 +2935,40 @@ mod tests { .unwrap(); cx.run_until_parked(); - let workspace = cx - .update(|cx| cx.windows().first().unwrap().downcast::()) + let multi_workspace = cx + .update(|cx| cx.windows().first().unwrap().downcast::()) .unwrap(); - let editor = workspace - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); - editor.update(cx, |editor, cx| { - assert!(editor.text(cx).is_empty()); - assert!(!editor.is_dirty(cx)); - }); + let editor = multi_workspace + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + let editor = workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap(); + editor.update(cx, |editor, cx| { + assert!(editor.text(cx).is_empty()); + assert!(!editor.is_dirty(cx)); + }); - editor + editor + }) }) .unwrap(); - let save_task = workspace - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) + let save_task = multi_workspace + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, window, cx) + }) }) .unwrap(); app_state.fs.create_dir(Path::new("/root")).await.unwrap(); cx.background_executor.run_until_parked(); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); save_task.await.unwrap(); - workspace + multi_workspace .update(cx, |_, _, cx| { editor.update(cx, |editor, cx| { assert!(!editor.is_dirty(cx)); @@ -2939,8 +2998,10 @@ mod tests { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -2949,8 +3010,10 @@ mod tests { // Open the first entry let entry_1 = window - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |w, cx| { + w.open_path(file1.clone(), None, true, window, cx) + }) }) .unwrap() .await @@ -2966,8 +3029,10 @@ mod tests { // Open the second entry window - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |w, cx| { + w.open_path(file2.clone(), None, true, window, cx) + }) }) .unwrap() .await @@ -2983,8 +3048,10 @@ mod tests { // Open the first entry again. The existing pane item is activated. let entry_1b = window - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |w, cx| { + w.open_path(file1.clone(), None, true, window, cx) + }) }) .unwrap() .await @@ -3002,40 +3069,46 @@ mod tests { // Split the pane with the first entry, then open the second entry again. window - .update(cx, |w, window, cx| { - w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |w, cx| { + w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx) + }) }) .unwrap() .await .unwrap(); window - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |w, cx| { + w.open_path(file2.clone(), None, true, window, cx) + }) }) .unwrap() .await .unwrap(); - window - .read_with(cx, |w, cx| { - assert_eq!( - w.active_pane() - .read(cx) - .active_item() - .unwrap() - .project_path(cx), - Some(file2.clone()) - ); - }) - .unwrap(); + cx.read(|cx| { + assert_eq!( + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .unwrap() + .project_path(cx), + Some(file2.clone()) + ); + }); // Open the third entry twice concurrently. Only one pane item is added. let (t1, t2) = window - .update(cx, |w, window, cx| { - ( - w.open_path(file3.clone(), None, true, window, cx), - w.open_path(file3.clone(), None, true, window, cx), - ) + .update(cx, |_, window, cx| { + workspace.update(cx, |w, cx| { + ( + w.open_path(file3.clone(), None, true, window, cx), + w.open_path(file3.clone(), None, true, window, cx), + ) + }) }) .unwrap(); t1.await.unwrap(); @@ -3090,8 +3163,10 @@ mod tests { .unwrap(); cx.run_until_parked(); assert_eq!(cx.update(|cx| cx.windows().len()), 1); - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - let workspace = window.root(cx).unwrap(); + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); #[track_caller] fn assert_project_panel_selection( @@ -3126,17 +3201,19 @@ mod tests { // Open a file within an existing worktree. window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir1/a.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + workspace.open_paths( + vec![path!("/dir1/a.txt").into()], + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + None, + window, + cx, + ) + }) }) .unwrap() .await; @@ -3165,17 +3242,19 @@ mod tests { // Open a file outside of any existing worktree. window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir2/b.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + workspace.open_paths( + vec![path!("/dir2/b.txt").into()], + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + None, + window, + cx, + ) + }) }) .unwrap() .await; @@ -3215,17 +3294,19 @@ mod tests { // Ensure opening a directory and one of its children only adds one worktree. window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir3").into(), path!("/dir3/c.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + workspace.open_paths( + vec![path!("/dir3").into(), path!("/dir3/c.txt").into()], + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + None, + window, + cx, + ) + }) }) .unwrap() .await; @@ -3265,17 +3346,19 @@ mod tests { // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/d.txt").into()], - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - None, - window, - cx, - ) + .update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + workspace.open_paths( + vec![path!("/d.txt").into()], + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + None, + window, + cx, + ) + }) }) .unwrap() .await; @@ -3369,8 +3452,13 @@ mod tests { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window({ + let project = project.clone(); + |window, cx| MultiWorkspace::test_new(project, window, cx) + }); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); let paths_to_open = [ @@ -3391,7 +3479,9 @@ mod tests { .unwrap(); assert_eq!( - opened_workspace.root(cx).unwrap().entity_id(), + opened_workspace + .read_with(cx, |mw, _| mw.workspace().entity_id()) + .unwrap(), workspace.entity_id(), "Excluded files in subfolders of a workspace root should be opened in the workspace" ); @@ -3461,22 +3551,26 @@ mod tests { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); // Open a file within an existing worktree. window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![PathBuf::from(path!("/root/a.txt"))], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.open_paths( + vec![PathBuf::from(path!("/root/a.txt"))], + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + None, + window, + cx, + ) + }) }) .unwrap() .await; @@ -3503,8 +3597,10 @@ mod tests { cx.read(|cx| assert!(editor.has_conflict(cx))); let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, window, cx) + }) }) .unwrap(); cx.background_executor.run_until_parked(); @@ -3534,20 +3630,22 @@ mod tests { project.languages().add(markdown_lang()); project.languages().add(rust_lang()); }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap()); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); // Create a new untitled buffer cx.dispatch_action(window.into(), NewFile); - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); + let editor = cx.read(|cx| { + workspace + .read(cx) + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); window .update(cx, |_, window, cx| { @@ -3570,8 +3668,10 @@ mod tests { // Save the buffer. This prompts for a filename. let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, window, cx) + }) }) .unwrap(); cx.background_executor.run_until_parked(); @@ -3616,8 +3716,10 @@ mod tests { .unwrap(); let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, window, cx) + }) }) .unwrap(); save_task.await.unwrap(); @@ -3636,39 +3738,42 @@ mod tests { // the same buffer. cx.dispatch_action(window.into(), NewFile); window - .update(cx, |workspace, window, cx| { - workspace.split_and_clone( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ) + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.split_and_clone( + workspace.active_pane().clone(), + SplitDirection::Right, + window, + cx, + ) + }) }) .unwrap() .await .unwrap(); window - .update(cx, |workspace, window, cx| { - workspace.open_path( - (worktree.read(cx).id(), rel_path("the-new-name.rs")), - None, - true, - window, - cx, - ) + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.open_path( + (worktree.read(cx).id(), rel_path("the-new-name.rs")), + None, + true, + window, + cx, + ) + }) }) .unwrap() .await .unwrap(); - let editor2 = window - .update(cx, |workspace, _, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); + let editor2 = cx.read(|cx| { + workspace + .read(cx) + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); cx.read(|cx| { assert_eq!( editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), @@ -3687,19 +3792,21 @@ mod tests { project.languages().add(language::rust_lang()); project.languages().add(language::markdown_lang()); }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); // Create a new untitled buffer cx.dispatch_action(window.into(), NewFile); - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); + let editor = cx.read(|cx| { + workspace + .read(cx) + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); window .update(cx, |_, window, cx| { editor.update(cx, |editor, cx| { @@ -3719,8 +3826,10 @@ mod tests { // Save the buffer. This prompts for a filename. let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, window, cx) + }) }) .unwrap(); cx.background_executor.run_until_parked(); @@ -3765,38 +3874,38 @@ mod tests { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); - window - .update(cx, |w, window, cx| { + workspace + .update_in(cx, |w, window, cx| { w.open_path(file1.clone(), None, true, window, cx) }) - .unwrap() .await .unwrap(); - let (editor_1, buffer) = window - .update(cx, |_, window, cx| { - pane_1.update(cx, |pane_1, cx| { - let editor = pane_1.active_item().unwrap().downcast::().unwrap(); - assert_eq!(editor.project_path(cx), Some(file1.clone())); - let buffer = editor.update(cx, |editor, cx| { - editor.insert("dirt", window, cx); - editor.buffer().downgrade() - }); - (editor.downgrade(), buffer) - }) + let (editor_1, buffer) = workspace.update_in(cx, |_, window, cx| { + pane_1.update(cx, |pane_1, cx| { + let editor = pane_1.active_item().unwrap().downcast::().unwrap(); + assert_eq!(editor.project_path(cx), Some(file1.clone())); + let buffer = editor.update(cx, |editor, cx| { + editor.insert("dirt", window, cx); + editor.buffer().downgrade() + }); + (editor.downgrade(), buffer) }) - .unwrap(); + }); - cx.dispatch_action(window.into(), pane::SplitRight::default()); - let editor_2 = cx.update(|cx| { + cx.dispatch_action(pane::SplitRight::default()); + let editor_2 = cx.update(|_, cx| { let pane_2 = workspace.read(cx).active_pane().clone(); assert_ne!(pane_1, pane_2); @@ -3805,43 +3914,33 @@ mod tests { pane2_item.downcast::().unwrap().downgrade() }); - cx.dispatch_action( - window.into(), - workspace::CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - ); + cx.dispatch_action(workspace::CloseActiveItem { + save_intent: None, + close_pinned: false, + }); cx.background_executor.run_until_parked(); - window - .read_with(cx, |workspace, _| { - assert_eq!(workspace.panes().len(), 1); - assert_eq!(workspace.active_pane(), &pane_1); - }) - .unwrap(); + workspace.read_with(cx, |workspace, _| { + assert_eq!(workspace.panes().len(), 1); + assert_eq!(workspace.active_pane(), &pane_1); + }); - cx.dispatch_action( - window.into(), - workspace::CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - ); + cx.dispatch_action(workspace::CloseActiveItem { + save_intent: None, + close_pinned: false, + }); cx.background_executor.run_until_parked(); cx.simulate_prompt_answer("Don't Save"); cx.background_executor.run_until_parked(); - window - .update(cx, |workspace, _, cx| { - assert_eq!(workspace.panes().len(), 1); - assert!(workspace.active_item(cx).is_none()); - }) - .unwrap(); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.panes().len(), 1); + assert!(workspace.active_item(cx).is_none()); + }); cx.background_executor .advance_clock(SERIALIZATION_THROTTLE_TIME); - cx.update(|_| {}); + cx.update(|_, _| {}); editor_1.assert_released(); editor_2.assert_released(); buffer.assert_released(); @@ -3867,58 +3966,56 @@ mod tests { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace - .read_with(cx, |workspace, _| workspace.active_pane().clone()) + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); + let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); let file2 = entries[1].clone(); let file3 = entries[2].clone(); let editor1 = workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.open_path(file1.clone(), None, true, window, cx) }) - .unwrap() .await .unwrap() .downcast::() .unwrap(); - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0) - ..DisplayPoint::new(DisplayRow(10), 0)]) - }); + workspace.update_in(cx, |_, window, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(Default::default(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(10), 0) + ]) }); - }) - .unwrap(); + }); + }); let editor2 = workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.open_path(file2.clone(), None, true, window, cx) }) - .unwrap() .await .unwrap() .downcast::() .unwrap(); let editor3 = workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.open_path(file3.clone(), None, true, window, cx) }) - .unwrap() .await .unwrap() .downcast::() .unwrap(); workspace - .update(cx, |_, window, cx| { + .update_in(cx, |_, window, cx| { editor3.update(cx, |editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0) @@ -3939,26 +4036,22 @@ mod tests { ) }) }) - .unwrap() .await .unwrap(); - workspace - .update(cx, |_, window, cx| { - editor3.update(cx, |editor, cx| { - editor.set_scroll_position(point(0., 12.5), window, cx) - }); - }) - .unwrap(); + workspace.update_in(cx, |_, window, cx| { + editor3.update(cx, |editor, cx| { + editor.set_scroll_position(point(0., 12.5), window, cx) + }); + }); assert_eq!( active_location(&workspace, cx), (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5) ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -3967,10 +4060,9 @@ mod tests { ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -3979,10 +4071,9 @@ mod tests { ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -3991,10 +4082,9 @@ mod tests { ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4004,10 +4094,9 @@ mod tests { // Go back one more time and ensure we don't navigate past the first item in the history. workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4016,10 +4105,9 @@ mod tests { ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_forward(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4028,10 +4116,9 @@ mod tests { ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_forward(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4042,21 +4129,19 @@ mod tests { // Go forward to an item that has been closed, ensuring it gets re-opened at the same // location. workspace - .update(cx, |_, window, cx| { + .update_in(cx, |_, window, cx| { pane.update(cx, |pane, cx| { let editor3_id = editor3.entity_id(); drop(editor3); pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx) }) }) - .unwrap() .await .unwrap(); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_forward(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4065,10 +4150,9 @@ mod tests { ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_forward(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4077,10 +4161,9 @@ mod tests { ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4090,14 +4173,13 @@ mod tests { // Go back to an item that has been closed and removed from disk workspace - .update(cx, |_, window, cx| { + .update_in(cx, |_, window, cx| { pane.update(cx, |pane, cx| { let editor2_id = editor2.entity_id(); drop(editor2); pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx) }) }) - .unwrap() .await .unwrap(); app_state @@ -4108,10 +4190,9 @@ mod tests { cx.background_executor.run_until_parked(); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4119,10 +4200,9 @@ mod tests { (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_forward(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4132,68 +4212,59 @@ mod tests { // Modify file to collapse multiple nav history entries into the same location. // Ensure we don't visit the same location twice when navigating. - workspace - .update(cx, |_, window, cx| { + workspace.update_in(cx, |_, window, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0) + ]) + }) + }); + }); + for _ in 0..5 { + workspace.update_in(cx, |_, window, cx| { editor1.update(cx, |editor, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0) - ..DisplayPoint::new(DisplayRow(15), 0)]) - }) - }); - }) - .unwrap(); - for _ in 0..5 { - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0) - ..DisplayPoint::new(DisplayRow(3), 0)]) - }); - }); - }) - .unwrap(); - - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0) - ..DisplayPoint::new(DisplayRow(13), 0)]) - }) + s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0) + ..DisplayPoint::new(DisplayRow(3), 0)]) }); - }) - .unwrap(); - } - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0) - ..DisplayPoint::new(DisplayRow(14), 0)]) - }); - editor.insert("", window, cx); - }) }); - }) - .unwrap(); + }); - workspace - .update(cx, |_, window, cx| { + workspace.update_in(cx, |_, window, cx| { editor1.update(cx, |editor, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0) - ..DisplayPoint::new(DisplayRow(1), 0)]) - }) + s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0) + ..DisplayPoint::new(DisplayRow(13), 0)]) + }); }); - }) - .unwrap(); + }); + } + workspace.update_in(cx, |_, window, cx| { + editor1.update(cx, |editor, cx| { + editor.transact(window, cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0) + ..DisplayPoint::new(DisplayRow(14), 0)]) + }); + editor.insert("", window, cx); + }) + }); + }); + + workspace.update_in(cx, |_, window, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0) + ]) + }) + }); + }); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4201,10 +4272,9 @@ mod tests { (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.) ); workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.go_back(w.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!( @@ -4213,28 +4283,26 @@ mod tests { ); fn active_location( - workspace: &WindowHandle, - cx: &mut TestAppContext, + workspace: &Entity, + cx: &mut VisualTestContext, ) -> (ProjectPath, DisplayPoint, f64) { - workspace - .update(cx, |workspace, _, cx| { - let item = workspace.active_item(cx).unwrap(); - let editor = item.downcast::().unwrap(); - let (selections, scroll_position) = editor.update(cx, |editor, cx| { - ( - editor - .selections - .display_ranges(&editor.display_snapshot(cx)), - editor.scroll_position(cx), - ) - }); + workspace.update(cx, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + let editor = item.downcast::().unwrap(); + + editor.update(cx, |editor_ref, cx| { + let selections = editor_ref + .selections + .display_ranges(&editor_ref.display_snapshot(cx)); + let scroll_position = editor_ref.scroll_position(cx); + ( - item.project_path(cx).unwrap(), + editor_ref.project_path(cx).unwrap(), selections[0].start, scroll_position.y, ) }) - .unwrap() + }) } } @@ -4259,46 +4327,44 @@ mod tests { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace - .read_with(cx, |workspace, _| workspace.active_pane().clone()) + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) .unwrap(); + let cx = &mut VisualTestContext::from_window(*window, cx); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); + let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); let file2 = entries[1].clone(); let file3 = entries[2].clone(); let file4 = entries[3].clone(); let file1_item_id = workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.open_path(file1.clone(), None, true, window, cx) }) - .unwrap() .await .unwrap() .item_id(); let file2_item_id = workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.open_path(file2.clone(), None, true, window, cx) }) - .unwrap() .await .unwrap() .item_id(); let file3_item_id = workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.open_path(file3.clone(), None, true, window, cx) }) - .unwrap() .await .unwrap() .item_id(); let file4_item_id = workspace - .update(cx, |w, window, cx| { + .update_in(cx, |w, window, cx| { w.open_path(file4.clone(), None, true, window, cx) }) - .unwrap() .await .unwrap() .item_id(); @@ -4306,44 +4372,40 @@ mod tests { // Close all the pane items in some arbitrary order. workspace - .update(cx, |_, window, cx| { + .update_in(cx, |_, window, cx| { pane.update(cx, |pane, cx| { pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx) }) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file4.clone())); workspace - .update(cx, |_, window, cx| { + .update_in(cx, |_, window, cx| { pane.update(cx, |pane, cx| { pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx) }) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); workspace - .update(cx, |_, window, cx| { + .update_in(cx, |_, window, cx| { pane.update(cx, |pane, cx| { pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx) }) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); workspace - .update(cx, |_, window, cx| { + .update_in(cx, |_, window, cx| { pane.update(cx, |pane, cx| { pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx) }) }) - .unwrap() .await .unwrap(); @@ -4352,124 +4414,109 @@ mod tests { // Reopen all the closed items, ensuring they are reopened in the same order // in which they were closed. workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() + .update_in(cx, Workspace::reopen_closed_item) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() + .update_in(cx, Workspace::reopen_closed_item) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file2.clone())); workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() + .update_in(cx, Workspace::reopen_closed_item) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file4.clone())); workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() + .update_in(cx, Workspace::reopen_closed_item) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file1.clone())); // Reopening past the last closed item is a no-op. workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() + .update_in(cx, Workspace::reopen_closed_item) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file1.clone())); // Reopening closed items doesn't interfere with navigation history. workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file4.clone())); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file2.clone())); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file4.clone())); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file2.clone())); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file1.clone())); workspace - .update(cx, |workspace, window, cx| { + .update_in(cx, |workspace, window, cx| { workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) - .unwrap() .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file1.clone())); fn active_path( - workspace: &WindowHandle, - cx: &TestAppContext, + workspace: &Entity, + cx: &VisualTestContext, ) -> Option { - workspace - .read_with(cx, |workspace, cx| { - let item = workspace.active_item(cx)?; - item.project_path(cx) - }) - .unwrap() + workspace.read_with(cx, |workspace, cx| { + let item = workspace.active_item(cx)?; + item.project_path(cx) + }) } } @@ -4492,8 +4539,11 @@ mod tests { let executor = cx.executor(); let app_state = init_keymap_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); // From the Atom keymap use workspace::ActivatePreviousPane; @@ -4545,19 +4595,21 @@ mod tests { ); handle_keymap_file_changes(keymap_rx, keymap_watcher, cx); }); - workspace - .update(cx, |workspace, _, cx| { - workspace.register_action(|_, _: &ActionA, _window, _cx| {}); - workspace.register_action(|_, _: &ActionB, _window, _cx| {}); - workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {}); - workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {}); - cx.notify(); + window + .update(cx, |_, _, cx| { + workspace.update(cx, |workspace, cx| { + workspace.register_action(|_, _: &ActionA, _window, _cx| {}); + workspace.register_action(|_, _: &ActionB, _window, _cx| {}); + workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {}); + workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {}); + cx.notify(); + }); }) .unwrap(); executor.run_until_parked(); // Test loading the keymap base at all assert_key_bindings_for( - workspace.into(), + window.into(), cx, vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)], line!(), @@ -4577,7 +4629,7 @@ mod tests { executor.run_until_parked(); assert_key_bindings_for( - workspace.into(), + window.into(), cx, vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)], line!(), @@ -4597,7 +4649,7 @@ mod tests { executor.run_until_parked(); assert_key_bindings_for( - workspace.into(), + window.into(), cx, vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)], line!(), @@ -4609,19 +4661,25 @@ mod tests { let executor = cx.executor(); let app_state = init_keymap_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); // From the Atom keymap use workspace::ActivatePreviousPane; // From the JetBrains keymap use diagnostics::Deploy; - workspace - .update(cx, |workspace, _, _| { - workspace.register_action(|_, _: &ActionA, _window, _cx| {}); - workspace.register_action(|_, _: &ActionB, _window, _cx| {}); - workspace.register_action(|_, _: &Deploy, _window, _cx| {}); + window + .update(cx, |_, _, cx| { + workspace.update(cx, |workspace, cx| { + workspace.register_action(|_, _: &ActionA, _window, _cx| {}); + workspace.register_action(|_, _: &ActionB, _window, _cx| {}); + workspace.register_action(|_, _: &Deploy, _window, _cx| {}); + cx.notify(); + }); }) .unwrap(); app_state @@ -4675,7 +4733,7 @@ mod tests { cx.background_executor.run_until_parked(); // Test loading the keymap base at all assert_key_bindings_for( - workspace.into(), + window.into(), cx, vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)], line!(), @@ -4695,7 +4753,7 @@ mod tests { cx.background_executor.run_until_parked(); assert_key_bindings_for( - workspace.into(), + window.into(), cx, vec![("k", &ActivatePreviousPane)], line!(), @@ -4714,7 +4772,7 @@ mod tests { cx.background_executor.run_until_parked(); - assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!()); + assert_key_bindings_for(window.into(), cx, vec![("6", &Deploy)], line!()); } #[gpui::test] @@ -4814,6 +4872,7 @@ mod tests { "lsp_tool", "markdown", "menu", + "multi_workspace", "new_process_modal", "notebook", "notification_panel", @@ -4901,7 +4960,7 @@ mod tests { cx.update(init); let project = Project::test(app_state.fs.clone(), [], cx).await; - let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); cx.update(|cx| { cx.dispatch_action(&OpenDefaultSettings); @@ -4910,10 +4969,12 @@ mod tests { assert_eq!(cx.read(|cx| cx.windows().len()), 1); - let workspace = cx.windows()[0].downcast::().unwrap(); - let active_editor = workspace - .update(cx, |workspace, _, cx| { - workspace.active_item_as::(cx) + let multi_workspace = cx.windows()[0].downcast::().unwrap(); + let active_editor = multi_workspace + .update(cx, |multi_workspace, _, cx| { + multi_workspace + .workspace() + .update(cx, |workspace, cx| workspace.active_item_as::(cx)) }) .unwrap(); assert!( @@ -5144,12 +5205,18 @@ mod tests { ); // 6. Create workspace and trigger the actual function that causes the bug - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let window = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); window - .update(cx, |workspace, window, cx| { - // Call the exact function that contains the bug - eprintln!("About to call open_project_settings_file"); - open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx); + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + // Call the exact function that contains the bug + eprintln!("About to call open_project_settings_file"); + open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx); + }); }) .unwrap(); @@ -5183,7 +5250,7 @@ mod tests { let app_state = init_test(cx); cx.update(init); let project = Project::test(app_state.fs.clone(), [], cx).await; - let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); cx.run_until_parked(); @@ -5217,16 +5284,22 @@ mod tests { .await; let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_a = - cx.add_window(|window, cx| Workspace::test_new(project_a.clone(), window, cx)); + let window_a = cx.add_window({ + let project = project_a.clone(); + |window, cx| MultiWorkspace::test_new(project, window, cx) + }); let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_b = - cx.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx)); + let window_b = cx.add_window({ + let project = project_b.clone(); + |window, cx| MultiWorkspace::test_new(project, window, cx) + }); let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_c = - cx.add_window(|window, cx| Workspace::test_new(project_c.clone(), window, cx)); + let window_c = cx.add_window({ + let project = project_c.clone(); + |window, cx| MultiWorkspace::test_new(project, window, cx) + }); for window in [window_a, window_b, window_c] { let _ = cx.update_window(*window, |_, window, _| { @@ -5247,8 +5320,8 @@ mod tests { cx.update_window(*window, |_, window, _| assert!(window.is_window_active())) .unwrap(); - let _ = window.read_with(cx, |workspace, cx| { - let pane = workspace.active_pane().read(cx); + let _ = window.read_with(cx, |multi_workspace, cx| { + let pane = multi_workspace.workspace().read(cx).active_pane().read(cx); let project_path = pane.active_item().unwrap().project_path(cx).unwrap(); assert_eq!( @@ -5258,4 +5331,709 @@ mod tests { }); } } + + #[gpui::test] + async fn test_open_paths_switches_to_best_workspace(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + use feature_flags::FeatureFlagAppExt as _; + cx.update_flags(false, vec!["agent-v2".to_string()]); + }); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/"), + json!({ + "dir1": { + "a.txt": "content a" + }, + "dir2": { + "b.txt": "content b" + }, + "dir3": { + "c.txt": "content c" + } + }), + ) + .await; + + // Create a window with workspace 0 containing /dir1 + let project1 = Project::test(app_state.fs.clone(), [path!("/dir1").as_ref()], cx).await; + + let window = cx.add_window({ + let project = project1.clone(); + |window, cx| MultiWorkspace::test_new(project, window, cx) + }); + + cx.run_until_parked(); + assert_eq!(cx.windows().len(), 1, "Should start with 1 window"); + + // Create workspace 2 with /dir2 + let project2 = Project::test(app_state.fs.clone(), [path!("/dir2").as_ref()], cx).await; + let workspace2 = window + .update(cx, |multi_workspace, window, cx| { + multi_workspace.test_add_workspace(project2.clone(), window, cx) + }) + .unwrap(); + + // Create workspace 3 with /dir3 + let project3 = Project::test(app_state.fs.clone(), [path!("/dir3").as_ref()], cx).await; + let workspace3 = window + .update(cx, |multi_workspace, window, cx| { + multi_workspace.test_add_workspace(project3.clone(), window, cx) + }) + .unwrap(); + + let workspace1 = window + .read_with(cx, |multi_workspace, _| { + multi_workspace.workspaces()[0].clone() + }) + .unwrap(); + + window + .update(cx, |multi_workspace, _, cx| { + multi_workspace.activate(workspace2.clone(), cx); + multi_workspace.activate(workspace3.clone(), cx); + // Switch back to workspace1 for test setup + multi_workspace.activate(workspace1, cx); + assert_eq!(multi_workspace.active_workspace_index(), 0); + }) + .unwrap(); + + cx.run_until_parked(); + + // Verify setup: 3 workspaces, workspace 0 active, still 1 window + window + .read_with(cx, |multi_workspace, _| { + assert_eq!(multi_workspace.workspaces().len(), 3); + assert_eq!(multi_workspace.active_workspace_index(), 0); + }) + .unwrap(); + assert_eq!(cx.windows().len(), 1); + + // Open a file in /dir3 - should switch to workspace 3 (not just "the other one") + cx.update(|cx| { + open_paths( + &[PathBuf::from(path!("/dir3/c.txt"))], + app_state.clone(), + OpenOptions::default(), + cx, + ) + }) + .await + .unwrap(); + + cx.run_until_parked(); + + // Verify workspace 2 is active and file opened there + window + .read_with(cx, |multi_workspace, cx| { + assert_eq!( + multi_workspace.active_workspace_index(), + 2, + "Should have switched to workspace 3 which contains /dir3" + ); + let active_item = multi_workspace + .workspace() + .read(cx) + .active_pane() + .read(cx) + .active_item() + .expect("Should have an active item"); + assert_eq!(active_item.tab_content_text(0, cx), "c.txt"); + }) + .unwrap(); + assert_eq!(cx.windows().len(), 1, "Should reuse existing window"); + + // Open a file in /dir2 - should switch to workspace 2 + cx.update(|cx| { + open_paths( + &[PathBuf::from(path!("/dir2/b.txt"))], + app_state.clone(), + OpenOptions::default(), + cx, + ) + }) + .await + .unwrap(); + + cx.run_until_parked(); + + // Verify workspace 1 is active and file opened there + window + .read_with(cx, |multi_workspace, cx| { + assert_eq!( + multi_workspace.active_workspace_index(), + 1, + "Should have switched to workspace 2 which contains /dir2" + ); + let active_item = multi_workspace + .workspace() + .read(cx) + .active_pane() + .read(cx) + .active_item() + .expect("Should have an active item"); + assert_eq!(active_item.tab_content_text(0, cx), "b.txt"); + }) + .unwrap(); + + // Verify c.txt is still in workspace 3 (file opened in correct workspace, not active one) + workspace3.read_with(cx, |workspace, cx| { + let active_item = workspace + .active_pane() + .read(cx) + .active_item() + .expect("Workspace 2 should have an active item"); + assert_eq!( + active_item.tab_content_text(0, cx), + "c.txt", + "c.txt should have been opened in workspace 3, not the active workspace" + ); + }); + + assert_eq!(cx.windows().len(), 1, "Should still have only 1 window"); + + // Open a file in /dir1 - should switch back to workspace 0 + cx.update(|cx| { + open_paths( + &[PathBuf::from(path!("/dir1/a.txt"))], + app_state.clone(), + OpenOptions::default(), + cx, + ) + }) + .await + .unwrap(); + + cx.run_until_parked(); + + // Verify workspace 0 is active and file opened there + window + .read_with(cx, |multi_workspace, cx| { + assert_eq!( + multi_workspace.active_workspace_index(), + 0, + "Should have switched back to workspace 0 which contains /dir1" + ); + let active_item = multi_workspace + .workspace() + .read(cx) + .active_pane() + .read(cx) + .active_item() + .expect("Should have an active item"); + assert_eq!(active_item.tab_content_text(0, cx), "a.txt"); + }) + .unwrap(); + assert_eq!(cx.windows().len(), 1, "Should still have only 1 window"); + } + + #[gpui::test] + async fn test_quit_checks_all_workspaces_for_dirty_items(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(init); + cx.update(|cx| { + use feature_flags::FeatureFlagAppExt as _; + cx.update_flags(false, vec!["agent-v2".to_string()]); + }); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/"), + json!({ + "dir1": { + "a.txt": "content a" + }, + "dir2": { + "b.txt": "content b" + }, + "dir3": { + "c.txt": "content c" + } + }), + ) + .await; + + // === Setup Window 1 with two workspaces === + let project1 = Project::test(app_state.fs.clone(), [path!("/dir1").as_ref()], cx).await; + let window1 = cx.add_window({ + let project = project1.clone(); + |window, cx| MultiWorkspace::test_new(project, window, cx) + }); + + cx.run_until_parked(); + + let project2 = Project::test(app_state.fs.clone(), [path!("/dir2").as_ref()], cx).await; + let workspace1_1 = window1 + .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone()) + .unwrap(); + let workspace1_2 = window1 + .update(cx, |multi_workspace, window, cx| { + multi_workspace.test_add_workspace(project2.clone(), window, cx) + }) + .unwrap(); + + window1 + .update(cx, |multi_workspace, _, cx| { + multi_workspace.activate(workspace1_2.clone(), cx); + multi_workspace.activate(workspace1_1.clone(), cx); + }) + .unwrap(); + + // === Setup Window 2 with one workspace === + let project3 = Project::test(app_state.fs.clone(), [path!("/dir3").as_ref()], cx).await; + let window2 = cx.add_window({ + let project = project3.clone(); + |window, cx| MultiWorkspace::test_new(project, window, cx) + }); + + cx.run_until_parked(); + assert_eq!(cx.windows().len(), 2); + + // === Case 1: Active workspace has dirty item, quit can be cancelled === + let worktree1_id = project1.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let editor1 = window1 + .update(cx, |_, window, cx| { + workspace1_1.update(cx, |workspace, cx| { + workspace.open_path((worktree1_id, rel_path("a.txt")), None, true, window, cx) + }) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + window1 + .update(cx, |_, window, cx| { + editor1.update(cx, |editor, cx| { + editor.insert("dirty in active workspace", window, cx); + }); + }) + .unwrap(); + + cx.run_until_parked(); + + // Verify workspace1_1 is active + window1 + .read_with(cx, |multi_workspace, _| { + assert_eq!(multi_workspace.active_workspace_index(), 0); + }) + .unwrap(); + + cx.dispatch_action(*window1, Quit); + cx.run_until_parked(); + + assert!( + cx.has_pending_prompt(), + "Case 1: Should prompt to save dirty item in active workspace" + ); + + cx.simulate_prompt_answer("Cancel"); + cx.run_until_parked(); + + assert_eq!( + cx.windows().len(), + 2, + "Case 1: Windows should still exist after cancelling quit" + ); + + // Clean up Case 1: Close the dirty item without saving + let close_task = window1 + .update(cx, |_, window, cx| { + workspace1_1.update(cx, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&Default::default(), window, cx) + }) + }) + }) + .unwrap(); + cx.run_until_parked(); + cx.simulate_prompt_answer("Don't Save"); + close_task.await.ok(); + cx.run_until_parked(); + + // === Case 2: Non-active workspace (same window) has dirty item === + let worktree2_id = project2.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let editor2 = window1 + .update(cx, |_, window, cx| { + workspace1_2.update(cx, |workspace, cx| { + workspace.open_path((worktree2_id, rel_path("b.txt")), None, true, window, cx) + }) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + window1 + .update(cx, |_, window, cx| { + editor2.update(cx, |editor, cx| { + editor.insert("dirty in non-active workspace", window, cx); + }); + }) + .unwrap(); + + cx.run_until_parked(); + + // Verify workspace1_1 is still active (not workspace1_2 with dirty item) + window1 + .read_with(cx, |multi_workspace, _| { + assert_eq!(multi_workspace.active_workspace_index(), 0); + }) + .unwrap(); + + cx.dispatch_action(*window1, Quit); + cx.run_until_parked(); + + // Verify the non-active workspace got activated to show the dirty item + window1 + .read_with(cx, |multi_workspace, _| { + assert_eq!( + multi_workspace.active_workspace_index(), + 1, + "Case 2: Non-active workspace should be activated when it has dirty item" + ); + }) + .unwrap(); + + assert!( + cx.has_pending_prompt(), + "Case 2: Should prompt to save dirty item in non-active workspace" + ); + + cx.simulate_prompt_answer("Cancel"); + cx.run_until_parked(); + + assert_eq!( + cx.windows().len(), + 2, + "Case 2: Windows should still exist after cancelling quit" + ); + + // Clean up Case 2: Close the dirty item without saving + let close_task = window1 + .update(cx, |_, window, cx| { + workspace1_2.update(cx, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&Default::default(), window, cx) + }) + }) + }) + .unwrap(); + cx.run_until_parked(); + cx.simulate_prompt_answer("Don't Save"); + close_task.await.ok(); + cx.run_until_parked(); + + // === Case 3: Non-active window has dirty item === + let workspace3 = window2 + .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone()) + .unwrap(); + + let worktree3_id = project3.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let editor3 = window2 + .update(cx, |_, window, cx| { + workspace3.update(cx, |workspace, cx| { + workspace.open_path((worktree3_id, rel_path("c.txt")), None, true, window, cx) + }) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + window2 + .update(cx, |_, window, cx| { + editor3.update(cx, |editor, cx| { + editor.insert("dirty in other window", window, cx); + }); + }) + .unwrap(); + + cx.run_until_parked(); + + // Activate window1 explicitly (editing in window2 may have activated it) + window1 + .update(cx, |_, window, _| window.activate_window()) + .unwrap(); + cx.run_until_parked(); + + // Verify window2 is not active (window1 should still be active) + assert_eq!( + cx.update(|cx| window2.is_active(cx)), + Some(false), + "Case 3: window2 should not be active before quit" + ); + + // Dispatch quit from window1 (window2 has the dirty item) + cx.dispatch_action(*window1, Quit); + cx.run_until_parked(); + + // Verify window2 is now active (quit handler activated it to show dirty item) + assert_eq!( + cx.update(|cx| window2.is_active(cx)), + Some(true), + "Case 3: window2 should be activated when it has dirty item" + ); + + assert!( + cx.has_pending_prompt(), + "Case 3: Should prompt to save dirty item in non-active window" + ); + + cx.simulate_prompt_answer("Cancel"); + cx.run_until_parked(); + + assert_eq!( + cx.windows().len(), + 2, + "Case 3: Windows should still exist after cancelling quit" + ); + } + + #[gpui::test] + async fn test_multi_workspace_session_restore(cx: &mut TestAppContext) { + use collections::HashMap; + use session::Session; + use workspace::{Workspace, WorkspaceId}; + + let app_state = init_test(cx); + + cx.update(|cx| { + use feature_flags::FeatureFlagAppExt as _; + cx.update_flags(false, vec!["agent-v2".to_string()]); + }); + + let dir1 = path!("/dir1"); + let dir2 = path!("/dir2"); + let dir3 = path!("/dir3"); + + let fs = app_state.fs.clone(); + let fake_fs = fs.as_fake(); + fake_fs.insert_tree(dir1, json!({})).await; + fake_fs.insert_tree(dir2, json!({})).await; + fake_fs.insert_tree(dir3, json!({})).await; + + let session_id = cx.read(|cx| app_state.session.read(cx).id().to_owned()); + + // --- Create 3 workspaces in 2 windows --- + // + // Window A: workspace for dir1, workspace for dir2 + // Window B: workspace for dir3 + let (window_a, _) = cx + .update(|cx| { + Workspace::new_local(vec![dir1.into()], app_state.clone(), None, None, None, cx) + }) + .await + .expect("failed to open first workspace"); + + window_a + .update(cx, |multi_workspace, window, cx| { + multi_workspace.open_project(vec![dir2.into()], window, cx) + }) + .unwrap() + .await + .expect("failed to open second workspace into window A"); + cx.run_until_parked(); + + let (window_b, _) = cx + .update(|cx| { + Workspace::new_local(vec![dir3.into()], app_state.clone(), None, None, None, cx) + }) + .await + .expect("failed to open third workspace"); + + // Currently dir2 is active because it was added last. + // So, switch window_a's active workspace to dir1 (index 0). + // This sets up a non-trivial assertion: after restore, dir1 should + // still be active rather than whichever workspace happened to restore last. + window_a + .update(cx, |multi_workspace, window, cx| { + multi_workspace.activate_index(0, window, cx); + }) + .unwrap(); + + // --- Flush serialization --- + cx.executor().advance_clock(SERIALIZATION_THROTTLE_TIME); + cx.run_until_parked(); + + // Verify all workspaces retained their session_ids. + let locations = workspace::last_session_workspace_locations(&session_id, None, fs.as_ref()) + .await + .expect("expected session workspace locations"); + assert_eq!( + locations.len(), + 3, + "all 3 workspaces should have session_ids in the DB" + ); + + // Close the original windows. + window_a + .update(cx, |_, window, _| window.remove_window()) + .unwrap(); + window_b + .update(cx, |_, window, _| window.remove_window()) + .unwrap(); + cx.run_until_parked(); + + // Simulate a new session launch: replace the session so that + // `last_session_id()` returns the ID used during workspace creation. + // `restore_on_startup` defaults to `LastSession`, which is what we need. + cx.update(|cx| { + app_state.session.update(cx, |app_session, _cx| { + app_session + .replace_session_for_test(Session::test_with_old_session(session_id.clone())); + }); + }); + + // --- Read back from DB and verify grouping --- + let locations = workspace::last_session_workspace_locations(&session_id, None, fs.as_ref()) + .await + .expect("expected session workspace locations"); + + assert_eq!(locations.len(), 3, "expected 3 session workspaces"); + + let mut groups_by_window: HashMap> = HashMap::default(); + for session_workspace in &locations { + if let Some(window_id) = session_workspace.window_id { + groups_by_window + .entry(window_id) + .or_default() + .push(session_workspace.workspace_id); + } + } + assert_eq!( + groups_by_window.len(), + 2, + "expected 2 window groups, got {groups_by_window:?}" + ); + assert!( + groups_by_window.values().any(|g| g.len() == 2), + "expected one group with 2 workspaces" + ); + assert!( + groups_by_window.values().any(|g| g.len() == 1), + "expected one group with 1 workspace" + ); + + let mut async_cx = cx.to_async(); + crate::restore_or_create_workspace(app_state.clone(), &mut async_cx) + .await + .expect("failed to restore workspaces"); + cx.run_until_parked(); + + // --- Verify the restored windows --- + let restored_windows: Vec> = cx.read(|cx| { + cx.windows() + .into_iter() + .filter_map(|window| window.downcast::()) + .collect() + }); + + assert_eq!( + restored_windows.len(), + 2, + "expected 2 restored windows, got {}", + restored_windows.len() + ); + + let workspace_counts: Vec = restored_windows + .iter() + .map(|window| { + window + .read_with(cx, |multi_workspace, _| multi_workspace.workspaces().len()) + .unwrap() + }) + .collect(); + let mut sorted_counts = workspace_counts.clone(); + sorted_counts.sort(); + assert_eq!( + sorted_counts, + vec![1, 2], + "expected one window with 1 workspace and one with 2, got {workspace_counts:?}" + ); + + let dir1_path: Arc = Path::new(dir1).into(); + let dir2_path: Arc = Path::new(dir2).into(); + let dir3_path: Arc = Path::new(dir3).into(); + + let all_restored_paths: Vec>>> = restored_windows + .iter() + .map(|window| { + window + .read_with(cx, |multi_workspace, cx| { + multi_workspace + .workspaces() + .iter() + .map(|ws| ws.read(cx).root_paths(cx)) + .collect() + }) + .unwrap() + }) + .collect(); + + let two_ws_window = all_restored_paths + .iter() + .find(|paths| paths.len() == 2) + .expect("expected a window with 2 workspaces"); + assert!( + two_ws_window.iter().any(|p| p.contains(&dir1_path)), + "2-workspace window should contain dir1, got {two_ws_window:?}" + ); + assert!( + two_ws_window.iter().any(|p| p.contains(&dir2_path)), + "2-workspace window should contain dir2, got {two_ws_window:?}" + ); + + let one_ws_window = all_restored_paths + .iter() + .find(|paths| paths.len() == 1) + .expect("expected a window with 1 workspace"); + assert!( + one_ws_window[0].contains(&dir3_path), + "1-workspace window should contain dir3, got {one_ws_window:?}" + ); + + // --- Verify the active workspace is preserved --- + for window in &restored_windows { + let (active_paths, workspace_count) = window + .read_with(cx, |multi_workspace, cx| { + let active = multi_workspace.workspace(); + ( + active.read(cx).root_paths(cx), + multi_workspace.workspaces().len(), + ) + }) + .unwrap(); + + if workspace_count == 2 { + assert!( + active_paths.contains(&dir1_path), + "2-workspace window should have dir1 active, got {active_paths:?}" + ); + } else { + assert!( + active_paths.contains(&dir3_path), + "1-workspace window should have dir3 active, got {active_paths:?}" + ); + } + } + } } diff --git a/crates/zed/src/zed/migrate.rs b/crates/zed/src/zed/migrate.rs index 2452f17d04007364861e9a262b492155daec0c55..f8bec397f1cf54fe37962c6a318a816a3158423e 100644 --- a/crates/zed/src/zed/migrate.rs +++ b/crates/zed/src/zed/migrate.rs @@ -1,6 +1,7 @@ use anyhow::{Context as _, Result}; use editor::Editor; use fs::Fs; +use gpui::WeakEntity; use migrator::{migrate_keymap, migrate_settings}; use settings::{KeymapFile, Settings, SettingsStore}; use util::ResultExt; @@ -22,6 +23,7 @@ pub enum MigrationType { } pub struct MigrationBanner { + workspace: WeakEntity, migration_type: Option, should_migrate_task: Option>, markdown: Option>, @@ -54,7 +56,7 @@ struct GlobalMigrationNotification(Entity); impl Global for GlobalMigrationNotification {} impl MigrationBanner { - pub fn new(_: &Workspace, cx: &mut Context) -> Self { + pub fn new(workspace: WeakEntity, cx: &mut Context) -> Self { if let Some(notifier) = MigrationNotification::try_global(cx) { cx.subscribe( ¬ifier, @@ -65,6 +67,7 @@ impl MigrationBanner { .detach(); } Self { + workspace, migration_type: None, should_migrate_task: None, markdown: None, @@ -235,22 +238,22 @@ impl Render for MigrationBanner { ), ) .child( - Button::new("backup-and-migrate", "Backup and Update").on_click( + Button::new("backup-and-migrate", "Backup and Update").on_click({ + let workspace = self.workspace.clone(); move |_, window, cx| { let fs = ::global(cx); - match migration_type { + let task = match migration_type { Some(MigrationType::Keymap) => { cx.background_spawn(write_keymap_migration(fs.clone())) - .detach_and_notify_err(window, cx); } Some(MigrationType::Settings) => { cx.background_spawn(write_settings_migration(fs.clone())) - .detach_and_notify_err(window, cx); } None => unreachable!(), - } - }, - ), + }; + task.detach_and_notify_err(workspace.clone(), window, cx); + } + }), ) .into_any_element() } diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 86d35d558dc024931a901c479f26e502de381ca7..2fdefff246a9cfd32bd27797451a545d2ab5e565 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -1,5 +1,5 @@ use crate::handle_open_request; -use crate::restorable_workspace_locations; +use crate::restore_or_create_workspace; use anyhow::{Context as _, Result, anyhow}; use cli::{CliRequest, CliResponse, ipc::IpcSender}; use cli::{IpcHandshake, ipc}; @@ -30,7 +30,7 @@ use util::ResultExt; use util::paths::PathWithPosition; use workspace::PathList; use workspace::item::ItemHandle; -use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; +use workspace::{AppState, MultiWorkspace, OpenOptions, SerializedWorkspaceLocation}; #[derive(Default, Debug)] pub struct OpenRequest { @@ -337,7 +337,7 @@ pub async fn open_paths_with_positions( open_options: workspace::OpenOptions, cx: &mut AsyncApp, ) -> Result<( - WindowHandle, + WindowHandle, Vec>>>, )> { let mut caret_positions = HashMap::default(); @@ -357,24 +357,29 @@ pub async fn open_paths_with_positions( }) .collect::>(); - let (workspace, mut items) = cx + let (multi_workspace, mut items) = cx .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx)) .await?; if diff_all && !diff_paths.is_empty() { - if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { - MultiDiffView::open(diff_paths.to_vec(), workspace, window, cx) + if let Ok(diff_view) = multi_workspace.update(cx, |multi_workspace, window, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + MultiDiffView::open(diff_paths.to_vec(), workspace, window, cx) + }) }) { if let Some(diff_view) = diff_view.await.log_err() { items.push(Some(Ok(Box::new(diff_view)))); } } } else { + let workspace_weak = multi_workspace.read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().downgrade() + })?; for diff_pair in diff_paths { let old_path = Path::new(&diff_pair[0]).canonicalize()?; let new_path = Path::new(&diff_pair[1]).canonicalize()?; - if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { - FileDiffView::open(old_path, new_path, workspace, window, cx) + if let Ok(diff_view) = multi_workspace.update(cx, |_multi_workspace, window, cx| { + FileDiffView::open(old_path, new_path, workspace_weak.clone(), window, cx) }) { if let Some(diff_view) = diff_view.await.log_err() { items.push(Some(Ok(Box::new(diff_view)))) @@ -395,7 +400,7 @@ pub async fn open_paths_with_positions( continue; }; if let Some(active_editor) = item.downcast::() { - workspace + multi_workspace .update(cx, |_, window, cx| { active_editor.update(cx, |editor, cx| { editor.go_to_singleton_buffer_point(point, window, cx); @@ -405,7 +410,7 @@ pub async fn open_paths_with_positions( } } - Ok((workspace, items)) + Ok((multi_workspace, items)) } pub async fn handle_cli_connection( @@ -488,20 +493,13 @@ async fn open_workspaces( env: Option>, cx: &mut AsyncApp, ) -> Result<()> { + if paths.is_empty() && diff_paths.is_empty() && open_new_workspace != Some(true) { + return restore_or_create_workspace(app_state, cx).await; + } + let grouped_locations: Vec<(SerializedWorkspaceLocation, PathList)> = if paths.is_empty() && diff_paths.is_empty() { - if open_new_workspace == Some(true) { - Vec::new() - } else { - // The workspace_id from the database is not used; - // open_paths will assign a new WorkspaceId when opening the workspace. - restorable_workspace_locations(cx, &app_state) - .await - .unwrap_or_default() - .into_iter() - .map(|(_workspace_id, location, paths)| (location, paths)) - .collect() - } + Vec::new() } else { vec![( SerializedWorkspaceLocation::Local, @@ -755,7 +753,7 @@ mod tests { use serde_json::json; use std::{sync::Arc, task::Poll}; use util::path; - use workspace::{AppState, Workspace}; + use workspace::{AppState, MultiWorkspace}; #[gpui::test] fn test_parse_ssh_url(cx: &mut TestAppContext) { @@ -891,10 +889,12 @@ mod tests { open_workspace_file(path!("/root/dir1"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); - let workspace = cx.windows()[0].downcast::().unwrap(); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_none()) + let multi_workspace = cx.windows()[0].downcast::().unwrap(); + multi_workspace + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + assert!(workspace.active_item_as::(cx).is_none()) + }); }) .unwrap(); @@ -902,9 +902,11 @@ mod tests { open_workspace_file(path!("/root/dir1/file1.txt"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()); + multi_workspace + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + assert!(workspace.active_item_as::(cx).is_some()); + }); }) .unwrap(); @@ -919,12 +921,14 @@ mod tests { assert_eq!(cx.windows().len(), 2); - let workspace_2 = cx.windows()[1].downcast::().unwrap(); - workspace_2 - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()); - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 1, "Workspace should have two items"); + let multi_workspace_2 = cx.windows()[1].downcast::().unwrap(); + multi_workspace_2 + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + assert!(workspace.active_item_as::(cx).is_some()); + let items = workspace.items(cx).collect::>(); + assert_eq!(items.len(), 1, "Workspace should have two items"); + }); }) .unwrap(); } @@ -1000,10 +1004,12 @@ mod tests { open_workspace_file(path!("/root/file5.txt"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); - let workspace_1 = cx.windows()[0].downcast::().unwrap(); - workspace_1 - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()) + let multi_workspace_1 = cx.windows()[0].downcast::().unwrap(); + multi_workspace_1 + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + assert!(workspace.active_item_as::(cx).is_some()) + }); }) .unwrap(); @@ -1012,10 +1018,12 @@ mod tests { open_workspace_file(path!("/root/file6.txt"), Some(false), app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); - workspace_1 - .update(cx, |workspace, _, cx| { - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 2, "Workspace should have two items"); + multi_workspace_1 + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + let items = workspace.items(cx).collect::>(); + assert_eq!(items.len(), 2, "Workspace should have two items"); + }); }) .unwrap(); @@ -1024,11 +1032,13 @@ mod tests { open_workspace_file(path!("/root/file7.txt"), Some(true), app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 2); - let workspace_2 = cx.windows()[1].downcast::().unwrap(); - workspace_2 - .update(cx, |workspace, _, cx| { - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 1, "Workspace should have two items"); + let multi_workspace_2 = cx.windows()[1].downcast::().unwrap(); + multi_workspace_2 + .update(cx, |multi_workspace, _, cx| { + multi_workspace.workspace().update(cx, |workspace, cx| { + let items = workspace.items(cx).collect::>(); + assert_eq!(items.len(), 1, "Workspace should have two items"); + }); }) .unwrap(); }