Sidebar and git cleanups (#52431)

Eric Holk created

A few minor cleanups in sidebar.rs and some related areas.

Release notes:

- N/A

Change summary

crates/project/src/git_store.rs |  17 +++
crates/sidebar/src/sidebar.rs   | 184 ++++++++++++++++++----------------
2 files changed, 114 insertions(+), 87 deletions(-)

Detailed changes

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
     }

crates/sidebar/src/sidebar.rs 🔗

@@ -190,21 +190,17 @@ fn fuzzy_match_positions(query: &str, candidate: &str) -> Option<Vec<usize>> {
 fn root_repository_snapshots(
     workspace: &Entity<Workspace>,
     cx: &App,
-) -> Vec<project::git_store::RepositorySnapshot> {
+) -> impl Iterator<Item = project::git_store::RepositorySnapshot> {
     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<Workspace>, cx: &App) -> PathList {
@@ -544,59 +540,6 @@ impl Sidebar {
         result
     }
 
-    fn all_thread_infos_for_workspace(
-        workspace: &Entity<Workspace>,
-        cx: &App,
-    ) -> Vec<ActiveThreadInfo> {
-        let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(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<ThreadEntry> = Vec::new();
             let mut has_running_threads = false;
@@ -831,7 +774,7 @@ impl Sidebar {
                     let mut linked_worktree_queries: Vec<(PathList, SharedString, Arc<Path>)> =
                         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<std::path::PathBuf> = 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<Workspace>,
+    cx: &App,
+) -> impl Iterator<Item = ActiveThreadInfo> {
+    enum ThreadInfoIterator<T: Iterator<Item = ActiveThreadInfo>> {
+        Empty,
+        Threads(T),
+    }
+
+    impl<T: Iterator<Item = ActiveThreadInfo>> Iterator for ThreadInfoIterator<T> {
+        type Item = ActiveThreadInfo;
+
+        fn next(&mut self) -> Option<Self::Item> {
+            match self {
+                ThreadInfoIterator::Empty => None,
+                ThreadInfoIterator::Threads(threads) => threads.next(),
+            }
+        }
+    }
+
+    let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(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::*;