From 165b404460ff94d079f2fe3a1aa38553cbd29609 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 9 Feb 2026 15:37:16 +0100 Subject: [PATCH] Revert "New multi workspace (#47795)" (#48776) Preparing this just in case. Release Notes: - N/A --- Cargo.lock | 31 +- Cargo.toml | 3 - assets/icons/workspace_nav_closed.svg | 5 - assets/icons/workspace_nav_open.svg | 5 - assets/keymaps/default-linux.json | 8 - assets/keymaps/default-macos.json | 8 - assets/keymaps/default-windows.json | 8 - crates/agent_ui/src/acp.rs | 2 +- crates/agent_ui/src/acp/message_editor.rs | 19 +- crates/agent_ui/src/acp/thread_view.rs | 198 +-- crates/agent_ui/src/agent_diff.rs | 8 +- crates/agent_ui/src/agent_panel.rs | 320 +--- crates/agent_ui/src/agent_ui.rs | 8 +- crates/agent_ui/src/inline_prompt_editor.rs | 11 +- crates/agent_ui/src/mention_set.rs | 17 +- crates/agent_ui/src/ui/agent_notification.rs | 18 +- .../tests/integration/channel_guest_tests.rs | 24 +- .../collab/tests/integration/editor_tests.rs | 64 +- .../tests/integration/following_tests.rs | 12 +- crates/collab/tests/integration/git_tests.rs | 32 +- .../remote_editing_collaboration_tests.rs | 24 +- .../collab/tests/integration/test_server.rs | 54 +- crates/collab_ui/src/collab_panel.rs | 27 +- crates/copilot_ui/src/sign_in.rs | 4 +- crates/db/src/kvp.rs | 127 +- crates/debugger_ui/src/tests.rs | 69 +- crates/debugger_ui/src/tests/attach_modal.rs | 54 +- .../src/tests/new_process_modal.rs | 32 +- .../debugger_ui/src/tests/stack_frame_list.rs | 6 +- crates/dev_container/Cargo.toml | 5 - crates/dev_container/src/devcontainer_api.rs | 566 ++++--- crates/dev_container/src/lib.rs | 81 +- crates/diagnostics/src/buffer_diagnostics.rs | 2 +- .../src/edit_prediction_button.rs | 2 +- crates/editor/src/editor.rs | 22 +- crates/editor/src/element.rs | 21 +- crates/editor/src/hover_popover.rs | 2 +- .../src/test/editor_lsp_test_context.rs | 25 +- crates/feature_flags/src/flags.rs | 4 - crates/file_finder/src/file_finder.rs | 7 +- crates/file_finder/src/file_finder_tests.rs | 14 +- crates/git_ui/src/commit_view.rs | 2 +- crates/git_ui/src/file_diff_view.rs | 9 +- crates/git_ui/src/git_panel.rs | 3 +- crates/git_ui/src/project_diff.rs | 3 +- crates/git_ui/src/worktree_picker.rs | 50 +- crates/icons/src/icons.rs | 2 - crates/inspector_ui/Cargo.toml | 1 + crates/inspector_ui/src/inspector.rs | 5 +- crates/journal/src/journal.rs | 25 +- crates/keymap_editor/src/keymap_editor.rs | 2 +- crates/miniprofiler_ui/src/miniprofiler_ui.rs | 27 +- crates/onboarding/src/onboarding.rs | 11 +- crates/outline/src/outline.rs | 24 +- crates/platform_title_bar/Cargo.toml | 1 - .../src/platform_title_bar.rs | 65 +- crates/platform_title_bar/src/platforms.rs | 1 + .../src/platforms/platform_mac.rs | 10 + crates/project_panel/src/project_panel.rs | 23 +- crates/recent_projects/Cargo.toml | 2 - .../src/disconnected_overlay.rs | 6 +- crates/recent_projects/src/recent_projects.rs | 210 ++- .../recent_projects/src/remote_connections.rs | 177 +-- crates/recent_projects/src/remote_servers.rs | 119 +- crates/recent_projects/src/wsl_picker.rs | 4 +- crates/repl/src/repl_sessions_ui.rs | 5 +- crates/rules_library/src/rules_library.rs | 14 +- crates/search/src/search.rs | 2 +- crates/session/src/session.rs | 14 - .../src/settings_profile_selector.rs | 9 +- crates/settings_ui/src/settings_ui.rs | 283 ++-- crates/sidebar/Cargo.toml | 43 - crates/sidebar/LICENSE-GPL | 1 - crates/sidebar/src/sidebar.rs | 1304 ----------------- crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/project_dropdown.rs | 64 +- crates/title_bar/src/title_bar.rs | 102 +- crates/ui/src/components/thread_item.rs | 56 +- crates/ui/src/utils.rs | 2 - crates/ui/src/utils/constants.rs | 27 - crates/vim/src/command.rs | 32 +- crates/vim/src/normal/mark.rs | 10 +- crates/vim/src/normal/repeat.rs | 4 +- crates/vim/src/normal/search.rs | 2 +- crates/vim/src/state.rs | 18 +- crates/vim/src/vim.rs | 8 +- crates/workspace/Cargo.toml | 1 - crates/workspace/src/history_manager.rs | 11 +- crates/workspace/src/multi_workspace.rs | 513 ------- crates/workspace/src/notifications.rs | 77 +- crates/workspace/src/pane.rs | 7 +- crates/workspace/src/persistence.rs | 457 ++---- crates/workspace/src/persistence/model.rs | 28 +- crates/workspace/src/welcome.rs | 25 +- crates/workspace/src/workspace.rs | 1052 +++++-------- crates/zed/Cargo.toml | 2 - crates/zed/src/main.rs | 294 ++-- crates/zed/src/visual_test_runner.rs | 319 +--- crates/zed/src/zed.rs | 1092 ++------------ crates/zed/src/zed/migrate.rs | 19 +- crates/zed/src/zed/open_listener.rs | 108 +- 101 files changed, 1997 insertions(+), 6740 deletions(-) delete mode 100644 assets/icons/workspace_nav_closed.svg delete mode 100644 assets/icons/workspace_nav_open.svg create mode 100644 crates/platform_title_bar/src/platforms/platform_mac.rs delete mode 100644 crates/sidebar/Cargo.toml delete mode 120000 crates/sidebar/LICENSE-GPL delete mode 100644 crates/sidebar/src/sidebar.rs delete mode 100644 crates/ui/src/utils/constants.rs delete mode 100644 crates/workspace/src/multi_workspace.rs diff --git a/Cargo.lock b/Cargo.lock index 2ae5f44ac976fbc08eccf8a8896c18d3bb982c5a..95aa65c06ceeb74ef03443b62912b3ee60fceb46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4942,7 +4942,6 @@ checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" name = "dev_container" version = "0.1.0" dependencies = [ - "fs", "futures 0.3.31", "gpui", "http 1.3.1", @@ -4952,12 +4951,10 @@ dependencies = [ "node_runtime", "paths", "picker", - "project", "serde", "serde_json", "settings", "smol", - "theme", "ui", "util", "workspace", @@ -8495,6 +8492,7 @@ dependencies = [ "fuzzy", "gpui", "language", + "platform_title_bar", "project", "serde_json", "serde_json_lenient", @@ -12382,7 +12380,6 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" name = "platform_title_bar" version = "0.1.0" dependencies = [ - "feature_flags", "gpui", "settings", "smallvec", @@ -15349,30 +15346,6 @@ 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" @@ -17252,7 +17225,6 @@ dependencies = [ "cloud_api_types", "collections", "db", - "feature_flags", "git_ui", "gpui", "http_client", @@ -21138,7 +21110,6 @@ 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 b0935c94e9bba08da262bdaabf4385ec9419578c..b641d42e4f4c76bde644a0b0795f3f9256a2962b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,7 +155,6 @@ members = [ "crates/schema_generator", "crates/search", "crates/session", - "crates/sidebar", "crates/settings", "crates/settings_content", "crates/settings_json", @@ -397,7 +396,6 @@ 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" } @@ -857,7 +855,6 @@ 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 deleted file mode 100644 index ed1fce52d6826a4d10299f331358ff84e4caa973..0000000000000000000000000000000000000000 --- a/assets/icons/workspace_nav_closed.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/workspace_nav_open.svg b/assets/icons/workspace_nav_open.svg deleted file mode 100644 index 464b6aac73c2aeaa9463a805aabc4559377bbfd3..0000000000000000000000000000000000000000 --- a/assets/icons/workspace_nav_open.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index d877cfb41206087f555e47e01dccffeb9357b2c8..43c58411a6c4b4140a59c55a24d37716f0ab1ad3 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -596,7 +596,6 @@ "ctrl-alt-b": "workspace::ToggleRightDock", "ctrl-b": "workspace::ToggleLeftDock", "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar", "ctrl-alt-y": "workspace::ToggleAllDocks", "ctrl-alt-0": "workspace::ResetActiveDockSize", // For 0px parameter, uses UI font size value. @@ -656,13 +655,6 @@ "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 19ce418bf13ef28e37149b3fc9dc3644f0fc782d..813f7442b6cb3ce93a130be01e0043c3ca025d9a 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -657,7 +657,6 @@ "cmd-alt-b": "workspace::ToggleRightDock", "cmd-r": "workspace::ToggleRightDock", "cmd-j": "workspace::ToggleBottomDock", - "cmd-alt-j": "multi_workspace::ToggleWorkspaceSidebar", "alt-cmd-y": "workspace::ToggleAllDocks", // For 0px parameter, uses UI font size value. "ctrl-alt-0": "workspace::ResetActiveDockSize", @@ -717,13 +716,6 @@ // "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 5e0cbdc8afabf0c4e9901a256f44d653e294b02c..ae00aff39ef3529fa906f18d3a9a28e8fa6b688c 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -591,7 +591,6 @@ "ctrl-alt-b": "workspace::ToggleRightDock", "ctrl-b": "workspace::ToggleLeftDock", "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar", "ctrl-shift-y": "workspace::ToggleAllDocks", "alt-r": "workspace::ResetActiveDockSize", // For 0px parameter, uses UI font size value. @@ -660,13 +659,6 @@ "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 f76e64b557e7ee2ec6054bd0fab0afc36b201e2c..904c9a6c7b7e383d09b54f58115be2303ef8754a 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; -pub(crate) mod thread_view; +mod thread_view; pub use mode_selector::ModeSelector; pub use model_selector::AcpModelSelector; diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index faea22c45e32ba53f22367f89d8abe292f6e0753..7c9966295483d5c0b0b5586b7d020c98db50f25f 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -815,13 +815,8 @@ impl MessageEditor { } if self.prompt_capabilities.borrow().image - && let Some(task) = paste_images_as_context( - self.editor.clone(), - self.mention_set.clone(), - self.workspace.clone(), - window, - cx, - ) + && let Some(task) = + paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) { task.detach(); return; @@ -1089,7 +1084,6 @@ 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, @@ -1140,14 +1134,7 @@ impl MessageEditor { images.push(gpui::Image::from_bytes(format, content)); } - crate::mention_set::insert_images_as_context( - images, - editor, - mention_set, - workspace, - cx, - ) - .await; + crate::mention_set::insert_images_as_context(images, editor, mention_set, cx).await; Ok(()) }) .detach_and_log_err(cx); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3dfdeccf7ec9f03b84752101502c708ecb08ac88..e294a08d14c2c993e4fb73e05e0e3eb001860c0e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -57,9 +57,7 @@ use ui::{ }; use util::defer; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; -use workspace::{ - CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId, -}; +use workspace::{CollaboratorId, NewTerminal, Toast, Workspace, notifications::NotificationId}; use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::assistant::OpenRulesLibrary; @@ -1988,30 +1986,9 @@ 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 && !self.agent_is_visible(window, cx) { + if settings.play_sound_when_agent_done && !window.is_window_active() { Audio::play_sound(Sound::AgentDone, cx); } } @@ -2029,7 +2006,14 @@ impl AcpServerView { let settings = AgentSettings::get_global(cx); - let should_notify = !self.agent_is_visible(window, 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; if !should_notify { return; @@ -2092,22 +2076,19 @@ impl AcpServerView { .push(cx.subscribe_in(&pop_up, window, { |this, _, event, window, cx| match event { AgentNotificationEvent::Accepted => { - let Some(handle) = window.window_handle().downcast::() - else { - log::error!("root view should be a MultiWorkspace"); - return; - }; + let handle = window.window_handle(); 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, |multi_workspace, window, cx| { + .update(cx, |_view, window, _cx| { window.activate_window(); + if let Some(workspace) = workspace_handle.upgrade() { - multi_workspace.activate(workspace.clone(), cx); - workspace.update(cx, |workspace, cx| { + workspace.update(_cx, |workspace, cx| { workspace.focus_panel::(window, cx); }); } @@ -2132,12 +2113,12 @@ impl AcpServerView { .push({ let pop_up_weak = pop_up.downgrade(); - cx.observe_window_activation(window, move |this, window, cx| { - if this.agent_is_visible(window, cx) + cx.observe_window_activation(window, move |_, window, cx| { + if window.is_window_active() && let Some(pop_up) = pop_up_weak.upgrade() { - pop_up.update(cx, |notification, cx| { - notification.dismiss(cx); + pop_up.update(cx, |_, cx| { + cx.emit(AgentNotificationEvent::Dismissed); }); } }) @@ -2388,7 +2369,6 @@ 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}; @@ -2398,7 +2378,7 @@ pub(crate) mod tests { use std::any::Any; use std::path::Path; use std::rc::Rc; - use workspace::{Item, MultiWorkspace}; + use workspace::Item; use super::*; @@ -2698,138 +2678,6 @@ 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| { - let workspace2 = cx.new(|cx| Workspace::test_new(project2, window, cx)); - mw.activate(workspace2, 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); @@ -2992,18 +2840,18 @@ pub(crate) mod tests { } } - pub(crate) struct StubAgentServer { + struct StubAgentServer { connection: C, } impl StubAgentServer { - pub(crate) fn new(connection: C) -> Self { + fn new(connection: C) -> Self { Self { connection } } } impl StubAgentServer { - pub(crate) fn default_response() -> Self { + fn default_response() -> Self { let conn = StubAgentConnection::new(); conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( acp::ContentChunk::new("Default response".into()), diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index bb00be46bad837a3b2242e885905709fbe38868c..850822679d2828b96ba6218c4d48e570764d6de6 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 => { - self.update_reviewing_editors(workspace, window, cx); - } - AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::Refusal => { + AcpThreadEvent::Stopped + | AcpThreadEvent::Error + | AcpThreadEvent::LoadError(_) + | AcpThreadEvent::Refusal => { self.update_reviewing_editors(workspace, window, cx); } AcpThreadEvent::TitleUpdated diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d5602b0d0913e4f22e0955b7561c7806c5f181bf..bd9c31a983b723c222987544561cea82a97bad2b 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -81,50 +81,10 @@ const AGENT_PANEL_KEY: &str = "agent_panel"; const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; const DEFAULT_THREAD_TITLE: &str = "New Thread"; -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)] +#[derive(Serialize, Deserialize, Debug)] 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) { @@ -468,7 +428,6 @@ 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, @@ -485,45 +444,19 @@ pub struct AgentPanel { } impl AgentPanel { - fn serialize(&mut self, cx: &mut App) { - let workspace_id = self - .workspace - .read_with(cx, |workspace, _| workspace.database_id()) - .ok() - .flatten(); - - let Some(workspace_id) = workspace_id else { - return; - }; - + fn serialize(&mut self, cx: &mut Context) { 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 { - save_serialized_panel( - workspace_id, - SerializedAgentPanel { - width, - selected_agent: Some(selected_agent), - last_active_thread, - }, - ) - .await?; + KEY_VALUE_STORE + .write_kvp( + AGENT_PANEL_KEY.into(), + serde_json::to_string(&SerializedAgentPanel { + width, + selected_agent: Some(selected_agent), + })?, + ) + .await?; anyhow::Ok(()) })); } @@ -539,18 +472,16 @@ impl AgentPanel { Ok(prompt_store) => prompt_store.await.ok(), Err(_) => 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 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 slash_commands = Arc::new(SlashCommandWorkingSet::default()); let text_thread_store = workspace @@ -569,30 +500,15 @@ 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.clone() { + if let Some(selected_agent) = serialized_panel.selected_agent { 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 })?; @@ -600,7 +516,7 @@ impl AgentPanel { }) } - pub(crate) fn new( + fn new( workspace: &Workspace, text_thread_store: Entity, prompt_store: Option>, @@ -730,7 +646,6 @@ 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(), @@ -799,7 +714,7 @@ impl AgentPanel { &self.context_server_registry } - pub fn is_visible(workspace: &Entity, cx: &App) -> bool { + pub fn is_hidden(workspace: &Entity, cx: &App) -> bool { let workspace_read = workspace.read(cx); workspace_read @@ -807,13 +722,15 @@ impl AgentPanel { .map(|panel| { let panel_id = Entity::entity_id(&panel); - workspace_read.all_docks().iter().any(|dock| { + let is_visible = 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(false) + .unwrap_or(true) } pub(crate) fn active_thread_view(&self) -> Option<&Entity> { @@ -1106,7 +1023,6 @@ 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 } => { @@ -1503,7 +1419,7 @@ impl AgentPanel { } } - pub fn active_agent_thread(&self, cx: &App) -> Option> { + pub(crate) fn active_agent_thread(&self, cx: &App) -> Option> { match &self.active_view { ActiveView::AgentThread { thread_view, .. } => thread_view .read(cx) @@ -1559,21 +1475,9 @@ 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( @@ -1846,12 +1750,7 @@ 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 { @@ -1894,14 +1793,7 @@ impl Panel for AgentPanel { DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Bottom => self.height = size, } - let this = cx.weak_entity(); - cx.defer(move |cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.serialize(cx); - }); - } - }); + self.serialize(cx); cx.notify(); } @@ -3392,151 +3284,3 @@ 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, Workspace}; - - #[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| { - let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx)); - multi_workspace.activate(workspace.clone(), cx); - workspace - }) - .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" - ); - }); - } -} diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index d2beb4b7199bf1536985bed49958b0643703bce7..ee035eb29188a7360c6d33df5334467f397532a4 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, AgentPanelEvent, ConcreteAssistantPanelDelegate}; +pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate}; use crate::agent_registry_ui::AgentRegistryPage; pub use crate::inline_assistant::InlineAssistant; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; @@ -422,12 +422,6 @@ 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/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 2066a7ad886614373b200f4e45dd3bb0034f72a2..48c597f0431c480ade5810db99c36a890ec65093 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -417,13 +417,8 @@ 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(), - self.workspace.clone(), - window, - cx, - ) + && let Some(task) = + paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) { task.detach(); } @@ -443,7 +438,7 @@ impl PromptEditor { self.mention_set .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); - if let Some(workspace) = Workspace::for_window(window, cx) { + if let Some(workspace) = window.root::().flatten() { 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 707e7b45343363b9db440998190e319df1da5b80..ee796323e28c64fb4162bbb05f6f6f9555a12d38 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -297,9 +297,8 @@ impl MentionSet { self.mentions.insert(crease_id, (mention_uri, task.clone())); // Notify the user if we failed to load the mentioned context - let workspace = workspace.downgrade(); - cx.spawn(async move |this, mut cx| { - let result = task.await.notify_workspace_async_err(workspace, &mut cx); + cx.spawn_in(window, async move |this, cx| { + let result = task.await.notify_async_err(cx); drop(tx); if result.is_none() { this.update(cx, |this, cx| { @@ -645,7 +644,6 @@ 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() { @@ -720,11 +718,7 @@ pub(crate) async fn insert_images_as_context( mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone()) }); - if task - .await - .notify_workspace_async_err(workspace.clone(), cx) - .is_none() - { + if task.await.notify_async_err(cx).is_none() { editor.update(cx, |editor, cx| { editor.edit([(start_anchor..end_anchor, "")], cx); }); @@ -738,12 +732,11 @@ 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 |mut cx| { + Some(window.spawn(cx, async move |cx| { use itertools::Itertools; let (mut images, paths) = clipboard .into_entries() @@ -790,7 +783,7 @@ pub(crate) fn paste_images_as_context( }) .ok(); - insert_images_as_context(images, editor, mention_set, workspace, &mut cx).await; + insert_images_as_context(images, editor, mention_set, cx).await; })) } diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index 371523f129869786f13d1a220747f4d0d944d1e5..34ca0bb32a82aa23d1b954554ce2dfec436bfe1c 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -75,16 +75,6 @@ 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); @@ -184,14 +174,14 @@ impl Render for AgentNotification { .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .full_width() .on_click({ - cx.listener(move |this, _event, _, cx| { - this.accept(cx); + cx.listener(move |_this, _event, _, cx| { + cx.emit(AgentNotificationEvent::Accepted); }) }), ) .child(Button::new("dismiss", "Dismiss").full_width().on_click({ - cx.listener(move |this, _event, _, cx| { - this.dismiss(cx); + cx.listener(move |_, _event, _, cx| { + cx.emit(AgentNotificationEvent::Dismissed); }) })), ) diff --git a/crates/collab/tests/integration/channel_guest_tests.rs b/crates/collab/tests/integration/channel_guest_tests.rs index 85d69914a832c65260014f5f5792eb664879f715..0d98af2a188ce18cfab5905e5b464c77101dfa00 100644 --- a/crates/collab/tests/integration/channel_guest_tests.rs +++ b/crates/collab/tests/integration/channel_guest_tests.rs @@ -34,11 +34,9 @@ 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, None, cx) - }) - .await - .unwrap(); + cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) + .await + .unwrap(); // b should be following a in the shared project. // B is a guest, @@ -78,11 +76,9 @@ 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, None, cx) - }) - .await - .unwrap(); + cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx)) + .await + .unwrap(); // Client A shares a project in the channel active_call_a @@ -92,11 +88,9 @@ 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, None, cx) - }) - .await - .unwrap(); + cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), 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 a973c9f17ec5488746a9ad6594a3e99fb711c203..1612e32833dd07dd5fa2294d5bb5a90442883f71 100644 --- a/crates/collab/tests/integration/editor_tests.rs +++ b/crates/collab/tests/integration/editor_tests.rs @@ -19,8 +19,7 @@ use fs::Fs; use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; use git::repository::repo_path; use gpui::{ - App, AppContext as _, Entity, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, - VisualTestContext, + App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext, }; use indoc::indoc; use language::{FakeLspAdapter, language_settings::language_settings, rust_lang}; @@ -52,7 +51,7 @@ use std::{ }; use text::Point; use util::{path, rel_path::rel_path, uri}; -use workspace::{CloseIntent, MultiWorkspace, Workspace}; +use workspace::{CloseIntent, Workspace}; #[gpui::test(iterations = 10)] async fn test_host_disconnect( @@ -96,46 +95,34 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); - 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 workspace_b = cx_b.add_window(|window, cx| { + Workspace::new( + None, + project_b.clone(), + client_b.app_state.clone(), + window, + cx, + ) }); - 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 cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); + let workspace_b_view = workspace_b.root(cx_b).unwrap(); - let editor_b: Entity = workspace_b - .update_in(cx_b, |workspace, window, cx| { + let editor_b = workspace_b + .update(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: &mut Editor, window, _| editor - .is_focused(window)) - ); - editor_b.update_in(cx_b, |editor: &mut Editor, window, cx| { - editor.insert("X", window, cx) - }); + 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)); cx_b.update(|_, cx| { - assert!(workspace_b.read(cx).is_edited()); + assert!(workspace_b_view.read(cx).is_edited()); }); // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. @@ -153,16 +140,19 @@ 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()); - }); + workspace_b + .update(cx_b, |workspace, _, cx| { + assert!(workspace.active_modal::(cx).is_some()); + assert!(!workspace.is_edited()); + }) + .unwrap(); // Ensure client B is not prompted to save edits when closing window after disconnecting. - let can_close: bool = workspace_b - .update_in(cx_b, |workspace, window, cx| { + let can_close = workspace_b + .update(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 6bdb06a6c5a0ffb95bc75a026a26c4797030f8ce..295105ecbd9f8663469276fe4d0d197708a4254e 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, MultiWorkspace, SplitDirection, Workspace, item::ItemHandle as _}; +use workspace::{CollaboratorId, 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() - .read_with(cx_b, |mw, _| mw.workspace().clone()) + .root(cx_b) .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() - .read_with(cx_a, |mw, _| mw.workspace().clone()) + .root(cx_a) .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, None, cx)) + cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx)) .await } diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs index 63cee5886d5096cb0e3fbee3886b90f66c675bfa..1378fcf95c63c883ee8dd424dc10ac67ccd774bd 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::{AppContext as _, TestAppContext, VisualTestContext}; +use gpui::{TestAppContext, VisualTestContext}; use project::ProjectPath; use serde_json::json; use util::{path, rel_path::rel_path}; -use workspace::{MultiWorkspace, Workspace}; +use workspace::Workspace; // use crate::TestServer; @@ -57,25 +57,17 @@ 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 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 workspace_b = cx_b.add_window(|window, cx| { + Workspace::new( + None, + project_b.clone(), + client_b.app_state.clone(), + window, + cx, + ) }); - 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 cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); + let workspace_b = workspace_b.root(cx_b).unwrap(); 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 c6daedff803b6f5cada32750f90dd1adca5aeda6..1f4dd0d353234f61675b5beefd2226c3d684c062 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -8,9 +8,7 @@ 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 as _, -}; +use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext}; use http_client::BlockedHttpClient; use language::{ FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, @@ -665,7 +663,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(); @@ -673,16 +671,13 @@ 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.clone() + session ) }); - session.update( - cx_a, - |session: &mut project::debugger::session::Session, _| { - assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock")); - }, - ); + session.update(cx_a, |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| { @@ -777,7 +772,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)); @@ -809,10 +804,7 @@ async fn test_slow_adapter_startup_retries( .unwrap(); cx_a.run_until_parked(); - let client = session.update( - cx_a, - |session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(), - ); + let client = session.update(cx_a, |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 0a2ec25cde361259344493d3532afeb2050aea71..f28d247f67a149ef6d489b9bc6ab7b43eb77350f 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::{MultiWorkspace, Workspace, WorkspaceStore}; +use workspace::{Workspace, WorkspaceStore}; use livekit_client::test::TestServer as LivekitTestServer; @@ -843,7 +843,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, None, cx)) + cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx)) .await .unwrap(); cx.run_until_parked(); @@ -897,19 +897,10 @@ impl TestClient { project: &Entity, cx: &'a mut TestAppContext, ) -> (Entity, &'a mut VisualTestContext) { - let app_state = self.app_state.clone(); - let project = project.clone(); - let window = cx.add_window(|window, cx| { + cx.add_window_view(|window, cx| { window.activate_window(); - 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) + Workspace::new(None, project.clone(), self.app_state.clone(), window, cx) + }) } pub async fn build_test_workspace<'a>( @@ -917,33 +908,19 @@ impl TestClient { cx: &'a mut TestAppContext, ) -> (Entity, &'a mut VisualTestContext) { let project = self.build_test_project(cx).await; - let app_state = self.app_state.clone(); - let window = cx.add_window(|window, cx| { + cx.add_window_view(|window, cx| { window.activate_window(); - 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) + Workspace::new(None, project.clone(), self.app_state.clone(), window, 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 - .read_with(cx, |mw, _| mw.workspace().clone()) - .unwrap(); + let entity = window.root(cx).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) @@ -954,15 +931,8 @@ 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 - .read_with(cx, |mw, _| mw.workspace().clone()) - .unwrap(); + let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::().unwrap()); + let entity = window.root(cx).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 c0a68efdc7107800a0abfd5c522e5b0ed541a964..663d64d56d3e9832a6a92c2916fa62d22afd23e6 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -36,8 +36,7 @@ use ui::{ }; use util::{ResultExt, TryFutureExt, maybe}; use workspace::{ - CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, ScreenShare, - ShareProject, Workspace, + CopyRoomId, Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace, dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyResultExt}, }; @@ -121,7 +120,6 @@ 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| { @@ -136,7 +134,7 @@ pub fn init(cx: &mut App) { ); }) }) - .detach_and_notify_err(workspace_handle, window, cx); + .detach_and_notify_err(window, cx); } else { workspace.show_error(&"There’s no active call; join one first.", cx); } @@ -2179,13 +2177,12 @@ impl CollabPanel { &["Remove", "Cancel"], cx, ); - let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |this, mut cx| { + cx.spawn_in(window, async move |this, cx| { if answer.await? == 0 { channel_store .update(cx, |channels, _| channels.remove_channel(channel_id)) .await - .notify_workspace_async_err(workspace, &mut cx); + .notify_async_err(cx); this.update_in(cx, |_, window, cx| cx.focus_self(window)) .ok(); } @@ -2214,13 +2211,12 @@ impl CollabPanel { &["Remove", "Cancel"], cx, ); - let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |_, mut cx| { + cx.spawn_in(window, async move |_, cx| { if answer.await? == 0 { user_store .update(cx, |store, cx| store.remove_contact(user_id, cx)) .await - .notify_workspace_async_err(workspace, &mut cx); + .notify_async_err(cx); } anyhow::Ok(()) }) @@ -2271,15 +2267,13 @@ 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) @@ -2322,13 +2316,12 @@ impl CollabPanel { .full_width() .on_click(cx.listener(|this, _, window, cx| { let client = this.client.clone(); - let workspace = this.workspace.clone(); - cx.spawn_in(window, async move |_, mut cx| { + cx.spawn_in(window, async move |_, cx| { client - .connect(true, &mut cx) + .connect(true, cx) .await .into_response() - .notify_workspace_async_err(workspace, &mut cx); + .notify_async_err(cx); }) .detach() })), diff --git a/crates/copilot_ui/src/sign_in.rs b/crates/copilot_ui/src/sign_in.rs index 24b1218305474a29ac2d2e7c8e0a212d6d757522..dd48f95e0af6daeaf2a0a15b7b9595cb4c08aba2 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) = Workspace::for_window(window, cx) { + if let Some(workspace) = window.root::().flatten() { 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) = Workspace::for_window(window, cx) else { + let Some(workspace) = window.root::().flatten() else { return; }; diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 438adcdf44921aa1d2590694608c139e9174d788..8ea877b35bfaf57bb258e7e179fa5b71f2b518ea 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -1,4 +1,3 @@ -use anyhow::Context as _; use gpui::App; use sqlez_macros::sql; use util::ResultExt as _; @@ -14,22 +13,12 @@ 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; - ), - 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; - ), - ]; + const MIGRATIONS: &[&str] = &[sql!( + CREATE TABLE IF NOT EXISTS kv_store( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) STRICT; + )]; } crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); @@ -80,64 +69,6 @@ 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)] @@ -168,52 +99,6 @@ 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 c183f8941c3f30cb43ffaa638eae4e6b387e226d..5aaf3538e86054346d82b1db10aa02d8e5aa34f1 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::MultiWorkspace; +use workspace::Workspace; use crate::{debugger_panel::DebugPanel, session::DebugSession}; @@ -52,16 +52,14 @@ 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| MultiWorkspace::test_new(project.clone(), window, cx)); + cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let debugger_panel = workspace_handle - .update(cx, |multi, window, cx| { - multi.workspace().update(cx, |_workspace, cx| { - cx.spawn_in(window, async move |this, cx| { - DebugPanel::load(this, cx).await - }) + .update(cx, |_, window, cx| { + cx.spawn_in(window, async move |this, cx| { + DebugPanel::load(this, cx).await }) }) .unwrap() @@ -69,10 +67,9 @@ pub async fn init_test_workspace( .expect("Failed to load debug panel"); let terminal_panel = workspace_handle - .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 + .update(cx, |_, window, cx| { + cx.spawn_in(window, async |this, cx| { + TerminalPanel::load(this, cx.clone()).await }) }) .unwrap() @@ -80,11 +77,9 @@ pub async fn init_test_workspace( .expect("Failed to load terminal panel"); workspace_handle - .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); - }); + .update(cx, |workspace, window, cx| { + workspace.add_panel(debugger_panel, window, cx); + workspace.add_panel(terminal_panel, window, cx); }) .unwrap(); workspace_handle @@ -92,45 +87,39 @@ 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, |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() - }) + .update(cx, |workspace, _window, 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, |multi, window, cx| { - multi.workspace().update(cx, |workspace, cx| { - workspace.start_debug_session( - config.to_scenario(), - SharedTaskContext::default(), - None, - None, - window, - cx, - ) - }) + workspace.update(cx, |workspace, window, 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()) @@ -142,7 +131,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 4e8839f82f4de69fd1851ef50ff0d55ad09d0aa9..b05ee591f3ac0ca2e138e25928552d93c4426152 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -60,13 +60,7 @@ 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 - .workspace() - .read(cx) - .active_modal::(cx) - .is_none() - ); + assert!(workspace.active_modal::(cx).is_none()); }) .unwrap(); } @@ -103,9 +97,9 @@ async fn test_show_attach_modal_and_select_process( }); }); let attach_modal = workspace - .update(cx, |multi, window, cx| { - let workspace_handle = multi.workspace().downgrade(); - multi.toggle_modal(window, cx, |window, cx| { + .update(cx, |workspace, window, cx| { + let workspace_handle = cx.weak_entity(); + workspace.toggle_modal(window, cx, |window, cx| { AttachModal::with_processes( workspace_handle, vec![ @@ -139,7 +133,7 @@ async fn test_show_attach_modal_and_select_process( ) }); - multi.active_modal::(cx).unwrap() + workspace.active_modal::(cx).unwrap() }) .unwrap(); @@ -214,26 +208,24 @@ async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &m let pick_pid_placeholder = task::VariableName::PickProcessId.template_value(); workspace - .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, - ); - }) + .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, + ) }) .unwrap(); diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 54c38d8b1cec8d043748338830d643d63479e533..7be2b8798e38108eaa05508002624b98c1595b3f 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -145,17 +145,15 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( }; workspace - .update(cx, |multi, window, cx| { - multi.workspace().update(cx, |workspace, cx| { - workspace.start_debug_session( - scenario, - task_context.clone(), - None, - None, - window, - cx, - ); - }) + .update(cx, |workspace, window, cx| { + workspace.start_debug_session( + scenario, + task_context.clone(), + None, + None, + window, + cx, + ) }) .unwrap(); @@ -184,10 +182,8 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut let cx = &mut VisualTestContext::from_window(*workspace, cx); workspace - .update(cx, |multi, window, cx| { - multi.workspace().update(cx, |workspace, cx| { - NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); - }); + .update(cx, |workspace, window, cx| { + NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); }) .unwrap(); @@ -328,10 +324,8 @@ async fn test_debug_modal_subtitles_with_multiple_worktrees( let cx = &mut VisualTestContext::from_window(*workspace, cx); workspace - .update(cx, |multi, window, cx| { - multi.workspace().update(cx, |workspace, cx| { - NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); - }); + .update(cx, |workspace, window, 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 1f5ac5dea4a19af338feceaa2ee51fd9322fa9a5..372bfa8f5f4eb1da13a59057d077a53a71fa2cea 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, _, cx| { - workspace.set_random_database_id(cx); + .update(cx, |workspace, _, _| { + workspace.set_random_database_id(); }) .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(cx)) + .update(cx, |workspace, _window, _cx| workspace.database_id()) .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 113ee22e5ac9956d0af7c1da1ef43403b81c7636..31f0466d45e84569b3e2609742d5ba2d1ac59568 100644 --- a/crates/dev_container/Cargo.toml +++ b/crates/dev_container/Cargo.toml @@ -23,12 +23,7 @@ util.workspace = true workspace.workspace = true [dev-dependencies] -fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -theme.workspace = true -workspace = { workspace = true, features = ["test-support"] } [lints] workspace = true diff --git a/crates/dev_container/src/devcontainer_api.rs b/crates/dev_container/src/devcontainer_api.rs index f6c6f6a7520ec66d786c6b1ae8eb5ee0d41e9ec5..bdba805ade04598d7fba23bbd717d8ec2d584c4f 100644 --- a/crates/dev_container/src/devcontainer_api.rs +++ b/crates/dev_container/src/devcontainer_api.rs @@ -2,16 +2,18 @@ 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; +use settings::{DevContainerConnection, Settings as _}; use smol::{fs, process::Command}; use util::rel_path::RelPath; use workspace::Workspace; -use crate::{DevContainerContext, DevContainerFeature, DevContainerTemplate}; +use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate}; /// Represents a discovered devcontainer configuration #[derive(Debug, Clone, PartialEq, Eq)] @@ -57,31 +59,6 @@ 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, @@ -122,6 +99,58 @@ 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. /// /// This function scans for: @@ -129,124 +158,160 @@ impl Display for DevContainerError { /// 2. `.devcontainer//devcontainer.json` (named configurations) /// /// Returns a list of found configurations, or an empty list if none are found. -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"); +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 worktree = worktree.read(cx); - let mut configs = Vec::new(); + let Ok(configs) = workspace.update(cx, |workspace, _, cx| { + let project = workspace.project().read(cx); - let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path"); + let worktree = project + .visible_worktrees(cx) + .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); - let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else { - log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree"); - return Vec::new(); - }; + let Some(worktree) = worktree else { + log::debug!("find_devcontainer_configs: No worktree found"); + return Vec::new(); + }; - if !devcontainer_entry.is_dir() { - log::debug!("find_devcontainer_configs: .devcontainer is not a directory"); - return Vec::new(); - } + let worktree = worktree.read(cx); + let mut configs = Vec::new(); - log::debug!("find_devcontainer_configs: Scanning .devcontainer directory"); - let devcontainer_json_path = - RelPath::unix(".devcontainer/devcontainer.json").expect("valid path"); - for entry in worktree.child_entries(devcontainer_path) { - log::debug!( - "find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}", - entry.path.as_unix_str(), - entry.is_file(), - entry.is_dir() - ); + let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path"); - if entry.is_file() && entry.path.as_ref() == devcontainer_json_path { - log::debug!("find_devcontainer_configs: Found default devcontainer.json"); - configs.push(DevContainerConfig::default_config()); - } else if entry.is_dir() { - let subfolder_name = entry - .path - .file_name() - .map(|n| n.to_string()) - .unwrap_or_default(); - - let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str()); - if let Ok(rel_config_path) = RelPath::unix(&config_json_path) { - if worktree.entry_for_path(rel_config_path).is_some() { - log::debug!( - "find_devcontainer_configs: Found config in subfolder: {}", - subfolder_name - ); - configs.push(DevContainerConfig { - name: subfolder_name, - config_path: PathBuf::from(&config_json_path), - }); - } else { - log::debug!( - "find_devcontainer_configs: Subfolder {} has no devcontainer.json", - subfolder_name - ); + let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else { + log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree"); + return Vec::new(); + }; + + if !devcontainer_entry.is_dir() { + log::debug!("find_devcontainer_configs: .devcontainer is not a directory"); + return Vec::new(); + } + + log::debug!("find_devcontainer_configs: Scanning .devcontainer directory"); + let devcontainer_json_path = + RelPath::unix(".devcontainer/devcontainer.json").expect("valid path"); + for entry in worktree.child_entries(devcontainer_path) { + log::debug!( + "find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}", + entry.path.as_unix_str(), + entry.is_file(), + entry.is_dir() + ); + + if entry.is_file() && entry.path.as_ref() == devcontainer_json_path { + log::debug!("find_devcontainer_configs: Found default devcontainer.json"); + configs.push(DevContainerConfig::default_config()); + } else if entry.is_dir() { + let subfolder_name = entry + .path + .file_name() + .map(|n| n.to_string()) + .unwrap_or_default(); + + let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str()); + if let Ok(rel_config_path) = RelPath::unix(&config_json_path) { + if worktree.entry_for_path(rel_config_path).is_some() { + log::debug!( + "find_devcontainer_configs: Found config in subfolder: {}", + subfolder_name + ); + configs.push(DevContainerConfig { + name: subfolder_name, + config_path: PathBuf::from(&config_json_path), + }); + } else { + log::debug!( + "find_devcontainer_configs: Subfolder {} has no devcontainer.json", + subfolder_name + ); + } } } } - } - log::info!( - "find_devcontainer_configs: Found {} configurations", - configs.len() - ); + log::info!( + "find_devcontainer_configs: Found {} configurations", + configs.len() + ); - configs.sort_by(|a, b| { - if a.name == "default" { - std::cmp::Ordering::Less - } else if b.name == "default" { - std::cmp::Ordering::Greater - } else { - a.name.cmp(&b.name) - } - }); + configs.sort_by(|a, b| { + if a.name == "default" { + std::cmp::Ordering::Less + } else if b.name == "default" { + std::cmp::Ordering::Greater + } else { + a.name.cmp(&b.name) + } + }); + + configs + }) else { + log::debug!("find_devcontainer_configs: Failed to update workspace"); + return Vec::new(); + }; configs } pub async fn start_dev_container_with_config( - context: DevContainerContext, + cx: &mut AsyncWindowContext, + node_runtime: NodeRuntime, config: Option, ) -> Result<(DevContainerConnection, String), DevContainerError> { - 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 use_podman = use_podman(cx); + check_for_docker(use_podman).await?; - match devcontainer_up(&context, &cli, config_path.as_deref()).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); + }; + + 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 + { Ok(DevContainerUp { container_id, remote_workspace_folder, remote_user, .. }) => { - 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 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 connection = DevContainerConnection { name: project_name, - container_id, - use_podman: context.use_podman, + container_id: container_id, + use_podman, remote_user, }; @@ -290,9 +355,9 @@ async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> { } } -pub(crate) async fn ensure_devcontainer_cli( +async fn ensure_devcontainer_cli( node_runtime: &NodeRuntime, -) -> Result { +) -> Result<(PathBuf, bool), DevContainerError> { let mut command = util::command::new_smol_command(&dev_container_cli()); command.arg("--version"); @@ -330,10 +395,7 @@ pub(crate) async fn ensure_devcontainer_cli( Ok(output) => { if output.status.success() { log::info!("Found devcontainer CLI in Data dir"); - return Ok(DevContainerCli { - path: datadir_cli_path.clone(), - node_runtime_path: Some(node_runtime_path.clone()), - }); + return Ok((datadir_cli_path.clone(), false)); } else { log::error!( "Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}", @@ -373,29 +435,32 @@ pub(crate) async fn ensure_devcontainer_cli( ); Err(DevContainerError::DevContainerCliNotAvailable) } else { - Ok(DevContainerCli { - path: datadir_cli_path, - node_runtime_path: Some(node_runtime_path), - }) + Ok((datadir_cli_path, false)) } } else { log::info!("Found devcontainer cli on $PATH, using it"); - Ok(DevContainerCli { - path: PathBuf::from(&dev_container_cli()), - node_runtime_path: None, - }) + Ok((PathBuf::from(&dev_container_cli()), true)) } } async fn devcontainer_up( - context: &DevContainerContext, - cli: &DevContainerCli, - config_path: Option<&Path>, + path_to_cli: &PathBuf, + found_in_path: bool, + node_runtime: &NodeRuntime, + path: Arc, + config_path: Option, + use_podman: bool, ) -> Result { - let mut command = cli.command(context.use_podman); + 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); command.arg("up"); command.arg("--workspace-folder"); - command.arg(context.project_directory.display().to_string()); + command.arg(path.display().to_string()); if let Some(config) = config_path { command.arg("--config"); @@ -428,15 +493,24 @@ async fn devcontainer_up( } } -pub(crate) async fn read_devcontainer_configuration( - context: &DevContainerContext, - cli: &DevContainerCli, - config_path: Option<&Path>, +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, ) -> Result { - let mut command = cli.command(context.use_podman); + 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); command.arg("read-configuration"); command.arg("--workspace-folder"); - command.arg(context.project_directory.display().to_string()); + command.arg(path.display().to_string()); if let Some(config) = config_path { command.arg("--config"); @@ -466,14 +540,23 @@ pub(crate) async fn read_devcontainer_configuration( } } -pub(crate) async fn apply_dev_container_template( +async fn devcontainer_template_apply( template: &DevContainerTemplate, template_options: &HashMap, features_selected: &HashSet, - context: &DevContainerContext, - cli: &DevContainerCli, + path_to_cli: &PathBuf, + found_in_path: bool, + node_runtime: &NodeRuntime, + path: &Arc, + use_podman: bool, ) -> Result { - let mut command = cli.command(context.use_podman); + 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 Ok(serialized_options) = serde_json::to_string(template_options) else { log::error!("Unable to serialize options for {:?}", template_options); @@ -483,7 +566,7 @@ pub(crate) async fn apply_dev_container_template( command.arg("templates"); command.arg("apply"); command.arg("--workspace-folder"); - command.arg(context.project_directory.display().to_string()); + command.arg(path.display().to_string()); command.arg("--template-id"); command.arg(format!( "{}/{}", @@ -547,6 +630,28 @@ 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() @@ -555,6 +660,22 @@ 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() @@ -580,160 +701,7 @@ fn template_features_to_json(features_selected: &HashSet) - #[cfg(test)] mod tests { - use crate::devcontainer_api::{DevContainerUp, find_devcontainer_configs, parse_json_from_cli}; - use fs::FakeFs; - use gpui::TestAppContext; - use project::Project; - use serde_json::json; - use settings::SettingsStore; - use workspace::Workspace; - - 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); - }); - } - - #[gpui::test] - async fn test_find_devcontainer_configs_no_devcontainer_dir(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "src": { "main.rs": "fn main() {}" } - }), - ) - .await; - - let project = Project::test(fs, ["/project".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx)); - assert!( - configs.is_empty(), - "Expected no configs when .devcontainer dir is absent, got: {configs:?}" - ); - } - - #[gpui::test] - async fn test_find_devcontainer_configs_single_default(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - ".devcontainer": { - "devcontainer.json": r#"{"image": "ubuntu"}"# - } - }), - ) - .await; - - let project = Project::test(fs, ["/project".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx)); - assert_eq!( - configs.len(), - 1, - "Expected exactly one config, got: {configs:?}" - ); - assert_eq!(configs[0].name, "default"); - assert_eq!( - configs[0].config_path.to_str().unwrap(), - ".devcontainer/devcontainer.json" - ); - } - - #[gpui::test] - async fn test_find_devcontainer_configs_multiple_subfolders(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - ".devcontainer": { - "python": { "devcontainer.json": r#"{}"# }, - "node": { "devcontainer.json": r#"{}"# } - } - }), - ) - .await; - - let project = Project::test(fs, ["/project".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx)); - assert_eq!(configs.len(), 2, "Expected two configs, got: {configs:?}"); - assert_eq!(configs[0].name, "node"); - assert_eq!(configs[1].name, "python"); - } - - #[gpui::test] - async fn test_find_devcontainer_configs_default_plus_subfolders(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - ".devcontainer": { - "devcontainer.json": r#"{}"#, - "python": { "devcontainer.json": r#"{}"# }, - "node": { "devcontainer.json": r#"{}"# } - } - }), - ) - .await; - - let project = Project::test(fs, ["/project".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx)); - assert_eq!(configs.len(), 3, "Expected three configs, got: {configs:?}"); - assert_eq!( - configs[0].name, "default", - "Default config should be sorted first" - ); - assert_eq!(configs[1].name, "node"); - assert_eq!(configs[2].name, "python"); - } - - #[gpui::test] - async fn test_find_devcontainer_configs_subfolder_without_json(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - ".devcontainer": { - "devcontainer.json": r#"{}"#, - "has_config": { "devcontainer.json": r#"{}"# }, - "no_config": { "README.md": "not a devcontainer" } - } - }), - ) - .await; - - let project = Project::test(fs, ["/project".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx)); - assert_eq!( - configs.len(), - 2, - "Subfolder without devcontainer.json should be skipped, got: {configs:?}" - ); - assert_eq!(configs[0].name, "default"); - assert_eq!(configs[1].name, "has_config"); - } + use crate::devcontainer_api::{DevContainerUp, parse_json_from_cli}; #[test] fn should_parse_from_devcontainer_json() { diff --git a/crates/dev_container/src/lib.rs b/crates/dev_container/src/lib.rs index 2a260f8895cb40b235851e0e800809dc5ae10200..699285e074f325bea78a240c1a2a696cfc578929 100644 --- a/crates/dev_container/src/lib.rs +++ b/crates/dev_container/src/lib.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use gpui::AppContext; use gpui::Entity; use gpui::Task; @@ -43,8 +41,7 @@ use http_client::{AsyncBody, HttpClient}; mod devcontainer_api; -use devcontainer_api::ensure_devcontainer_cli; -use devcontainer_api::read_devcontainer_configuration; +use devcontainer_api::read_devcontainer_configuration_for_project; use crate::devcontainer_api::DevContainerError; use crate::devcontainer_api::apply_dev_container_template; @@ -53,34 +50,11 @@ pub use devcontainer_api::{ DevContainerConfig, find_devcontainer_configs, 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 { @@ -1445,41 +1419,22 @@ fn dispatch_apply_templates( cx: &mut Context, ) { cx.spawn_in(window, async move |this, cx| { - 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 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() + }); - { if check_for_existing - && read_devcontainer_configuration(&context, &cli, None) + && read_devcontainer_configuration_for_project(cx, &node_runtime) .await .is_ok() { @@ -1498,8 +1453,8 @@ fn dispatch_apply_templates( &template_entry.template, &template_entry.options_selected, &template_entry.features_selected, - &context, - &cli, + cx, + &node_runtime, ) .await { @@ -1541,6 +1496,8 @@ 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 42139b697d40362578bac4fae6b58d2a1ca10b27..6360e868d88ddeec677935beeba536d04cbc9131 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) = Workspace::for_window(window, cx) { + if let Some(workspace) = window.root::().flatten() { workspace.update(cx, |workspace, cx| { workspace .open_path( diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs index cd95929e206696cd13942e9e37092ee8d621d0f2..8835dd5507dc9deccb57ad4f4ba15d8af017bfd3 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) = Workspace::for_window(window, cx) { + if let Some(workspace) = window.root::().flatten() { workspace.update(cx, |workspace, cx| { let copilot = copilot.clone(); workspace.show_toast( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4ac8f39e325626ae40b06567e8cdc87b61274f5c..3ff0ee300f70f9546f6be0289d946f35c026095e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3105,24 +3105,6 @@ 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 @@ -11479,8 +11461,8 @@ impl Editor { let Some(project) = self.project.clone() else { return; }; - let task = self.reload(project, window, cx); - self.detach_and_notify_err(task, window, cx); + self.reload(project, window, cx) + .detach_and_notify_err(window, cx); } pub fn restore_file( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 340610faa984d993728416d4b026bd67e2809003..d1fd99f09217dc736c12c3f8902fc2bdb777e03d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -99,6 +99,7 @@ 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. @@ -540,21 +541,21 @@ impl EditorElement { register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.format(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.format_selections(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.organize_imports(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } @@ -564,49 +565,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) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_completion_replace(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_completion_insert(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.compose_completion(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_code_action(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.rename(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } }); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.confirm_rename(action, window, cx) { - editor.detach_and_notify_err(task, window, cx); + task.detach_and_notify_err(window, cx); } else { cx.propagate(); } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index b3039a1545f634302e2767f3c0a2073f3a772827..1e8b42988cc7abaa4fb8a55e3580a70566d8046c 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) = Workspace::for_window(window, cx) + && let Some(workspace) = window.root::().flatten() { workspace.update(cx, |workspace, cx| { let task = workspace.open_abs_path( diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index d1e5270d6c76e166a33a41df2843138f4b12c411..e372fdbe4ac93325532b96e43f11d501977418d4 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, MultiWorkspace, Workspace, WorkspaceHandle}; +use workspace::{AppState, Workspace, WorkspaceHandle}; use super::editor_test_context::{AssertionContextManager, EditorTestContext}; @@ -95,8 +95,7 @@ impl EditorLspTestContext { ) .await; - let window = - cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let workspace = window.root(cx).unwrap(); @@ -107,20 +106,12 @@ impl EditorLspTestContext { }) .await .unwrap(); - 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()); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let item = workspace .update_in(&mut cx, |workspace, window, cx| { - workspace.workspace().update(cx, |workspace, cx| { - workspace.open_path(file, None, true, window, cx) - }) + workspace.open_path(file, None, true, window, cx) }) .await .expect("Could not open test file"); @@ -130,8 +121,6 @@ impl EditorLspTestContext { }); editor.update_in(&mut cx, |editor, window, cx| { let nav_history = workspace - .read(cx) - .workspace() .read(cx) .active_pane() .read(cx) @@ -145,8 +134,6 @@ 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 2ecc4eb0d277fa94885a125bdce1e6fb5028370f..43f4898da40e4a3f8edfb973ad2a4c5a1c79a1fe 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -16,10 +16,6 @@ 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 71fcfba76363f7d6a3d6d5d37d8f87f3b6a6cdfb..73533c57f156cdfba04ca736eeed5b0d23d2ee8f 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1566,12 +1566,9 @@ 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 |_, mut cx| { - let item = open_task - .await - .notify_workspace_async_err(workspace, &mut cx)?; + cx.spawn_in(window, async move |_, cx| { + let item = open_task.await.notify_async_err(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 7a3dd2342e988790c2ca1bb1d3664e18148f74e3..1b1421e8b8978a55d89db746b894486888342a65 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -9,9 +9,7 @@ use project::{FS_WATCH_LATENCY, RemoveOptions}; use serde_json::json; use settings::SettingsStore; use util::{path, rel_path::rel_path}; -use workspace::{ - AppState, CloseActiveItem, MultiWorkspace, OpenOptions, ToggleFileFinder, Workspace, open_paths, -}; +use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths}; #[ctor::ctor] fn init_logger() { @@ -2536,14 +2534,8 @@ 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 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(); + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.update(|_, cx| { open_paths( diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 7eee1ce7640784fd37efe69b5f6f92b7cbc438ec..79f581777485b08952b95f2097f2e7083de35c98 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(workspace.weak_handle(), window, cx); + .detach_and_notify_err(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 229ebb5f31baa9735e7fc2fe9455d13cb17365f1..1967254db664bf4a1f52c25295a7860fdbba82da 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, WeakEntity, Window, + Focusable, IntoElement, Render, Task, Window, }; use language::{Buffer, LanguageRegistry}; use project::Project; @@ -39,10 +39,11 @@ impl FileDiffView { pub fn open( old_path: PathBuf, new_path: PathBuf, - workspace: WeakEntity, + workspace: &Workspace, 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 @@ -405,7 +406,7 @@ mod tests { FileDiffView::open( path!("/test/old_file.txt").into(), path!("/test/new_file.txt").into(), - workspace.weak_handle(), + workspace, window, cx, ) @@ -539,7 +540,7 @@ mod tests { FileDiffView::open( PathBuf::from(path!("/test/old_file.txt")), PathBuf::from(path!("/test/new_file.txt")), - workspace.weak_handle(), + workspace, window, cx, ) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 614e395f35bbb13e4f92d5669cc06a79eb284a0d..a4d26bc5a5a995f7bbc7af7df1cfef18dfe0b3d8 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1274,11 +1274,10 @@ impl GitPanel { }) .ok()?; - let workspace = self.workspace.clone(); cx.spawn_in(window, async move |_, mut cx| { let item = open_task .await - .notify_workspace_async_err(workspace, &mut cx) + .notify_async_err(&mut cx) .ok_or_else(|| anyhow::anyhow!("Failed to open file"))?; if let Some(active_editor) = item.downcast::() { if let Some(diff_task) = diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 34c429b6549f400d33a46bf4f743fa6ed04e1db6..ccda40c98a50c05907534e2f853f7678cb52fb9e 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -124,7 +124,6 @@ impl ProjectDiff { return; } let workspace = cx.entity(); - let workspace_weak = workspace.downgrade(); window .spawn(cx, async move |cx| { let this = cx @@ -139,7 +138,7 @@ impl ProjectDiff { .ok(); anyhow::Ok(()) }) - .detach_and_notify_err(workspace_weak, window, cx); + .detach_and_notify_err(window, cx); } pub fn deploy_at( diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index a14336e0058ea64b8a78deae78d98df9c34d3dd9..f65eb80e582e20121d4aab2a6b2471784ade45a5 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, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, + Action, App, AsyncApp, 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, MultiWorkspace, Workspace, notifications::DetachAndPromptErr}; +use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr}; actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]); @@ -289,6 +289,7 @@ 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 { @@ -354,7 +355,7 @@ impl WorktreeListDelegate { connection_options, vec![new_worktree_path], app_state, - workspace.clone(), + window_handle, replace_current_window, cx, ) @@ -406,12 +407,13 @@ 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, - workspace, + window_handle, replace_current_window, cx, ) @@ -439,16 +441,15 @@ async fn open_remote_worktree( connection_options: RemoteConnectionOptions, paths: Vec, app_state: Arc, - workspace: WeakEntity, + window: gpui::AnyWindowHandle, replace_current_window: bool, - cx: &mut AsyncWindowContext, + cx: &mut AsyncApp, ) -> anyhow::Result<()> { - let workspace_window = cx - .window_handle() - .downcast::() + let workspace_window = window + .downcast::() .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?; - let connect_task = workspace.update_in(cx, |workspace, window, cx| { + let connect_task = workspace_window.update(cx, |workspace, window, cx| { workspace.toggle_modal(window, cx, |window, cx| { RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx) }); @@ -472,19 +473,17 @@ async fn open_remote_worktree( let session = connect_task.await; - workspace - .update_in(cx, |workspace, _window, cx| { - if let Some(prompt) = workspace.active_modal::(cx) { - prompt.update(cx, |prompt, cx| prompt.finished(cx)) - } - }) - .ok(); + workspace_window.update(cx, |workspace, _window, cx| { + if let Some(prompt) = workspace.active_modal::(cx) { + prompt.update(cx, |prompt, cx| prompt.finished(cx)) + } + })?; 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(), @@ -495,30 +494,29 @@ 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| { - let workspace = cx.new(|cx| { + 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/icons/src/icons.rs b/crates/icons/src/icons.rs index 054f95ef549697420e4a600d363e716424401a29..42a8360f4738e3e7440f22ffc77a19c1588fb4a0 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -265,8 +265,6 @@ 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 53d2f74b9c663496da083152ead17d479f5030eb..b74b20191c2668e59b0ad44d3a8ccce165c5cba7 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -18,6 +18,7 @@ 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 f1f3ed8d38e4f0947741a0eeb72481e225904929..be5bb14ff84da92b7e64baa588a20de345c2442f 100644 --- a/crates/inspector_ui/src/inspector.rs +++ b/crates/inspector_ui/src/inspector.rs @@ -1,7 +1,8 @@ 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::*, utils::platform_title_bar_height}; +use ui::{Label, Tooltip, prelude::*}; use util::{ResultExt as _, command::new_smol_command}; use workspace::AppState; @@ -60,7 +61,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 = platform_title_bar_height(window); + let toolbar_height = PlatformTitleBar::height(window); v_flex() .size_full() diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index ba97bcf66a77659fb3196ba45ebb3f831452e008..f43949c0051f56559388203e387a540b8c593467 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -118,20 +118,17 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap })? .await?; new_workspace - .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, - ) - }) + .update(cx, |workspace, window, 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 ff3389a4d4a10bc8472d0931d18ffa5be839c631..de22ae01b503fde9aabdd99be5253d7c4e3f1b71 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(self.workspace.clone(), window, cx); + .detach_and_notify_err(window, cx); } fn copy_context_to_clipboard( diff --git a/crates/miniprofiler_ui/src/miniprofiler_ui.rs b/crates/miniprofiler_ui/src/miniprofiler_ui.rs index 697027570a46afc550fd4f96d6a204e7e8c23f27..cf916c0b5415d9643c9609715d66f77a98ba7222 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, WeakEntity, WindowBounds, + Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WindowBounds, WindowHandle, WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list, }; use util::ResultExt; @@ -22,10 +22,13 @@ use workspace::{ use zed_actions::OpenPerformanceProfiler; pub fn init(startup_time: Instant, cx: &mut App) { - 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); + 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); }); }) .detach(); @@ -33,8 +36,8 @@ pub fn init(startup_time: Instant, cx: &mut App) { fn open_performance_profiler( startup_time: Instant, - workspace_handle: WeakEntity, - _window: &mut gpui::Window, + _workspace: &mut workspace::Workspace, + workspace_handle: WindowHandle, cx: &mut App, ) { let existing_window = cx @@ -45,7 +48,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.clone()); + profiler_window.workspace = Some(workspace_handle); window.activate_window(); }) .log_err(); @@ -94,14 +97,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 { @@ -277,7 +280,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.as_ref() else { + let Some(workspace) = this.workspace else { return; }; @@ -294,7 +297,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 866df0e9d91e75b3522f957f54d05db7614c7e5a..495a55411fc936d476dfa0d443e155d1fa7faecd 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -238,16 +238,15 @@ impl Onboarding { go_to_welcome_page(cx); } - fn handle_sign_in(&mut self, _: &SignIn, window: &mut Window, cx: &mut Context) { + fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) { let client = Client::global(cx); - let workspace = self.workspace.clone(); window - .spawn(cx, async move |mut cx| { + .spawn(cx, async move |cx| { client - .sign_in_with_optional_connect(true, &cx) + .sign_in_with_optional_connect(true, cx) .await - .notify_workspace_async_err(workspace, &mut cx); + .notify_async_err(cx); }) .detach(); } @@ -275,7 +274,7 @@ impl Render for Onboarding { .size_full() .bg(cx.theme().colors().editor_background) .on_action(Self::on_finish) - .on_action(cx.listener(Self::handle_sign_in)) + .on_action(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/outline/src/outline.rs b/crates/outline/src/outline.rs index 7344a7369f56887274c42467af2e287093ffd619..9e6cc045a76204c71bd5812d002a873cfc5dd461 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -41,15 +41,15 @@ pub fn toggle( window: &mut Window, cx: &mut App, ) { - if let Some((workspace, outline)) = Workspace::for_window(window, cx).and_then(|workspace| { - editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - .outline(Some(cx.theme().syntax())) - .map(|outline| (workspace, outline)) - }) { + let outline = editor + .read(cx) + .buffer() + .read(cx) + .snapshot(cx) + .outline(Some(cx.theme().syntax())); + + let workspace = window.root::().flatten(); + if let Some((workspace, outline)) = workspace.zip(outline) { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { OutlineView::new(outline, editor, window, cx) @@ -396,7 +396,7 @@ mod tests { use project::{FakeFs, Project}; use serde_json::json; use util::{path, rel_path::rel_path}; - use workspace::{AppState, MultiWorkspace, Workspace}; + use workspace::{AppState, Workspace}; #[gpui::test] async fn test_outline_view_row_highlights(cx: &mut TestAppContext) { @@ -424,9 +424,7 @@ mod tests { }); let (workspace, cx) = - cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); - - let workspace = cx.read(|cx| workspace.read(cx).workspace().clone()); + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); 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/platform_title_bar/Cargo.toml b/crates/platform_title_bar/Cargo.toml index 2f1f6d2cd9297136077780aafdc75d22ecf6b845..a8db1e37f206b90ca1cc18f933d5ab20ff45cdf1 100644 --- a/crates/platform_title_bar/Cargo.toml +++ b/crates/platform_title_bar/Cargo.toml @@ -13,7 +13,6 @@ 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 8d63da7448f07b3f4d4901a341dd5184b4352da5..d53e8ae86cdba32b33e6959032667f9748de871e 100644 --- a/crates/platform_title_bar/src/platform_title_bar.rs +++ b/crates/platform_title_bar/src/platform_title_bar.rs @@ -1,21 +1,16 @@ mod platforms; mod system_window_tabs; -use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; use gpui::{ - AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, - MouseButton, ParentElement, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, - px, + AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, + ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, }; use smallvec::SmallVec; use std::mem; -use ui::{ - prelude::*, - utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height}, -}; +use ui::prelude::*; use crate::{ - platforms::{platform_linux, platform_windows}, + platforms::{platform_linux, platform_mac, platform_windows}, system_window_tabs::SystemWindowTabs, }; @@ -29,8 +24,6 @@ pub struct PlatformTitleBar { children: SmallVec<[AnyElement; 2]>, should_move: bool, system_window_tabs: Entity, - workspace_sidebar_open: bool, - sidebar_has_notifications: bool, } impl PlatformTitleBar { @@ -44,11 +37,20 @@ 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 { @@ -71,46 +73,17 @@ 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 = platform_title_bar_height(window); + let height = Self::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() @@ -159,10 +132,8 @@ impl Render for PlatformTitleBar { .map(|this| { if window.is_fullscreen() { this.pl_2() - } else if self.platform_style == PlatformStyle::Mac - && !is_multiworkspace_sidebar_open - { - this.pl(px(TRAFFIC_LIGHT_PADDING)) + } else if self.platform_style == PlatformStyle::Mac { + this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING)) } else { this.pl_2() } diff --git a/crates/platform_title_bar/src/platforms.rs b/crates/platform_title_bar/src/platforms.rs index 26e9c4b4f044eff172d165e3851279fa07c3a269..67e87d45ea5d290077af1326e613c6819e0f41dc 100644 --- a/crates/platform_title_bar/src/platforms.rs +++ b/crates/platform_title_bar/src/platforms.rs @@ -1,2 +1,3 @@ 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 new file mode 100644 index 0000000000000000000000000000000000000000..5e8e4e5087054e59f66527915ae97e352a9ff525 --- /dev/null +++ b/crates/platform_title_bar/src/platforms/platform_mac.rs @@ -0,0 +1,10 @@ +// 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 b1c07a3f94f1317dd5169b68072cc701c3fde548..76dd4abe2f90e285e85ea8778c5ad785e1bbfab5 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -772,11 +772,7 @@ impl ProjectPanel { { match project_panel.confirm_edit(false, window, cx) { Some(task) => { - task.detach_and_notify_err( - project_panel.workspace.clone(), - window, - cx, - ); + task.detach_and_notify_err(window, cx); } None => { project_panel.discard_edit_state(window, cx); @@ -1652,7 +1648,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(self.workspace.clone(), window, cx); + task.detach_and_notify_err(window, cx); } } @@ -3037,25 +3033,20 @@ impl ProjectPanel { } let item_count = paste_tasks.len(); - let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |project_panel, mut cx| { + cx.spawn_in(window, async move |project_panel, 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_workspace_async_err(workspace.clone(), &mut cx) + if let Some(CreatedEntry::Included(entry)) = + task.await.notify_async_err(cx) { last_succeed = Some(entry); } } PasteTask::Copy(task) => { - if let Some(Some(entry)) = task - .await - .notify_workspace_async_err(workspace.clone(), &mut cx) - { + if let Some(Some(entry)) = task.await.notify_async_err(cx) { last_succeed = Some(entry); } } @@ -3397,7 +3388,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.weak_handle(), window, cx) + FileDiffView::open(file_path1, file_path2, workspace, window, cx) .detach_and_log_err(cx); }) .ok(); diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 7b605084d6213ef17ffac83d782bb02bc4213e27..9ef3250e315d00bf4b6f669b9a5313ea3251a5fe 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -23,7 +23,6 @@ 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 @@ -67,7 +66,6 @@ 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 82ff0699054e5614b8078d3223d5e9282e5034b5..f45673c38dbad1abab717b3f9f1081a2ffae4bd2 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -7,9 +7,7 @@ use ui::{ HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal, ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems, }; -use workspace::{ - ModalView, MultiWorkspace, OpenOptions, Workspace, notifications::DetachAndPromptErr, -}; +use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr}; use crate::open_remote_project; @@ -111,7 +109,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 c0a22b43e37a55ac5a3380b1d5e903ea5b06b80e..7f67627ad4d6841419292e58b5b41a5fd5ef7d5b 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -4,9 +4,7 @@ mod remote_connections; mod remote_servers; mod ssh_config; -use std::{path::PathBuf, sync::Arc}; - -use fs::Fs; +use std::path::PathBuf; #[cfg(target_os = "windows")] mod wsl_picker; @@ -29,11 +27,11 @@ use picker::{ pub use remote_connections::RemoteSettings; pub use remote_servers::RemoteServerProjects; use settings::Settings; -use std::path::Path; +use std::{path::Path, sync::Arc}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; use workspace::{ - HistoryManager, ModalView, MultiWorkspace, OpenOptions, PathList, SerializedWorkspaceLocation, + CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, WORKSPACE_DB, Workspace, WorkspaceId, notifications::DetachAndPromptErr, with_active_or_new_workspace, }; @@ -50,10 +48,9 @@ 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(fs.as_ref()) + .recent_workspaces_on_disk() .await .unwrap_or_default(); @@ -179,7 +176,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() }; @@ -235,8 +232,10 @@ pub fn init(cx: &mut App) { cx.on_action(|_: &OpenDevContainer, cx| { with_active_or_new_workspace(cx, move |workspace, window, cx| { - if !workspace.project().read(cx).is_local() { - cx.spawn_in(window, async move |_, cx| { + let is_local = workspace.project().read(cx).is_local(); + + cx.spawn_in(window, async move |_, cx| { + if !is_local { cx.prompt( gpui::PromptLevel::Critical, "Cannot open Dev Container from remote project", @@ -245,16 +244,21 @@ pub fn init(cx: &mut App) { ) .await .ok(); - }) - .detach(); - return; - } + 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) - }); + 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(); }); }); @@ -330,7 +334,6 @@ impl ModalView for RecentProjects {} impl RecentProjects { fn new( delegate: RecentProjectsDelegate, - fs: Option>, rem_width: f32, window: &mut Window, cx: &mut Context, @@ -347,9 +350,8 @@ 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(fs.as_ref()) + .recent_workspaces_on_disk() .await .log_err() .unwrap_or_default(); @@ -359,7 +361,7 @@ impl RecentProjects { picker.update_matches(picker.query(cx), window, cx) }) }) - .ok(); + .ok() }) .detach(); Self { @@ -377,11 +379,10 @@ 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, fs, 34., window, cx) + Self::new(delegate, 34., window, cx) }) } @@ -392,13 +393,10 @@ 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, fs, 34., window, cx); + let list = Self::new(delegate, 34., window, cx); list.picker.focus_handle(cx).focus(window, cx); list }) @@ -582,21 +580,27 @@ impl PickerDelegate for RecentProjectsDelegate { SerializedWorkspaceLocation::Local => { let paths = candidate_workspace_paths.paths().to_vec(); if replace_current_window { - 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; + 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(()) + } + }) } else { workspace.open_workspace_for_paths(false, paths, window, cx) } @@ -605,7 +609,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 }; @@ -880,18 +884,10 @@ 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| { - WORKSPACE_DB - .delete_workspace_by_id(workspace_id) - .await - .log_err(); - let Some(fs) = fs else { return }; + let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk(fs.as_ref()) + .recent_workspaces_on_disk() .await .unwrap_or_default(); this.update_in(cx, move |picker, window, cx| { @@ -908,7 +904,6 @@ impl RecentProjectsDelegate { .update(cx, |this, cx| this.delete_history(workspace_id, cx)); } }) - .ok(); }) .detach(); } @@ -956,7 +951,7 @@ mod tests { use super::*; #[gpui::test] - async fn test_dirty_workspace_survives_when_opening_recent_project(cx: &mut TestAppContext) { + async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { @@ -980,11 +975,6 @@ 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"))], @@ -997,40 +987,31 @@ mod tests { .unwrap(); assert_eq!(cx.update(|cx| cx.windows().len()), 1); - 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()) - }) + let workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + workspace + .update(cx, |workspace, _, _| assert!(!workspace.is_edited())) .unwrap(); - let editor = multi_workspace - .read_with(cx, |multi_workspace, cx| { - multi_workspace - .workspace() - .read(cx) + let editor = workspace + .read_with(cx, |workspace, cx| { + workspace .active_item(cx) .unwrap() .downcast::() .unwrap() }) .unwrap(); - multi_workspace + workspace .update(cx, |_, window, cx| { editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); }) .unwrap(); - 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" - ) - }) + workspace + .update(cx, |workspace, _, _| assert!(workspace.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(&multi_workspace, cx); - multi_workspace + let recent_projects_picker = open_recent_projects(&workspace, cx); + workspace .update(cx, |_, _, cx| { recent_projects_picker.update(cx, |picker, cx| { assert_eq!(picker.query(cx), ""); @@ -1054,64 +1035,47 @@ mod tests { !cx.has_pending_prompt(), "Should have no pending prompt on dirty project before opening the new recent project" ); - 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| { + cx.dispatch_action(*workspace, menu::Confirm); + workspace + .update(cx, |workspace, _, cx| { assert!( - multi_workspace - .workspace() - .read(cx) - .active_modal::(cx) - .is_none(), + workspace.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(), - "No save prompt in multi-workspace mode — dirty workspace survives in background" + "Should have no pending prompt after cancelling" ); + workspace + .update(cx, |workspace, _, _| { + assert!( + workspace.is_edited(), + "Should be in the same dirty project after cancelling" + ) + }) + .unwrap(); } fn open_recent_projects( - multi_workspace: &WindowHandle, + workspace: &WindowHandle, cx: &mut TestAppContext, ) -> Entity> { cx.dispatch_action( - (*multi_workspace).into(), + (*workspace).into(), OpenRecent { create_new_window: false, }, ); - multi_workspace - .update(cx, |multi_workspace, _, cx| { - multi_workspace - .workspace() - .read(cx) + workspace + .update(cx, |workspace, _, cx| { + workspace .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 52304c211a4d38ef1e408093d1fbdc3c8f07c1bf..9d6199786cb6f251a792fa32e8caccd9351d00d3 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, MultiWorkspace, Workspace}; +use workspace::{AppState, Workspace}; pub use remote_connection::{ RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader, @@ -131,11 +131,8 @@ pub async fn open_remote_project( cx: &mut AsyncApp, ) -> Result<()> { let created_new_window = open_options.replace_window.is_none(); - 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) + let window = if let Some(window) = open_options.replace_window { + window } else { let workspace_position = cx .update(|cx| { @@ -148,7 +145,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; - let window = cx.open_window(options, |window, cx| { + cx.open_window(options, |window, cx| { let project = project::Project::local( app_state.client.clone(), app_state.node_runtime.clone(), @@ -162,17 +159,12 @@ pub async fn open_remote_project( }, cx, ); - let workspace = cx.new(|cx| { + 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 { @@ -180,38 +172,35 @@ pub async fn open_remote_project( let delegate = window.update(cx, { let paths = paths.clone(); let connection_options = connection_options.clone(); - let initial_workspace = initial_workspace.clone(); - move |_multi_workspace: &mut MultiWorkspace, window, cx| { + move |workspace, window, cx| { window.activate_window(); - 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 - }, - ))) - }) + 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 + }, + ))) } })?; @@ -220,11 +209,13 @@ pub async fn open_remote_project( let connection = remote::connect(connection_options.clone(), delegate.clone(), cx); let connection = select! { _ = cancel_rx => { - initial_workspace.update(cx, |workspace, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }); + window + .update(cx, |workspace, _, cx| { + if let Some(ui) = workspace.active_modal::(cx) { + ui.update(cx, |modal, cx| modal.finished(cx)) + } + }) + .ok(); break; }, @@ -233,11 +224,13 @@ pub async fn open_remote_project( let remote_connection = match connection { Ok(connection) => connection, Err(e) => { - initial_workspace.update(cx, |workspace, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }); + window + .update(cx, |workspace, _, cx| { + if let Some(ui) = workspace.active_modal::(cx) { + ui.update(cx, |modal, cx| modal.finished(cx)) + } + }) + .ok(); log::error!("Failed to open project: {e:#}"); let response = window .update(cx, |_, window, cx| { @@ -291,11 +284,13 @@ pub async fn open_remote_project( }) .await; - initial_workspace.update(cx, |workspace, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }); + window + .update(cx, |workspace, _, cx| { + if let Some(ui) = workspace.active_modal::(cx) { + ui.update(cx, |modal, cx| modal.finished(cx)) + } + }) + .ok(); match opened_items { Err(e) => { @@ -325,20 +320,20 @@ pub async fn open_remote_project( continue; } - 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, - ); - }); + 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(); } Ok(items) => { @@ -371,20 +366,14 @@ 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, |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)); - } + .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(()) @@ -511,16 +500,12 @@ mod tests { let windows = cx.update(|cx| cx.windows().len()); assert_eq!(windows, 1, "Should have opened a window"); - let multi_workspace_handle = - cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + let workspace_handle = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - 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"); - }); + workspace_handle + .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 921b19686ab49bb4704fa72b376fc8370b8f354b..5b719940f958ac0e4ecb6e186052e3e09987f80e 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -6,8 +6,7 @@ use crate::{ ssh_config::parse_ssh_config_hosts, }; use dev_container::{ - DevContainerConfig, DevContainerContext, find_devcontainer_configs, - start_dev_container_with_config, + DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config, }; use editor::Editor; @@ -52,7 +51,7 @@ use util::{ rel_path::RelPath, }; use workspace::{ - ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace, + ModalView, OpenLog, OpenOptions, Toast, Workspace, notifications::{DetachAndPromptErr, NotificationId}, open_remote_project_with_existing_connection, }; @@ -479,11 +478,10 @@ impl ProjectPicker { .log_err()?; let window = cx .open_window(options, |window, cx| { - let workspace = cx.new(|cx| { + 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()?; @@ -810,18 +808,11 @@ impl RemoteServerProjects { workspace: WeakEntity, cx: &mut Context, ) -> Self { - 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)), + let this = Self::new_inner( + Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new( + DevContainerCreationProgress::Creating, + cx, + )), false, fs, window, @@ -829,15 +820,35 @@ impl RemoteServerProjects { cx, ); - 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); - } + // 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(); this } @@ -1540,9 +1551,7 @@ 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| { @@ -1794,25 +1803,25 @@ impl RemoteServerProjects { } fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context) { - let configs = self - .workspace - .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx)) - .unwrap_or_default(); + cx.spawn_in(window, async move |entity, cx| { + let configs = find_devcontainer_configs(cx); - 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))); + 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))); - 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); - } + let state = CreateRemoteDevContainer::new( + DevContainerCreationProgress::SelectingConfig, + cx, + ); + this.mode = Mode::CreateRemoteDevContainer(state); + cx.notify(); + }) + .log_err(); + }) + .detach(); } fn open_dev_container( @@ -1821,25 +1830,21 @@ impl RemoteServerProjects { window: &mut Window, cx: &mut Context, ) { - let Some((app_state, context)) = self + let Some(app_state) = self .workspace - .read_with(cx, |workspace, cx| { - let app_state = workspace.app_state().clone(); - let context = DevContainerContext::from_workspace(workspace, cx)?; - Some((app_state, context)) - }) + .read_with(cx, |workspace, _| workspace.app_state().clone()) .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(context, config).await { + match start_dev_container_with_config(cx, app_state.node_runtime.clone(), 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 7f2a69eb68cb93742d98f438f75f74c95bf3f7d5..e386b723fa43777e496565c11b8308f16031d837 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, MultiWorkspace}; +use workspace::{ModalView, Workspace}; 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/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index 4ad820feeca3eb86080097a56d2d5f368ec32a02..d8bd8869f28ac4a9bdf396073f8948d15aef9e3e 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -78,8 +78,9 @@ pub fn init(cx: &mut App) { return; } - cx.defer_in(window, |editor, _window, cx| { - let project = editor.project().cloned(); + cx.defer_in(window, |editor, window, cx| { + let workspace = Workspace::for_window(window, cx); + let project = workspace.map(|workspace| workspace.read(cx).project().clone()); let is_local_project = project .as_ref() diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 9a96f711de12b2891e84691c691fbc7460a35c35..30a986add52ec935aeb5752d9d2b2fc214d60a84 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -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::{MultiWorkspace, Workspace, WorkspaceSettings, client_side_decorations}; +use workspace::{Workspace, WorkspaceSettings, client_side_decorations}; use zed_actions::assistant::InlineAssist; use prompt_store::*; @@ -968,14 +968,12 @@ impl RulesLibrary { .assist(rule_editor, initial_prompt, window, cx); } else { for window in cx.windows() { - if let Some(multi_workspace) = window.downcast::() { - let panel = multi_workspace - .update(cx, |multi_workspace, window, cx| { + if let Some(workspace) = window.downcast::() { + let panel = workspace + .update(cx, |workspace, window, cx| { window.activate_window(); - multi_workspace.workspace().update(cx, |workspace, cx| { - self.inline_assist_delegate - .focus_agent_panel(workspace, window, cx) - }) + self.inline_assist_delegate + .focus_agent_panel(workspace, window, cx) }) .ok(); if panel == Some(true) { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index d2104492bebf529821f8ad8571fd3fbb8bdbc69e..ac337fb4c8f53e407178d9ccf1be7e91d89fadcb 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) = Workspace::for_window(window, cx) else { + let Some(workspace) = window.root::().flatten() else { return; }; workspace.update(cx, |workspace, cx| { diff --git a/crates/session/src/session.rs b/crates/session/src/session.rs index de6be034f9732f2c24dd860ebccd0c677d4fc623..687c9e20b7a4b92131d77470509a7e5f0b7193ce 100644 --- a/crates/session/src/session.rs +++ b/crates/session/src/session.rs @@ -47,15 +47,6 @@ 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 } @@ -118,11 +109,6 @@ 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 7ca91e3767efb6b550af7887e70a0187fed6daad..42d714283a4e1ed569bd03a5386ab16988a8014a 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, MultiWorkspace}; + use workspace::{self, AppState}; use zed_actions::settings_profile_selector; async fn init_test( @@ -320,11 +320,8 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, ["/test".as_ref()], cx).await; - 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(); + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); 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 25609e5eb9d14f18b1c63597f3765d61c1e3145d..d7327650fc636ca33c2bf35bd9f60d5ddcd78e49 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -40,9 +40,7 @@ use ui::{ }; use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; -use workspace::{ - AppState, MultiWorkspace, OpenOptions, OpenVisible, Workspace, client_side_decorations, -}; +use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations}; use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt}; use crate::components::{ @@ -396,7 +394,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); }, @@ -404,14 +402,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 +547,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 +715,7 @@ fn active_language_mut() -> Option>, - original_window: Option>, + original_window: Option>, files: Vec<(SettingsUiFile, FocusHandle)>, worktree_root_dirs: HashMap, current_file: SettingsUiFile, @@ -1449,7 +1447,7 @@ impl SettingsUiFile { impl SettingsWindow { fn new( - original_window: Option>, + original_window: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -1520,21 +1518,34 @@ impl SettingsWindow { .detach(); if let Some(app_state) = AppState::global(cx).upgrade() { - let workspaces: Vec> = app_state + for project in app_state .workspace_store .read(cx) .workspaces() - .filter_map(|weak| weak.upgrade()) - .collect(); - - for workspace in workspaces { - let project = workspace.read(cx).project().clone(); + .iter() + .filter_map(|space| { + space + .read(cx) + .ok() + .map(|workspace| workspace.project().clone()) + }) + .collect::>() + { 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) }) @@ -3309,19 +3320,56 @@ impl SettingsWindow { return; }; original_window - .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(); - }); + .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(); }) .ok(); @@ -3333,22 +3381,22 @@ impl SettingsWindow { return; }; - let Some((workspace_window, worktree, corresponding_workspace)) = app_state + let Some((worktree, corresponding_workspace)) = app_state .workspace_store .read(cx) - .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)| { + .workspaces() + .iter() + .find_map(|workspace| { workspace - .read(cx) - .project() - .read(cx) - .worktree_for_id(*worktree_id, cx) - .map(|worktree| (window, worktree, workspace)) + .read_with(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .worktree_for_id(*worktree_id, cx) + }) + .ok() + .flatten() + .zip(Some(*workspace)) }) else { log::error!( @@ -3376,15 +3424,14 @@ impl SettingsWindow { // TODO: move zed::open_local_file() APIs to this crate, and // re-implement the "initial_contents" behavior - let workspace_weak = corresponding_workspace.downgrade(); - workspace_window + corresponding_workspace .update(cx, |_, window, cx| { - cx.spawn_in(window, async move |_, cx| { + cx.spawn_in(window, async move |workspace, cx| { if let Some(create_task) = create_task { create_task.await.ok()?; }; - workspace_weak + workspace .update_in(cx, |workspace, window, cx| { workspace.open_path( (worktree_id, settings_path.clone()), @@ -3398,7 +3445,7 @@ impl SettingsWindow { .await .log_err()?; - workspace_weak + workspace .update_in(cx, |_, window, cx| { window.activate_window(); cx.notify(); @@ -3705,7 +3752,7 @@ impl Render for SettingsWindow { } fn all_projects( - window: Option<&WindowHandle>, + window: Option<&WindowHandle>, cx: &App, ) -> impl Iterator> { let mut seen_project_ids = std::collections::HashSet::new(); @@ -3716,19 +3763,10 @@ fn all_projects( .workspace_store .read(cx) .workspaces() - .filter_map(|weak| weak.upgrade()) - .map(|workspace: Entity| workspace.read(cx).project().clone()) + .iter() + .filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone())) .chain( - 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::>() - }), + window.and_then(|workspace| Some(workspace.read(cx).ok()?.project().clone())), ) .filter(move |project| seen_project_ids.insert(project.entity_id())) }) @@ -3736,51 +3774,6 @@ 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>, @@ -4761,33 +4754,29 @@ pub mod test { .await .expect("Failed to create worktree_c"); - 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, cx) = cx.add_window_view(|window, cx| { + Workspace::new( + Default::default(), + project1.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 _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 workspace2_handle = cx.window_handle().downcast::().unwrap(); + let workspace2_handle = cx.window_handle().downcast::().unwrap(); cx.run_until_parked(); @@ -4906,20 +4895,17 @@ pub mod test { .await .expect("Failed to create worktree_a"); - 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, cx) = cx.add_window_view(|window, cx| { + Workspace::new( + Default::default(), + project1.clone(), + app_state.clone(), + window, + cx, + ) }); - let workspace1_handle = cx.window_handle().downcast::().unwrap(); + let workspace1_handle = cx.window_handle().downcast::().unwrap(); cx.run_until_parked(); @@ -4956,17 +4942,14 @@ pub mod test { .await .expect("Failed to create worktree_b"); - 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, cx) = cx.add_window_view(|window, cx| { + Workspace::new( + Default::default(), + project2.clone(), + app_state.clone(), + window, + cx, + ) }); cx.run_until_parked(); diff --git a/crates/sidebar/Cargo.toml b/crates/sidebar/Cargo.toml deleted file mode 100644 index da4f29da8208540a483049687bae5a9715b2c710..0000000000000000000000000000000000000000 --- a/crates/sidebar/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[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 deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/sidebar/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs deleted file mode 100644 index 16a03e76a1a1b393fe6814c6943af662e5bf7869..0000000000000000000000000000000000000000 --- a/crates/sidebar/src/sidebar.rs +++ /dev/null @@ -1,1304 +0,0 @@ -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::{CommonAnimationExt, Divider, HighlightedLabel, ListItem, Tab, Tooltip, prelude::*}; -use ui_input::ErasedEditor; -use util::ResultExt as _; -use workspace::{ - 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) - .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, - 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(), - notified_workspaces: HashSet::new(), - } - } - - fn set_entries( - &mut self, - workspace_threads: Vec, - active_workspace_index: usize, - cx: &App, - ) { - 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<()> { - self.query = query.clone(); - 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; - - fn render_title(text: SharedString, positions: &[usize]) -> AnyElement { - if positions.is_empty() { - div() - .p_0p5() - .child(Label::new(text).truncate()) - .into_any_element() - } else { - div() - .p_0p5() - .child(HighlightedLabel::new(text, positions.to_vec()).truncate()) - .into_any_element() - } - } - - fn render_thread_status_icon( - workspace_index: usize, - status: &AgentThreadStatus, - has_notification: bool, - ) -> AnyElement { - match status { - AgentThreadStatus::Running => Icon::new(IconName::LoadCircle) - .size(IconSize::XSmall) - .color(Color::Muted) - .with_keyed_rotate_animation( - SharedString::from(format!("workspace-{}-spinner", workspace_index)), - 3, - ) - .into_any_element(), - AgentThreadStatus::Completed => { - let color = if has_notification { - Color::Accent - } else { - Color::Muted - }; - Icon::new(IconName::Check) - .size(IconSize::XSmall) - .color(color) - .into_any_element() - } - } - } - - fn render_project_row( - title: AnyElement, - thread_subtitle: Option, - status_icon: Option, - cx: &App, - ) -> Div { - h_flex() - .items_start() - .gap(DynamicSpacing::Base06.rems(cx)) - .child( - div().pt(px(4.0)).child( - Icon::new(IconName::Folder) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ) - .child(v_flex().overflow_hidden().child(title).when_some( - thread_subtitle, - |this, subtitle| { - this.child( - h_flex() - .gap_1() - .items_center() - .px_0p5() - .when_some(status_icon, |this, icon| this.child(icon)) - .child( - Label::new(subtitle) - .size(LabelSize::Small) - .color(Color::Muted) - .truncate(), - ), - ) - }, - )) - } - - 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 title = render_title(worktree_label.clone(), positions); - 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 close_button = if workspace_count > 1 { - Some( - IconButton::new( - SharedString::from(format!("close-workspace-{}", workspace_index)), - IconName::Close, - ) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Close Workspace")) - .on_click({ - let multi_workspace = multi_workspace; - move |_, window, cx| { - multi_workspace.update(cx, |mw, cx| { - mw.remove_workspace(workspace_index, window, cx); - }); - } - }), - ) - } else { - None - }; - - let has_notification = self.notified_workspaces.contains(&workspace_index); - let (thread_subtitle, status_icon) = match thread_info { - Some(info) => ( - Some(info.title), - Some(render_thread_status_icon( - workspace_index, - &info.status, - has_notification, - )), - ), - None => (None, None), - }; - - Some( - ListItem::new(("workspace-item", thread_entry.index)) - .toggle_state(selected) - .when_some(close_button, |item, button| item.end_hover_slot(button)) - .child(render_project_row(title, thread_subtitle, status_icon, _cx)) - .when(!full_path.is_empty(), |item| { - item.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 title = render_title(name.clone(), positions); - let item_id: SharedString = - format!("recent-project-{:?}", project_entry.workspace_id).into(); - let thread_title = self - .recent_project_thread_titles - .get(&project_entry.full_path) - .cloned(); - - Some( - ListItem::new(item_id) - .toggle_state(selected) - .child(render_project_row(title, thread_title, None, _cx)) - .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); - - 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_2() - .when(cfg!(target_os = "macos"), |this| { - this.pl(px(TRAFFIC_LIGHT_PADDING)) - }) - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - IconButton::new("close-sidebar", IconName::WorkspaceNavOpen) - .icon_size(IconSize::Small) - .tooltip(|_window, cx| { - Tooltip::for_action("Close Sidebar", &ToggleWorkspaceSidebar, cx) - }) - .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/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index a980070d6b9fc6abef72542ea82d1f4482a44c00..40c6ba6ae60ef06cab84c8be35150f0bccc748f8 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -38,7 +38,6 @@ 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 1f4c6376c4fb6d3f366fdf8c4008c347004a763f..a0927918c7493c1da711fcab3fa0af546bc4a0e5 100644 --- a/crates/title_bar/src/project_dropdown.rs +++ b/crates/title_bar/src/project_dropdown.rs @@ -11,8 +11,7 @@ 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 util::ResultExt as _; -use workspace::{MultiWorkspace, Workspace}; +use workspace::{CloseIntent, Workspace}; actions!(project_dropdown, [RemoveSelectedFolder]); @@ -67,12 +66,8 @@ 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 @@ -82,7 +77,7 @@ impl ProjectDropdown { .ok() .flatten(); - let projects = get_recent_projects(current_workspace_id, None, fs).await; + let projects = get_recent_projects(current_workspace_id, None).await; cx.update(|window, cx| { *recent_projects_for_fetch.borrow_mut() = projects; @@ -93,7 +88,7 @@ impl ProjectDropdown { }); } }) - .ok(); + .ok() }) .detach(); @@ -401,31 +396,36 @@ impl ProjectDropdown { window: &mut Window, cx: &mut App, ) { - 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; - }; + let Some(workspace) = workspace.upgrade() 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); - } - }); - } + 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); + }); } /// 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 38c2b028a9a4b161ef4397a29b510390f4dd58e9..38fc531a76ee1bbdac2b821e7f0d270919219ac6 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -22,7 +22,6 @@ 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,13 +38,10 @@ use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; use ui::{ Avatar, ButtonLike, Chip, ContextMenu, IconWithIndicator, Indicator, PopoverMenu, - PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height, + PopoverMenuHandle, TintColor, Tooltip, prelude::*, }; use util::ResultExt; -use workspace::{ - MultiWorkspace, SwitchProject, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace, - notifications::NotifyResultExt, -}; +use workspace::{SwitchProject, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt}; use zed_actions::OpenRemote; pub use onboarding_banner::restore_banner; @@ -84,10 +80,8 @@ pub fn init(cx: &mut App) { .titlebar_item() .and_then(|item| item.downcast::().ok()) { - window.defer(cx, move |window, cx| { - titlebar.update(cx, |titlebar, cx| { - titlebar.show_project_dropdown(window, cx); - }) + titlebar.update(cx, |titlebar, cx| { + titlebar.show_project_dropdown(window, cx); }); } }); @@ -164,7 +158,7 @@ impl Render for TitleBar { children.push( h_flex() - .gap_0p5() + .gap_1() .map(|title_bar| { let mut render_project_items = title_bar_settings.show_branch_name || title_bar_settings.show_project_items; @@ -177,7 +171,6 @@ impl Render for TitleBar { title_bar.child(menu) }, ) - .children(self.render_workspace_sidebar_toggle(window, cx)) .children(self.render_restricted_mode(cx)) .when(render_project_items, |title_bar| { title_bar @@ -239,7 +232,7 @@ impl Render for TitleBar { ); }); - let height = platform_title_bar_height(window); + let height = PlatformTitleBar::height(window); let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| { platform_titlebar.title_bar_color(window, cx) }); @@ -347,48 +340,6 @@ impl TitleBar { 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, @@ -676,41 +627,6 @@ 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(); @@ -995,18 +911,16 @@ 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 |mut cx| { + .spawn(cx, async move |cx| { client .sign_in_with_optional_connect(true, cx) .await - .notify_workspace_async_err(workspace, &mut cx); + .notify_async_err(cx); }) .detach(); }) diff --git a/crates/ui/src/components/thread_item.rs b/crates/ui/src/components/thread_item.rs index b3cf0ca8e8aa807764c7078c42093022197ebf72..a4f6a8a53348d78563900c2a53b30e95588c2aac 100644 --- a/crates/ui/src/components/thread_item.rs +++ b/crates/ui/src/components/thread_item.rs @@ -1,6 +1,5 @@ use crate::{ - DecoratedIcon, DiffStat, HighlightedLabel, IconDecoration, IconDecorationKind, SpinnerLabel, - prelude::*, + Chip, DecoratedIcon, DiffStat, IconDecoration, IconDecorationKind, SpinnerLabel, prelude::*, }; use gpui::{ClickEvent, SharedString}; @@ -9,7 +8,6 @@ pub struct ThreadItem { id: ElementId, icon: IconName, title: SharedString, - highlight_positions: Vec, timestamp: SharedString, running: bool, generation_done: bool, @@ -26,7 +24,6 @@ impl ThreadItem { id: id.into(), icon: IconName::ZedAgent, title: title.into(), - highlight_positions: Vec::new(), timestamp: "".into(), running: false, generation_done: false, @@ -78,11 +75,6 @@ impl ThreadItem { self } - pub fn highlight_positions(mut self, positions: Vec) -> Self { - self.highlight_positions = positions; - self - } - pub fn on_click( mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, @@ -120,17 +112,7 @@ impl RenderOnce for ThreadItem { agent_icon.into_any_element() }; - // 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() - }; + let has_no_changes = self.added.is_none() && self.removed.is_none(); v_flex() .id(self.id.clone()) @@ -145,7 +127,7 @@ impl RenderOnce for ThreadItem { .w_full() .gap_1p5() .child(icon) - .child(title_label) + .child(Label::new(self.title).truncate()) .when(self.running, |this| { this.child(icon_container().child(SpinnerLabel::new().color(Color::Accent))) }), @@ -155,32 +137,26 @@ impl RenderOnce for ThreadItem { .gap_1p5() .child(icon_container()) // Icon Spacing .when_some(self.worktree, |this, name| { - this.child(Label::new(name).size(LabelSize::Small).color(Color::Muted)) + this.child(Chip::new(name).label_size(LabelSize::XSmall)) }) .child( - Label::new("•") + Label::new(self.timestamp) .size(LabelSize::Small) - .color(Color::Muted) - .alpha(0.5), + .color(Color::Muted), ) .child( - Label::new(self.timestamp) + Label::new("•") .size(LabelSize::Small) - .color(Color::Muted), + .color(Color::Muted) + .alpha(0.5), ) - // .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(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, diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index b73915162f9e6be937af7323e95fb9d6a82d6c52..cd7d8eb497328baed356692e1d88d0286568d344 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -5,7 +5,6 @@ use theme::ActiveTheme; mod apca_contrast; mod color_contrast; -mod constants; mod corner_solver; mod format_distance; mod search_input; @@ -13,7 +12,6 @@ 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 deleted file mode 100644 index 823155889f7b4c370ea7998ec7f09340f94ef2a5..0000000000000000000000000000000000000000 --- a/crates/ui/src/utils/constants.rs +++ /dev/null @@ -1,27 +0,0 @@ -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 ae0461339d2f7fccb1ddc8f7a0e40fafa727483b..423c3b387b197edd2d8e86398b09157fdcb7711a 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) { + if let Some(workspace) = vim.workspace(window) { 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, cx) { + if let Some(workspace) = vim.workspace(window) { 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) 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, cx) else { + let Some(workspace) = self.workspace(window) 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, cx) else { + let Some(workspace) = self.workspace(window) 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, cx) else { + let Some(workspace) = vim.workspace(window) else { return; }; diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 48cf8739b725f64e1dd5930b23e046e92fd72392..a4d85e87b24fa6e2753f0dbcfcbb43be9488f41a 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, cx) else { + let Some(workspace) = self.workspace(window) 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, cx) else { + let Some(workspace) = self.workspace(window) 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, cx) else { + let Some(workspace) = self.workspace(window) 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, cx)?.entity_id(); + let workspace_id = self.workspace(window)?.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, cx) else { + let Some(workspace) = self.workspace(window) else { return; }; if name == "`" || name == "'" { diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 8a4bfc241d1b0c62b17464bfb1dd5076015ac638..5b480a86d846ff719d8784f619be861db9e44c9f 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(workspace) = Workspace::for_window(window, cx) else { + let Some(Some(workspace)) = window.root::() else { return; }; let Some(editor) = workspace @@ -165,7 +165,7 @@ impl Replayer { text, utf16_range_to_replace, } => { - let Some(workspace) = Workspace::for_window(window, cx) else { + let Some(Some(workspace)) = window.root::() else { return; }; let Some(editor) = workspace diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 248f43c08192182cb266dbfc43a5a769f87429cd..c11784d163e18451129656aa92d23dba568bd723 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, cx)) + .zip(self.workspace(window)) .zip(self.editor()) else { return; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 9546c822ef68f1515745e67a4ec82fca684a6a94..1075a1144355083bd410b3aee4d015031f946a4e 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::{MultiWorkspace, Workspace, WorkspaceDb, WorkspaceId}; +use workspace::{Workspace, WorkspaceDb, WorkspaceId}; #[derive(Clone, Copy, Default, Debug, PartialEq, Serialize, Deserialize)] pub enum Mode { @@ -731,16 +731,12 @@ impl VimGlobals { }); GlobalCommandPaletteInterceptor::set(cx, command_interceptor); for window in cx.windows() { - 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) - }); - }); - } + if let Some(workspace) = window.downcast::() { + workspace + .update(cx, |workspace, _, cx| { + Vim::update_globals(cx, |globals, cx| { + globals.register_workspace(workspace, cx) + }); }) .ok(); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 1100db4585e286a4e100b9d62b8be552471c2095..8e21b2b7a795a20947d5697c034a2bb6ee425f55 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: &Window, cx: &App) -> Option> { - Workspace::for_window(window, cx) + pub fn workspace(&self, window: &mut Window) -> Option> { + window.root::().flatten() } - pub fn pane(&self, window: &Window, cx: &Context) -> Option> { - self.workspace(window, cx) + pub fn pane(&self, window: &mut Window, cx: &mut Context) -> Option> { + self.workspace(window) .map(|workspace| workspace.read(cx).focused_pane(window, cx)) } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index b8296d13af4275b6eef8fccc654be5c813a9ef61..6f4aced4259acdc986e2a3f14aee191fe497c23b 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -79,7 +79,6 @@ 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 ebb4792ef0d6a49c05e2d98f166f7e8260a2ae0a..ee5dc550b2fba6d449cb87a0da3ca8b909da1970 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -1,6 +1,5 @@ -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; -use fs::Fs; use gpui::{AppContext, Entity, Global, MenuItem}; use smallvec::SmallVec; use ui::{App, Context}; @@ -10,10 +9,10 @@ use crate::{ NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, }; -pub fn init(fs: Arc, cx: &mut App) { +pub fn init(cx: &mut App) { let manager = cx.new(|_| HistoryManager::new()); HistoryManager::set_global(manager.clone(), cx); - HistoryManager::init(manager, fs, cx); + HistoryManager::init(manager, cx); } pub struct HistoryManager { @@ -39,10 +38,10 @@ impl HistoryManager { } } - fn init(this: Entity, fs: Arc, cx: &App) { + fn init(this: Entity, cx: &App) { cx.spawn(async move |cx| { let recent_folders = WORKSPACE_DB - .recent_workspaces_on_disk(fs.as_ref()) + .recent_workspaces_on_disk() .await .unwrap_or_default() .into_iter() diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs deleted file mode 100644 index 71d8d6cb0a5823b12027a00c7eec2b6ee7622953..0000000000000000000000000000000000000000 --- a/crates/workspace/src/multi_workspace.rs +++ /dev/null @@ -1,513 +0,0 @@ -use anyhow::Result; -use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; -use gpui::{ - AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, Focusable, ManagedView, - MouseButton, Pixels, Render, Subscription, Task, 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, - ] -); - -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(&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(&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); - let pane = self.workspace().read(cx).active_pane().clone(); - window.focus(&pane.read(cx).focus_handle(cx), 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; - self.serialize(window, cx); - cx.notify(); - } - - fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context) { - self.sidebar_open = false; - 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 { - 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) - } - - 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); - }, - )) - .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, - ) - } -} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 84f479b77e4f0274e0775353d3a7cd5579768f1c..10437743df39b22722638357976d5a8d6224eaf8 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1,9 +1,9 @@ -use crate::{MultiWorkspace, SuppressNotification, Toast, Workspace}; +use crate::{SuppressNotification, Toast, Workspace}; use anyhow::Context as _; use gpui::{ - AnyEntity, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, ClickEvent, Context, + AnyEntity, AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, - Task, TextStyleRefinement, UnderlineStyle, WeakEntity, svg, + Task, TextStyleRefinement, UnderlineStyle, svg, }; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; @@ -1037,18 +1037,14 @@ pub fn show_app_notification( .insert(id.clone(), build_notification.clone()); for window in cx.windows() { - 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), - ); - }); - } + 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), + ); }) .ok(); // Doesn't matter if the windows are dropped } @@ -1062,15 +1058,11 @@ 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(multi_workspace) = window.downcast::() { + if let Some(workspace_window) = window.downcast::() { let id = id.clone(); - multi_workspace - .update(cx, |multi_workspace, _window, cx| { - for workspace in multi_workspace.workspaces() { - workspace.update(cx, |workspace, cx| { - workspace.dismiss_notification(&id, cx) - }); - } + workspace_window + .update(cx, |workspace, _window, cx| { + workspace.dismiss_notification(&id, cx) }) .ok(); } @@ -1084,11 +1076,7 @@ pub trait NotifyResultExt { fn notify_err(self, workspace: &mut Workspace, cx: &mut Context) -> Option; - fn notify_workspace_async_err( - self, - workspace: WeakEntity, - cx: &mut AsyncApp, - ) -> Option; + fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option; /// Notifies the active workspace if there is one, otherwise notifies all workspaces. fn notify_app_err(self, cx: &mut App) -> Option; @@ -1111,18 +1099,17 @@ where } } - fn notify_workspace_async_err( - self, - workspace: WeakEntity, - cx: &mut AsyncApp, - ) -> Option { + fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option { match self { Ok(value) => Some(value), Err(err) => { log::error!("{err:?}"); - workspace - .update(cx, |workspace, cx| workspace.show_error(&err, cx)) - .ok(); + cx.update_root(|view, _, cx| { + if let Ok(workspace) = view.downcast::() { + workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) + } + }) + .ok(); None } } @@ -1150,12 +1137,7 @@ where } pub trait NotifyTaskExt { - fn detach_and_notify_err( - self, - workspace: WeakEntity, - window: &mut Window, - cx: &mut App, - ); + fn detach_and_notify_err(self, window: &mut Window, cx: &mut App); } impl NotifyTaskExt for Task> @@ -1163,16 +1145,9 @@ where E: std::fmt::Debug + std::fmt::Display + Sized + 'static, R: 'static, { - fn detach_and_notify_err( - self, - workspace: WeakEntity, - window: &mut Window, - cx: &mut App, - ) { + fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) { window - .spawn(cx, async move |mut cx| { - self.await.notify_workspace_async_err(workspace, &mut cx) - }) + .spawn(cx, async move |cx| self.await.notify_async_err(cx)) .detach(); } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3658895d10cf887f629acacaec7ba409d9fa7cf1..f025131758760d6c4db5250e2bbdb12a50d4ee01 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3881,10 +3881,9 @@ 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, mut cx| { - if let Some((project_entry_id, build_item)) = load_path_task - .await - .notify_workspace_async_err(workspace.clone(), &mut 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) { 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 785f3fd9f32aa3bb8d12d6d4dd87cfb6bfe3a1e7..08ea880fd573b608400613d54bfec47b3984f260 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -8,8 +8,6 @@ use std::{ sync::Arc, }; -use fs::Fs; - use anyhow::{Context as _, Result, bail}; use collections::{HashMap, HashSet, IndexSet}; use db::{ @@ -50,7 +48,7 @@ use model::{ SerializedPaneGroup, SerializedWorkspace, }; -use self::model::{DockStructure, SerializedWorkspaceLocation, SessionWorkspace}; +use self::model::{DockStructure, SerializedWorkspaceLocation}; // https://www.sqlite.org/limits.html // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, @@ -283,64 +281,6 @@ 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 { @@ -1768,26 +1708,10 @@ 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(); @@ -1820,8 +1744,11 @@ 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 && Self::all_paths_exist_with_a_directory(paths.paths(), fs).await { - result.push((id, SerializedWorkspaceLocation::Local, paths)); + 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)); + } } else { delete_tasks.push(self.delete_workspace_by_id(id)); } @@ -1833,67 +1760,65 @@ impl WorkspaceDb { pub async fn last_workspace( &self, - fs: &dyn Fs, ) -> Result> { - Ok(self.recent_workspaces_on_disk(fs).await?.into_iter().next()) + Ok(self.recent_workspaces_on_disk().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 async fn last_session_workspace_locations( + pub fn last_session_workspace_locations( &self, last_session_id: &str, last_session_window_stack: Option>, - fs: &dyn Fs, - ) -> Result> { + ) -> 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(SessionWorkspace { + workspaces.push(( workspace_id, - location: SerializedWorkspaceLocation::Remote( + SerializedWorkspaceLocation::Remote( self.remote_connection(remote_connection_id)?, ), paths, - window_id, - }); + window_id.map(WindowId::from), + )); } else if paths.is_empty() { // Empty workspace with items (drafts, files) - include for restoration - workspaces.push(SessionWorkspace { + workspaces.push(( workspace_id, - location: SerializedWorkspaceLocation::Local, + SerializedWorkspaceLocation::Local, paths, - 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, - }); - } + window_id.map(WindowId::from), + )); + } else if paths.paths().iter().all(|path| path.exists()) + && paths.paths().iter().any(|path| path.is_dir()) + { + workspaces.push(( + workspace_id, + SerializedWorkspaceLocation::Local, + paths, + window_id.map(WindowId::from), + )); } } if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|workspace| { - workspace - .window_id + workspaces.sort_by_key(|(_, _, _, window_id)| { + window_id .and_then(|id| stack.iter().position(|&order_id| order_id == id)) .unwrap_or(usize::MAX) }); } - Ok(workspaces) + Ok(workspaces + .into_iter() + .map(|(workspace_id, location, paths, _)| (workspace_id, location, paths)) + .collect::>()) } fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { @@ -2347,12 +2272,11 @@ pub fn delete_unloaded_items( mod tests { use super::*; use crate::persistence::model::{ - SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, SessionWorkspace, + SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, }; use gpui; use pretty_assertions::assert_eq; use remote::SshConnectionOptions; - use serde_json::json; use std::{thread, time::Duration}; #[gpui::test] @@ -3116,18 +3040,12 @@ mod tests { } #[gpui::test] - async fn test_last_session_workspace_locations(cx: &mut gpui::TestAppContext) { + async fn test_last_session_workspace_locations() { 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; @@ -3170,55 +3088,47 @@ mod tests { ])); let locations = db - .last_session_workspace_locations("one-session", stack, fs.as_ref()) - .await + .last_session_workspace_locations("one-session", stack) .unwrap(); assert_eq!( locations, [ - 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)), - }, + ( + 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()]) + ), ] ); } #[gpui::test] - async fn test_last_session_workspace_locations_remote(cx: &mut gpui::TestAppContext) { - let fs = fs::FakeFs::new(cx.executor()); + async fn test_last_session_workspace_locations_remote() { let db = WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote") .await; @@ -3280,45 +3190,40 @@ mod tests { ])); let have = db - .last_session_workspace_locations("one-session", stack, fs.as_ref()) - .await + .last_session_workspace_locations("one-session", stack) .unwrap(); assert_eq!(have.len(), 4); assert_eq!( have[0], - SessionWorkspace { - workspace_id: WorkspaceId(4), - location: SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), - paths: PathList::default(), - window_id: Some(WindowId::from(2u64)), - } + ( + WorkspaceId(4), + SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), + PathList::default() + ) ); assert_eq!( have[1], - SessionWorkspace { - workspace_id: WorkspaceId(3), - location: SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), - paths: PathList::default(), - window_id: Some(WindowId::from(8u64)), - } + ( + WorkspaceId(3), + SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), + PathList::default() + ) ); assert_eq!( have[2], - SessionWorkspace { - workspace_id: WorkspaceId(2), - location: SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), - paths: PathList::default(), - window_id: Some(WindowId::from(5u64)), - } + ( + WorkspaceId(2), + SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), + PathList::default() + ) ); assert_eq!( have[3], - SessionWorkspace { - workspace_id: WorkspaceId(1), - location: SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), - paths: PathList::default(), - window_id: Some(WindowId::from(9u64)), - } + ( + WorkspaceId(1), + SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), + PathList::default() + ) ); } @@ -3650,192 +3555,4 @@ 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 cdb646ec3b8248bdd0b5784424ed7b8df8ac0ee8..417896c584a1906f5d2f712a864cb2807c69af0a 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, WindowId}; +use gpui::{AsyncWindowContext, Entity, WeakEntity}; use language::{Toolchain, ToolchainScope}; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; @@ -49,32 +49,6 @@ 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/welcome.rs b/crates/workspace/src/welcome.rs index 301f7884dac909f01db1baa2b883253dd7ee3890..071bf0826798d382329c9f6e3ced86388e4c0b3e 100644 --- a/crates/workspace/src/welcome.rs +++ b/crates/workspace/src/welcome.rs @@ -114,9 +114,7 @@ impl RenderOnce for SectionButton { .size(rems_from_px(12.)), ), ) - .on_click(move |_, window, cx| { - self.focus_handle.dispatch_action(&*self.action, window, cx) - }) + .on_click(move |_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) } } @@ -227,13 +225,9 @@ 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(fs.as_ref()) + .recent_workspaces_on_disk() .await .log_err() .unwrap_or_default(); @@ -273,18 +267,21 @@ 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(); - self.workspace - .update(cx, |workspace, cx| { + cx.spawn_in(window, async move |_, cx| { + let _ = workspace.update_in(cx, |workspace, window, cx| { workspace .open_workspace_for_paths(true, paths, window, cx) - .detach_and_log_err(cx); - }) - .log_err(); + .detach(); + }); + }) + .detach(); } 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 ed7030bd428517f834c315465c534ecf0deb968c..8dff340e264abd583c471b95d96c90e14486a2c5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3,7 +3,6 @@ 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; @@ -23,10 +22,6 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; -pub use multi_workspace::{ - DraggedSidebar, MultiWorkspace, NewWorkspaceInWindow, NextWorkspaceInWindow, - PreviousWorkspaceInWindow, Sidebar, SidebarEvent, SidebarHandle, ToggleWorkspaceSidebar, -}; pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -76,8 +71,7 @@ pub use pane_group::{ use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace}, - read_serialized_multi_workspaces, + model::{ItemId, SerializedWorkspaceLocation}, }; use postage::stream::Stream; use project::{ @@ -568,27 +562,9 @@ pub struct OpenTerminal { pub local: bool, } -#[derive( - Clone, - Copy, - Debug, - Default, - Hash, - PartialEq, - Eq, - PartialOrd, - Ord, - serde::Serialize, - serde::Deserialize, -)] +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] 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 { @@ -623,14 +599,11 @@ 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, |multi_workspace, _, cx| { - let workspace = multi_workspace.workspace().clone(); - workspace.update(cx, |workspace, cx| { - workspace.show_portal_error(err.to_string(), cx); - }); + .update(cx, |workspace, _, cx| { + workspace.show_portal_error(err.to_string(), cx); }) .ok(); } @@ -645,7 +618,7 @@ pub fn init(app_state: Arc, cx: &mut App) { component::init(); theme_preview::init(cx); toast_layer::init(cx); - history_manager::init(app_state.fs.clone(), cx); + history_manager::init(cx); cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)) .on_action(|_: &Reload, cx| reload(cx)) @@ -996,7 +969,7 @@ struct GlobalAppState(Weak); impl Global for GlobalAppState {} pub struct WorkspaceStore { - workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity)>, + workspaces: HashSet>, client: Arc, _subscriptions: Vec, } @@ -1482,11 +1455,9 @@ impl Workspace { cx.emit(Event::PaneAdded(center_pane.clone())); - let any_window_handle = window.window_handle(); + let window_handle = window.window_handle().downcast::().unwrap(); app_state.workspace_store.update(cx, |store, _| { - store - .workspaces - .insert((any_window_handle, weak_handle.clone())); + store.workspaces.insert(window_handle); }); let mut current_user = app_state.user_store.read(cx).watch_current_user(); @@ -1611,13 +1582,10 @@ impl Workspace { GlobalTheme::reload_theme(cx); GlobalTheme::reload_icon_theme(cx); }), - 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); - }) - } + cx.on_release(move |this, cx| { + this.app_state.workspace_store.update(cx, move |store, _| { + store.workspaces.remove(&window_handle); + }) }), ]; @@ -1691,13 +1659,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>>>, )>, > { @@ -1795,23 +1763,71 @@ impl Workspace { }); } - 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); + 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); + } + + 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) + }; - let workspace = window.update(cx, |multi_workspace, window, cx| { - let workspace = cx.new(|cx| { + // 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 mut workspace = Workspace::new( Some(workspace_id), - project_handle.clone(), - app_state.clone(), + project_handle, + app_state, window, cx, ); - workspace.centered_layout = centered_layout; // Call init callback to add items before window renders @@ -1820,69 +1836,10 @@ 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) @@ -1895,10 +1852,8 @@ impl Workspace { .unwrap_or(false); let opened_items = window - .update(cx, |_, window, cx| { - workspace.update(cx, |_workspace: &mut Workspace, cx| { - open_items(serialized_workspace, project_paths, window, cx) - }) + .update(cx, |_workspace, window, cx| { + open_items(serialized_workspace, project_paths, window, cx) })? .await .unwrap_or_default(); @@ -1910,30 +1865,29 @@ impl Workspace { if is_empty_workspace && !serialized_workspace_has_paths { if let Some(default_docks) = persistence::read_default_dock_state() { window - .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(); - }); + .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(); }) .log_err(); } } window - .update(cx, |_, _window, cx| { - workspace.update(cx, |this: &mut Workspace, cx| { - this.update_history(cx); - }); + .update(cx, |workspace, window, cx| { + window.activate_window(); + workspace.update_history(cx); }) .log_err(); Ok((window, opened_items)) @@ -2539,11 +2493,8 @@ 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 (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)) - }) + let (workspace, _) = task.await?; + workspace.update(cx, callback) }) } } @@ -2569,11 +2520,8 @@ 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 (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)) - }) + let (workspace, _) = task.await?; + workspace.update(cx, callback) }) } } @@ -2675,7 +2623,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() })?; @@ -2688,12 +2636,10 @@ impl Workspace { let remaining_workspaces = cx.update(|_window, cx| { cx.windows() .iter() - .filter_map(|window| window.downcast::()) - .filter_map(|multi_workspace| { - multi_workspace - .update(cx, |multi_workspace, _, cx| { - multi_workspace.workspace().read(cx).removing - }) + .filter_map(|window| window.downcast::()) + .filter_map(|workspace| { + workspace + .update(cx, |workspace, _, _| workspace.removing) .ok() }) .filter(|removing| !removing) @@ -2729,18 +2675,13 @@ impl Workspace { } if close_intent == CloseIntent::ReplaceWindow { _ = active_call.update(cx, |this, cx| { - let multi_workspace = cx + let workspace = cx .windows() .iter() - .filter_map(|window| window.downcast::()) + .filter_map(|window| window.downcast::()) .next() .unwrap(); - let project = multi_workspace - .read(cx)? - .workspace() - .read(cx) - .project - .clone(); + let project = workspace.read(cx)?.project.clone(); if project.read(cx).is_shared() { this.unshare_project(project, cx)?; } @@ -2948,7 +2889,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)); @@ -5133,27 +5074,21 @@ impl Workspace { self.update_window_edited(window, cx); return; } - - 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) + 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) }) .ok(); - }) - .ok(); - }); - - let s = item.on_release(cx, on_release_callback); - self.dirty_items.insert(item_id, s); - self.update_window_edited(window, cx); + }), + ); + self.dirty_items.insert(item_id, s); + self.update_window_edited(window, cx); + } } fn render_notifications(&self, _window: &mut Window, _cx: &mut Context) -> Option
{ @@ -6731,11 +6666,8 @@ impl Workspace { ) } - pub fn for_window(window: &Window, cx: &App) -> Option> { - window - .root::() - .flatten() - .map(|multi_workspace| multi_workspace.read(cx).workspace().clone()) + pub fn for_window(window: &mut Window, _: &mut App) -> Option> { + window.root().flatten() } pub fn zoomed_item(&self) -> Option<&AnyWeakView> { @@ -7110,30 +7042,27 @@ enum ActivateInDirectionTarget { Dock(Entity), } -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) - }) - }) - }, - ); - } - }); +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) + }) + }) + }, + ); + } }) .log_err(); } @@ -7287,14 +7216,15 @@ impl Render for Workspace { .collect::>(); let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout; - self.actions(div(), window, cx) - .key_context(context) - .relative() - .size_full() - .flex() - .flex_col() - .font(ui_font) - .gap_0() + client_side_decorations( + 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) @@ -7777,7 +7707,10 @@ impl Render for Workspace { }) .child(self.modal_layer.clone()) .child(self.toast_layer.clone()), - ) + ), + window, + cx, + ) } } @@ -7822,22 +7755,16 @@ impl WorkspaceStore { }; let mut response = proto::FollowResponse::default(); - - 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) - } - }); + 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) + } }) .is_ok() }); @@ -7855,24 +7782,14 @@ impl WorkspaceStore { let update = envelope.payload; this.update(&mut cx, |this, 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, - ); - }); + 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); }) .is_ok() }); @@ -7880,14 +7797,8 @@ impl WorkspaceStore { }) } - 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)) + pub fn workspaces(&self) -> &HashSet> { + &self.workspaces } } @@ -7939,119 +7850,19 @@ impl WorkspaceHandle for Entity { } } -pub async fn last_opened_workspace_location( - fs: &dyn fs::Fs, -) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> { - DB.last_workspace(fs).await.log_err().flatten() +pub async fn last_opened_workspace_location() +-> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> { + DB.last_workspace().await.log_err().flatten() } -pub async fn last_session_workspace_locations( +pub fn last_session_workspace_locations( last_session_id: &str, last_session_window_stack: Option>, - fs: &dyn fs::Fs, -) -> Option> { - DB.last_session_workspace_locations(last_session_id, last_session_window_stack, fs) - .await +) -> Option> { + DB.last_session_workspace_locations(last_session_id, last_session_window_stack) .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, [ @@ -8091,8 +7902,7 @@ actions!( async fn join_channel_internal( channel_id: ChannelId, app_state: &Arc, - requesting_window: Option>, - requesting_workspace: Option>, + requesting_window: Option>, active_call: &Entity, cx: &mut AsyncApp, ) -> Result { @@ -8128,8 +7938,8 @@ async fn join_channel_internal( } if should_prompt { - if let Some(multi_workspace) = requesting_window { - let answer = multi_workspace + if let Some(workspace) = requesting_window { + let answer = workspace .update(cx, |_, window, cx| { window.prompt( PromptLevel::Warning, @@ -8198,9 +8008,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_workspace.as_ref().and_then(|w| w.upgrade()) + && let Some(workspace) = requesting_window { - 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 { @@ -8219,7 +8029,7 @@ async fn join_channel_internal( None } }); - if let Some(project) = project { + if let Ok(Some(project)) = project { return Some(cx.spawn(async move |room, cx| { room.update(cx, |room, cx| room.share_project(project, cx))? .await?; @@ -8240,21 +8050,14 @@ async fn join_channel_internal( pub fn join_channel( channel_id: ChannelId, app_state: Arc, - requesting_window: Option>, - requesting_workspace: Option>, + requesting_window: 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, - requesting_workspace, - &active_call, - cx, - ) - .await; + let result = + join_channel_internal(channel_id, &app_state, requesting_window, &active_call, cx) + .await; // join channel succeeded, and opened a window if matches!(result, Ok(true)) { @@ -8278,12 +8081,6 @@ 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); @@ -8338,10 +8135,10 @@ pub fn join_channel( }) } -pub async fn get_any_active_multi_workspace( +pub async fn get_any_active_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() { @@ -8351,17 +8148,17 @@ pub async fn get_any_active_multi_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(); @@ -8372,17 +8169,14 @@ 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(|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()) - }) + .filter_map(|window| window.downcast::()) + .filter(|workspace| { + workspace + .read(cx) + .is_ok_and(|workspace| workspace.project.read(cx).is_local()) }) .collect() } @@ -8393,7 +8187,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>, } @@ -8401,9 +8195,8 @@ 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(), @@ -8423,87 +8216,52 @@ pub fn open_workspace_by_id( .workspace_for_id(workspace_id) .with_context(|| format!("Workspace {workspace_id:?} not found"))?; - let centered_layout = serialized_workspace.centered_layout; + let window_bounds_override = window_bounds_env_override(); - 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) + 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 { - 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)) - } - })?; + (None, None) + }; - let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| { - multi_workspace.workspace().clone() - })?; + 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; - (window, workspace) - }; + 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); + workspace.centered_layout = centered_layout; + workspace + }) + } + })?; notify_if_database_failed(window, cx); // Restore items from the serialized workspace window - .update(cx, |_, window, cx| { - workspace.update(cx, |_workspace, cx| { - open_items(Some(serialized_workspace), vec![], window, cx) - }) + .update(cx, |_workspace, window, cx| { + open_items(Some(serialized_workspace), vec![], window, cx) })? .await?; - window.update(cx, |_, window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.serialize_workspace(window, cx); - }); + window.update(cx, |workspace, window, cx| { + window.activate_window(); + workspace.serialize_workspace(window, cx); })?; Ok(window) @@ -8518,12 +8276,12 @@ pub fn open_paths( cx: &mut App, ) -> Task< anyhow::Result<( - WindowHandle, + WindowHandle, Vec>>>, )>, > { let abs_paths = abs_paths.to_vec(); - let mut existing: Option<(WindowHandle, Entity)> = None; + let mut existing = None; let mut best_match = None; let mut open_visible = OpenVisible::All; #[cfg(target_os = "windows")] @@ -8542,22 +8300,20 @@ pub fn open_paths( cx.update(|cx| { for window in local_workspace_windows(cx) { - 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())) - } + 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) } } } @@ -8570,118 +8326,95 @@ pub fn open_paths( cx.update(|cx| { if let Some(window) = cx .active_window() - .and_then(|window| window.downcast::()) - && let Ok(multi_workspace) = window.read(cx) + .and_then(|window| window.downcast::()) + && let Ok(workspace) = window.read(cx) { - let active_workspace = multi_workspace.workspace().clone(); - let project = active_workspace.read(cx).project().read(cx); + let project = workspace.project().read(cx); if project.is_local() && !project.is_via_collab() { - existing = Some((window, active_workspace)); + existing = Some(window); open_visible = OpenVisible::None; return; } } - '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; + 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; } + existing = Some(window); + open_visible = OpenVisible::None; + break; } } }); } } - let result = if let Some((existing, target_workspace)) = existing { + let result = if let Some(existing) = existing { let open_task = existing - .update(cx, |multi_workspace, window, cx| { + .update(cx, |workspace, window, cx| { window.activate_window(); - 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, - ) - }) + workspace.open_paths( + abs_paths, + OpenOptions { + visible: Some(open_visible), + ..Default::default() + }, + None, + window, + cx, + ) })? .await; - _ = 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); - } + _ = existing.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 { - 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 + cx.update(move |cx| { + Workspace::new_local( + abs_paths, + app_state.clone(), + open_options.replace_window, + open_options.env, + None, + cx, + ) + }) + .await }; #[cfg(target_os = "windows")] if let Some(util::paths::WslPath{distro, path}) = wsl_path - && let Ok((multi_workspace_window, _)) = &result + && let Ok((workspace, _)) = &result { - multi_workspace_window - .update(cx, move |multi_workspace, _window, cx| { + workspace + .update(cx, move |workspace, _window, cx| { struct OpenInWsl; - 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) - }) - }) - }); + 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(); @@ -8704,13 +8437,9 @@ pub fn open_new( Some(Box::new(init)), cx, ); - cx.spawn(async move |cx| { - let (window, _opened_paths) = task.await?; - window - .update(cx, |_, window, _cx| { - window.activate_window(); - }) - .ok(); + cx.spawn(async move |_cx| { + let (_workspace, _opened_paths) = task.await?; + // Init callback is called synchronously during workspace creation Ok(()) }) } @@ -8762,7 +8491,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, @@ -8822,7 +8551,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| { @@ -8848,7 +8577,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?; @@ -8893,10 +8622,21 @@ async fn open_remote_project_inner( return Err(project_path_errors.pop().context("no paths given")?); } - let workspace = window.update(cx, |multi_workspace, window, cx| { - telemetry::event!("SSH Project Opened"); + 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 new_workspace = cx.new(|cx| { let mut workspace = Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); workspace.update_history(cx); @@ -8907,21 +8647,16 @@ 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(); - workspace.update(cx, |_workspace, cx| { - open_items(serialized_workspace, project_paths_to_open, window, cx) - }) + open_items(serialized_workspace, project_paths_to_open, window, cx) })? .await?; - workspace.update(cx, |workspace, cx| { + window.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") { @@ -8931,7 +8666,7 @@ async fn open_remote_project_inner( workspace.show_error(&error, cx) } } - }); + })?; Ok(items.into_iter().map(|item| item?.ok()).collect()) } @@ -8969,37 +8704,24 @@ pub fn join_in_room_project( ) -> Task> { let windows = cx.windows(); cx.spawn(async move |cx| { - let existing_window_and_workspace: Option<( - WindowHandle, - Entity, - )> = windows.into_iter().find_map(|window_handle| { + let existing_workspace = windows.into_iter().find_map(|window_handle| { window_handle - .downcast::() + .downcast::() .and_then(|window_handle| { window_handle - .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())); - } + .update(cx, |workspace, _window, cx| { + if workspace.project().read(cx).remote_id() == Some(project_id) { + Some(window_handle) + } else { + None } - None }) .unwrap_or(None) }) }); - 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 + let workspace = if let Some(existing_workspace) = existing_workspace { + existing_workspace } else { let active_call = cx.update(|cx| ActiveCall::global(cx)); let room = active_call @@ -9021,44 +8743,39 @@ 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| { - let workspace = cx.new(|cx| { + cx.new(|cx| { Workspace::new(Default::default(), project, app_state.clone(), window, cx) - }); - cx.new(|cx| MultiWorkspace::new(workspace, cx)) + }) }) })? }; - multi_workspace_window.update(cx, |multi_workspace, window, cx| { + workspace.update(cx, |workspace, window, cx| { cx.activate(true); window.activate_window(); - // 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(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(()) @@ -9070,7 +8787,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, @@ -9102,11 +8819,8 @@ 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, |multi_workspace, window, cx| { - let workspace = multi_workspace.workspace().clone(); - workspace.update(cx, |workspace, cx| { - workspace.prepare_to_close(CloseIntent::Quit, window, cx) - }) + if let Ok(should_close) = window.update(cx, |workspace, window, cx| { + workspace.prepare_to_close(CloseIntent::Quit, window, cx) }) && !should_close.await? { return anyhow::Ok(()); @@ -9579,17 +9293,11 @@ 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(multi_workspace) => { + match cx.active_window().and_then(|w| w.downcast::()) { + Some(workspace) => { cx.defer(move |cx| { - multi_workspace - .update(cx, |multi_workspace, window, cx| { - let workspace = multi_workspace.workspace().clone(); - workspace.update(cx, |workspace, cx| f(workspace, window, cx)); - }) + workspace + .update(cx, |workspace, window, cx| f(workspace, window, cx)) .log_err(); }); } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 066f52ab6220280d657f7264e21f8d1f4869134f..052aba46b67a32c7607c60a049c3eb3eba3f06dd 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -49,7 +49,6 @@ visual-tests = [ "language_model/test-support", "fs/test-support", "recent_projects/test-support", - "sidebar/test-support", "title_bar/test-support", ] @@ -188,7 +187,6 @@ 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 6dc23fa4c585fa58502c794d8d2480ce6a1b278d..9a2cea33882b1a9d8434bf0f3e2cb1dba8471007 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, MultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace, Toast, - WorkspaceSettings, WorkspaceStore, notifications::NotificationId, restore_multiworkspace, + AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceId, + WorkspaceSettings, WorkspaceStore, notifications::NotificationId, }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, @@ -511,13 +511,15 @@ fn main() { let workspace_store = workspace_store.clone(); Arc::new(move |cx: &mut App| { workspace_store.update(cx, |workspace_store, cx| { - Ok(workspace_store + workspace_store .workspaces() - .filter_map(|weak| weak.upgrade()) - .map(|workspace: gpui::Entity| { - workspace.read(cx).project().read(cx).lsp_store() + .iter() + .map(|workspace| { + workspace.update(cx, |workspace, _, cx| { + workspace.project().read(cx).lsp_store() + }) }) - .collect()) + .collect() }) }) }), @@ -847,7 +849,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_multi_workspace(app_state, cx.clone()).await?; + workspace::get_any_active_workspace(app_state, cx.clone()).await?; workspace.update(cx, |_, window, cx| { window.dispatch_action( Box::new(zed_actions::Extensions { @@ -862,40 +864,31 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut } OpenRequestKind::AgentPanel { initial_prompt } => { cx.spawn(async move |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); - }); - } - }); + 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); + }); + } }) }) .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 = - multi_workspace.read_with(cx, |mw, _| mw.workspace().clone())?; + workspace::get_any_active_workspace(app_state.clone(), cx.clone()).await?; let (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)) - }) - })??; + 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) + })?; let Some(thread_store): Option> = thread_store else { anyhow::bail!("Agent panel not available"); @@ -928,27 +921,25 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut meta: None, }; - let sharer_username = response.sharer_username.clone(); - - 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); - } + 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); + } + })?; - struct ImportedThreadToast; - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - format!("Imported shared thread from {}", sharer_username), - ) - .autohide(), - cx, - ); - }); + 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, + ); })?; anyhow::Ok(()) @@ -1023,7 +1014,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_multi_workspace(app_state, cx.clone()).await?; + workspace::get_any_active_workspace(app_state, cx.clone()).await?; workspace.update(cx, |_, window, cx| match setting_path { None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx), @@ -1085,29 +1076,23 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut .await?; workspace - .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(()) - }) + .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(()) }) .log_err(); @@ -1177,7 +1162,6 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut client::ChannelId(channel_id), app_state.clone(), None, - None, cx, ) }) @@ -1185,9 +1169,8 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut } let workspace_window = - workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?; - - let workspace = workspace_window.read_with(cx, |mw, _| mw.workspace().clone())?; + workspace::get_any_active_workspace(app_state, cx.clone()).await?; + let workspace = workspace_window.entity(cx)?; let mut promises = Vec::new(); for (channel_id, heading) in request.open_channel_notes { @@ -1277,53 +1260,78 @@ async fn installation_id() -> Result { Ok(IdType::New(installation_id)) } -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 - { +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); let mut results: Vec> = Vec::new(); let mut tasks = Vec::new(); - let mut local_results = Vec::new(); - for multi_workspace in multi_workspaces { - local_results - .push(restore_multiworkspace(multi_workspace, app_state.clone(), cx).await); - } - - for result in local_results { - results.push(result.map(|_| ())); - } + 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(|_| ()) + }); - 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) - }); + 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); + } } - 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 window groups and remote workspaces to open concurrently + // Wait for all workspaces to open concurrently results.extend(future::join_all(tasks).await); // Show notifications for any errors that occurred @@ -1348,16 +1356,12 @@ pub(crate) async fn restore_or_create_workspace( // 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(multi_workspace) = window.downcast::() + && let Some(workspace) = window.downcast::() { - multi_workspace - .update(cx, |multi_workspace, _, cx| { - multi_workspace.workspace().update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new(NotificationId::unique::<()>(), message), - cx, - ) - }); + workspace + .update(cx, |workspace, _, cx| { + workspace + .show_toast(Toast::new(NotificationId::unique::<()>(), message), cx) }) .ok(); return true; @@ -1398,25 +1402,10 @@ pub(crate) async fn restore_or_create_workspace( 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(); @@ -1440,16 +1429,9 @@ pub(crate) async fn restorable_workspace_locations( match restore_behavior { workspace::RestoreOnStartupBehavior::LastWorkspace => { - workspace::last_opened_workspace_location(app_state.fs.as_ref()) + workspace::last_opened_workspace_location() .await - .map(|(workspace_id, location, paths)| { - vec![SessionWorkspace { - workspace_id, - location, - paths, - window_id: None, - }] - }) + .map(|location| vec![location]) } workspace::RestoreOnStartupBehavior::LastSession => { if let Some(last_session_id) = last_session_id { @@ -1458,9 +1440,7 @@ 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 68e6dde49ccfe55a582a293030bc368e32f1c67a..e713c7b440263734fb1202f15ada029f7a3e2cab 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -59,7 +59,6 @@ use { }, image::RgbaImage, project_panel::ProjectPanel, - recent_projects::RecentProjectEntry, settings::{NotifyWhenAgentWaiting, Settings as _}, settings_ui::SettingsWindow, std::{ @@ -71,7 +70,7 @@ use { }, util::ResultExt as _, watch, - workspace::{AppState, MultiWorkspace, Workspace, WorkspaceId}, + workspace::{AppState, Workspace}, zed_actions::OpenSettingsAt, }; @@ -436,24 +435,7 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()> } } - // 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 + // Run Test 3: Agent Thread View tests #[cfg(feature = "visual-tests")] { println!("\n--- Test 3: agent_thread_with_image (collapsed + expanded) ---"); @@ -2799,300 +2781,3 @@ 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 380594c3ddc06e05f150f8ce3b9babe8e8a3a0d7..6e80f13cec5bebe67062aed2a2f722af4269b2e1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -68,7 +68,6 @@ 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, @@ -89,9 +88,9 @@ use workspace::notifications::{ }; use workspace::utility_pane::utility_slot_for_dock_position; use workspace::{ - AppState, MultiWorkspace, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, - WorkspaceSettings, create_and_open_local_file, - notifications::simple_message_notification::MessageNotification, open_new, + AppState, 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, @@ -371,16 +370,6 @@ 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; @@ -1163,7 +1152,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() { @@ -1259,7 +1248,6 @@ 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()); @@ -1292,12 +1280,11 @@ 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); @@ -1372,10 +1359,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::>() }); @@ -1385,8 +1372,8 @@ fn quit(_: &Quit, cx: &mut App) { workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false)); }); - if should_confirm && let Some(multi_workspace) = workspace_windows.first() { - let answer = multi_workspace + if should_confirm && let Some(workspace) = workspace_windows.first() { + let answer = workspace .update(cx, |_, window, cx| { window.prompt( PromptLevel::Info, @@ -1410,30 +1397,14 @@ fn quit(_: &Quit, cx: &mut App) { // If the user cancels any save prompt, then keep the app open. for window in workspace_windows { - let workspaces = window - .update(cx, |multi_workspace, _, _| { - multi_workspace.workspaces().to_vec() + if let Some(should_close) = window + .update(cx, |workspace, window, cx| { + workspace.prepare_to_close(CloseIntent::Quit, window, cx) }) - .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(()); - } + .log_err() + { + if !should_close.await? { + return Ok(()); } } } @@ -2385,7 +2356,6 @@ mod tests { use settings::{SaturatingBool, SettingsStore, watch_config_file}; use std::{ path::{Path, PathBuf}, - sync::Arc, time::Duration, }; use theme::ThemeRegistry; @@ -2393,7 +2363,6 @@ mod tests { path, rel_path::{RelPath, rel_path}, }; - use workspace::MultiWorkspace; use workspace::{ NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection, WorkspaceHandle, @@ -2429,12 +2398,10 @@ mod tests { .unwrap(); assert_eq!(cx.read(|cx| cx.windows().len()), 1); - 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()) - }); + let workspace = cx.windows()[0].downcast::().unwrap(); + workspace + .update(cx, |workspace, _, cx| { + assert!(workspace.active_item_as::(cx).is_some()) }) .unwrap(); } @@ -2442,10 +2409,6 @@ 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() @@ -2499,23 +2462,21 @@ mod tests { .await .unwrap(); assert_eq!(cx.read(|cx| cx.windows().len()), 1); - let multi_workspace_1 = cx - .read(|cx| cx.windows()[0].downcast::()) + let workspace_1 = cx + .read(|cx| cx.windows()[0].downcast::()) .unwrap(); cx.run_until_parked(); - 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) - ); - }); + 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) + ); }) .unwrap(); @@ -2533,7 +2494,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( @@ -2550,12 +2511,11 @@ mod tests { .unwrap(); cx.background_executor.run_until_parked(); assert_eq!(cx.read(|cx| cx.windows().len()), 2); - let multi_workspace_1 = cx - .update(|cx| cx.windows()[0].downcast::()) + let workspace_1 = cx + .update(|cx| cx.windows()[0].downcast::()) .unwrap(); - multi_workspace_1 - .update(cx, |multi_workspace, window, cx| { - let workspace = multi_workspace.workspace().read(cx); + workspace_1 + .update(cx, |workspace, window, cx| { assert_eq!( workspace .worktrees(cx) @@ -2727,21 +2687,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().workspace().read(cx).is_edited()) + let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { + cx.update(|cx| window.read(cx).unwrap().is_edited()) }; let pane = window - .read_with(cx, |multi_workspace, cx| { - multi_workspace.workspace().read(cx).active_pane().clone() - }) + .read_with(cx, |workspace, _| workspace.active_pane().clone()) .unwrap(); let editor = window - .read_with(cx, |multi_workspace, cx| { - multi_workspace - .workspace() - .read(cx) + .read_with(cx, |workspace, cx| { + workspace .active_item(cx) .unwrap() .downcast::() @@ -2814,26 +2770,22 @@ mod tests { executor.run_until_parked(); window - .update(cx, |multi_workspace, _, cx| { - multi_workspace.workspace().update(cx, |workspace, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); + .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, |multi_workspace, cx| { - multi_workspace - .workspace() - .read(cx) + .read_with(cx, |workspace, cx| { + workspace .active_item(cx) .unwrap() .downcast::() @@ -2886,17 +2838,15 @@ 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().workspace().read(cx).is_edited()) + let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { + cx.update(|cx| window.read(cx).unwrap().is_edited()) }; let editor = window - .read_with(cx, |multi_workspace, cx| { - multi_workspace - .workspace() - .read(cx) + .read_with(cx, |workspace, cx| { + workspace .active_item(cx) .unwrap() .downcast::() @@ -2943,27 +2893,22 @@ 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, |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)); - }); + .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(); } @@ -2985,40 +2930,36 @@ mod tests { .unwrap(); cx.run_until_parked(); - let multi_workspace = cx - .update(|cx| cx.windows().first().unwrap().downcast::()) + let workspace = cx + .update(|cx| cx.windows().first().unwrap().downcast::()) .unwrap(); - 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)); - }); + 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)); + }); - editor - }) + editor }) .unwrap(); - 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) - }) + let save_task = workspace + .update(cx, |workspace, window, 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(); - multi_workspace + workspace .update(cx, |_, _, cx| { editor.update(cx, |editor, cx| { assert!(!editor.is_dirty(cx)); @@ -3199,10 +3140,8 @@ 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 - .read_with(cx, |mw, _| mw.workspace().clone()) - .unwrap(); + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + let workspace = window.root(cx).unwrap(); #[track_caller] fn assert_project_panel_selection( @@ -3237,19 +3176,17 @@ mod tests { // Open a file within an existing worktree. window - .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, - ) - }) + .update(cx, |workspace, window, cx| { + workspace.open_paths( + vec![path!("/dir1/a.txt").into()], + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + None, + window, + cx, + ) }) .unwrap() .await; @@ -3278,19 +3215,17 @@ mod tests { // Open a file outside of any existing worktree. window - .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, - ) - }) + .update(cx, |workspace, window, cx| { + workspace.open_paths( + vec![path!("/dir2/b.txt").into()], + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + None, + window, + cx, + ) }) .unwrap() .await; @@ -3330,19 +3265,17 @@ mod tests { // Ensure opening a directory and one of its children only adds one worktree. window - .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, - ) - }) + .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, + ) }) .unwrap() .await; @@ -3382,19 +3315,17 @@ mod tests { // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. window - .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, - ) - }) + .update(cx, |workspace, window, cx| { + workspace.open_paths( + vec![path!("/d.txt").into()], + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + None, + window, + cx, + ) }) .unwrap() .await; @@ -3488,13 +3419,8 @@ 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({ - let project = project.clone(); - |window, cx| MultiWorkspace::test_new(project, window, cx) - }); - let workspace = window - .read_with(cx, |mw, _| mw.workspace().clone()) - .unwrap(); + let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + let workspace = window.root(cx).unwrap(); let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); let paths_to_open = [ @@ -3515,9 +3441,7 @@ mod tests { .unwrap(); assert_eq!( - opened_workspace - .read_with(cx, |mw, _| mw.workspace().entity_id()) - .unwrap(), + opened_workspace.root(cx).unwrap().entity_id(), workspace.entity_id(), "Excluded files in subfolders of a workspace root should be opened in the workspace" ); @@ -4940,7 +4864,6 @@ mod tests { "lsp_tool", "markdown", "menu", - "multi_workspace", "new_process_modal", "notebook", "notification_panel", @@ -5028,7 +4951,7 @@ mod tests { cx.update(init); let project = Project::test(app_state.fs.clone(), [], cx).await; - let _window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); cx.update(|cx| { cx.dispatch_action(&OpenDefaultSettings); @@ -5037,12 +4960,10 @@ mod tests { assert_eq!(cx.read(|cx| cx.windows().len()), 1); - 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)) + let workspace = cx.windows()[0].downcast::().unwrap(); + let active_editor = workspace + .update(cx, |workspace, _, cx| { + workspace.active_item_as::(cx) }) .unwrap(); assert!( @@ -5346,22 +5267,16 @@ mod tests { .await; let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_a = cx.add_window({ - let project = project_a.clone(); - |window, cx| MultiWorkspace::test_new(project, window, cx) - }); + let window_a = + cx.add_window(|window, cx| Workspace::test_new(project_a.clone(), window, cx)); let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_b = cx.add_window({ - let project = project_b.clone(); - |window, cx| MultiWorkspace::test_new(project, window, cx) - }); + let window_b = + cx.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx)); let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_c = cx.add_window({ - let project = project_c.clone(); - |window, cx| MultiWorkspace::test_new(project, window, cx) - }); + let window_c = + cx.add_window(|window, cx| Workspace::test_new(project_c.clone(), window, cx)); for window in [window_a, window_b, window_c] { let _ = cx.update_window(*window, |_, window, _| { @@ -5382,8 +5297,8 @@ mod tests { cx.update_window(*window, |_, window, _| assert!(window.is_window_active())) .unwrap(); - let _ = window.read_with(cx, |multi_workspace, cx| { - let pane = multi_workspace.workspace().read(cx).active_pane().read(cx); + let _ = window.read_with(cx, |workspace, cx| { + let pane = workspace.active_pane().read(cx); let project_path = pane.active_item().unwrap().project_path(cx).unwrap(); assert_eq!( @@ -5393,707 +5308,4 @@ 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, |_, window, cx| { - cx.new(|cx| Workspace::test_new(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, |_, window, cx| { - cx.new(|cx| Workspace::test_new(project3.clone(), window, cx)) - }) - .unwrap(); - - let workspace1 = window - .read_with(cx, |multi_workspace, _| multi_workspace.workspace().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, |_, window, cx| { - cx.new(|cx| Workspace::test_new(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 f8bec397f1cf54fe37962c6a318a816a3158423e..2452f17d04007364861e9a262b492155daec0c55 100644 --- a/crates/zed/src/zed/migrate.rs +++ b/crates/zed/src/zed/migrate.rs @@ -1,7 +1,6 @@ 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; @@ -23,7 +22,6 @@ pub enum MigrationType { } pub struct MigrationBanner { - workspace: WeakEntity, migration_type: Option, should_migrate_task: Option>, markdown: Option>, @@ -56,7 +54,7 @@ struct GlobalMigrationNotification(Entity); impl Global for GlobalMigrationNotification {} impl MigrationBanner { - pub fn new(workspace: WeakEntity, cx: &mut Context) -> Self { + pub fn new(_: &Workspace, cx: &mut Context) -> Self { if let Some(notifier) = MigrationNotification::try_global(cx) { cx.subscribe( ¬ifier, @@ -67,7 +65,6 @@ impl MigrationBanner { .detach(); } Self { - workspace, migration_type: None, should_migrate_task: None, markdown: None, @@ -238,22 +235,22 @@ impl Render for MigrationBanner { ), ) .child( - Button::new("backup-and-migrate", "Backup and Update").on_click({ - let workspace = self.workspace.clone(); + Button::new("backup-and-migrate", "Backup and Update").on_click( move |_, window, cx| { let fs = ::global(cx); - let task = match migration_type { + 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 2fdefff246a9cfd32bd27797451a545d2ab5e565..86d35d558dc024931a901c479f26e502de381ca7 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::restore_or_create_workspace; +use crate::restorable_workspace_locations; 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, MultiWorkspace, OpenOptions, SerializedWorkspaceLocation}; +use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; #[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,29 +357,24 @@ pub async fn open_paths_with_positions( }) .collect::>(); - let (multi_workspace, mut items) = cx + let (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) = 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 Ok(diff_view) = workspace.update(cx, |workspace, window, 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) = multi_workspace.update(cx, |_multi_workspace, window, cx| { - FileDiffView::open(old_path, new_path, workspace_weak.clone(), window, cx) + if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { + FileDiffView::open(old_path, new_path, workspace, window, cx) }) { if let Some(diff_view) = diff_view.await.log_err() { items.push(Some(Ok(Box::new(diff_view)))) @@ -400,7 +395,7 @@ pub async fn open_paths_with_positions( continue; }; if let Some(active_editor) = item.downcast::() { - multi_workspace + workspace .update(cx, |_, window, cx| { active_editor.update(cx, |editor, cx| { editor.go_to_singleton_buffer_point(point, window, cx); @@ -410,7 +405,7 @@ pub async fn open_paths_with_positions( } } - Ok((multi_workspace, items)) + Ok((workspace, items)) } pub async fn handle_cli_connection( @@ -493,13 +488,20 @@ 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() { - Vec::new() + 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() + } } else { vec![( SerializedWorkspaceLocation::Local, @@ -753,7 +755,7 @@ mod tests { use serde_json::json; use std::{sync::Arc, task::Poll}; use util::path; - use workspace::{AppState, MultiWorkspace}; + use workspace::{AppState, Workspace}; #[gpui::test] fn test_parse_ssh_url(cx: &mut TestAppContext) { @@ -889,12 +891,10 @@ mod tests { open_workspace_file(path!("/root/dir1"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); - 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()) - }); + let workspace = cx.windows()[0].downcast::().unwrap(); + workspace + .update(cx, |workspace, _, cx| { + assert!(workspace.active_item_as::(cx).is_none()) }) .unwrap(); @@ -902,11 +902,9 @@ mod tests { open_workspace_file(path!("/root/dir1/file1.txt"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); - multi_workspace - .update(cx, |multi_workspace, _, cx| { - multi_workspace.workspace().update(cx, |workspace, cx| { - assert!(workspace.active_item_as::(cx).is_some()); - }); + workspace + .update(cx, |workspace, _, cx| { + assert!(workspace.active_item_as::(cx).is_some()); }) .unwrap(); @@ -921,14 +919,12 @@ mod tests { assert_eq!(cx.windows().len(), 2); - 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"); - }); + 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"); }) .unwrap(); } @@ -1004,12 +1000,10 @@ mod tests { open_workspace_file(path!("/root/file5.txt"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); - 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()) - }); + let workspace_1 = cx.windows()[0].downcast::().unwrap(); + workspace_1 + .update(cx, |workspace, _, cx| { + assert!(workspace.active_item_as::(cx).is_some()) }) .unwrap(); @@ -1018,12 +1012,10 @@ mod tests { open_workspace_file(path!("/root/file6.txt"), Some(false), app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); - 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"); - }); + workspace_1 + .update(cx, |workspace, _, cx| { + let items = workspace.items(cx).collect::>(); + assert_eq!(items.len(), 2, "Workspace should have two items"); }) .unwrap(); @@ -1032,13 +1024,11 @@ mod tests { open_workspace_file(path!("/root/file7.txt"), Some(true), app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 2); - 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"); - }); + 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"); }) .unwrap(); }