From: Danny Milosavljevic <dannym@friendly-machines.com>
Date: Fri, 6 Mar 2026 20:20:00 +0000
Subject: [PATCH] workspace: Keep local workspaces whose roots are only regular files
License: expat

Zed persists local workspaces for standalone regular files as well as
directory-backed projects. Recent-workspace cleanup and last-session
restoration were using a helper that only kept local workspaces if all
saved paths existed and at least one of them was a directory.

That made local workspaces whose roots were only regular files look
invalid. A background recent-workspaces pass could then delete the
workspace row, which in turn cascaded to item-specific state such as
editors. In the window edited-state restore test, reopening a
standalone regular file could therefore fail because the saved editor
row had been deleted by that cleanup path.

Fix this by treating a local workspace as valid whenever all of its
saved paths still exist, whether those paths are directories or
regular files. Apply the same rule to last-session workspace
restoration, and add regression coverage for both code paths.

diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs
--- a/crates/workspace/src/persistence.rs
+++ b/crates/workspace/src/persistence.rs
@@ -1783,19 +1783,13 @@ impl WorkspaceDb {
         }
     }
 
-    async fn all_paths_exist_with_a_directory(paths: &[PathBuf], fs: &dyn Fs) -> bool {
-        let mut any_dir = false;
+    async fn all_paths_exist(paths: &[PathBuf], fs: &dyn Fs) -> bool {
         for path in paths {
-            match fs.metadata(path).await.ok().flatten() {
-                None => return false,
-                Some(meta) => {
-                    if meta.is_dir {
-                        any_dir = true;
-                    }
-                }
+            if fs.metadata(path).await.ok().flatten().is_none() {
+                return false;
             }
         }
-        any_dir
+        true
     }
 
     // Returns the recent locations which are still valid on disk and deletes ones which no longer
@@ -1843,7 +1837,11 @@ impl WorkspaceDb {
             // If a local workspace points to WSL, this check will cause us to wait for the
             // WSL VM and file server to boot up. This can block for many seconds.
             // Supported scenarios use remote workspaces.
-            if !has_wsl_path && Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
+            //
+            // Local workspaces may legitimately contain only regular files (for example, a
+            // standalone file opened outside any worktree). Those should be preserved as
+            // long as all of their saved paths still exist.
+            if !has_wsl_path && Self::all_paths_exist(paths.paths(), fs).await {
                 result.push((id, SerializedWorkspaceLocation::Local, paths, timestamp));
             } else {
                 delete_tasks.push(self.delete_workspace_by_id(id));
@@ -1903,7 +1901,7 @@ impl WorkspaceDb {
                     window_id,
                 });
             } else {
-                if Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
+                if Self::all_paths_exist(paths.paths(), fs).await {
                     workspaces.push(SessionWorkspace {
                         workspace_id,
                         location: SerializedWorkspaceLocation::Local,
@@ -3425,6 +3423,91 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_recent_workspaces_on_disk_keeps_regular_file_only_workspace(
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let dir = tempfile::TempDir::with_prefix("regular-file-workspace").unwrap();
+        let regular_file = dir.path().join("a");
+
+        let fs = fs::FakeFs::new(cx.executor());
+        fs.insert_tree(dir.path(), json!({"a": "hey"})).await;
+
+        let db = WorkspaceDb::open_test_db("test_recent_workspaces_keeps_regular_file_only")
+            .await;
+        let workspace = SerializedWorkspace {
+            id: WorkspaceId(1),
+            paths: PathList::new(&[regular_file.clone()]),
+            location: SerializedWorkspaceLocation::Local,
+            center_group: Default::default(),
+            window_bounds: Default::default(),
+            display: Default::default(),
+            docks: Default::default(),
+            centered_layout: false,
+            breakpoints: Default::default(),
+            session_id: None,
+            window_id: Some(1),
+            user_toolchains: Default::default(),
+        };
+
+        db.save_workspace(workspace.clone()).await;
+
+        let recent = db.recent_workspaces_on_disk(fs.as_ref()).await.unwrap();
+        assert_eq!(
+            recent,
+            vec![(
+                workspace.id,
+                SerializedWorkspaceLocation::Local,
+                workspace.paths.clone(),
+                recent[0].3,
+            )]
+        );
+        assert_eq!(db.workspace_for_roots(&[regular_file]).unwrap().id, workspace.id);
+    }
+
+    #[gpui::test]
+    async fn test_last_session_workspace_locations_keeps_regular_file_only_workspace(
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let dir = tempfile::TempDir::with_prefix("regular-file-workspace-session").unwrap();
+        let regular_file = dir.path().join("a");
+
+        let fs = fs::FakeFs::new(cx.executor());
+        fs.insert_tree(dir.path(), json!({"a": "hey"})).await;
+
+        let db = WorkspaceDb::open_test_db("test_last_session_keeps_regular_file_only").await;
+        let workspace = SerializedWorkspace {
+            id: WorkspaceId(1),
+            paths: PathList::new(&[regular_file.clone()]),
+            location: SerializedWorkspaceLocation::Local,
+            center_group: Default::default(),
+            window_bounds: Default::default(),
+            display: Default::default(),
+            docks: Default::default(),
+            centered_layout: false,
+            breakpoints: Default::default(),
+            session_id: Some("one-session".to_owned()),
+            window_id: Some(7),
+            user_toolchains: Default::default(),
+        };
+
+        db.save_workspace(workspace.clone()).await;
+
+        let locations = db
+            .last_session_workspace_locations("one-session", None, fs.as_ref())
+            .await
+            .unwrap();
+        assert_eq!(
+            locations,
+            vec![SessionWorkspace {
+                workspace_id: workspace.id,
+                location: SerializedWorkspaceLocation::Local,
+                paths: workspace.paths,
+                window_id: Some(WindowId::from(7u64)),
+            }]
+        );
+    }
+
     #[gpui::test]
     async fn test_get_or_create_ssh_project() {
         let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
