From 46120c907fe463e4d0e2f677ddc6cef194e99fd4 Mon Sep 17 00:00:00 2001 From: Eric Holk Date: Wed, 25 Mar 2026 10:16:51 -0700 Subject: [PATCH] Sidebar and git cleanups (#52431) A few minor cleanups in sidebar.rs and some related areas. Release notes: - N/A --- crates/project/src/git_store.rs | 17 +++ crates/sidebar/src/sidebar.rs | 184 +++++++++++++++++--------------- 2 files changed, 114 insertions(+), 87 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 3a4653345d7a84e702e657e80a360eeae00385ab..346ebb1614e3b536d78765ce7ca90ad1e30f6bfc 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -3706,6 +3706,23 @@ impl RepositorySnapshot { } } + /// The main worktree is the original checkout that other worktrees were + /// created from. + /// + /// For example, if you had both `~/code/zed` and `~/code/worktrees/zed-2`, + /// then `~/code/zed` is the main worktree and `~/code/worktrees/zed-2` is a linked worktree. + pub fn is_main_worktree(&self) -> bool { + self.work_directory_abs_path == self.original_repo_abs_path + } + + /// Returns true if this repository is a linked worktree, that is, one that + /// was created from another worktree. + /// + /// This is by definition the opposite of [`Self::is_main_worktree`]. + pub fn is_linked_worktree(&self) -> bool { + !self.is_main_worktree() + } + pub fn linked_worktrees(&self) -> &[GitWorktree] { &self.linked_worktrees } diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index da78c634764fedf0643a7b225190ae5cb28f8e58..dfde1f7454178fdd383f5fbf5e3a7e65548d595d 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -190,21 +190,17 @@ fn fuzzy_match_positions(query: &str, candidate: &str) -> Option> { fn root_repository_snapshots( workspace: &Entity, cx: &App, -) -> Vec { +) -> impl Iterator { let path_list = workspace_path_list(workspace, cx); let project = workspace.read(cx).project().read(cx); - project - .repositories(cx) - .values() - .filter_map(|repo| { - let snapshot = repo.read(cx).snapshot(); - let is_root = path_list - .paths() - .iter() - .any(|p| p.as_path() == snapshot.work_directory_abs_path.as_ref()); - is_root.then_some(snapshot) - }) - .collect() + project.repositories(cx).values().filter_map(move |repo| { + let snapshot = repo.read(cx).snapshot(); + let is_root = path_list + .paths() + .iter() + .any(|p| p.as_path() == snapshot.work_directory_abs_path.as_ref()); + is_root.then_some(snapshot) + }) } fn workspace_path_list(workspace: &Entity, cx: &App) -> PathList { @@ -544,59 +540,6 @@ impl Sidebar { result } - fn all_thread_infos_for_workspace( - workspace: &Entity, - cx: &App, - ) -> Vec { - let Some(agent_panel) = workspace.read(cx).panel::(cx) else { - return Vec::new(); - }; - let agent_panel_ref = agent_panel.read(cx); - - agent_panel_ref - .parent_threads(cx) - .into_iter() - .map(|thread_view| { - let thread_view_ref = thread_view.read(cx); - let thread = thread_view_ref.thread.read(cx); - - let icon = thread_view_ref.agent_icon; - let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone(); - let title = thread - .title() - .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()); - let is_native = thread_view_ref.as_native_thread(cx).is_some(); - let is_title_generating = is_native && thread.has_provisional_title(); - let session_id = thread.session_id().clone(); - let is_background = agent_panel_ref.is_background_thread(&session_id); - - let status = if thread.is_waiting_for_confirmation() { - AgentThreadStatus::WaitingForConfirmation - } else if thread.had_error() { - AgentThreadStatus::Error - } else { - match thread.status() { - ThreadStatus::Generating => AgentThreadStatus::Running, - ThreadStatus::Idle => AgentThreadStatus::Completed, - } - }; - - let diff_stats = thread.action_log().read(cx).diff_stats(cx); - - ActiveThreadInfo { - session_id, - title, - status, - icon, - icon_from_external_svg, - is_background, - is_title_generating, - diff_stats, - } - }) - .collect() - } - /// When modifying this thread, aim for a single forward pass over workspaces /// and threads plus an O(T log T) sort. Avoid adding extra scans over the data. fn rebuild_contents(&mut self, cx: &App) { @@ -686,7 +629,7 @@ impl Sidebar { for (i, workspace) in workspaces.iter().enumerate() { for snapshot in root_repository_snapshots(workspace, cx) { - if snapshot.work_directory_abs_path == snapshot.original_repo_abs_path { + if snapshot.is_main_worktree() { main_repo_workspace .entry(snapshot.work_directory_abs_path.clone()) .or_insert(i); @@ -773,7 +716,7 @@ impl Sidebar { .is_some_and(|(main_idx, _)| *main_idx == ws_index) }); - let mut live_infos = Self::all_thread_infos_for_workspace(workspace, cx); + let mut live_infos: Vec<_> = all_thread_infos_for_workspace(workspace, cx).collect(); let mut threads: Vec = Vec::new(); let mut has_running_threads = false; @@ -831,7 +774,7 @@ impl Sidebar { let mut linked_worktree_queries: Vec<(PathList, SharedString, Arc)> = Vec::new(); for snapshot in root_repository_snapshots(workspace, cx) { - if snapshot.work_directory_abs_path != snapshot.original_repo_abs_path { + if snapshot.is_linked_worktree() { continue; } @@ -852,17 +795,16 @@ impl Sidebar { for (worktree_path_list, worktree_name, worktree_path) in &linked_worktree_queries { - let target_workspace = - match absorbed_workspace_by_path.get(worktree_path.as_ref()) { - Some(&idx) => { - live_infos.extend(Self::all_thread_infos_for_workspace( - &workspaces[idx], - cx, - )); - ThreadEntryWorkspace::Open(workspaces[idx].clone()) - } - None => ThreadEntryWorkspace::Closed(worktree_path_list.clone()), - }; + let target_workspace = match absorbed_workspace_by_path + .get(worktree_path.as_ref()) + { + Some(&idx) => { + live_infos + .extend(all_thread_infos_for_workspace(&workspaces[idx], cx)); + ThreadEntryWorkspace::Open(workspaces[idx].clone()) + } + None => ThreadEntryWorkspace::Closed(worktree_path_list.clone()), + }; let worktree_rows: Vec<_> = thread_store .read(cx) @@ -1721,7 +1663,7 @@ impl Sidebar { let mut known_worktree_paths: HashSet = HashSet::new(); for workspace in &workspaces { for snapshot in root_repository_snapshots(workspace, cx) { - if snapshot.work_directory_abs_path != snapshot.original_repo_abs_path { + if snapshot.is_linked_worktree() { continue; } for git_worktree in snapshot.linked_worktrees() { @@ -1740,12 +1682,10 @@ impl Sidebar { if path_list.paths().len() != 1 { continue; } - let should_prune = root_repository_snapshots(workspace, cx) - .iter() - .any(|snapshot| { - snapshot.work_directory_abs_path != snapshot.original_repo_abs_path - && !known_worktree_paths.contains(snapshot.work_directory_abs_path.as_ref()) - }); + let should_prune = root_repository_snapshots(workspace, cx).any(|snapshot| { + snapshot.is_linked_worktree() + && !known_worktree_paths.contains(snapshot.work_directory_abs_path.as_ref()) + }); if should_prune { to_remove.push(workspace.clone()); } @@ -3217,6 +3157,76 @@ impl Render for Sidebar { } } +fn all_thread_infos_for_workspace( + workspace: &Entity, + cx: &App, +) -> impl Iterator { + enum ThreadInfoIterator> { + Empty, + Threads(T), + } + + impl> Iterator for ThreadInfoIterator { + type Item = ActiveThreadInfo; + + fn next(&mut self) -> Option { + match self { + ThreadInfoIterator::Empty => None, + ThreadInfoIterator::Threads(threads) => threads.next(), + } + } + } + + let Some(agent_panel) = workspace.read(cx).panel::(cx) else { + return ThreadInfoIterator::Empty; + }; + let agent_panel = agent_panel.read(cx); + + let threads = agent_panel + .parent_threads(cx) + .into_iter() + .map(|thread_view| { + let thread_view_ref = thread_view.read(cx); + let thread = thread_view_ref.thread.read(cx); + + let icon = thread_view_ref.agent_icon; + let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone(); + let title = thread + .title() + .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()); + let is_native = thread_view_ref.as_native_thread(cx).is_some(); + let is_title_generating = is_native && thread.has_provisional_title(); + let session_id = thread.session_id().clone(); + let is_background = agent_panel.is_background_thread(&session_id); + + let status = if thread.is_waiting_for_confirmation() { + AgentThreadStatus::WaitingForConfirmation + } else if thread.had_error() { + AgentThreadStatus::Error + } else { + match thread.status() { + ThreadStatus::Generating => AgentThreadStatus::Running, + ThreadStatus::Idle => AgentThreadStatus::Completed, + } + }; + + let diff_stats = thread.action_log().read(cx).diff_stats(cx); + + ActiveThreadInfo { + session_id, + title, + status, + icon, + icon_from_external_svg, + is_background, + is_title_generating, + diff_stats, + } + }); + + ThreadInfoIterator::Threads(threads) +} + #[cfg(test)] mod tests { use super::*;