From 807207e27ff09507c225e4da740f9a1fb87de962 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 30 Mar 2026 15:06:53 -0700 Subject: [PATCH] Own the workspace list in MultiWorkspace (#52546) ## Context TODO ## Self-Review Checklist - [ ] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [ ] Performance impact has been considered and is acceptable Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- crates/agent_ui/src/agent_panel.rs | 19 +- crates/agent_ui/src/conversation_view.rs | 2 +- crates/git_ui/src/worktree_picker.rs | 33 +- crates/project_panel/src/project_panel.rs | 6 +- .../src/disconnected_overlay.rs | 2 +- crates/recent_projects/src/recent_projects.rs | 66 ++-- .../recent_projects/src/remote_connections.rs | 8 +- crates/recent_projects/src/remote_servers.rs | 4 +- .../src/sidebar_recent_projects.rs | 8 +- crates/recent_projects/src/wsl_picker.rs | 10 +- crates/sidebar/src/sidebar.rs | 54 ++- crates/sidebar/src/sidebar_tests.rs | 48 ++- crates/workspace/src/multi_workspace.rs | 321 +++++++++++++----- crates/workspace/src/persistence.rs | 20 +- crates/workspace/src/welcome.rs | 4 +- crates/workspace/src/workspace.rs | 142 +++++--- crates/zed/src/visual_test_runner.rs | 5 +- crates/zed/src/zed.rs | 31 +- crates/zed/src/zed/open_listener.rs | 4 +- 19 files changed, 512 insertions(+), 275 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 8d6cac0e647d78f9746b1dcf3d1966ec6b6653af..37dc2a86a8e4225ecc5bef3315714900240c13fb 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -83,8 +83,9 @@ use ui::{ }; use util::{ResultExt as _, debug_panic}; use workspace::{ - CollaboratorId, DraggedSelection, DraggedTab, OpenResult, PathList, SerializedPathList, - ToggleWorkspaceSidebar, ToggleZoom, ToolbarItemView, Workspace, WorkspaceId, + CollaboratorId, DraggedSelection, DraggedTab, OpenMode, OpenResult, PathList, + SerializedPathList, ToggleWorkspaceSidebar, ToggleZoom, ToolbarItemView, Workspace, + WorkspaceId, dock::{DockPosition, Panel, PanelEvent}, }; use zed_actions::{ @@ -2939,7 +2940,15 @@ impl AgentPanel { .. } = cx .update(|_window, cx| { - Workspace::new_local(all_paths, app_state, window_handle, None, None, false, cx) + Workspace::new_local( + all_paths, + app_state, + window_handle, + None, + None, + OpenMode::Add, + cx, + ) })? .await?; @@ -3062,8 +3071,8 @@ impl AgentPanel { }); })?; - new_window_handle.update(cx, |multi_workspace, _window, cx| { - multi_workspace.activate(new_workspace.clone(), cx); + new_window_handle.update(cx, |multi_workspace, window, cx| { + multi_workspace.activate(new_workspace.clone(), window, cx); })?; this.update_in(cx, |this, window, cx| { diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index b29b02db7341378b58935d2184d4dc90c7a51098..d6808aa4e1d882f4a73abac3d5e72173772a5634 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -2413,7 +2413,7 @@ impl ConversationView { .update(cx, |multi_workspace, window, cx| { window.activate_window(); if let Some(workspace) = workspace_handle.upgrade() { - multi_workspace.activate(workspace.clone(), cx); + multi_workspace.activate(workspace.clone(), window, cx); workspace.update(cx, |workspace, cx| { workspace.focus_panel::(window, cx); }); diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index d7488cc1bddb7e9d2825b8d21d3dc6c4c4fdde5a..9486dbc7b6ddb94e59f9da44069d55d8709fbea7 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -20,7 +20,9 @@ use settings::Settings; use std::{path::PathBuf, sync::Arc}; use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::{ResultExt, debug_panic}; -use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr}; +use workspace::{ + ModalView, MultiWorkspace, OpenMode, Workspace, notifications::DetachAndPromptErr, +}; use crate::git_panel::show_error_toast; @@ -354,7 +356,7 @@ impl WorktreeListDelegate { workspace .update_in(cx, |workspace, window, cx| { workspace.open_workspace_for_paths( - replace_current_window, + OpenMode::Replace, vec![new_worktree_path], window, cx, @@ -407,10 +409,15 @@ impl WorktreeListDelegate { else { return; }; + let open_mode = if replace_current_window { + OpenMode::Replace + } else { + OpenMode::NewWindow + }; if is_local { let open_task = workspace.update(cx, |workspace, cx| { - workspace.open_workspace_for_paths(replace_current_window, vec![path], window, cx) + workspace.open_workspace_for_paths(open_mode, vec![path], window, cx) }); cx.spawn(async move |_, _| { open_task?.await?; @@ -951,16 +958,6 @@ impl PickerDelegate for WorktreeListDelegate { }) .child( Button::new("open-in-new-window", "Open in New Window") - .key_binding( - KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx) - }), - ) - .child( - Button::new("open-in-window", "Open") .key_binding( KeyBinding::for_action_in( &menu::SecondaryConfirm, @@ -973,6 +970,16 @@ impl PickerDelegate for WorktreeListDelegate { window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) }), ) + .child( + Button::new("open-in-window", "Open") + .key_binding( + KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx) + }), + ) .into_any(), ) } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 599afd99b37cee5602934a14df117b6162819806..e9062364fc73ed6e266e3f8904be51eaaf5b6535 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -71,8 +71,8 @@ use util::{ rel_path::{RelPath, RelPathBuf}, }; use workspace::{ - DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry, - SplitDirection, Workspace, + DraggedSelection, OpenInTerminal, OpenMode, OpenOptions, OpenVisible, PreviewTabsSettings, + SelectedEntry, SplitDirection, Workspace, dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt}, }; @@ -7126,7 +7126,7 @@ impl Render for ProjectPanel { .workspace .update(cx, |workspace, cx| { workspace.open_workspace_for_paths( - true, + OpenMode::Replace, external_paths.paths().to_owned(), window, cx, diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 732b50c123d9d61750781df81ce00b392997af3c..e78762eb283160f84b163771b9835188d2ffce4a 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -125,7 +125,7 @@ impl DisconnectedOverlay { paths, app_state, OpenOptions { - replace_window: Some(window_handle), + requesting_window: Some(window_handle), ..Default::default() }, cx, diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index cd01af2ce9778af441c31d17e4424627997b2495..4dc06036ef8416fd859cc815ab090ba5896c0040 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -46,7 +46,7 @@ use ui::{ }; use util::{ResultExt, paths::PathExt}; use workspace::{ - HistoryManager, ModalView, MultiWorkspace, OpenOptions, OpenVisible, PathList, + HistoryManager, ModalView, MultiWorkspace, OpenMode, OpenOptions, OpenVisible, PathList, SerializedWorkspaceLocation, Workspace, WorkspaceDb, WorkspaceId, notifications::DetachAndPromptErr, with_active_or_new_workspace, }; @@ -262,13 +262,13 @@ pub fn init(cx: &mut App) { user: None, }); - let replace_window = match create_new_window { + let requesting_window = match create_new_window { false => window_handle, true => None, }; let open_options = workspace::OpenOptions { - replace_window, + requesting_window, ..Default::default() }; @@ -321,7 +321,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::(), + requesting_window: window.window_handle().downcast::(), ..Default::default() }; @@ -1031,14 +1031,14 @@ impl PickerDelegate for RecentProjectsDelegate { if let Some(handle) = window.window_handle().downcast::() { cx.defer(move |cx| { handle - .update(cx, |multi_workspace, _window, cx| { + .update(cx, |multi_workspace, window, cx| { let workspace = multi_workspace .workspaces() .iter() .find(|ws| ws.read(cx).database_id() == Some(workspace_id)) .cloned(); if let Some(workspace) = workspace { - multi_workspace.activate(workspace, cx); + multi_workspace.activate(workspace, window, cx); } }) .log_err(); @@ -1079,7 +1079,12 @@ impl PickerDelegate for RecentProjectsDelegate { cx.defer(move |cx| { if let Some(task) = handle .update(cx, |multi_workspace, window, cx| { - multi_workspace.open_project(paths, window, cx) + multi_workspace.open_project( + paths, + OpenMode::Replace, + window, + cx, + ) }) .log_err() { @@ -1090,7 +1095,12 @@ impl PickerDelegate for RecentProjectsDelegate { return; } else { workspace - .open_workspace_for_paths(false, paths, window, cx) + .open_workspace_for_paths( + OpenMode::NewWindow, + paths, + window, + cx, + ) .detach_and_prompt_err( "Failed to open project", window, @@ -1107,7 +1117,7 @@ impl PickerDelegate for RecentProjectsDelegate { None }; let open_options = OpenOptions { - replace_window, + requesting_window: replace_window, ..Default::default() }; if let RemoteConnectionOptions::Ssh(connection) = &mut connection { @@ -1804,12 +1814,13 @@ impl RecentProjectsDelegate { cx.defer(move |cx| { handle .update(cx, |multi_workspace, window, cx| { - let index = multi_workspace + let workspace = multi_workspace .workspaces() .iter() - .position(|ws| ws.read(cx).database_id() == Some(workspace_id)); - if let Some(index) = index { - multi_workspace.remove_workspace(index, window, cx); + .find(|ws| ws.read(cx).database_id() == Some(workspace_id)) + .cloned(); + if let Some(workspace) = workspace { + multi_workspace.remove(&workspace, window, cx); } }) .log_err(); @@ -1886,7 +1897,7 @@ mod tests { use super::*; #[gpui::test] - async fn test_dirty_workspace_survives_when_opening_recent_project(cx: &mut TestAppContext) { + async fn test_dirty_workspace_replaced_when_opening_recent_project(cx: &mut TestAppContext) { let app_state = init_test(cx); cx.update(|cx| { @@ -1995,6 +2006,15 @@ mod tests { cx.dispatch_action(*multi_workspace, menu::Confirm); cx.run_until_parked(); + // prepare_to_close triggers a save prompt for the dirty buffer. + // Choose "Don't Save" (index 2) to discard and continue replacing. + assert!( + cx.has_pending_prompt(), + "Should prompt to save dirty buffer before replacing workspace" + ); + cx.simulate_prompt_answer("Don't Save"); + cx.run_until_parked(); + multi_workspace .update(cx, |multi_workspace, _, cx| { assert!( @@ -2007,26 +2027,16 @@ mod tests { ); assert!( - multi_workspace.workspaces().len() >= 2, - "Should have at least 2 workspaces: the dirty one and the newly opened one" + !multi_workspace.workspaces().contains(&dirty_workspace), + "The original dirty workspace should have been replaced" ); 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" + !multi_workspace.workspace().read(cx).is_edited(), + "The active workspace should be the freshly opened one, not dirty" ); }) .unwrap(); - - assert!( - !cx.has_pending_prompt(), - "No save prompt in multi-workspace mode — dirty workspace survives in background" - ); } fn open_recent_projects( diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 5275cdaa1526a670e817ff3b229d7e92b94bb309..3611b55ec65c94695e4e8835fa7afe8badc80a29 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -132,7 +132,7 @@ pub async fn open_remote_project( open_options: workspace::OpenOptions, cx: &mut AsyncApp, ) -> Result<()> { - let created_new_window = open_options.replace_window.is_none(); + let created_new_window = open_options.requesting_window.is_none(); let (existing, open_visible) = find_existing_workspace( &paths, @@ -159,7 +159,7 @@ pub async fn open_remote_project( let open_results = existing_window .update(cx, |multi_workspace, window, cx| { window.activate_window(); - multi_workspace.activate(existing_workspace.clone(), cx); + multi_workspace.activate(existing_workspace.clone(), window, cx); existing_workspace.update(cx, |workspace, cx| { workspace.open_paths( resolved_paths, @@ -201,7 +201,7 @@ pub async fn open_remote_project( ); } - let (window, initial_workspace) = if let Some(window) = open_options.replace_window { + let (window, initial_workspace) = if let Some(window) = open_options.requesting_window { let workspace = window.update(cx, |multi_workspace, _, _| { multi_workspace.workspace().clone() })?; @@ -854,7 +854,7 @@ mod tests { paths, app_state, workspace::OpenOptions { - replace_window: Some(window), + requesting_window: Some(window), ..Default::default() }, &mut async_cx, diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 2d285cbd36396b8cc30d456c7b37f4b5f187aeb3..f7054687579155d4895ae191de1b7fa7cd14fbf6 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1566,7 +1566,7 @@ impl RemoteServerProjects { project.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions { - replace_window, + requesting_window: replace_window, ..OpenOptions::default() }, cx, @@ -1894,7 +1894,7 @@ impl RemoteServerProjects { vec![starting_dir].into_iter().map(PathBuf::from).collect(), app_state, OpenOptions { - replace_window, + requesting_window: replace_window, ..OpenOptions::default() }, cx, diff --git a/crates/recent_projects/src/sidebar_recent_projects.rs b/crates/recent_projects/src/sidebar_recent_projects.rs index 72006cf6b769d23e4d2e4d535d33b61c605bad8c..4741c23049b34263c9b65d6c751675543d01c3df 100644 --- a/crates/recent_projects/src/sidebar_recent_projects.rs +++ b/crates/recent_projects/src/sidebar_recent_projects.rs @@ -17,8 +17,8 @@ use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; use ui_input::ErasedEditor; use util::{ResultExt, paths::PathExt}; use workspace::{ - MultiWorkspace, OpenOptions, PathList, SerializedWorkspaceLocation, Workspace, WorkspaceDb, - WorkspaceId, notifications::DetachAndPromptErr, + MultiWorkspace, OpenMode, OpenOptions, PathList, SerializedWorkspaceLocation, Workspace, + WorkspaceDb, WorkspaceId, notifications::DetachAndPromptErr, }; use crate::{highlights_for_path, icon_for_remote_connection, open_remote_project}; @@ -272,7 +272,7 @@ impl PickerDelegate for SidebarRecentProjectsDelegate { cx.defer(move |cx| { if let Some(task) = handle .update(cx, |multi_workspace, window, cx| { - multi_workspace.open_project(paths, window, cx) + multi_workspace.open_project(paths, OpenMode::Activate, window, cx) }) .log_err() { @@ -287,7 +287,7 @@ impl PickerDelegate for SidebarRecentProjectsDelegate { let app_state = workspace.app_state().clone(); let replace_window = window.window_handle().downcast::(); let open_options = OpenOptions { - replace_window, + requesting_window: replace_window, ..Default::default() }; if let RemoteConnectionOptions::Ssh(connection) = &mut connection { diff --git a/crates/recent_projects/src/wsl_picker.rs b/crates/recent_projects/src/wsl_picker.rs index d366f1090dac91ac0e778c578ceb864dac80cf86..9c08c4f5f4941a80afdd2d9cbb6f2c51ee8ec754 100644 --- a/crates/recent_projects/src/wsl_picker.rs +++ b/crates/recent_projects/src/wsl_picker.rs @@ -245,14 +245,16 @@ impl WslOpenModal { true => secondary, false => !secondary, }; - let replace_window = match replace_current_window { - true => window.window_handle().downcast::(), - false => None, + let open_mode = if replace_current_window { + workspace::OpenMode::Replace + } else { + workspace::OpenMode::NewWindow }; let paths = self.paths.clone(); let open_options = workspace::OpenOptions { - replace_window, + requesting_window: window.window_handle().downcast::(), + open_mode, ..Default::default() }; diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 96a3b4e9c41618a54637bfdd78642e559b975228..ee329fc3b415b3268d772e3ba7171f76a2fe0c37 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -1334,8 +1334,11 @@ impl Sidebar { this.focused_thread = None; if let Some(multi_workspace) = this.multi_workspace.upgrade() { multi_workspace.update(cx, |multi_workspace, cx| { - multi_workspace - .activate(workspace_for_open.clone(), cx); + multi_workspace.activate( + workspace_for_open.clone(), + window, + cx, + ); }); } if AgentPanel::is_visible(&workspace_for_open, cx) { @@ -1438,13 +1441,7 @@ impl Sidebar { if let Some(mw) = multi_workspace_for_worktree.upgrade() { let ws = workspace_for_remove_worktree.clone(); mw.update(cx, |multi_workspace, cx| { - if let Some(index) = multi_workspace - .workspaces() - .iter() - .position(|w| *w == ws) - { - multi_workspace.remove_workspace(index, window, cx); - } + multi_workspace.remove(&ws, window, cx); }); } } else { @@ -1474,7 +1471,7 @@ impl Sidebar { move |window, cx| { if let Some(mw) = multi_workspace_for_add.upgrade() { mw.update(cx, |mw, cx| { - mw.activate(workspace_for_add.clone(), cx); + mw.activate(workspace_for_add.clone(), window, cx); }); } workspace_for_add.update(cx, |workspace, cx| { @@ -1497,14 +1494,11 @@ impl Sidebar { move |window, cx| { if let Some(mw) = multi_workspace_for_move.upgrade() { mw.update(cx, |multi_workspace, cx| { - if let Some(index) = multi_workspace - .workspaces() - .iter() - .position(|w| *w == workspace_for_move) - { - multi_workspace - .move_workspace_to_new_window(index, window, cx); - } + multi_workspace.move_workspace_to_new_window( + &workspace_for_move, + window, + cx, + ); }); } }, @@ -1520,11 +1514,7 @@ impl Sidebar { if let Some(mw) = multi_workspace_for_remove.upgrade() { let ws = workspace_for_remove.clone(); mw.update(cx, |multi_workspace, cx| { - if let Some(index) = - multi_workspace.workspaces().iter().position(|w| *w == ws) - { - multi_workspace.remove_workspace(index, window, cx); - } + multi_workspace.remove(&ws, window, cx); }); } }) @@ -1942,7 +1932,7 @@ impl Sidebar { self.record_thread_access(&metadata.session_id); multi_workspace.update(cx, |multi_workspace, cx| { - multi_workspace.activate(workspace.clone(), cx); + multi_workspace.activate(workspace.clone(), window, cx); }); Self::load_agent_thread_in_workspace(workspace, metadata, true, window, cx); @@ -1962,7 +1952,7 @@ impl Sidebar { let activated = target_window .update(cx, |multi_workspace, window, cx| { window.activate_window(); - multi_workspace.activate(workspace.clone(), cx); + multi_workspace.activate(workspace.clone(), window, cx); Self::load_agent_thread_in_workspace(&workspace, &metadata, true, window, cx); }) .log_err() @@ -2024,7 +2014,9 @@ impl Sidebar { let paths: Vec = path_list.paths().iter().map(|p| p.to_path_buf()).collect(); - let open_task = multi_workspace.update(cx, |mw, cx| mw.open_project(paths, window, cx)); + let open_task = multi_workspace.update(cx, |mw, cx| { + mw.open_project(paths, workspace::OpenMode::Activate, window, cx) + }); cx.spawn_in(window, async move |this, cx| { let workspace = open_task.await?; @@ -2512,7 +2504,7 @@ impl Sidebar { } => { if let Some(mw) = weak_multi_workspace.upgrade() { mw.update(cx, |mw, cx| { - mw.activate(workspace.clone(), cx); + mw.activate(workspace.clone(), window, cx); }); } this.focused_thread = Some(metadata.session_id.clone()); @@ -2527,7 +2519,7 @@ impl Sidebar { } => { if let Some(mw) = weak_multi_workspace.upgrade() { mw.update(cx, |mw, cx| { - mw.activate(workspace.clone(), cx); + mw.activate(workspace.clone(), window, cx); }); } this.record_thread_access(&metadata.session_id); @@ -2543,7 +2535,7 @@ impl Sidebar { if let Some(mw) = weak_multi_workspace.upgrade() { if let Some(original_ws) = &original_workspace { mw.update(cx, |mw, cx| { - mw.activate(original_ws.clone(), cx); + mw.activate(original_ws.clone(), window, cx); }); } } @@ -2594,7 +2586,7 @@ impl Sidebar { if let Some((metadata, workspace)) = initial_preview { if let Some(mw) = self.multi_workspace.upgrade() { mw.update(cx, |mw, cx| { - mw.activate(workspace.clone(), cx); + mw.activate(workspace.clone(), window, cx); }); } self.focused_thread = Some(metadata.session_id.clone()); @@ -2895,7 +2887,7 @@ impl Sidebar { self.focused_thread = None; multi_workspace.update(cx, |multi_workspace, cx| { - multi_workspace.activate(workspace.clone(), cx); + multi_workspace.activate(workspace.clone(), window, cx); }); workspace.update(cx, |workspace, cx| { diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 63c3124ae2557558f7644fe0257520f175dd35d1..ecc3ce37600d9b903facd019408c08765dd5d094 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -385,7 +385,8 @@ async fn test_workspace_lifecycle(cx: &mut TestAppContext) { // Remove the second workspace multi_workspace.update_in(cx, |mw, window, cx| { - mw.remove_workspace(1, window, cx); + let workspace = mw.workspaces()[1].clone(); + mw.remove(&workspace, window, cx); }); cx.run_until_parked(); @@ -1737,7 +1738,8 @@ async fn test_confirm_on_historical_thread_activates_workspace(cx: &mut TestAppC // Switch to workspace 1 so we can verify the confirm switches back. multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(1, window, cx); + let workspace = mw.workspaces()[1].clone(); + mw.activate(workspace, window, cx); }); cx.run_until_parked(); assert_eq!( @@ -2001,7 +2003,8 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) { }); multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }); cx.run_until_parked(); @@ -2056,7 +2059,8 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) { // a workspace header) should clear focused_thread. multi_workspace.update_in(cx, |mw, window, cx| { if let Some(index) = mw.workspaces().iter().position(|w| w == &workspace_b) { - mw.activate_index(index, window, cx); + let workspace = mw.workspaces()[index].clone(); + mw.activate(workspace, window, cx); } }); cx.run_until_parked(); @@ -2317,7 +2321,8 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp // Switch to the worktree workspace. multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(1, window, cx); + let workspace = mw.workspaces()[1].clone(); + mw.activate(workspace, window, cx); }); let sidebar = setup_sidebar(&multi_workspace, cx); @@ -2887,7 +2892,8 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp // Switch back to the main workspace before setting up the sidebar. multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }); let sidebar = setup_sidebar(&multi_workspace, cx); @@ -2993,7 +2999,8 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp let worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx); multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }); let sidebar = setup_sidebar(&multi_workspace, cx); @@ -3338,7 +3345,8 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace( // Activate the main workspace before setting up the sidebar. multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }); let sidebar = setup_sidebar(&multi_workspace, cx); @@ -3422,7 +3430,8 @@ async fn test_activate_archived_thread_with_saved_paths_activates_matching_works // Ensure workspace A is active. multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }); cx.run_until_parked(); assert_eq!( @@ -3484,7 +3493,8 @@ async fn test_activate_archived_thread_cwd_fallback_with_matching_workspace( // Start with workspace A active. multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }); cx.run_until_parked(); assert_eq!( @@ -3545,7 +3555,8 @@ async fn test_activate_archived_thread_no_paths_no_cwd_uses_active_workspace( // Activate workspace B (index 1) to make it the active one. multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(1, window, cx); + let workspace = mw.workspaces()[1].clone(); + mw.activate(workspace, window, cx); }); cx.run_until_parked(); assert_eq!( @@ -3928,7 +3939,8 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon // Activate main workspace so the sidebar tracks the main panel. multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }); let sidebar = setup_sidebar(&multi_workspace, cx); @@ -4784,9 +4796,11 @@ mod property_test { state.workspace_paths.push(worktree.path); } Operation::RemoveWorkspace { index } => { - let removed = multi_workspace - .update_in(cx, |mw, window, cx| mw.remove_workspace(index, window, cx)); - if removed.is_some() { + let removed = multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = mw.workspaces()[index].clone(); + mw.remove(&workspace, window, cx) + }); + if removed { state.workspace_paths.remove(index); state.main_repo_indices.retain(|i| *i != index); for i in &mut state.main_repo_indices { @@ -4799,8 +4813,8 @@ mod property_test { Operation::SwitchWorkspace { index } => { let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspaces()[index].clone()); - multi_workspace.update_in(cx, |mw, _window, cx| { - mw.activate(workspace, cx); + multi_workspace.update_in(cx, |mw, window, cx| { + mw.activate(workspace, window, cx); }); } Operation::AddLinkedWorktree { workspace_index } => { diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index adfbd99866544fe1045d2edebd271111928cf6f4..ae3af8046bef3efced56e31cc4d063b7dcc80811 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -24,8 +24,8 @@ use ui::{ContextMenu, right_click_menu}; const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0); use crate::{ - CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, - Workspace, WorkspaceId, client_side_decorations, + CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, OpenMode, + Panel, Workspace, WorkspaceId, client_side_decorations, }; actions!( @@ -233,7 +233,7 @@ impl MultiWorkspace { this.close_sidebar(window, cx); } }); - Self::subscribe_to_workspace(&workspace, cx); + Self::subscribe_to_workspace(&workspace, window, cx); let weak_self = cx.weak_entity(); workspace.update(cx, |workspace, cx| { workspace.set_multi_workspace(weak_self, cx); @@ -398,10 +398,14 @@ impl MultiWorkspace { .detach_and_log_err(cx); } - fn subscribe_to_workspace(workspace: &Entity, cx: &mut Context) { - cx.subscribe(workspace, |this, workspace, event, cx| { + fn subscribe_to_workspace( + workspace: &Entity, + window: &Window, + cx: &mut Context, + ) { + cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| { if let WorkspaceEvent::Activate = event { - this.activate(workspace, cx); + this.activate(workspace.clone(), window, cx); } }) .detach(); @@ -419,54 +423,107 @@ impl MultiWorkspace { self.active_workspace_index } - pub fn activate(&mut self, workspace: Entity, cx: &mut Context) { + /// Adds a workspace to this window without changing which workspace is + /// active. + pub fn add(&mut self, workspace: Entity, window: &Window, cx: &mut Context) { if !self.multi_workspace_enabled(cx) { - self.workspaces[0] = workspace; - self.active_workspace_index = 0; - cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged); - cx.notify(); + self.set_single_workspace(workspace, cx); return; } - let old_index = self.active_workspace_index; - let new_index = self.set_active_workspace(workspace, cx); - if old_index != new_index { - self.serialize(cx); - } + self.insert_workspace(workspace, window, cx); } - fn set_active_workspace( + /// Ensures the workspace is in the multiworkspace and makes it the active one. + pub fn activate( &mut self, workspace: Entity, + window: &mut Window, cx: &mut Context, - ) -> usize { - let index = self.add_workspace(workspace, cx); + ) { + if !self.multi_workspace_enabled(cx) { + self.set_single_workspace(workspace, cx); + return; + } + + let index = self.insert_workspace(workspace, &*window, cx); let changed = self.active_workspace_index != index; self.active_workspace_index = index; if changed { cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged); + self.serialize(cx); } + self.focus_active_workspace(window, cx); + cx.notify(); + } + + /// Replaces the currently active workspace with a new one. If the + /// workspace is already in the list, this just switches to it. + pub fn replace( + &mut self, + workspace: Entity, + window: &Window, + cx: &mut Context, + ) { + if !self.multi_workspace_enabled(cx) { + self.set_single_workspace(workspace, cx); + return; + } + + if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) { + let changed = self.active_workspace_index != index; + self.active_workspace_index = index; + if changed { + cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged); + self.serialize(cx); + } + cx.notify(); + return; + } + + let old_workspace = std::mem::replace( + &mut self.workspaces[self.active_workspace_index], + workspace.clone(), + ); + + let old_entity_id = old_workspace.entity_id(); + self.detach_workspace(&old_workspace, cx); + + Self::subscribe_to_workspace(&workspace, window, cx); + self.sync_sidebar_to_workspace(&workspace, cx); + + cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old_entity_id)); + cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace)); + cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged); + self.serialize(cx); + cx.notify(); + } + + fn set_single_workspace(&mut self, workspace: Entity, cx: &mut Context) { + self.workspaces[0] = workspace; + self.active_workspace_index = 0; + cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged); cx.notify(); - index } - /// 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 { + /// Inserts a workspace into the list if not already present. Returns the + /// index of the workspace (existing or newly inserted). Does not change + /// the active workspace index. + fn insert_workspace( + &mut self, + workspace: Entity, + window: &Window, + cx: &mut Context, + ) -> usize { if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) { index } else { - if self.sidebar_open { - let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx)); - workspace.update(cx, |workspace, _cx| { - workspace.set_sidebar_focus_handle(sidebar_focus_handle); - }); - } + Self::subscribe_to_workspace(&workspace, window, cx); + self.sync_sidebar_to_workspace(&workspace, cx); let weak_self = cx.weak_entity(); workspace.update(cx, |workspace, cx| { workspace.set_multi_workspace(weak_self, cx); }); - Self::subscribe_to_workspace(&workspace, cx); self.workspaces.push(workspace.clone()); cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace)); cx.notify(); @@ -474,19 +531,35 @@ impl MultiWorkspace { } } - 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" - ); - let changed = self.active_workspace_index != index; - self.active_workspace_index = index; - self.serialize(cx); - self.focus_active_workspace(window, cx); - if changed { - cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged); + /// Clears session state and DB binding for a workspace that is being + /// removed or replaced. The DB row is preserved so the workspace still + /// appears in the recent-projects list. + fn detach_workspace(&mut self, workspace: &Entity, cx: &mut Context) { + workspace.update(cx, |workspace, _cx| { + workspace.session_id.take(); + workspace._schedule_serialize_workspace.take(); + workspace._serialize_workspace_task.take(); + }); + + if let Some(workspace_id) = workspace.read(cx).database_id() { + let db = crate::persistence::WorkspaceDb::global(cx); + self.pending_removal_tasks.retain(|task| !task.is_ready()); + self.pending_removal_tasks + .push(cx.background_spawn(async move { + db.set_session_binding(workspace_id, None, None) + .await + .log_err(); + })); + } + } + + fn sync_sidebar_to_workspace(&self, workspace: &Entity, cx: &mut Context) { + if self.sidebar_open { + let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx)); + workspace.update(cx, |workspace, _| { + workspace.set_sidebar_focus_handle(sidebar_focus_handle); + }); } - cx.notify(); } fn cycle_workspace(&mut self, delta: isize, window: &mut Window, cx: &mut Context) { @@ -496,7 +569,8 @@ impl MultiWorkspace { } let current = self.active_workspace_index as isize; let next = ((current + delta).rem_euclid(count)) as usize; - self.activate_index(next, window, cx); + let workspace = self.workspaces[next].clone(); + self.activate(workspace, window, cx); } fn next_workspace(&mut self, _: &NextWorkspace, window: &mut Window, cx: &mut Context) { @@ -666,7 +740,7 @@ impl MultiWorkspace { cx: &mut Context, ) -> Entity { let workspace = cx.new(|cx| Workspace::test_new(project, window, cx)); - self.activate(workspace.clone(), cx); + self.activate(workspace.clone(), window, cx); workspace } @@ -688,8 +762,7 @@ impl MultiWorkspace { cx, ); let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx)); - self.set_active_workspace(new_workspace.clone(), cx); - self.focus_active_workspace(window, cx); + self.activate(new_workspace.clone(), window, cx); let weak_workspace = new_workspace.downgrade(); let db = crate::persistence::WorkspaceDb::global(cx); @@ -716,14 +789,17 @@ impl MultiWorkspace { }) } - pub fn remove_workspace( + pub fn remove( &mut self, - index: usize, + workspace: &Entity, window: &mut Window, cx: &mut Context, - ) -> Option> { - if self.workspaces.len() <= 1 || index >= self.workspaces.len() { - return None; + ) -> bool { + let Some(index) = self.workspaces.iter().position(|w| w == workspace) else { + return false; + }; + if self.workspaces.len() <= 1 { + return false; } let removed_workspace = self.workspaces.remove(index); @@ -734,28 +810,7 @@ impl MultiWorkspace { self.active_workspace_index -= 1; } - // Clear session_id and cancel any in-flight serialization on the - // removed workspace. Without this, a pending throttle timer from - // `serialize_workspace` could fire and write the old session_id - // back to the DB, resurrecting the workspace on next launch. - removed_workspace.update(cx, |workspace, _cx| { - workspace.session_id.take(); - workspace._schedule_serialize_workspace.take(); - workspace._serialize_workspace_task.take(); - }); - - if let Some(workspace_id) = removed_workspace.read(cx).database_id() { - let db = crate::persistence::WorkspaceDb::global(cx); - self.pending_removal_tasks.retain(|task| !task.is_ready()); - self.pending_removal_tasks - .push(cx.background_spawn(async move { - // Clear the session binding instead of deleting the row so - // the workspace still appears in the recent-projects list. - db.set_session_binding(workspace_id, None, None) - .await - .log_err(); - })); - } + self.detach_workspace(&removed_workspace, cx); self.serialize(cx); self.focus_active_workspace(window, cx); @@ -765,23 +820,20 @@ impl MultiWorkspace { cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged); cx.notify(); - Some(removed_workspace) + true } pub fn move_workspace_to_new_window( &mut self, - index: usize, + workspace: &Entity, window: &mut Window, cx: &mut Context, ) { - if self.workspaces.len() <= 1 || index >= self.workspaces.len() { + let workspace = workspace.clone(); + if !self.remove(&workspace, window, cx) { return; } - let Some(workspace) = self.remove_workspace(index, window, cx) else { - return; - }; - let app_state: Arc = workspace.read(cx).app_state().clone(); cx.defer(move |cx| { @@ -805,23 +857,28 @@ impl MultiWorkspace { window: &mut Window, cx: &mut Context, ) { - let index = self.active_workspace_index; - self.move_workspace_to_new_window(index, window, cx); + let workspace = self.workspace().clone(); + self.move_workspace_to_new_window(&workspace, window, cx); } pub fn open_project( &mut self, paths: Vec, + open_mode: OpenMode, 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) - }) + let needs_close_prompt = + open_mode == OpenMode::Replace || !self.multi_workspace_enabled(cx); + let open_mode = if self.multi_workspace_enabled(cx) { + open_mode } else { + OpenMode::Replace + }; + + if needs_close_prompt { cx.spawn_in(window, async move |_this, cx| { let should_continue = workspace .update_in(cx, |workspace, window, cx| { @@ -831,13 +888,17 @@ impl MultiWorkspace { if should_continue { workspace .update_in(cx, |workspace, window, cx| { - workspace.open_workspace_for_paths(true, paths, window, cx) + workspace.open_workspace_for_paths(open_mode, paths, window, cx) })? .await } else { Ok(workspace) } }) + } else { + workspace.update(cx, |workspace, cx| { + workspace.open_workspace_for_paths(open_mode, paths, window, cx) + }) } } } @@ -1091,4 +1152,90 @@ mod tests { ); }); } + + #[gpui::test] + async fn test_replace(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project_a = Project::test(fs.clone(), [], cx).await; + let project_b = Project::test(fs.clone(), [], cx).await; + let project_c = Project::test(fs.clone(), [], cx).await; + let project_d = Project::test(fs.clone(), [], cx).await; + + let (multi_workspace, cx) = cx + .add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); + + let workspace_a_id = + multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].entity_id()); + + // Replace the only workspace (single-workspace case). + let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx)); + mw.replace(workspace.clone(), &*window, cx); + workspace + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 1); + assert_eq!( + mw.workspaces()[0].entity_id(), + workspace_b.entity_id(), + "slot should now be project_b" + ); + assert_ne!( + mw.workspaces()[0].entity_id(), + workspace_a_id, + "project_a should be gone" + ); + }); + + // Add project_c as a second workspace, then replace it with project_d. + let workspace_c = multi_workspace.update_in(cx, |mw, window, cx| { + mw.test_add_workspace(project_c.clone(), window, cx) + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 2); + assert_eq!(mw.active_workspace_index(), 1); + }); + + let workspace_d = multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| Workspace::test_new(project_d.clone(), window, cx)); + mw.replace(workspace.clone(), &*window, cx); + workspace + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 2, "should still have 2 workspaces"); + assert_eq!(mw.active_workspace_index(), 1); + assert_eq!( + mw.workspaces()[1].entity_id(), + workspace_d.entity_id(), + "active slot should now be project_d" + ); + assert_ne!( + mw.workspaces()[1].entity_id(), + workspace_c.entity_id(), + "project_c should be gone" + ); + }); + + // Replace with workspace_b which is already in the list — should just switch. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.replace(workspace_b.clone(), &*window, cx); + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!( + mw.workspaces().len(), + 2, + "no workspace should be added or removed" + ); + assert_eq!( + mw.active_workspace_index(), + 0, + "should have switched to workspace_b" + ); + }); + } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 08f4fa84613436e6c5c34b3410df324e666b5fb8..c8952dfecb137cce02998225a9346556a0fc2776 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -2523,7 +2523,7 @@ mod tests { let workspace2 = multi_workspace.update_in(cx, |mw, window, cx| { let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx)); workspace.update(cx, |ws, _cx| ws.set_random_database_id()); - mw.activate(workspace.clone(), cx); + mw.activate(workspace.clone(), window, cx); workspace }); @@ -2541,7 +2541,8 @@ mod tests { // --- Remove the second workspace (index 1) --- multi_workspace.update_in(cx, |mw, window, cx| { - mw.remove_workspace(1, window, cx); + let ws = mw.workspaces()[1].clone(); + mw.remove(&ws, window, cx); }); cx.run_until_parked(); @@ -4193,7 +4194,7 @@ mod tests { workspace.update(cx, |ws: &mut crate::Workspace, _cx| { ws.set_database_id(workspace2_db_id) }); - mw.activate(workspace.clone(), cx); + mw.activate(workspace.clone(), window, cx); }); // Save a full workspace row to the DB directly. @@ -4221,7 +4222,8 @@ mod tests { // Remove workspace at index 1 (the second workspace). multi_workspace.update_in(cx, |mw, window, cx| { - mw.remove_workspace(1, window, cx); + let ws = mw.workspaces()[1].clone(); + mw.remove(&ws, window, cx); }); cx.run_until_parked(); @@ -4291,7 +4293,7 @@ mod tests { workspace.update(cx, |ws: &mut crate::Workspace, _cx| { ws.set_database_id(ws2_id) }); - mw.activate(workspace.clone(), cx); + mw.activate(workspace.clone(), window, cx); }); let session_id = "test-zombie-session"; @@ -4331,7 +4333,8 @@ mod tests { // Remove workspace2 (index 1). multi_workspace.update_in(cx, |mw, window, cx| { - mw.remove_workspace(1, window, cx); + let ws = mw.workspaces()[1].clone(); + mw.remove(&ws, window, cx); }); cx.run_until_parked(); @@ -4390,7 +4393,7 @@ mod tests { workspace.update(cx, |ws: &mut crate::Workspace, _cx| { ws.set_database_id(workspace2_db_id) }); - mw.activate(workspace.clone(), cx); + mw.activate(workspace.clone(), window, cx); }); // Save a full workspace row to the DB directly and let it settle. @@ -4414,7 +4417,8 @@ mod tests { // Remove workspace2 — this pushes a task to pending_removal_tasks. multi_workspace.update_in(cx, |mw, window, cx| { - mw.remove_workspace(1, window, cx); + let ws = mw.workspaces()[1].clone(); + mw.remove(&ws, window, cx); }); // Simulate the quit handler pattern: collect flush tasks + pending diff --git a/crates/workspace/src/welcome.rs b/crates/workspace/src/welcome.rs index 1b0566bf561b80137bf222a9d7c3348012cfce27..efd9b75a6802f888f43654e21006f202cc36c5a4 100644 --- a/crates/workspace/src/welcome.rs +++ b/crates/workspace/src/welcome.rs @@ -1,5 +1,5 @@ use crate::{ - NewFile, Open, PathList, SerializedWorkspaceLocation, Workspace, WorkspaceId, + NewFile, Open, OpenMode, PathList, SerializedWorkspaceLocation, Workspace, WorkspaceId, item::{Item, ItemEvent}, persistence::WorkspaceDb, }; @@ -326,7 +326,7 @@ impl WelcomePage { self.workspace .update(cx, |workspace, cx| { workspace - .open_workspace_for_paths(true, paths, window, cx) + .open_workspace_for_paths(OpenMode::Replace, paths, window, cx) .detach_and_log_err(cx); }) .log_err(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b9c243d49586c7087bb39711c2316ffee1e8d00b..d4b1cebca6b6b71b9efd3394f639a5cb32384682 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -664,7 +664,15 @@ fn prompt_and_open_paths(app_state: Arc, options: PathPromptOptions, c }) .ok(); } else { - let task = Workspace::new_local(Vec::new(), app_state.clone(), None, None, None, true, cx); + let task = Workspace::new_local( + Vec::new(), + app_state.clone(), + None, + None, + None, + OpenMode::Replace, + cx, + ); cx.spawn(async move |cx| { let OpenResult { window, .. } = task.await?; window.update(cx, |multi_workspace, window, cx| { @@ -703,7 +711,7 @@ pub fn prompt_for_open_path_and_open( if let Some(handle) = multi_workspace_handle { if let Some(task) = handle .update(cx, |multi_workspace, window, cx| { - multi_workspace.open_project(paths, window, cx) + multi_workspace.open_project(paths, OpenMode::Replace, window, cx) }) .log_err() { @@ -714,7 +722,7 @@ pub fn prompt_for_open_path_and_open( } if let Some(task) = this .update_in(cx, |this, window, cx| { - this.open_workspace_for_paths(false, paths, window, cx) + this.open_workspace_for_paths(OpenMode::NewWindow, paths, window, cx) }) .log_err() { @@ -1359,6 +1367,19 @@ struct FollowerView { location: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum OpenMode { + /// Open the workspace in a new window. + NewWindow, + /// Add to the window's multi workspace without activating it (used during deserialization). + Add, + /// Add to the window's multi workspace and activate it. + #[default] + Activate, + /// Replace the currently active workspace, and any of it's linked workspaces + Replace, +} + impl Workspace { pub fn new( workspace_id: Option, @@ -1764,7 +1785,7 @@ impl Workspace { requesting_window: Option>, env: Option>, init: Option) + Send>>, - activate: bool, + open_mode: OpenMode, cx: &mut App, ) -> Task> { let project_handle = Project::local( @@ -1862,8 +1883,13 @@ impl Workspace { }); } + let window_to_replace = match open_mode { + OpenMode::NewWindow => None, + _ => requesting_window, + }; + let (window, workspace): (WindowHandle, Entity) = - if let Some(window) = requesting_window { + if let Some(window) = window_to_replace { let centered_layout = serialized_workspace .as_ref() .map(|w| w.centered_layout) @@ -1888,10 +1914,19 @@ impl Workspace { workspace }); - if activate { - multi_workspace.activate(workspace.clone(), cx); - } else { - multi_workspace.add_workspace(workspace.clone(), cx); + match open_mode { + OpenMode::Replace => { + multi_workspace.replace(workspace.clone(), &*window, cx); + } + OpenMode::Activate => { + multi_workspace.activate(workspace.clone(), window, cx); + } + OpenMode::Add => { + multi_workspace.add(workspace.clone(), &*window, cx); + } + OpenMode::NewWindow => { + unreachable!() + } } workspace })?; @@ -2921,7 +2956,7 @@ impl Workspace { None, env, None, - true, + OpenMode::Activate, cx, ); cx.spawn_in(window, async move |_vh, cx| { @@ -2962,7 +2997,7 @@ impl Workspace { None, env, None, - true, + OpenMode::Activate, cx, ); cx.spawn_in(window, async move |_vh, cx| { @@ -3344,23 +3379,22 @@ impl Workspace { pub fn open_workspace_for_paths( &mut self, - replace_current_window: bool, + // replace_current_window: bool, + mut open_mode: OpenMode, paths: Vec, window: &mut Window, cx: &mut Context, ) -> Task>> { - let window_handle = window.window_handle().downcast::(); + let requesting_window = 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)); - let window_to_replace = if replace_current_window { - window_handle - } else if is_remote || has_worktree || has_dirty_items { - None - } else { - window_handle - }; + let workspace_is_empty = !is_remote && !has_worktree && !has_dirty_items; + if workspace_is_empty { + open_mode = OpenMode::Replace; + } + let app_state = self.app_state.clone(); cx.spawn(async move |_, cx| { @@ -3370,7 +3404,8 @@ impl Workspace { &paths, app_state, OpenOptions { - replace_window: window_to_replace, + requesting_window, + open_mode, ..Default::default() }, cx, @@ -8578,7 +8613,7 @@ pub async fn restore_multiworkspace( None, None, None, - true, + OpenMode::Activate, cx, ) }) @@ -8608,7 +8643,7 @@ pub async fn restore_multiworkspace( Some(window_handle), None, None, - false, + OpenMode::Add, cx, ) }) @@ -8628,18 +8663,17 @@ pub async fn restore_multiworkspace( .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); + let index = target_index.unwrap_or(0); + if let Some(workspace) = multi_workspace.workspaces().get(index).cloned() { + multi_workspace.activate(workspace, 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); + if let Some(workspace) = multi_workspace.workspaces().first().cloned() { + multi_workspace.activate(workspace, window, cx); } }) .ok(); @@ -8890,7 +8924,7 @@ pub fn join_channel( requesting_window, None, None, - true, + OpenMode::Activate, cx, ) }) @@ -8963,8 +8997,18 @@ pub async fn get_any_active_multi_workspace( // find an existing workspace to focus and show call controls let active_window = activate_any_workspace_window(&mut cx); if active_window.is_none() { - cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, true, cx)) - .await?; + cx.update(|cx| { + Workspace::new_local( + vec![], + app_state.clone(), + None, + None, + None, + OpenMode::Activate, + cx, + ) + }) + .await?; } activate_any_workspace_window(&mut cx).context("could not open zed") } @@ -9134,7 +9178,8 @@ pub struct OpenOptions { pub focus: Option, pub open_new_workspace: Option, pub wait: bool, - pub replace_window: Option>, + pub requesting_window: Option>, + pub open_mode: OpenMode, pub env: Option>, } @@ -9189,7 +9234,7 @@ pub fn open_workspace_by_id( workspace.centered_layout = centered_layout; workspace }); - multi_workspace.add_workspace(workspace.clone(), cx); + multi_workspace.add(workspace.clone(), &*window, cx); workspace })?; (window, workspace) @@ -9319,7 +9364,7 @@ pub fn open_paths( let open_task = existing .update(cx, |multi_workspace, window, cx| { window.activate_window(); - multi_workspace.activate(target_workspace.clone(), cx); + multi_workspace.activate(target_workspace.clone(), window, cx); target_workspace.update(cx, |workspace, cx| { workspace.open_paths( abs_paths, @@ -9353,10 +9398,10 @@ pub fn open_paths( Workspace::new_local( abs_paths, app_state.clone(), - open_options.replace_window, + open_options.requesting_window, open_options.env, None, - true, + open_options.open_mode, cx, ) }) @@ -9414,13 +9459,14 @@ pub fn open_new( cx: &mut App, init: impl FnOnce(&mut Workspace, &mut Window, &mut Context) + 'static + Send, ) -> Task> { + let addition = open_options.open_mode; let task = Workspace::new_local( Vec::new(), app_state, - open_options.replace_window, + open_options.requesting_window, open_options.env, Some(Box::new(init)), - true, + addition, cx, ); cx.spawn(async move |cx| { @@ -9631,7 +9677,7 @@ async fn open_remote_project_inner( workspace }); - multi_workspace.activate(new_workspace.clone(), cx); + multi_workspace.activate(new_workspace.clone(), window, cx); new_workspace })?; @@ -9718,8 +9764,8 @@ pub fn join_in_room_project( existing_window_and_workspace { existing_window - .update(cx, |multi_workspace, _, cx| { - multi_workspace.activate(target_workspace, cx); + .update(cx, |multi_workspace, window, cx| { + multi_workspace.activate(target_workspace, window, cx); }) .ok(); existing_window @@ -10653,7 +10699,8 @@ mod tests { // Activate workspace A multi_workspace_handle .update(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }) .unwrap(); @@ -14422,7 +14469,8 @@ mod tests { // Switch to workspace A multi_workspace_handle .update(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }) .unwrap(); @@ -14467,7 +14515,8 @@ mod tests { // Switch to workspace B multi_workspace_handle .update(cx, |mw, window, cx| { - mw.activate_index(1, window, cx); + let workspace = mw.workspaces()[1].clone(); + mw.activate(workspace, window, cx); }) .unwrap(); cx.run_until_parked(); @@ -14475,7 +14524,8 @@ mod tests { // Switch back to workspace A multi_workspace_handle .update(cx, |mw, window, cx| { - mw.activate_index(0, window, cx); + let workspace = mw.workspaces()[0].clone(); + mw.activate(workspace, window, cx); }) .unwrap(); cx.run_until_parked(); diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index cb70a8573f831c5da20afc15fd0e55cd6ca2c3e6..d0c0a2a2a5c443d493a568186dd090cc967cad64 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -2594,7 +2594,7 @@ fn run_multi_workspace_sidebar_visual_tests( }); cx.new(|cx| { let mut multi_workspace = MultiWorkspace::new(workspace1, window, cx); - multi_workspace.activate(workspace2, cx); + multi_workspace.activate(workspace2, window, cx); multi_workspace }) }, @@ -2645,7 +2645,8 @@ fn run_multi_workspace_sidebar_visual_tests( // 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); + let workspace = multi_workspace.workspaces()[0].clone(); + multi_workspace.activate(workspace, window, cx); }) .context("Failed to activate workspace 1")?; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a560a077220500259d72101f7890bc8edd2cf552..bf7f4dd2d4dab50d4478515ec9b0a328a7730e5a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1121,7 +1121,7 @@ fn register_actions( let task = cx.update(|_window, cx| { open_new( workspace::OpenOptions { - replace_window: Some(window_handle), + requesting_window: Some(window_handle), ..Default::default() }, app_state, @@ -1375,7 +1375,7 @@ fn quit(_: &Quit, cx: &mut App) { for workspace in workspaces { if let Some(should_close) = window .update(cx, |multi_workspace, window, cx| { - multi_workspace.activate(workspace.clone(), cx); + multi_workspace.activate(workspace.clone(), window, cx); window.activate_window(); workspace.update(cx, |workspace, cx| { workspace.prepare_to_close(CloseIntent::Quit, window, cx) @@ -2456,7 +2456,7 @@ mod tests { &[PathBuf::from(path!("/root/e"))], app_state, workspace::OpenOptions { - replace_window: Some(window), + requesting_window: Some(window), ..Default::default() }, cx, @@ -5372,11 +5372,11 @@ mod tests { .unwrap(); window - .update(cx, |multi_workspace, _, cx| { - multi_workspace.activate(workspace2.clone(), cx); - multi_workspace.activate(workspace3.clone(), cx); + .update(cx, |multi_workspace, window, cx| { + multi_workspace.activate(workspace2.clone(), window, cx); + multi_workspace.activate(workspace3.clone(), window, cx); // Switch back to workspace1 for test setup - multi_workspace.activate(workspace1, cx); + multi_workspace.activate(workspace1, window, cx); assert_eq!(multi_workspace.active_workspace_index(), 0); }) .unwrap(); @@ -5558,9 +5558,9 @@ mod tests { .unwrap(); window1 - .update(cx, |multi_workspace, _, cx| { - multi_workspace.activate(workspace1_2.clone(), cx); - multi_workspace.activate(workspace1_1.clone(), cx); + .update(cx, |multi_workspace, window, cx| { + multi_workspace.activate(workspace1_2.clone(), window, cx); + multi_workspace.activate(workspace1_1.clone(), window, cx); }) .unwrap(); @@ -5791,7 +5791,7 @@ mod tests { async fn test_multi_workspace_session_restore(cx: &mut TestAppContext) { use collections::HashMap; use session::Session; - use workspace::{Workspace, WorkspaceId}; + use workspace::{OpenMode, Workspace, WorkspaceId}; let app_state = init_test(cx); @@ -5826,7 +5826,7 @@ mod tests { None, None, None, - true, + OpenMode::Activate, cx, ) }) @@ -5835,7 +5835,7 @@ mod tests { window_a .update(cx, |multi_workspace, window, cx| { - multi_workspace.open_project(vec![dir2.into()], window, cx) + multi_workspace.open_project(vec![dir2.into()], OpenMode::Activate, window, cx) }) .unwrap() .await @@ -5852,7 +5852,7 @@ mod tests { None, None, None, - true, + OpenMode::Activate, cx, ) }) @@ -5865,7 +5865,8 @@ mod tests { // 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); + let workspace = multi_workspace.workspaces()[0].clone(); + multi_workspace.activate(workspace, window, cx); }) .unwrap(); diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 53347e501f7ba23be62466779f7775d0d432dfab..7645eae88d69f777f650ac9f86724bfef0f10bc5 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -529,7 +529,7 @@ async fn open_workspaces( }; let open_options = workspace::OpenOptions { open_new_workspace, - replace_window, + requesting_window: replace_window, wait, env: env.clone(), ..Default::default() @@ -1292,7 +1292,7 @@ mod tests { vec![], false, workspace::OpenOptions { - replace_window: Some(window_to_replace), + requesting_window: Some(window_to_replace), ..Default::default() }, &response_tx,