workspace: Persist dock state for empty workspaces (#45966)

Pedro Paulo Magno and Kirill Bulatov created

Closes #4568

Release Notes:

- Fixed project-less workspaces not persisting their dock state

Co-authored-by: Kirill Bulatov <kirill@zed.dev>

Change summary

crates/workspace/src/persistence.rs       | 19 ++++++++++
crates/workspace/src/persistence/model.rs |  5 +-
crates/workspace/src/workspace.rs         | 46 ++++++++++++++++++++++++
3 files changed, 67 insertions(+), 3 deletions(-)

Detailed changes

crates/workspace/src/persistence.rs 🔗

@@ -281,6 +281,25 @@ impl From<WindowBoundsJson> for WindowBounds {
     }
 }
 
+const DEFAULT_DOCK_STATE_KEY: &str = "default_dock_state";
+
+pub fn read_default_dock_state() -> Option<DockStructure> {
+    let json_str = KEY_VALUE_STORE
+        .read_kvp(DEFAULT_DOCK_STATE_KEY)
+        .log_err()
+        .flatten()?;
+
+    serde_json::from_str::<DockStructure>(&json_str).ok()
+}
+
+pub async fn write_default_dock_state(docks: DockStructure) -> anyhow::Result<()> {
+    let json_str = serde_json::to_string(&docks)?;
+    KEY_VALUE_STORE
+        .write_kvp(DEFAULT_DOCK_STATE_KEY.to_string(), json_str)
+        .await?;
+    Ok(())
+}
+
 #[derive(Debug)]
 pub struct Breakpoint {
     pub position: u32,

crates/workspace/src/persistence/model.rs 🔗

@@ -15,6 +15,7 @@ use gpui::{AsyncWindowContext, Entity, WeakEntity};
 use language::{Toolchain, ToolchainScope};
 use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
 use remote::RemoteConnectionOptions;
+use serde::{Deserialize, Serialize};
 use std::{
     collections::BTreeMap,
     path::{Path, PathBuf},
@@ -64,7 +65,7 @@ pub(crate) struct SerializedWorkspace {
     pub(crate) window_id: Option<u64>,
 }
 
-#[derive(Debug, PartialEq, Clone, Default)]
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
 pub struct DockStructure {
     pub(crate) left: DockData,
     pub(crate) right: DockData,
@@ -114,7 +115,7 @@ impl Bind for DockStructure {
     }
 }
 
-#[derive(Debug, PartialEq, Clone, Default)]
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
 pub struct DockData {
     pub(crate) visible: bool,
     pub(crate) active_panel: Option<String>,

crates/workspace/src/workspace.rs 🔗

@@ -1823,6 +1823,15 @@ impl Workspace {
             };
 
             notify_if_database_failed(window, cx);
+            // Check if this is an empty workspace (no paths to open)
+            // An empty workspace is one where project_paths is empty
+            let is_empty_workspace = project_paths.is_empty();
+            // Check if serialized workspace has paths before it's moved
+            let serialized_workspace_has_paths = serialized_workspace
+                .as_ref()
+                .map(|ws| !ws.paths.is_empty())
+                .unwrap_or(false);
+
             let opened_items = window
                 .update(cx, |_workspace, window, cx| {
                     open_items(serialized_workspace, project_paths, window, cx)
@@ -1830,6 +1839,32 @@ impl Workspace {
                 .await
                 .unwrap_or_default();
 
+            // Restore default dock state for empty workspaces
+            // Only restore if:
+            // 1. This is an empty workspace (no paths), AND
+            // 2. The serialized workspace either doesn't exist or has no paths
+            if is_empty_workspace && !serialized_workspace_has_paths {
+                if let Some(default_docks) = persistence::read_default_dock_state() {
+                    window
+                        .update(cx, |workspace, window, cx| {
+                            for (dock, serialized_dock) in [
+                                (&mut workspace.right_dock, default_docks.right),
+                                (&mut workspace.left_dock, default_docks.left),
+                                (&mut workspace.bottom_dock, default_docks.bottom),
+                            ]
+                            .iter_mut()
+                            {
+                                dock.update(cx, |dock, cx| {
+                                    dock.serialized_dock = Some(serialized_dock.clone());
+                                    dock.restore_state(window, cx);
+                                });
+                            }
+                            cx.notify();
+                        })
+                        .log_err();
+                }
+            }
+
             window
                 .update(cx, |workspace, window, cx| {
                     window.activate_window();
@@ -5858,6 +5893,8 @@ impl Workspace {
             WorkspaceLocation::DetachFromSession => {
                 let window_bounds = SerializedWindowBounds(window.window_bounds());
                 let display = window.display(cx).and_then(|d| d.uuid().ok());
+                // Save dock state for empty local workspaces
+                let docks = build_serialized_docks(self, window, cx);
                 window.spawn(cx, async move |_| {
                     persistence::DB
                         .set_window_open_status(
@@ -5871,9 +5908,16 @@ impl Workspace {
                         .set_session_id(database_id, None)
                         .await
                         .log_err();
+                    persistence::write_default_dock_state(docks).await.log_err();
+                })
+            }
+            WorkspaceLocation::None => {
+                // Save dock state for empty non-local workspaces
+                let docks = build_serialized_docks(self, window, cx);
+                window.spawn(cx, async move |_| {
+                    persistence::write_default_dock_state(docks).await.log_err();
                 })
             }
-            WorkspaceLocation::None => Task::ready(()),
         }
     }