From: Danny Milosavljevic 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;