Detailed changes
@@ -5175,7 +5175,7 @@ mod tests {
multi_workspace
.read_with(cx, |multi_workspace, _cx| {
assert_eq!(
- multi_workspace.workspaces().len(),
+ multi_workspace.workspaces().count(),
1,
"LocalProject should not create a new workspace"
);
@@ -5451,6 +5451,11 @@ mod tests {
let multi_workspace =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ multi_workspace
+ .update(cx, |multi_workspace, _, cx| {
+ multi_workspace.open_sidebar(cx);
+ })
+ .unwrap();
let workspace = multi_workspace
.read_with(cx, |multi_workspace, _cx| {
@@ -5538,15 +5543,14 @@ mod tests {
.read_with(cx, |multi_workspace, cx| {
// There should be more than one workspace now (the original + the new worktree).
assert!(
- multi_workspace.workspaces().len() > 1,
+ multi_workspace.workspaces().count() > 1,
"expected a new workspace to have been created, found {}",
- multi_workspace.workspaces().len(),
+ multi_workspace.workspaces().count(),
);
// Check the newest workspace's panel for the correct agent.
let new_workspace = multi_workspace
.workspaces()
- .iter()
.find(|ws| ws.entity_id() != workspace.entity_id())
.expect("should find the new workspace");
let new_panel = new_workspace
@@ -3375,7 +3375,6 @@ pub(crate) mod tests {
// 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();
@@ -353,7 +353,6 @@ impl ThreadsArchiveView {
.map(|mw| {
mw.read(cx)
.workspaces()
- .iter()
.filter_map(|ws| ws.read(cx).database_id())
.collect()
})
@@ -357,7 +357,6 @@ pub fn init(cx: &mut App) {
.update(cx, |multi_workspace, window, cx| {
let sibling_workspace_ids: HashSet<WorkspaceId> = multi_workspace
.workspaces()
- .iter()
.filter_map(|ws| ws.read(cx).database_id())
.collect();
@@ -1113,7 +1112,6 @@ impl PickerDelegate for RecentProjectsDelegate {
.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 {
@@ -1932,7 +1930,6 @@ impl RecentProjectsDelegate {
.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 {
@@ -2055,6 +2052,11 @@ mod tests {
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
+ multi_workspace
+ .update(cx, |multi_workspace, _, cx| {
+ multi_workspace.open_sidebar(cx);
+ })
+ .unwrap();
multi_workspace
.update(cx, |multi_workspace, _, cx| {
assert!(!multi_workspace.workspace().read(cx).is_edited())
@@ -2141,7 +2143,7 @@ mod tests {
);
assert!(
- multi_workspace.workspaces().contains(&dirty_workspace),
+ multi_workspace.workspaces().any(|w| w == &dirty_workspace),
"The dirty workspace should still be present in multi-workspace mode"
);
@@ -3753,7 +3753,6 @@ fn all_projects(
.flat_map(|multi_workspace| {
multi_workspace
.workspaces()
- .iter()
.map(|workspace| workspace.read(cx).project().clone())
.collect::<Vec<_>>()
}),
@@ -434,7 +434,7 @@ impl Sidebar {
})
.detach();
- let workspaces = multi_workspace.read(cx).workspaces().to_vec();
+ let workspaces: Vec<_> = multi_workspace.read(cx).workspaces().cloned().collect();
cx.defer_in(window, move |this, window, cx| {
for workspace in &workspaces {
this.subscribe_to_workspace(workspace, window, cx);
@@ -673,7 +673,6 @@ impl Sidebar {
let mw = self.multi_workspace.upgrade()?;
let mw = mw.read(cx);
mw.workspaces()
- .iter()
.find(|ws| ws.read(cx).project_group_key(cx).path_list() == path_list)
.cloned()
}
@@ -716,8 +715,8 @@ impl Sidebar {
return;
};
let mw = multi_workspace.read(cx);
- let workspaces = mw.workspaces().to_vec();
- let active_workspace = mw.workspaces().get(mw.active_workspace_index()).cloned();
+ let workspaces: Vec<_> = mw.workspaces().cloned().collect();
+ let active_workspace = Some(mw.workspace().clone());
let agent_server_store = workspaces
.first()
@@ -1993,7 +1992,6 @@ impl Sidebar {
let workspace = window.read(cx).ok().and_then(|multi_workspace| {
multi_workspace
.workspaces()
- .iter()
.find(|workspace| predicate(workspace, cx))
.cloned()
})?;
@@ -2010,7 +2008,6 @@ impl Sidebar {
multi_workspace
.read(cx)
.workspaces()
- .iter()
.find(|workspace| predicate(workspace, cx))
.cloned()
})
@@ -2203,12 +2200,10 @@ impl Sidebar {
return;
}
- let active_workspace = self.multi_workspace.upgrade().and_then(|w| {
- w.read(cx)
- .workspaces()
- .get(w.read(cx).active_workspace_index())
- .cloned()
- });
+ let active_workspace = self
+ .multi_workspace
+ .upgrade()
+ .map(|w| w.read(cx).workspace().clone());
if let Some(workspace) = active_workspace {
self.activate_thread_locally(&metadata, &workspace, window, cx);
@@ -2343,7 +2338,7 @@ impl Sidebar {
return;
};
- let workspaces = multi_workspace.read(cx).workspaces().to_vec();
+ let workspaces: Vec<_> = multi_workspace.read(cx).workspaces().cloned().collect();
for workspace in workspaces {
if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
let cancelled =
@@ -2936,7 +2931,6 @@ impl Sidebar {
.map(|mw| {
mw.read(cx)
.workspaces()
- .iter()
.filter_map(|ws| ws.read(cx).database_id())
.collect()
})
@@ -3404,12 +3398,9 @@ impl Sidebar {
}
fn active_workspace(&self, cx: &App) -> Option<Entity<Workspace>> {
- self.multi_workspace.upgrade().and_then(|w| {
- w.read(cx)
- .workspaces()
- .get(w.read(cx).active_workspace_index())
- .cloned()
- })
+ self.multi_workspace
+ .upgrade()
+ .map(|w| w.read(cx).workspace().clone())
}
fn show_thread_import_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -3517,12 +3508,11 @@ impl Sidebar {
}
fn show_archive(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(active_workspace) = self.multi_workspace.upgrade().and_then(|w| {
- w.read(cx)
- .workspaces()
- .get(w.read(cx).active_workspace_index())
- .cloned()
- }) else {
+ let Some(active_workspace) = self
+ .multi_workspace
+ .upgrade()
+ .map(|w| w.read(cx).workspace().clone())
+ else {
return;
};
let Some(agent_panel) = active_workspace.read(cx).panel::<AgentPanel>(cx) else {
@@ -3824,12 +3814,12 @@ pub fn dump_workspace_info(
let multi_workspace = workspace.multi_workspace().and_then(|weak| weak.upgrade());
let workspaces: Vec<gpui::Entity<Workspace>> = match &multi_workspace {
- Some(mw) => mw.read(cx).workspaces().to_vec(),
+ Some(mw) => mw.read(cx).workspaces().cloned().collect(),
None => vec![this_entity.clone()],
};
- let active_index = multi_workspace
+ let active_workspace = multi_workspace
.as_ref()
- .map(|mw| mw.read(cx).active_workspace_index());
+ .map(|mw| mw.read(cx).workspace().clone());
writeln!(output, "MultiWorkspace: {} workspace(s)", workspaces.len()).ok();
@@ -3841,13 +3831,10 @@ pub fn dump_workspace_info(
}
}
- if let Some(index) = active_index {
- writeln!(output, "Active workspace index: {index}").ok();
- }
writeln!(output).ok();
for (index, ws) in workspaces.iter().enumerate() {
- let is_active = active_index == Some(index);
+ let is_active = active_workspace.as_ref() == Some(ws);
writeln!(
output,
"--- Workspace {index}{} ---",
@@ -77,6 +77,18 @@ async fn init_test_project(
fn setup_sidebar(
multi_workspace: &Entity<MultiWorkspace>,
cx: &mut gpui::VisualTestContext,
+) -> Entity<Sidebar> {
+ let sidebar = setup_sidebar_closed(multi_workspace, cx);
+ multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.toggle_sidebar(window, cx);
+ });
+ cx.run_until_parked();
+ sidebar
+}
+
+fn setup_sidebar_closed(
+ multi_workspace: &Entity<MultiWorkspace>,
+ cx: &mut gpui::VisualTestContext,
) -> Entity<Sidebar> {
let multi_workspace = multi_workspace.clone();
let sidebar =
@@ -172,16 +184,7 @@ fn save_thread_metadata(
cx.run_until_parked();
}
-fn open_and_focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {
- let multi_workspace = sidebar.read_with(cx, |s, _| s.multi_workspace.upgrade());
- if let Some(multi_workspace) = multi_workspace {
- multi_workspace.update_in(cx, |mw, window, cx| {
- if !mw.sidebar_open() {
- mw.toggle_sidebar(window, cx);
- }
- });
- }
- cx.run_until_parked();
+fn focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {
sidebar.update_in(cx, |_, window, cx| {
cx.focus_self(window);
});
@@ -544,7 +547,7 @@ async fn test_workspace_lifecycle(cx: &mut TestAppContext) {
// Remove the second workspace
multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[1].clone();
+ let workspace = mw.workspaces().nth(1).cloned().unwrap();
mw.remove(&workspace, window, cx);
});
cx.run_until_parked();
@@ -604,7 +607,7 @@ async fn test_view_more_batched_expansion(cx: &mut TestAppContext) {
assert!(entries.iter().any(|e| e.contains("View More")));
// Focus and navigate to View More, then confirm to expand by one batch
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
for _ in 0..7 {
cx.dispatch_action(SelectNext);
}
@@ -915,7 +918,7 @@ async fn test_keyboard_select_next_and_previous(cx: &mut TestAppContext) {
// Entries: [header, thread3, thread2, thread1]
// Focusing the sidebar does not set a selection; select_next/select_previous
// handle None gracefully by starting from the first or last entry.
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
// First SelectNext from None starts at index 0
@@ -970,7 +973,7 @@ async fn test_keyboard_select_first_and_last(cx: &mut TestAppContext) {
multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
cx.run_until_parked();
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
// SelectLast jumps to the end
cx.dispatch_action(SelectLast);
@@ -993,7 +996,7 @@ async fn test_keyboard_focus_in_does_not_set_selection(cx: &mut TestAppContext)
// Open the sidebar so it's rendered, then focus it to trigger focus_in.
// focus_in no longer sets a default selection.
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
// Manually set a selection, blur, then refocus — selection should be preserved
@@ -1030,7 +1033,7 @@ async fn test_keyboard_confirm_on_project_header_toggles_collapse(cx: &mut TestA
);
// Focus the sidebar and select the header (index 0)
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(0);
});
@@ -1071,7 +1074,7 @@ async fn test_keyboard_confirm_on_view_more_expands(cx: &mut TestAppContext) {
assert!(entries.iter().any(|e| e.contains("View More")));
// Focus sidebar (selection starts at None), then navigate down to the "View More" entry (index 6)
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
for _ in 0..7 {
cx.dispatch_action(SelectNext);
}
@@ -1105,7 +1108,7 @@ async fn test_keyboard_expand_and_collapse_selected_entry(cx: &mut TestAppContex
);
// Focus sidebar and manually select the header (index 0). Press left to collapse.
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(0);
});
@@ -1144,7 +1147,7 @@ async fn test_keyboard_collapse_from_child_selects_parent(cx: &mut TestAppContex
cx.run_until_parked();
// Focus sidebar (selection starts at None), then navigate down to the thread (child)
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
cx.dispatch_action(SelectNext);
cx.dispatch_action(SelectNext);
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
@@ -1179,7 +1182,7 @@ async fn test_keyboard_navigation_on_empty_list(cx: &mut TestAppContext) {
);
// Focus sidebar — focus_in does not set a selection
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
// First SelectNext from None starts at index 0 (header)
@@ -1211,7 +1214,7 @@ async fn test_selection_clamps_after_entry_removal(cx: &mut TestAppContext) {
cx.run_until_parked();
// Focus sidebar (selection starts at None), navigate down to the thread (index 1)
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
cx.dispatch_action(SelectNext);
cx.dispatch_action(SelectNext);
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
@@ -1492,7 +1495,7 @@ async fn test_escape_clears_search_and_restores_full_list(cx: &mut TestAppContex
);
// User types a search query to filter down.
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
type_in_search(&sidebar, "alpha", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
@@ -1540,8 +1543,9 @@ async fn test_search_only_shows_workspace_headers_with_matches(cx: &mut TestAppC
});
cx.run_until_parked();
- let project_b =
- multi_workspace.read_with(cx, |mw, cx| mw.workspaces()[1].read(cx).project().clone());
+ let project_b = multi_workspace.read_with(cx, |mw, cx| {
+ mw.workspaces().nth(1).unwrap().read(cx).project().clone()
+ });
for (id, title, hour) in [
("b1", "Refactor sidebar layout", 3),
@@ -1621,8 +1625,9 @@ async fn test_search_matches_workspace_name(cx: &mut TestAppContext) {
});
cx.run_until_parked();
- let project_b =
- multi_workspace.read_with(cx, |mw, cx| mw.workspaces()[1].read(cx).project().clone());
+ let project_b = multi_workspace.read_with(cx, |mw, cx| {
+ mw.workspaces().nth(1).unwrap().read(cx).project().clone()
+ });
for (id, title, hour) in [
("b1", "Refactor sidebar layout", 3),
@@ -1764,7 +1769,7 @@ async fn test_search_finds_threads_inside_collapsed_groups(cx: &mut TestAppConte
// User focuses the sidebar and collapses the group using keyboard:
// manually select the header, then press SelectParent to collapse.
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(0);
});
@@ -1807,7 +1812,7 @@ async fn test_search_then_keyboard_navigate_and_confirm(cx: &mut TestAppContext)
}
cx.run_until_parked();
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
// User types "fix" — two threads match.
type_in_search(&sidebar, "fix", cx);
@@ -1856,6 +1861,13 @@ async fn test_confirm_on_historical_thread_activates_workspace(cx: &mut TestAppC
});
cx.run_until_parked();
+ let (workspace_0, workspace_1) = multi_workspace.read_with(cx, |mw, _| {
+ (
+ mw.workspaces().next().unwrap().clone(),
+ mw.workspaces().nth(1).unwrap().clone(),
+ )
+ });
+
save_thread_metadata(
acp::SessionId::new(Arc::from("hist-1")),
"Historical Thread".into(),
@@ -1875,13 +1887,13 @@ 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| {
- let workspace = mw.workspaces()[1].clone();
+ let workspace = mw.workspaces().nth(1).unwrap().clone();
mw.activate(workspace, window, cx);
});
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 1
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ workspace_1
);
// Confirm on the historical (non-live) thread at index 1.
@@ -1895,8 +1907,8 @@ async fn test_confirm_on_historical_thread_activates_workspace(cx: &mut TestAppC
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 0
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ workspace_0
);
}
@@ -2037,7 +2049,8 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
let panel_b = add_agent_panel(&workspace_b, cx);
cx.run_until_parked();
- let workspace_a = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].clone());
+ let workspace_a =
+ multi_workspace.read_with(cx, |mw, _cx| mw.workspaces().next().unwrap().clone());
// ── 1. Initial state: focused thread derived from active panel ─────
sidebar.read_with(cx, |sidebar, _cx| {
@@ -2135,7 +2148,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
});
multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
});
cx.run_until_parked();
@@ -2190,8 +2203,8 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
// Switching workspaces via the multi_workspace (simulates clicking
// 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) {
- let workspace = mw.workspaces()[index].clone();
+ let workspace = mw.workspaces().find(|w| *w == &workspace_b).cloned();
+ if let Some(workspace) = workspace {
mw.activate(workspace, window, cx);
}
});
@@ -2477,6 +2490,8 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(main_project.clone(), window, cx));
+ let sidebar = setup_sidebar(&multi_workspace, cx);
+
let worktree_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(worktree_project.clone(), window, cx)
});
@@ -2485,12 +2500,10 @@ 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| {
- let workspace = mw.workspaces()[1].clone();
+ let workspace = mw.workspaces().nth(1).unwrap().clone();
mw.activate(workspace, window, cx);
});
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
// Create a non-empty thread in the worktree workspace.
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
@@ -3027,6 +3040,8 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(main_project.clone(), window, cx));
+ let sidebar = setup_sidebar(&multi_workspace, cx);
+
let worktree_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(worktree_project.clone(), window, cx)
});
@@ -3037,12 +3052,10 @@ 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| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
});
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
// Start a thread in the worktree workspace's panel and keep it
// generating (don't resolve it).
let connection = StubAgentConnection::new();
@@ -3127,6 +3140,8 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(main_project.clone(), window, cx));
+ let sidebar = setup_sidebar(&multi_workspace, cx);
+
let worktree_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(worktree_project.clone(), window, cx)
});
@@ -3134,12 +3149,10 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp
let worktree_panel = add_agent_panel(&worktree_workspace, cx);
multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
});
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
let connection = StubAgentConnection::new();
open_thread_with_connection(&worktree_panel, connection.clone(), cx);
send_message(&worktree_panel, cx);
@@ -3231,12 +3244,12 @@ async fn test_clicking_worktree_thread_opens_workspace_when_none_exists(cx: &mut
// Only 1 workspace should exist.
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.workspaces().len()),
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
1,
);
// Focus the sidebar and select the worktree thread.
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(1); // index 0 is header, 1 is the thread
});
@@ -3248,11 +3261,11 @@ async fn test_clicking_worktree_thread_opens_workspace_when_none_exists(cx: &mut
// A new workspace should have been created for the worktree path.
let new_workspace = multi_workspace.read_with(cx, |mw, _| {
assert_eq!(
- mw.workspaces().len(),
+ mw.workspaces().count(),
2,
"confirming a worktree thread without a workspace should open one",
);
- mw.workspaces()[1].clone()
+ mw.workspaces().nth(1).unwrap().clone()
});
let new_path_list =
@@ -3318,7 +3331,7 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
vec!["v [project]", " WT Thread {wt-feature-a}"],
);
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(1); // index 0 is header, 1 is the thread
});
@@ -3444,18 +3457,19 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace(
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(main_project.clone(), window, cx));
+ let sidebar = setup_sidebar(&multi_workspace, cx);
+
let worktree_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(worktree_project.clone(), window, cx)
});
// Activate the main workspace before setting up the sidebar.
- multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
- mw.activate(workspace, window, cx);
+ let main_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
+ let workspace = mw.workspaces().next().unwrap().clone();
+ mw.activate(workspace.clone(), window, cx);
+ workspace
});
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
save_named_thread_metadata("thread-main", "Main Thread", &main_project, cx).await;
save_named_thread_metadata("thread-wt", "WT Thread", &worktree_project, cx).await;
@@ -3475,13 +3489,13 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace(
.expect("should find the worktree thread entry");
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 0,
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ main_workspace,
"main workspace should be active initially"
);
// Focus the sidebar and select the absorbed worktree thread.
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(wt_thread_index);
});
@@ -3491,9 +3505,7 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace(
cx.run_until_parked();
// The worktree workspace should now be active, not the main one.
- let active_workspace = multi_workspace.read_with(cx, |mw, _| {
- mw.workspaces()[mw.active_workspace_index()].clone()
- });
+ let active_workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
assert_eq!(
active_workspace, worktree_workspace,
"clicking an absorbed worktree thread should activate the worktree workspace"
@@ -3520,25 +3532,27 @@ async fn test_activate_archived_thread_with_saved_paths_activates_matching_works
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
- multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(project_b.clone(), window, cx);
- });
-
let sidebar = setup_sidebar(&multi_workspace, cx);
+ let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.test_add_workspace(project_b.clone(), window, cx)
+ });
+ let workspace_a =
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().next().unwrap().clone());
+
// Save a thread with path_list pointing to project-b.
let session_id = acp::SessionId::new(Arc::from("archived-1"));
save_test_thread_metadata(&session_id, &project_b, cx).await;
// Ensure workspace A is active.
multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
});
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 0
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ workspace_a
);
// Call activate_archived_thread – should resolve saved paths and
@@ -3562,8 +3576,8 @@ async fn test_activate_archived_thread_with_saved_paths_activates_matching_works
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 1,
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ workspace_b,
"should have activated the workspace matching the saved path_list"
);
}
@@ -3588,21 +3602,23 @@ async fn test_activate_archived_thread_cwd_fallback_with_matching_workspace(
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
- multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(project_b, window, cx);
- });
-
let sidebar = setup_sidebar(&multi_workspace, cx);
+ let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.test_add_workspace(project_b, window, cx)
+ });
+ let workspace_a =
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().next().unwrap().clone());
+
// Start with workspace A active.
multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
});
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 0
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ workspace_a
);
// No thread saved to the store – cwd is the only path hint.
@@ -3625,8 +3641,8 @@ async fn test_activate_archived_thread_cwd_fallback_with_matching_workspace(
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 1,
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ workspace_b,
"should have activated the workspace matching the cwd"
);
}
@@ -3651,21 +3667,21 @@ async fn test_activate_archived_thread_no_paths_no_cwd_uses_active_workspace(
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
- multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(project_b, window, cx);
- });
-
let sidebar = setup_sidebar(&multi_workspace, cx);
+ let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.test_add_workspace(project_b, window, cx)
+ });
+
// Activate workspace B (index 1) to make it the active one.
multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[1].clone();
+ let workspace = mw.workspaces().nth(1).unwrap().clone();
mw.activate(workspace, window, cx);
});
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 1
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ workspace_b
);
// No saved thread, no cwd – should fall back to the active workspace.
@@ -3688,8 +3704,8 @@ async fn test_activate_archived_thread_no_paths_no_cwd_uses_active_workspace(
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
- 1,
+ multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()),
+ workspace_b,
"should have stayed on the active workspace when no path info is available"
);
}
@@ -3719,7 +3735,7 @@ async fn test_activate_archived_thread_saved_paths_opens_new_workspace(cx: &mut
let session_id = acp::SessionId::new(Arc::from("archived-new-ws"));
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.workspaces().len()),
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
1,
"should start with one workspace"
);
@@ -3743,7 +3759,7 @@ async fn test_activate_archived_thread_saved_paths_opens_new_workspace(cx: &mut
cx.run_until_parked();
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.workspaces().len()),
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
2,
"should have opened a second workspace for the archived thread's saved paths"
);
@@ -3768,6 +3784,10 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window(cx: &m
cx.add_window(|window, cx| MultiWorkspace::test_new(project_b, window, cx));
let multi_workspace_a_entity = multi_workspace_a.root(cx).unwrap();
+ let multi_workspace_b_entity = multi_workspace_b.root(cx).unwrap();
+
+ let cx_b = &mut gpui::VisualTestContext::from_window(multi_workspace_b.into(), cx);
+ let _sidebar_b = setup_sidebar(&multi_workspace_b_entity, cx_b);
let cx_a = &mut gpui::VisualTestContext::from_window(multi_workspace_a.into(), cx);
let sidebar = setup_sidebar(&multi_workspace_a_entity, cx_a);
@@ -3794,14 +3814,14 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window(cx: &m
assert_eq!(
multi_workspace_a
- .read_with(cx_a, |mw, _| mw.workspaces().len())
+ .read_with(cx_a, |mw, _| mw.workspaces().count())
.unwrap(),
1,
"should not add the other window's workspace into the current window"
);
assert_eq!(
multi_workspace_b
- .read_with(cx_a, |mw, _| mw.workspaces().len())
+ .read_with(cx_a, |mw, _| mw.workspaces().count())
.unwrap(),
1,
"should reuse the existing workspace in the other window"
@@ -3871,14 +3891,14 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window_with_t
assert_eq!(
multi_workspace_a
- .read_with(cx_a, |mw, _| mw.workspaces().len())
+ .read_with(cx_a, |mw, _| mw.workspaces().count())
.unwrap(),
1,
"should not add the other window's workspace into the current window"
);
assert_eq!(
multi_workspace_b
- .read_with(cx_a, |mw, _| mw.workspaces().len())
+ .read_with(cx_a, |mw, _| mw.workspaces().count())
.unwrap(),
1,
"should reuse the existing workspace in the other window"
@@ -3921,6 +3941,10 @@ async fn test_activate_archived_thread_prefers_current_window_for_matching_paths
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
let multi_workspace_a_entity = multi_workspace_a.root(cx).unwrap();
+ let multi_workspace_b_entity = multi_workspace_b.root(cx).unwrap();
+
+ let cx_b = &mut gpui::VisualTestContext::from_window(multi_workspace_b.into(), cx);
+ let _sidebar_b = setup_sidebar(&multi_workspace_b_entity, cx_b);
let cx_a = &mut gpui::VisualTestContext::from_window(multi_workspace_a.into(), cx);
let sidebar_a = setup_sidebar(&multi_workspace_a_entity, cx_a);
@@ -3958,14 +3982,14 @@ async fn test_activate_archived_thread_prefers_current_window_for_matching_paths
});
assert_eq!(
multi_workspace_a
- .read_with(cx_a, |mw, _| mw.workspaces().len())
+ .read_with(cx_a, |mw, _| mw.workspaces().count())
.unwrap(),
1,
"current window should continue reusing its existing workspace"
);
assert_eq!(
multi_workspace_b
- .read_with(cx_a, |mw, _| mw.workspaces().len())
+ .read_with(cx_a, |mw, _| mw.workspaces().count())
.unwrap(),
1,
"other windows should not be activated just because they also match the saved paths"
@@ -4029,19 +4053,20 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(main_project.clone(), window, cx));
+ let sidebar = setup_sidebar(&multi_workspace, cx);
+
let worktree_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(worktree_project.clone(), window, cx)
});
// Activate main workspace so the sidebar tracks the main panel.
multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
});
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
- let main_workspace = multi_workspace.read_with(cx, |mw, _| mw.workspaces()[0].clone());
+ let main_workspace =
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().next().unwrap().clone());
let main_panel = add_agent_panel(&main_workspace, cx);
let _worktree_panel = add_agent_panel(&worktree_workspace, cx);
@@ -4195,10 +4220,10 @@ async fn test_linked_worktree_threads_not_duplicated_across_groups(cx: &mut Test
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_only.clone(), window, cx));
+ let sidebar = setup_sidebar(&multi_workspace, cx);
multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(multi_root.clone(), window, cx);
});
- let sidebar = setup_sidebar(&multi_workspace, cx);
// Save a thread under the linked worktree path.
save_named_thread_metadata("wt-thread", "Worktree Thread", &worktree_project, cx).await;
@@ -4313,8 +4338,8 @@ async fn test_thread_switcher_ordering(cx: &mut TestAppContext) {
// so all three have last_accessed_at set.
// Access order is: A (most recent), B, C (oldest).
- // ── 1. Open switcher: threads sorted by last_accessed_at ───────────
- open_and_focus_sidebar(&sidebar, cx);
+ // ── 1. Open switcher: threads sorted by last_accessed_at ─────────────────
+ focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.on_toggle_thread_switcher(&ToggleThreadSwitcher::default(), window, cx);
});
@@ -4759,6 +4784,170 @@ async fn test_linked_worktree_workspace_shows_main_worktree_threads(cx: &mut Tes
);
}
+async fn init_multi_project_test(
+ paths: &[&str],
+ cx: &mut TestAppContext,
+) -> (Arc<FakeFs>, Entity<project::Project>) {
+ agent_ui::test_support::init_test(cx);
+ cx.update(|cx| {
+ cx.update_flags(false, vec!["agent-v2".into()]);
+ ThreadStore::init_global(cx);
+ ThreadMetadataStore::init_global(cx);
+ language_model::LanguageModelRegistry::test(cx);
+ prompt_store::init(cx);
+ });
+ let fs = FakeFs::new(cx.executor());
+ for path in paths {
+ fs.insert_tree(path, serde_json::json!({ ".git": {}, "src": {} }))
+ .await;
+ }
+ cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+ let project =
+ project::Project::test(fs.clone() as Arc<dyn fs::Fs>, [paths[0].as_ref()], cx).await;
+ (fs, project)
+}
+
+async fn add_test_project(
+ path: &str,
+ fs: &Arc<FakeFs>,
+ multi_workspace: &Entity<MultiWorkspace>,
+ cx: &mut gpui::VisualTestContext,
+) -> Entity<Workspace> {
+ let project = project::Project::test(fs.clone() as Arc<dyn fs::Fs>, [path.as_ref()], cx).await;
+ let workspace = multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.test_add_workspace(project, window, cx)
+ });
+ cx.run_until_parked();
+ workspace
+}
+
+#[gpui::test]
+async fn test_transient_workspace_lifecycle(cx: &mut TestAppContext) {
+ let (fs, project_a) =
+ init_multi_project_test(&["/project-a", "/project-b", "/project-c"], cx).await;
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+ let _sidebar = setup_sidebar_closed(&multi_workspace, cx);
+
+ // Sidebar starts closed. Initial workspace A is transient.
+ let workspace_a = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+ assert!(!multi_workspace.read_with(cx, |mw, _| mw.sidebar_open()));
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 1
+ );
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.workspace() == &workspace_a));
+
+ // Add B — replaces A as the transient workspace.
+ let workspace_b = add_test_project("/project-b", &fs, &multi_workspace, cx).await;
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 1
+ );
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.workspace() == &workspace_b));
+
+ // Add C — replaces B as the transient workspace.
+ let workspace_c = add_test_project("/project-c", &fs, &multi_workspace, cx).await;
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 1
+ );
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.workspace() == &workspace_c));
+}
+
+#[gpui::test]
+async fn test_transient_workspace_retained(cx: &mut TestAppContext) {
+ let (fs, project_a) = init_multi_project_test(
+ &["/project-a", "/project-b", "/project-c", "/project-d"],
+ cx,
+ )
+ .await;
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+ let _sidebar = setup_sidebar(&multi_workspace, cx);
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.sidebar_open()));
+
+ // Add B — retained since sidebar is open.
+ let workspace_a = add_test_project("/project-b", &fs, &multi_workspace, cx).await;
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 2
+ );
+
+ // Switch to A — B survives. (Switching from one internal workspace, to another)
+ multi_workspace.update_in(cx, |mw, window, cx| mw.activate(workspace_a, window, cx));
+ cx.run_until_parked();
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 2
+ );
+
+ // Close sidebar — both A and B remain retained.
+ multi_workspace.update_in(cx, |mw, window, cx| mw.close_sidebar(window, cx));
+ cx.run_until_parked();
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 2
+ );
+
+ // Add C — added as new transient workspace. (switching from retained, to transient)
+ let workspace_c = add_test_project("/project-c", &fs, &multi_workspace, cx).await;
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 3
+ );
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.workspace() == &workspace_c));
+
+ // Add D — replaces C as the transient workspace (Have retained and transient workspaces, transient workspace is dropped)
+ let workspace_d = add_test_project("/project-d", &fs, &multi_workspace, cx).await;
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 3
+ );
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.workspace() == &workspace_d));
+}
+
+#[gpui::test]
+async fn test_transient_workspace_promotion(cx: &mut TestAppContext) {
+ let (fs, project_a) =
+ init_multi_project_test(&["/project-a", "/project-b", "/project-c"], cx).await;
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+ setup_sidebar_closed(&multi_workspace, cx);
+
+ // Add B — replaces A as the transient workspace (A is discarded).
+ let workspace_b = add_test_project("/project-b", &fs, &multi_workspace, cx).await;
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 1
+ );
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.workspace() == &workspace_b));
+
+ // Open sidebar — promotes the transient B to retained.
+ multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.toggle_sidebar(window, cx);
+ });
+ cx.run_until_parked();
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 1
+ );
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.workspaces().any(|w| w == &workspace_b)));
+
+ // Close sidebar — the retained B remains.
+ multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.toggle_sidebar(window, cx);
+ });
+
+ // Add C — added as new transient workspace.
+ let workspace_c = add_test_project("/project-c", &fs, &multi_workspace, cx).await;
+ assert_eq!(
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
+ 2
+ );
+ assert!(multi_workspace.read_with(cx, |mw, _| mw.workspace() == &workspace_c));
+}
+
#[gpui::test]
async fn test_legacy_thread_with_canonical_path_opens_main_repo_workspace(cx: &mut TestAppContext) {
init_test(cx);
@@ -4843,12 +5032,12 @@ async fn test_legacy_thread_with_canonical_path_opens_main_repo_workspace(cx: &m
// Verify only 1 workspace before clicking.
assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.workspaces().len()),
+ multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()),
1,
);
// Focus and select the legacy thread, then confirm.
- open_and_focus_sidebar(&sidebar, cx);
+ focus_sidebar(&sidebar, cx);
let thread_index = sidebar.read_with(cx, |sidebar, _| {
sidebar
.contents
@@ -5057,7 +5246,12 @@ mod property_test {
match operation {
Operation::SaveThread { workspace_index } => {
let project = multi_workspace.read_with(cx, |mw, cx| {
- mw.workspaces()[workspace_index].read(cx).project().clone()
+ mw.workspaces()
+ .nth(workspace_index)
+ .unwrap()
+ .read(cx)
+ .project()
+ .clone()
});
save_thread_to_path(state, &project, cx);
}
@@ -5144,7 +5338,7 @@ mod property_test {
}
Operation::RemoveWorkspace { index } => {
let removed = multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[index].clone();
+ let workspace = mw.workspaces().nth(index).unwrap().clone();
mw.remove(&workspace, window, cx)
});
if removed {
@@ -5158,8 +5352,8 @@ mod property_test {
}
}
Operation::SwitchWorkspace { index } => {
- let workspace =
- multi_workspace.read_with(cx, |mw, _| mw.workspaces()[index].clone());
+ let workspace = multi_workspace
+ .read_with(cx, |mw, _| mw.workspaces().nth(index).unwrap().clone());
multi_workspace.update_in(cx, |mw, window, cx| {
mw.activate(workspace, window, cx);
});
@@ -5209,8 +5403,9 @@ mod property_test {
.await;
// Re-scan the main workspace's project so it discovers the new worktree.
- let main_workspace =
- multi_workspace.read_with(cx, |mw, _| mw.workspaces()[workspace_index].clone());
+ let main_workspace = multi_workspace.read_with(cx, |mw, _| {
+ mw.workspaces().nth(workspace_index).unwrap().clone()
+ });
let main_project = main_workspace.read_with(cx, |ws, _| ws.project().clone());
main_project
.update(cx, |p, cx| p.git_scans_complete(cx))
@@ -5297,7 +5492,11 @@ mod property_test {
let Some(multi_workspace) = sidebar.multi_workspace.upgrade() else {
anyhow::bail!("sidebar should still have an associated multi-workspace");
};
- let workspaces = multi_workspace.read(cx).workspaces().to_vec();
+ let workspaces = multi_workspace
+ .read(cx)
+ .workspaces()
+ .cloned()
+ .collect::<Vec<_>>();
let thread_store = ThreadMetadataStore::global(cx);
let sidebar_thread_ids: HashSet<acp::SessionId> = sidebar
@@ -740,7 +740,6 @@ impl TitleBar {
.map(|mw| {
mw.read(cx)
.workspaces()
- .iter()
.filter_map(|ws| ws.read(cx).database_id())
.collect()
})
@@ -803,7 +802,6 @@ impl TitleBar {
.map(|mw| {
mw.read(cx)
.workspaces()
- .iter()
.filter_map(|ws| ws.read(cx).database_id())
.collect()
})
@@ -40,10 +40,7 @@ actions!(
CloseWorkspaceSidebar,
/// Moves focus to or from the workspace sidebar without closing it.
FocusWorkspaceSidebar,
- /// Switches to the next workspace.
- NextWorkspace,
- /// Switches to the previous workspace.
- PreviousWorkspace,
+ //TODO: Restore next/previous workspace
]
);
@@ -221,10 +218,57 @@ impl<T: Sidebar> SidebarHandle for Entity<T> {
}
}
+/// Tracks which workspace the user is currently looking at.
+///
+/// `Persistent` workspaces live in the `workspaces` vec and are shown in the
+/// sidebar. `Transient` workspaces exist outside the vec and are discarded
+/// when the user switches away.
+enum ActiveWorkspace {
+ /// A persistent workspace, identified by index into the `workspaces` vec.
+ Persistent(usize),
+ /// A workspace not in the `workspaces` vec that will be discarded on
+ /// switch or promoted to persistent when the sidebar is opened.
+ Transient(Entity<Workspace>),
+}
+
+impl ActiveWorkspace {
+ fn persistent_index(&self) -> Option<usize> {
+ match self {
+ Self::Persistent(index) => Some(*index),
+ Self::Transient(_) => None,
+ }
+ }
+
+ fn transient_workspace(&self) -> Option<&Entity<Workspace>> {
+ match self {
+ Self::Transient(workspace) => Some(workspace),
+ Self::Persistent(_) => None,
+ }
+ }
+
+ /// Sets the active workspace to transient, returning the previous
+ /// transient workspace (if any).
+ fn set_transient(&mut self, workspace: Entity<Workspace>) -> Option<Entity<Workspace>> {
+ match std::mem::replace(self, Self::Transient(workspace)) {
+ Self::Transient(old) => Some(old),
+ Self::Persistent(_) => None,
+ }
+ }
+
+ /// Sets the active workspace to persistent at the given index,
+ /// returning the previous transient workspace (if any).
+ fn set_persistent(&mut self, index: usize) -> Option<Entity<Workspace>> {
+ match std::mem::replace(self, Self::Persistent(index)) {
+ Self::Transient(workspace) => Some(workspace),
+ Self::Persistent(_) => None,
+ }
+ }
+}
+
pub struct MultiWorkspace {
window_id: WindowId,
workspaces: Vec<Entity<Workspace>>,
- active_workspace_index: usize,
+ active_workspace: ActiveWorkspace,
project_group_keys: Vec<ProjectGroupKey>,
sidebar: Option<Box<dyn SidebarHandle>>,
sidebar_open: bool,
@@ -260,12 +304,15 @@ impl MultiWorkspace {
}
});
let quit_subscription = cx.on_app_quit(Self::app_will_quit);
- let settings_subscription =
- cx.observe_global_in::<settings::SettingsStore>(window, |this, window, cx| {
- if DisableAiSettings::get_global(cx).disable_ai && this.sidebar_open {
- this.close_sidebar(window, cx);
+ let settings_subscription = cx.observe_global_in::<settings::SettingsStore>(window, {
+ let mut previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
+ move |this, window, cx| {
+ if DisableAiSettings::get_global(cx).disable_ai != previous_disable_ai {
+ this.collapse_to_single_workspace(window, cx);
+ previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
}
- });
+ }
+ });
Self::subscribe_to_workspace(&workspace, window, cx);
let weak_self = cx.weak_entity();
workspace.update(cx, |workspace, cx| {
@@ -273,9 +320,9 @@ impl MultiWorkspace {
});
Self {
window_id: window.window_handle().window_id(),
- project_group_keys: vec![workspace.read(cx).project_group_key(cx)],
- workspaces: vec![workspace],
- active_workspace_index: 0,
+ project_group_keys: Vec::new(),
+ workspaces: Vec::new(),
+ active_workspace: ActiveWorkspace::Transient(workspace),
sidebar: None,
sidebar_open: false,
sidebar_overlay: None,
@@ -337,7 +384,7 @@ impl MultiWorkspace {
return;
}
- if self.sidebar_open {
+ if self.sidebar_open() {
self.close_sidebar(window, cx);
} else {
self.open_sidebar(cx);
@@ -353,7 +400,7 @@ impl MultiWorkspace {
return;
}
- if self.sidebar_open {
+ if self.sidebar_open() {
self.close_sidebar(window, cx);
}
}
@@ -363,7 +410,7 @@ impl MultiWorkspace {
return;
}
- if self.sidebar_open {
+ if self.sidebar_open() {
let sidebar_is_focused = self
.sidebar
.as_ref()
@@ -388,8 +435,13 @@ impl MultiWorkspace {
pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
self.sidebar_open = true;
+ if let ActiveWorkspace::Transient(workspace) = &self.active_workspace {
+ let workspace = workspace.clone();
+ let index = self.promote_transient(workspace, cx);
+ self.active_workspace = ActiveWorkspace::Persistent(index);
+ }
let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
- for workspace in &self.workspaces {
+ for workspace in self.workspaces.iter() {
workspace.update(cx, |workspace, _cx| {
workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
});
@@ -400,7 +452,7 @@ impl MultiWorkspace {
pub fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.sidebar_open = false;
- for workspace in &self.workspaces {
+ for workspace in self.workspaces.iter() {
workspace.update(cx, |workspace, _cx| {
workspace.set_sidebar_focus_handle(None);
});
@@ -415,7 +467,7 @@ impl MultiWorkspace {
pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
cx.spawn_in(window, async move |this, cx| {
let workspaces = this.update(cx, |multi_workspace, _cx| {
- multi_workspace.workspaces().to_vec()
+ multi_workspace.workspaces().cloned().collect::<Vec<_>>()
})?;
for workspace in workspaces {
@@ -657,6 +709,12 @@ impl MultiWorkspace {
return Task::ready(Ok(workspace));
}
+ if let Some(transient) = self.active_workspace.transient_workspace() {
+ if transient.read(cx).project_group_key(cx).path_list() == &path_list {
+ return Task::ready(Ok(transient.clone()));
+ }
+ }
+
let paths = path_list.paths().to_vec();
let app_state = self.workspace().read(cx).app_state().clone();
let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
@@ -680,25 +738,23 @@ impl MultiWorkspace {
}
pub fn workspace(&self) -> &Entity<Workspace> {
- &self.workspaces[self.active_workspace_index]
- }
-
- pub fn workspaces(&self) -> &[Entity<Workspace>] {
- &self.workspaces
+ match &self.active_workspace {
+ ActiveWorkspace::Persistent(index) => &self.workspaces[*index],
+ ActiveWorkspace::Transient(workspace) => workspace,
+ }
}
- pub fn active_workspace_index(&self) -> usize {
- self.active_workspace_index
+ pub fn workspaces(&self) -> impl Iterator<Item = &Entity<Workspace>> {
+ self.workspaces
+ .iter()
+ .chain(self.active_workspace.transient_workspace())
}
- /// Adds a workspace to this window without changing which workspace is
- /// active.
+ /// Adds a workspace to this window as persistent without changing which
+ /// workspace is active. Unlike `activate()`, this always inserts into the
+ /// persistent list regardless of sidebar state — it's used for system-
+ /// initiated additions like deserialization and worktree discovery.
pub fn add(&mut self, workspace: Entity<Workspace>, window: &Window, cx: &mut Context<Self>) {
- if !self.multi_workspace_enabled(cx) {
- self.set_single_workspace(workspace, cx);
- return;
- }
-
self.insert_workspace(workspace, window, cx);
}
@@ -709,26 +765,74 @@ impl MultiWorkspace {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if !self.multi_workspace_enabled(cx) {
- self.set_single_workspace(workspace, cx);
+ // Re-activating the current workspace is a no-op.
+ if self.workspace() == &workspace {
+ self.focus_active_workspace(window, 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);
+ // Resolve where we're going.
+ let new_index = if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
+ Some(index)
+ } else if self.sidebar_open {
+ Some(self.insert_workspace(workspace.clone(), &*window, cx))
+ } else {
+ None
+ };
+
+ // Transition the active workspace.
+ if let Some(index) = new_index {
+ if let Some(old) = self.active_workspace.set_persistent(index) {
+ if self.sidebar_open {
+ self.promote_transient(old, cx);
+ } else {
+ self.detach_workspace(&old, cx);
+ cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
+ }
+ }
+ } else {
+ 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);
+ });
+ if let Some(old) = self.active_workspace.set_transient(workspace) {
+ self.detach_workspace(&old, cx);
+ cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
+ }
}
+
+ cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
+ self.serialize(cx);
self.focus_active_workspace(window, cx);
cx.notify();
}
- fn set_single_workspace(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
- self.workspaces[0] = workspace;
- self.active_workspace_index = 0;
- cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
+ /// Promotes a former transient workspace into the persistent list.
+ /// Returns the index of the newly inserted workspace.
+ fn promote_transient(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
+ let project_group_key = workspace.read(cx).project().read(cx).project_group_key(cx);
+ self.add_project_group_key(project_group_key);
+ self.workspaces.push(workspace.clone());
+ cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
+ self.workspaces.len() - 1
+ }
+
+ /// Collapses to a single transient workspace, discarding all persistent
+ /// workspaces. Used when multi-workspace is disabled (e.g. disable_ai).
+ fn collapse_to_single_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if self.sidebar_open {
+ self.close_sidebar(window, cx);
+ }
+ let active = self.workspace().clone();
+ for workspace in std::mem::take(&mut self.workspaces) {
+ if workspace != active {
+ self.detach_workspace(&workspace, cx);
+ cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
+ }
+ }
+ self.project_group_keys.clear();
+ self.active_workspace = ActiveWorkspace::Transient(active);
cx.notify();
}
@@ -784,7 +888,7 @@ impl MultiWorkspace {
}
fn sync_sidebar_to_workspace(&self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
- if self.sidebar_open {
+ 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);
@@ -792,30 +896,6 @@ impl MultiWorkspace {
}
}
- fn cycle_workspace(&mut self, delta: isize, window: &mut Window, cx: &mut Context<Self>) {
- let count = self.workspaces.len() as isize;
- if count <= 1 {
- return;
- }
- let current = self.active_workspace_index as isize;
- let next = ((current + delta).rem_euclid(count)) as usize;
- let workspace = self.workspaces[next].clone();
- self.activate(workspace, window, cx);
- }
-
- fn next_workspace(&mut self, _: &NextWorkspace, window: &mut Window, cx: &mut Context<Self>) {
- self.cycle_workspace(1, window, cx);
- }
-
- fn previous_workspace(
- &mut self,
- _: &PreviousWorkspace,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.cycle_workspace(-1, window, cx);
- }
-
pub(crate) fn serialize(&mut self, cx: &mut Context<Self>) {
self._serialize_task = Some(cx.spawn(async move |this, cx| {
let Some((window_id, state)) = this
@@ -1070,7 +1150,7 @@ impl MultiWorkspace {
let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
self.workspaces[0] = new_workspace.clone();
- self.active_workspace_index = 0;
+ self.active_workspace = ActiveWorkspace::Persistent(0);
Self::subscribe_to_workspace(&new_workspace, window, cx);
@@ -1090,10 +1170,12 @@ impl MultiWorkspace {
} else {
let removed_workspace = 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;
+ if let Some(active_index) = self.active_workspace.persistent_index() {
+ if active_index >= self.workspaces.len() {
+ self.active_workspace = ActiveWorkspace::Persistent(self.workspaces.len() - 1);
+ } else if active_index > index {
+ self.active_workspace = ActiveWorkspace::Persistent(active_index - 1);
+ }
}
self.detach_workspace(&removed_workspace, cx);
@@ -1343,8 +1425,6 @@ impl Render for MultiWorkspace {
this.focus_sidebar(window, cx);
},
))
- .on_action(cx.listener(Self::next_workspace))
- .on_action(cx.listener(Self::previous_workspace))
.on_action(cx.listener(Self::move_active_workspace_to_new_window))
.on_action(cx.listener(
|this: &mut Self, action: &ToggleThreadSwitcher, window, cx| {
@@ -99,6 +99,10 @@ async fn test_project_group_keys_initial(cx: &mut TestAppContext) {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.read_with(cx, |mw, _cx| {
let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
assert_eq!(keys.len(), 1, "should have exactly one key on creation");
@@ -125,6 +129,10 @@ async fn test_project_group_keys_add_workspace(cx: &mut TestAppContext) {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.read_with(cx, |mw, _cx| {
assert_eq!(mw.project_group_keys().count(), 1);
});
@@ -162,6 +170,10 @@ async fn test_project_group_keys_duplicate_not_added(cx: &mut TestAppContext) {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project_a2, window, cx);
});
@@ -189,6 +201,10 @@ async fn test_project_group_keys_on_worktree_added(cx: &mut TestAppContext) {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
// Add a second worktree to the same project.
let (worktree, _) = project
.update(cx, |project, cx| {
@@ -232,6 +248,10 @@ async fn test_project_group_keys_on_worktree_removed(cx: &mut TestAppContext) {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
// Remove one worktree.
let worktree_b_id = project.read_with(cx, |project, cx| {
project
@@ -282,6 +302,10 @@ async fn test_project_group_keys_across_multiple_workspaces_and_worktree_changes
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project_b, window, cx);
});
@@ -2535,6 +2535,10 @@ mod tests {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.update_in(cx, |mw, _, cx| {
mw.set_random_database_id(cx);
});
@@ -2564,7 +2568,7 @@ mod tests {
// --- Remove the second workspace (index 1) ---
multi_workspace.update_in(cx, |mw, window, cx| {
- let ws = mw.workspaces()[1].clone();
+ let ws = mw.workspaces().nth(1).unwrap().clone();
mw.remove(&ws, window, cx);
});
@@ -4191,6 +4195,10 @@ mod tests {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.update_in(cx, |mw, _, cx| {
mw.set_random_database_id(cx);
});
@@ -4233,7 +4241,7 @@ mod tests {
// Remove workspace at index 1 (the second workspace).
multi_workspace.update_in(cx, |mw, window, cx| {
- let ws = mw.workspaces()[1].clone();
+ let ws = mw.workspaces().nth(1).unwrap().clone();
mw.remove(&ws, window, cx);
});
@@ -4288,6 +4296,10 @@ mod tests {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.update_in(cx, |mw, _, cx| {
mw.workspace().update(cx, |ws, _cx| {
ws.set_database_id(ws1_id);
@@ -4339,7 +4351,7 @@ mod tests {
// Remove workspace2 (index 1).
multi_workspace.update_in(cx, |mw, window, cx| {
- let ws = mw.workspaces()[1].clone();
+ let ws = mw.workspaces().nth(1).unwrap().clone();
mw.remove(&ws, window, cx);
});
@@ -4385,6 +4397,10 @@ mod tests {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.update_in(cx, |mw, _, cx| {
mw.set_random_database_id(cx);
});
@@ -4418,7 +4434,7 @@ mod tests {
// Remove workspace2 — this pushes a task to pending_removal_tasks.
multi_workspace.update_in(cx, |mw, window, cx| {
- let ws = mw.workspaces()[1].clone();
+ let ws = mw.workspaces().nth(1).unwrap().clone();
mw.remove(&ws, window, cx);
});
@@ -4427,7 +4443,6 @@ mod tests {
let all_tasks = multi_workspace.update_in(cx, |mw, window, cx| {
let mut tasks: Vec<Task<()>> = mw
.workspaces()
- .iter()
.map(|workspace| {
workspace.update(cx, |workspace, cx| {
workspace.flush_serialization(window, cx)
@@ -4747,6 +4762,10 @@ mod tests {
let (multi_workspace, cx) = cx
.add_window_view(|window, cx| MultiWorkspace::test_new(project_2.clone(), window, cx));
+ multi_workspace.update(cx, |mw, cx| {
+ mw.open_sidebar(cx);
+ });
+
multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project_1.clone(), window, cx);
});
@@ -32,8 +32,8 @@ pub use crate::notifications::NotificationFrame;
pub use dock::Panel;
pub use multi_workspace::{
CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace,
- MultiWorkspaceEvent, NextWorkspace, PreviousWorkspace, Sidebar, SidebarEvent, SidebarHandle,
- SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, sidebar_side_context_menu,
+ MultiWorkspaceEvent, Sidebar, SidebarEvent, SidebarHandle, SidebarRenderState, SidebarSide,
+ ToggleWorkspaceSidebar, sidebar_side_context_menu,
};
pub use path_list::{PathList, SerializedPathList};
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
@@ -9079,7 +9079,7 @@ pub fn workspace_windows_for_location(
};
multi_workspace.read(cx).is_ok_and(|multi_workspace| {
- multi_workspace.workspaces().iter().any(|workspace| {
+ multi_workspace.workspaces().any(|workspace| {
match workspace.read(cx).workspace_location(cx) {
WorkspaceLocation::Location(location, _) => {
match (&location, serialized_location) {
@@ -10741,6 +10741,12 @@ mod tests {
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
cx.run_until_parked();
+ multi_workspace_handle
+ .update(cx, |mw, _window, cx| {
+ mw.open_sidebar(cx);
+ })
+ .unwrap();
+
let workspace_a = multi_workspace_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
@@ -10754,7 +10760,7 @@ mod tests {
// Activate workspace A
multi_workspace_handle
.update(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
})
.unwrap();
@@ -10776,7 +10782,7 @@ mod tests {
// Verify workspace A is active
multi_workspace_handle
.read_with(cx, |mw, _| {
- assert_eq!(mw.active_workspace_index(), 0);
+ assert_eq!(mw.workspace(), &workspace_a);
})
.unwrap();
@@ -10792,8 +10798,8 @@ mod tests {
multi_workspace_handle
.read_with(cx, |mw, _| {
assert_eq!(
- mw.active_workspace_index(),
- 1,
+ mw.workspace(),
+ &workspace_b,
"workspace B should be activated when it prompts"
);
})
@@ -14511,6 +14517,12 @@ mod tests {
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
cx.run_until_parked();
+ multi_workspace_handle
+ .update(cx, |mw, _window, cx| {
+ mw.open_sidebar(cx);
+ })
+ .unwrap();
+
let workspace_a = multi_workspace_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
@@ -14524,7 +14536,7 @@ mod tests {
// Switch to workspace A
multi_workspace_handle
.update(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
})
.unwrap();
@@ -14570,7 +14582,7 @@ mod tests {
// Switch to workspace B
multi_workspace_handle
.update(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[1].clone();
+ let workspace = mw.workspaces().nth(1).unwrap().clone();
mw.activate(workspace, window, cx);
})
.unwrap();
@@ -14579,7 +14591,7 @@ mod tests {
// Switch back to workspace A
multi_workspace_handle
.update(cx, |mw, window, cx| {
- let workspace = mw.workspaces()[0].clone();
+ let workspace = mw.workspaces().next().unwrap().clone();
mw.activate(workspace, window, cx);
})
.unwrap();
@@ -2606,7 +2606,7 @@ fn run_multi_workspace_sidebar_visual_tests(
// 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 workspace1 = multi_workspace.workspaces().next().unwrap();
let project = workspace1.read(cx).project().clone();
project.update(cx, |project, cx| {
project.find_or_create_worktree(&workspace1_dir, true, cx)
@@ -2625,7 +2625,7 @@ fn run_multi_workspace_sidebar_visual_tests(
// 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 workspace2 = multi_workspace.workspaces().nth(1).unwrap();
let project = workspace2.read(cx).project().clone();
project.update(cx, |project, cx| {
project.find_or_create_worktree(&workspace2_dir, true, cx)
@@ -2644,7 +2644,7 @@ 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| {
- let workspace = multi_workspace.workspaces()[0].clone();
+ let workspace = multi_workspace.workspaces().next().unwrap().clone();
multi_workspace.activate(workspace, window, cx);
})
.context("Failed to activate workspace 1")?;
@@ -2672,7 +2672,7 @@ fn run_multi_workspace_sidebar_visual_tests(
let save_tasks = multi_workspace_window
.update(cx, |multi_workspace, _window, cx| {
let thread_store = agent::ThreadStore::global(cx);
- let workspaces = multi_workspace.workspaces().to_vec();
+ let workspaces: Vec<_> = multi_workspace.workspaces().cloned().collect();
let mut tasks = Vec::new();
for (index, workspace) in workspaces.iter().enumerate() {
@@ -3211,7 +3211,7 @@ edition = "2021"
// Add the git project as a worktree
let add_worktree_task = workspace_window
.update(cx, |multi_workspace, _window, cx| {
- let workspace = &multi_workspace.workspaces()[0];
+ let workspace = multi_workspace.workspaces().next().unwrap();
let project = workspace.read(cx).project().clone();
project.update(cx, |project, cx| {
project.find_or_create_worktree(&project_path, true, cx)
@@ -3236,7 +3236,7 @@ edition = "2021"
// Open the project panel
let (weak_workspace, async_window_cx) = workspace_window
.update(cx, |multi_workspace, window, cx| {
- let workspace = &multi_workspace.workspaces()[0];
+ let workspace = multi_workspace.workspaces().next().unwrap();
(workspace.read(cx).weak_handle(), window.to_async(cx))
})
.context("Failed to get workspace handle")?;
@@ -3250,7 +3250,7 @@ edition = "2021"
workspace_window
.update(cx, |multi_workspace, window, cx| {
- let workspace = &multi_workspace.workspaces()[0];
+ let workspace = multi_workspace.workspaces().next().unwrap();
workspace.update(cx, |workspace, cx| {
workspace.add_panel(project_panel, window, cx);
workspace.open_panel::<ProjectPanel>(window, cx);
@@ -3263,7 +3263,7 @@ edition = "2021"
// Open main.rs in the editor
let open_file_task = workspace_window
.update(cx, |multi_workspace, window, cx| {
- let workspace = &multi_workspace.workspaces()[0];
+ let workspace = multi_workspace.workspaces().next().unwrap();
workspace.update(cx, |workspace, cx| {
let worktree = workspace.project().read(cx).worktrees(cx).next();
if let Some(worktree) = worktree {
@@ -3291,7 +3291,7 @@ edition = "2021"
// Load the AgentPanel
let (weak_workspace, async_window_cx) = workspace_window
.update(cx, |multi_workspace, window, cx| {
- let workspace = &multi_workspace.workspaces()[0];
+ let workspace = multi_workspace.workspaces().next().unwrap();
(workspace.read(cx).weak_handle(), window.to_async(cx))
})
.context("Failed to get workspace handle for agent panel")?;
@@ -3335,7 +3335,7 @@ edition = "2021"
workspace_window
.update(cx, |multi_workspace, window, cx| {
- let workspace = &multi_workspace.workspaces()[0];
+ let workspace = multi_workspace.workspaces().next().unwrap();
workspace.update(cx, |workspace, cx| {
workspace.add_panel(panel.clone(), window, cx);
workspace.open_panel::<AgentPanel>(window, cx);
@@ -3512,7 +3512,7 @@ edition = "2021"
.is_none()
});
let workspace_count = workspace_window.update(cx, |multi_workspace, _window, _cx| {
- multi_workspace.workspaces().len()
+ multi_workspace.workspaces().count()
})?;
if workspace_count == 2 && status_cleared {
creation_complete = true;
@@ -3531,7 +3531,7 @@ edition = "2021"
// error state by injecting the stub server, and shrink the panel so the
// editor content is visible.
workspace_window.update(cx, |multi_workspace, window, cx| {
- let new_workspace = &multi_workspace.workspaces()[1];
+ let new_workspace = multi_workspace.workspaces().nth(1).unwrap();
new_workspace.update(cx, |workspace, cx| {
if let Some(new_panel) = workspace.panel::<AgentPanel>(cx) {
new_panel.update(cx, |panel, cx| {
@@ -3544,7 +3544,7 @@ edition = "2021"
// Type and send a message so the thread target dropdown disappears.
let new_panel = workspace_window.update(cx, |multi_workspace, _window, cx| {
- let new_workspace = &multi_workspace.workspaces()[1];
+ let new_workspace = multi_workspace.workspaces().nth(1).unwrap();
new_workspace.read(cx).panel::<AgentPanel>(cx)
})?;
if let Some(new_panel) = new_panel {
@@ -3585,7 +3585,7 @@ edition = "2021"
workspace_window
.update(cx, |multi_workspace, _window, cx| {
- let workspace = &multi_workspace.workspaces()[0];
+ let workspace = multi_workspace.workspaces().next().unwrap();
let project = workspace.read(cx).project().clone();
project.update(cx, |project, cx| {
let worktree_ids: Vec<_> =
@@ -1524,7 +1524,7 @@ fn quit(_: &Quit, cx: &mut App) {
let window = *window;
let workspaces = window
.update(cx, |multi_workspace, _, _| {
- multi_workspace.workspaces().to_vec()
+ multi_workspace.workspaces().cloned().collect::<Vec<_>>()
})
.log_err();
@@ -2458,7 +2458,6 @@ mod tests {
.update(cx, |multi_workspace, window, cx| {
let mut tasks = multi_workspace
.workspaces()
- .iter()
.map(|workspace| {
workspace.update(cx, |workspace, cx| {
workspace.flush_serialization(window, cx)
@@ -2610,7 +2609,7 @@ mod tests {
cx.run_until_parked();
multi_workspace_1
.update(cx, |multi_workspace, _window, cx| {
- assert_eq!(multi_workspace.workspaces().len(), 2);
+ assert_eq!(multi_workspace.workspaces().count(), 2);
assert!(multi_workspace.sidebar_open());
let workspace = multi_workspace.workspace().read(cx);
assert_eq!(
@@ -5512,6 +5511,11 @@ mod tests {
let project = project1.clone();
|window, cx| MultiWorkspace::test_new(project, window, cx)
});
+ window
+ .update(cx, |multi_workspace, _, cx| {
+ multi_workspace.open_sidebar(cx);
+ })
+ .unwrap();
cx.run_until_parked();
assert_eq!(cx.windows().len(), 1, "Should start with 1 window");
@@ -5534,7 +5538,7 @@ mod tests {
let workspace1 = window
.read_with(cx, |multi_workspace, _| {
- multi_workspace.workspaces()[0].clone()
+ multi_workspace.workspaces().next().unwrap().clone()
})
.unwrap();
@@ -5543,8 +5547,8 @@ mod tests {
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, window, cx);
- assert_eq!(multi_workspace.active_workspace_index(), 0);
+ multi_workspace.activate(workspace1.clone(), window, cx);
+ assert_eq!(multi_workspace.workspace(), &workspace1);
})
.unwrap();
@@ -5553,8 +5557,8 @@ mod tests {
// 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);
+ assert_eq!(multi_workspace.workspaces().count(), 3);
+ assert_eq!(multi_workspace.workspace(), &workspace1);
})
.unwrap();
assert_eq!(cx.windows().len(), 1);
@@ -5577,8 +5581,8 @@ mod tests {
window
.read_with(cx, |multi_workspace, cx| {
assert_eq!(
- multi_workspace.active_workspace_index(),
- 2,
+ multi_workspace.workspace(),
+ &workspace3,
"Should have switched to workspace 3 which contains /dir3"
);
let active_item = multi_workspace
@@ -5611,8 +5615,8 @@ mod tests {
window
.read_with(cx, |multi_workspace, cx| {
assert_eq!(
- multi_workspace.active_workspace_index(),
- 1,
+ multi_workspace.workspace(),
+ &workspace2,
"Should have switched to workspace 2 which contains /dir2"
);
let active_item = multi_workspace
@@ -5660,8 +5664,8 @@ mod tests {
window
.read_with(cx, |multi_workspace, cx| {
assert_eq!(
- multi_workspace.active_workspace_index(),
- 0,
+ multi_workspace.workspace(),
+ &workspace1,
"Should have switched back to workspace 0 which contains /dir1"
);
let active_item = multi_workspace
@@ -5711,6 +5715,11 @@ mod tests {
let project = project1.clone();
|window, cx| MultiWorkspace::test_new(project, window, cx)
});
+ window1
+ .update(cx, |multi_workspace, _, cx| {
+ multi_workspace.open_sidebar(cx);
+ })
+ .unwrap();
cx.run_until_parked();
@@ -5737,6 +5746,11 @@ mod tests {
let project = project3.clone();
|window, cx| MultiWorkspace::test_new(project, window, cx)
});
+ window2
+ .update(cx, |multi_workspace, _, cx| {
+ multi_workspace.open_sidebar(cx);
+ })
+ .unwrap();
cx.run_until_parked();
assert_eq!(cx.windows().len(), 2);
@@ -5771,7 +5785,7 @@ mod tests {
// Verify workspace1_1 is active
window1
.read_with(cx, |multi_workspace, _| {
- assert_eq!(multi_workspace.active_workspace_index(), 0);
+ assert_eq!(multi_workspace.workspace(), &workspace1_1);
})
.unwrap();
@@ -5837,7 +5851,7 @@ mod tests {
// 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);
+ assert_eq!(multi_workspace.workspace(), &workspace1_1);
})
.unwrap();
@@ -5848,8 +5862,8 @@ mod tests {
window1
.read_with(cx, |multi_workspace, _| {
assert_eq!(
- multi_workspace.active_workspace_index(),
- 1,
+ multi_workspace.workspace(),
+ &workspace1_2,
"Case 2: Non-active workspace should be activated when it has dirty item"
);
})
@@ -6002,6 +6016,12 @@ mod tests {
.await
.expect("failed to open first workspace");
+ window_a
+ .update(cx, |multi_workspace, _, cx| {
+ multi_workspace.open_sidebar(cx);
+ })
+ .unwrap();
+
window_a
.update(cx, |multi_workspace, window, cx| {
multi_workspace.open_project(vec![dir2.into()], OpenMode::Activate, window, cx)
@@ -6028,13 +6048,19 @@ mod tests {
.await
.expect("failed to open third workspace");
+ window_b
+ .update(cx, |multi_workspace, _, cx| {
+ multi_workspace.open_sidebar(cx);
+ })
+ .unwrap();
+
// 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| {
- let workspace = multi_workspace.workspaces()[0].clone();
+ let workspace = multi_workspace.workspaces().next().unwrap().clone();
multi_workspace.activate(workspace, window, cx);
})
.unwrap();
@@ -6150,7 +6176,7 @@ mod tests {
ProjectGroupKey::new(None, PathList::new(&[dir2])),
]
);
- assert_eq!(mw.workspaces().len(), 1);
+ assert_eq!(mw.workspaces().count(), 1);
})
.unwrap();
@@ -6161,7 +6187,7 @@ mod tests {
mw.project_group_keys().cloned().collect::<Vec<_>>(),
vec![ProjectGroupKey::new(None, PathList::new(&[dir3]))]
);
- assert_eq!(mw.workspaces().len(), 1);
+ assert_eq!(mw.workspaces().count(), 1);
})
.unwrap();
}