From 4a5826ba626393ec4fbfbc571ed10af51f70e331 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 7 Apr 2026 22:09:30 -0700 Subject: [PATCH] Make deserialization a bit more resilient to data changes (#53362) This PR makes sidebar deserialization enforce the invariants that the multiworkspace is supposed to enforce. Also, this PR makes it so that failing to deserialize the active workspace no longer totally fails to deserialize the multiworkspace. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- crates/project/src/project.rs | 1 + crates/workspace/src/multi_workspace.rs | 7 +- crates/workspace/src/workspace.rs | 96 ++++++++++++++++++++----- crates/worktree/src/worktree.rs | 2 +- 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b90972b3489c25f8a2bf10d7dbdb6d6cfe0c4c6c..4d1d4a5da809559a36829b1c171556e9ad4eccd8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -143,6 +143,7 @@ use worktree::{CreatedEntry, Snapshot, Traversal}; pub use worktree::{ Entry, EntryKind, FS_WATCH_LATENCY, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, + discover_root_repo_common_dir, }; use worktree_store::{WorktreeStore, WorktreeStoreEvent}; diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index d5308124a8d076914643fcb215c1acfa780c849c..8c0cf287bff8fd0a8ace8a484de031884b3b99f7 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -605,7 +605,12 @@ impl MultiWorkspace { } pub fn restore_project_group_keys(&mut self, keys: Vec) { - let mut restored = keys; + let mut restored: Vec = Vec::with_capacity(keys.len()); + for key in keys { + if !restored.contains(&key) { + restored.push(key); + } + } for existing_key in &self.project_group_keys { if !restored.contains(existing_key) { restored.push(existing_key.clone()); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 29684a282476b438e8e60b74e0fccf340cd17edf..ba25f8abda6dbb9cfe58fc8be542f593bbba183a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -8712,34 +8712,92 @@ pub async fn restore_multiworkspace( .. } = state; - let window_handle = if active_workspace.paths.is_empty() { + let workspace_result = if active_workspace.paths.is_empty() { cx.update(|cx| { open_workspace_by_id(active_workspace.workspace_id, app_state.clone(), None, cx) }) - .await? + .await } else { - let OpenResult { window, .. } = cx - .update(|cx| { - Workspace::new_local( - active_workspace.paths.paths().to_vec(), - app_state.clone(), - None, - None, - None, - OpenMode::Activate, - cx, - ) - }) - .await?; - window + cx.update(|cx| { + Workspace::new_local( + active_workspace.paths.paths().to_vec(), + app_state.clone(), + None, + None, + None, + OpenMode::Activate, + cx, + ) + }) + .await + .map(|result| result.window) + }; + + let window_handle = match workspace_result { + Ok(handle) => handle, + Err(err) => { + log::error!("Failed to restore active workspace: {err:#}"); + + // Try each project group's paths as a fallback. + let mut fallback_handle = None; + for key in &project_group_keys { + let key: ProjectGroupKey = key.clone().into(); + let paths = key.path_list().paths().to_vec(); + match cx + .update(|cx| { + Workspace::new_local( + paths, + app_state.clone(), + None, + None, + None, + OpenMode::Activate, + cx, + ) + }) + .await + { + Ok(OpenResult { window, .. }) => { + fallback_handle = Some(window); + break; + } + Err(fallback_err) => { + log::error!("Fallback project group also failed: {fallback_err:#}"); + } + } + } + + fallback_handle.ok_or(err)? + } }; if !project_group_keys.is_empty() { - let restored_keys: Vec = - project_group_keys.into_iter().map(Into::into).collect(); + let fs = app_state.fs.clone(); + + // Resolve linked worktree paths to their main repo paths so + // stale keys from previous sessions get normalized and deduped. + let mut resolved_keys: Vec = Vec::new(); + for key in project_group_keys.into_iter().map(ProjectGroupKey::from) { + let mut resolved_paths = Vec::new(); + for path in key.path_list().paths() { + if let Some(common_dir) = + project::discover_root_repo_common_dir(path, fs.as_ref()).await + { + let main_path = common_dir.parent().unwrap_or(&common_dir); + resolved_paths.push(main_path.to_path_buf()); + } else { + resolved_paths.push(path.to_path_buf()); + } + } + let resolved = ProjectGroupKey::new(key.host(), PathList::new(&resolved_paths)); + if !resolved_keys.contains(&resolved) { + resolved_keys.push(resolved); + } + } + window_handle .update(cx, |multi_workspace, _window, _cx| { - multi_workspace.restore_project_group_keys(restored_keys); + multi_workspace.restore_project_group_keys(resolved_keys); }) .ok(); } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 864858073db70c984e61dbf43bf98be44f6c1c58..1494ba5fce2e46bdc2d199324ee5021b19f99408 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -6109,7 +6109,7 @@ fn parse_gitfile(content: &str) -> anyhow::Result<&Path> { Ok(Path::new(path.trim())) } -async fn discover_root_repo_common_dir(root_abs_path: &Path, fs: &dyn Fs) -> Option> { +pub async fn discover_root_repo_common_dir(root_abs_path: &Path, fs: &dyn Fs) -> Option> { let root_dot_git = root_abs_path.join(DOT_GIT); if !fs.metadata(&root_dot_git).await.is_ok_and(|m| m.is_some()) { return None;