From a87af7ea1ca3614c513d7f1c9ed6a09598802977 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 10 Apr 2026 23:01:09 -0400 Subject: [PATCH] Always remove linked worktree workspaces before archive cleanup When archiving a thread's last reference to a linked worktree, the worktree workspace must be removed from the MultiWorkspace before the background cleanup task runs git worktree remove. Previously, only the workspace_to_remove (found via exact PathList match on the thread's folder_paths) was removed. This missed cases where the workspace's root paths diverged from the thread's folder_paths (e.g. after folders were added/removed from the workspace). Now we also scan roots_to_archive for any linked worktree workspaces that contain the worktree being archived and include them in the removal set. This ensures all editors are dropped, releasing their Entity references (held through File structs in buffers), so wait_for_worktree_release completes and git worktree remove can proceed. --- crates/sidebar/src/sidebar.rs | 47 ++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 4d88ddeffdd6625768dd0207176c0984e9833a29..6f6e3e51e637ce7b933c74d031e676baed5c90dd 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -2831,17 +2831,56 @@ impl Sidebar { (group_key.path_list() != folder_paths).then_some(workspace) }); - if let Some(workspace_to_remove) = workspace_to_remove { + // Also find workspaces for root plans that aren't covered by + // workspace_to_remove. This handles edge cases where the thread's + // folder_paths don't exactly match any workspace's root paths + // (e.g. after a folder was added/removed), but a linked worktree + // workspace does contain the worktree that needs to be removed + // from disk. Without removing these workspaces first, their open + // editors hold Entity references (through File structs + // in buffers), preventing the worktree entity from being released + // and blocking git worktree removal indefinitely. + let mut workspaces_to_remove: Vec> = Vec::new(); + if let Some(ws) = workspace_to_remove { + workspaces_to_remove.push(ws); + } + if let Some(multi_workspace) = self.multi_workspace.upgrade() { + let mw = multi_workspace.read(cx); + for root in &roots_to_archive { + for workspace in mw.workspaces() { + if workspaces_to_remove.contains(workspace) { + continue; + } + let has_worktree = workspace + .read(cx) + .project() + .read(cx) + .visible_worktrees(cx) + .any(|wt| wt.read(cx).abs_path().as_ref() == root.root_path.as_path()); + if !has_worktree { + continue; + } + let group_key = workspace.read(cx).project_group_key(cx); + let root_paths = PathList::new(&workspace.read(cx).root_paths(cx)); + if root_paths != *group_key.path_list() { + workspaces_to_remove.push(workspace.clone()); + } + } + } + } + + if !workspaces_to_remove.is_empty() { let multi_workspace = self.multi_workspace.upgrade().unwrap(); let session_id = session_id.clone(); // For the workspace-removal fallback, use the neighbor's workspace - // paths if available, otherwise fall back to the project group key. + // paths if available, otherwise fall back to the project group key + // of the first workspace being removed. let fallback_paths = neighbor .as_ref() .map(|(_, paths)| paths.clone()) .unwrap_or_else(|| { - workspace_to_remove + workspaces_to_remove[0] .read(cx) .project_group_key(cx) .path_list() @@ -2850,7 +2889,7 @@ impl Sidebar { let remove_task = multi_workspace.update(cx, |mw, cx| { mw.remove( - [workspace_to_remove], + workspaces_to_remove, move |this, window, cx| { this.find_or_create_local_workspace(fallback_paths, window, cx) },