Show linked worktree names consistently in sidebar and titlebar (#51883)

Max Brunsfeld and Eric Holk created

This is a follow-up to https://github.com/zed-industries/zed/pull/51775,
making worktree names in the sidebar appear consistent with the
titlebar.

## Context

- [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

---------

Co-authored-by: Eric Holk <eric@zed.dev>

Change summary

crates/project/src/git_store.rs               | 26 ++++++++++++++++++
crates/project/src/project.rs                 |  2 
crates/project/tests/integration/git_store.rs | 30 ++++++++++++++++++++
crates/sidebar/src/sidebar.rs                 | 16 +++++-----
crates/title_bar/src/title_bar.rs             | 19 ++++++-------
5 files changed, 73 insertions(+), 20 deletions(-)

Detailed changes

crates/project/src/git_store.rs 🔗

@@ -6829,6 +6829,32 @@ pub fn worktrees_directory_for_repo(
     Ok(resolved)
 }
 
+/// Returns a short name for a linked worktree suitable for UI display
+///
+/// Uses the main worktree path to come up with a short name that disambiguates
+/// the linked worktree from the main worktree.
+pub fn linked_worktree_short_name(
+    main_worktree_path: &Path,
+    linked_worktree_path: &Path,
+) -> Option<SharedString> {
+    if main_worktree_path == linked_worktree_path {
+        return None;
+    }
+
+    let project_name = main_worktree_path.file_name()?.to_str()?;
+    let directory_name = linked_worktree_path.file_name()?.to_str()?;
+    let name = if directory_name != project_name {
+        directory_name.to_string()
+    } else {
+        linked_worktree_path
+            .parent()?
+            .file_name()?
+            .to_str()?
+            .to_string()
+    };
+    Some(name.into())
+}
+
 fn get_permalink_in_rust_registry_src(
     provider_registry: Arc<GitHostingProviderRegistry>,
     path: PathBuf,

crates/project/src/project.rs 🔗

@@ -47,7 +47,7 @@ pub use agent_server_store::{AgentId, AgentServerStore, AgentServersUpdated, Ext
 pub use git_store::{
     ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
     git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},
-    worktrees_directory_for_repo,
+    linked_worktree_short_name, worktrees_directory_for_repo,
 };
 pub use manifest_tree::ManifestTree;
 pub use project_search::{Search, SearchResults};

crates/project/tests/integration/git_store.rs 🔗

@@ -1539,7 +1539,7 @@ mod trust_tests {
 mod resolve_worktree_tests {
     use fs::FakeFs;
     use gpui::TestAppContext;
-    use project::git_store::resolve_git_worktree_to_main_repo;
+    use project::{git_store::resolve_git_worktree_to_main_repo, linked_worktree_short_name};
     use serde_json::json;
     use std::path::{Path, PathBuf};
 
@@ -1616,4 +1616,32 @@ mod resolve_worktree_tests {
             resolve_git_worktree_to_main_repo(fs.as_ref(), Path::new("/does-not-exist")).await;
         assert_eq!(result, None);
     }
+
+    #[test]
+    fn test_linked_worktree_short_name() {
+        let examples = [
+            (
+                "/home/bob/zed",
+                "/home/bob/worktrees/olivetti/zed",
+                Some("olivetti".into()),
+            ),
+            ("/home/bob/zed", "/home/bob/zed2", Some("zed2".into())),
+            (
+                "/home/bob/zed",
+                "/home/bob/worktrees/zed/selectric",
+                Some("selectric".into()),
+            ),
+            ("/home/bob/zed", "/home/bob/zed", None),
+        ];
+        for (main_worktree_path, linked_worktree_path, expected) in examples {
+            let short_name = linked_worktree_short_name(
+                Path::new(main_worktree_path),
+                Path::new(linked_worktree_path),
+            );
+            assert_eq!(
+                short_name, expected,
+                "short name for {linked_worktree_path:?}, linked worktree of {main_worktree_path:?}, should be {expected:?}"
+            );
+        }
+    }
 }

crates/sidebar/src/sidebar.rs 🔗

@@ -16,7 +16,7 @@ use gpui::{
 use menu::{
     Cancel, Confirm, SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious,
 };
-use project::{AgentId, Event as ProjectEvent};
+use project::{AgentId, Event as ProjectEvent, linked_worktree_short_name};
 use recent_projects::RecentProjects;
 use ui::utils::platform_title_bar_height;
 
@@ -780,16 +780,16 @@ impl Sidebar {
                         if snapshot.work_directory_abs_path != snapshot.original_repo_abs_path {
                             continue;
                         }
+
+                        let main_worktree_path = snapshot.original_repo_abs_path.clone();
+
                         for git_worktree in snapshot.linked_worktrees() {
-                            let name = git_worktree
-                                .path
-                                .file_name()
-                                .unwrap_or_default()
-                                .to_string_lossy()
-                                .to_string();
+                            let worktree_name =
+                                linked_worktree_short_name(&main_worktree_path, &git_worktree.path)
+                                    .unwrap_or_default();
                             linked_worktree_queries.push((
                                 PathList::new(std::slice::from_ref(&git_worktree.path)),
-                                name.into(),
+                                worktree_name,
                                 Arc::from(git_worktree.path.as_path()),
                             ));
                         }

crates/title_bar/src/title_bar.rs 🔗

@@ -14,6 +14,7 @@ pub use platform_title_bar::{
     self, DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, PlatformTitleBar,
     ShowNextWindowTab, ShowPreviousWindowTab,
 };
+use project::linked_worktree_short_name;
 
 #[cfg(not(target_os = "macos"))]
 use crate::application_menu::{
@@ -173,21 +174,19 @@ impl Render for TitleBar {
         let mut repository = None;
         let mut linked_worktree_name = None;
         if let Some(worktree) = self.effective_active_worktree(cx) {
+            repository = self.get_repository_for_worktree(&worktree, cx);
+            let worktree = worktree.read(cx);
             project_name = worktree
-                .read(cx)
                 .root_name()
                 .file_name()
                 .map(|name| SharedString::from(name.to_string()));
-            repository = self.get_repository_for_worktree(&worktree, cx);
             linked_worktree_name = repository.as_ref().and_then(|repo| {
-                let path = repo.read(cx).linked_worktree_path()?;
-                let directory_name = path.file_name()?.to_str()?;
-                let unique_worktree_name = if directory_name != project_name.as_ref()?.as_str() {
-                    directory_name.to_string()
-                } else {
-                    path.parent()?.file_name()?.to_str()?.to_string()
-                };
-                Some(SharedString::from(unique_worktree_name))
+                let repo = repo.read(cx);
+                linked_worktree_short_name(
+                    repo.original_repo_abs_path.as_ref(),
+                    repo.work_directory_abs_path.as_ref(),
+                )
+                .filter(|name| Some(name) != project_name.as_ref())
             });
         }