sidebar: Serialize after adding/removing workspaces (#49372)

Before this PR we wouldn't always serialize workspaces when a
mutliworkspace adds/removes a workspace. This PR fixes this by adding a
test and calling serialization in remove

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [ ] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- N/A
This commit is contained in:
Anthony Eid authored and GitHub committed 2026-02-17 12:38:03 -05:00
1 parent f59baa942a
commit a65307d817
11 files changed
+99 -29

No files matched your search

@@ -107,7 +107,7 @@ async fn test_host_disconnect(
cx,
)
});
MultiWorkspace::new(workspace, cx)
MultiWorkspace::new(workspace, window, cx)
});
let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
let workspace_b = window_b
+1 -1
View File
@@ -67,7 +67,7 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
cx,
)
});
MultiWorkspace::new(workspace, cx)
MultiWorkspace::new(workspace, window, cx)
});
let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
let workspace_b = window_b
@@ -886,7 +886,7 @@ impl TestClient {
let window = cx.add_window(|window, cx| {
window.activate_window();
let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
MultiWorkspace::new(workspace, cx)
MultiWorkspace::new(workspace, window, cx)
});
let cx = VisualTestContext::from_window(*window, cx).into_mut();
cx.run_until_parked();
@@ -905,7 +905,7 @@ impl TestClient {
let window = cx.add_window(|window, cx| {
window.activate_window();
let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
MultiWorkspace::new(workspace, cx)
MultiWorkspace::new(workspace, window, cx)
});
let cx = VisualTestContext::from_window(*window, cx).into_mut();
let workspace = window
+1 -1
View File
@@ -518,7 +518,7 @@ async fn open_remote_worktree(
workspace.centered_layout = workspace_position.centered_layout;
workspace
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
})?
};
@@ -233,7 +233,7 @@ pub async fn open_remote_project(
workspace.centered_layout = workspace_position.centered_layout;
workspace
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
})?;
let workspace = window.update(cx, |multi_workspace, _, _cx| {
multi_workspace.workspace().clone()
+1 -1
View File
@@ -495,7 +495,7 @@ impl ProjectPicker {
telemetry::event!("SSH Project Created");
Workspace::new(None, project.clone(), app_state.clone(), window, cx)
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
})
.log_err()?;
+4 -4
View File
@@ -4800,7 +4800,7 @@ pub mod test {
cx,
)
});
MultiWorkspace::new(workspace, cx)
MultiWorkspace::new(workspace, window, cx)
});
let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
@@ -4813,7 +4813,7 @@ pub mod test {
cx,
)
});
MultiWorkspace::new(workspace, cx)
MultiWorkspace::new(workspace, window, cx)
});
let workspace2_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
@@ -4945,7 +4945,7 @@ pub mod test {
cx,
)
});
MultiWorkspace::new(workspace, cx)
MultiWorkspace::new(workspace, window, cx)
});
let workspace1_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
@@ -4995,7 +4995,7 @@ pub mod test {
cx,
)
});
MultiWorkspace::new(workspace, cx)
MultiWorkspace::new(workspace, window, cx)
});
cx.run_until_parked();
+16 -12
View File
@@ -2,8 +2,8 @@ use anyhow::Result;
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
use gpui::{
AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, actions,
deferred, px,
ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
actions, deferred, px,
};
use project::Project;
use std::path::PathBuf;
@@ -93,6 +93,7 @@ impl<T: Sidebar> SidebarHandle for Entity<T> {
}
pub struct MultiWorkspace {
window_id: WindowId,
workspaces: Vec<Entity<Workspace>>,
active_workspace_index: usize,
sidebar: Option<Box<dyn SidebarHandle>>,
@@ -101,8 +102,9 @@ pub struct MultiWorkspace {
}
impl MultiWorkspace {
pub fn new(workspace: Entity<Workspace>, _cx: &mut Context<Self>) -> Self {
pub fn new(workspace: Entity<Workspace>, window: &mut Window, _cx: &mut Context<Self>) -> Self {
Self {
window_id: window.window_handle().window_id(),
workspaces: vec![workspace],
active_workspace_index: 0,
sidebar: None,
@@ -154,7 +156,7 @@ impl MultiWorkspace {
if self.sidebar_open {
self.close_sidebar(window, cx);
} else {
self.open_sidebar(window, cx);
self.open_sidebar(cx);
if let Some(sidebar) = &self.sidebar {
sidebar.focus(window, cx);
}
@@ -180,21 +182,21 @@ impl MultiWorkspace {
sidebar.focus(window, cx);
}
} else {
self.open_sidebar(window, cx);
self.open_sidebar(cx);
if let Some(sidebar) = &self.sidebar {
sidebar.focus(window, cx);
}
}
}
pub fn open_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
self.sidebar_open = true;
for workspace in &self.workspaces {
workspace.update(cx, |workspace, cx| {
workspace.set_workspace_sidebar_open(true, cx);
});
}
self.serialize(window, cx);
self.serialize(cx);
cx.notify();
}
@@ -208,7 +210,7 @@ impl MultiWorkspace {
let pane = self.workspace().read(cx).active_pane().clone();
let pane_focus = pane.read(cx).focus_handle(cx);
window.focus(&pane_focus, cx);
self.serialize(window, cx);
self.serialize(cx);
cx.notify();
}
@@ -239,6 +241,7 @@ impl MultiWorkspace {
let index = self.add_workspace(workspace, cx);
if self.active_workspace_index != index {
self.active_workspace_index = index;
self.serialize(cx);
cx.notify();
}
}
@@ -266,7 +269,7 @@ impl MultiWorkspace {
"workspace index out of bounds"
);
self.active_workspace_index = index;
self.serialize(window, cx);
self.serialize(cx);
self.focus_active_workspace(window, cx);
cx.notify();
}
@@ -289,8 +292,8 @@ impl MultiWorkspace {
}
}
fn serialize(&self, window: &mut Window, cx: &mut App) {
let window_id = window.window_handle().window_id();
fn serialize(&self, cx: &mut App) {
let window_id = self.window_id;
let state = crate::persistence::model::MultiWorkspaceState {
active_workspace_id: self.workspace().read(cx).database_id(),
sidebar_open: self.sidebar_open,
@@ -404,7 +407,7 @@ impl MultiWorkspace {
#[cfg(any(test, feature = "test-support"))]
pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
Self::new(workspace, cx)
Self::new(workspace, window, cx)
}
#[cfg(any(test, feature = "test-support"))]
@@ -453,6 +456,7 @@ impl MultiWorkspace {
}
self.focus_active_workspace(window, cx);
self.serialize(cx);
cx.notify();
}
+66
View File
@@ -2355,6 +2355,72 @@ mod tests {
use serde_json::json;
use std::{thread, time::Duration};
#[gpui::test]
async fn test_multi_workspace_serializes_on_add_and_remove(cx: &mut gpui::TestAppContext) {
use crate::multi_workspace::MultiWorkspace;
use crate::persistence::read_multi_workspace_state;
use feature_flags::FeatureFlagAppExt;
use gpui::AppContext as _;
use project::Project;
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
cx.update_flags(true, vec!["agent-v2".to_string()]);
});
let fs = fs::FakeFs::new(cx.executor());
let project1 = Project::test(fs.clone(), [], cx).await;
let project2 = Project::test(fs.clone(), [], cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
multi_workspace.update_in(cx, |mw, _, cx| {
mw.set_random_database_id(cx);
});
let window_id =
multi_workspace.update_in(cx, |_, window, _cx| window.window_handle().window_id());
// --- Add a second workspace ---
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);
workspace
});
// Run background tasks so serialize has a chance to flush.
cx.run_until_parked();
// Read back the persisted state and check that the active workspace ID was written.
let state_after_add = read_multi_workspace_state(window_id);
let active_workspace2_db_id = workspace2.read_with(cx, |ws, _| ws.database_id());
assert_eq!(
state_after_add.active_workspace_id, active_workspace2_db_id,
"After adding a second workspace, the serialized active_workspace_id should match \
the newly activated workspace's database id"
);
// --- Remove the second workspace (index 1) ---
multi_workspace.update_in(cx, |mw, window, cx| {
mw.remove_workspace(1, window, cx);
});
cx.run_until_parked();
let state_after_remove = read_multi_workspace_state(window_id);
let remaining_db_id =
multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
assert_eq!(
state_after_remove.active_workspace_id, remaining_db_id,
"After removing a workspace, the serialized active_workspace_id should match \
the remaining active workspace's database id"
);
}
#[gpui::test]
async fn test_breakpoints() {
zlog::init_test();
+5 -5
View File
@@ -1876,7 +1876,7 @@ impl Workspace {
workspace
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
}
})?;
let workspace =
@@ -7972,8 +7972,8 @@ pub async fn restore_multiworkspace(
if state.sidebar_open {
window_handle
.update(cx, |multi_workspace, window, cx| {
multi_workspace.open_sidebar(window, cx);
.update(cx, |multi_workspace, _, cx| {
multi_workspace.open_sidebar(cx);
})
.ok();
}
@@ -8536,7 +8536,7 @@ pub fn open_workspace_by_id(
workspace.centered_layout = centered_layout;
workspace
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
}
})?;
@@ -9053,7 +9053,7 @@ pub fn join_in_room_project(
let workspace = cx.new(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), window, cx)
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
})
})?
};
+1 -1
View File
@@ -2574,7 +2574,7 @@ fn run_multi_workspace_sidebar_visual_tests(
Workspace::new(None, project2.clone(), app_state.clone(), window, cx)
});
cx.new(|cx| {
let mut multi_workspace = MultiWorkspace::new(workspace1, cx);
let mut multi_workspace = MultiWorkspace::new(workspace1, window, cx);
multi_workspace.activate(workspace2, cx);
multi_workspace
})