Allow user-defined worktree names in title bar and platform windows (#36713)

Warpten created

Closes #36637 

Release Notes:
- Adds the ability to specify a human-readable project name for each
worktree.


https://github.com/user-attachments/assets/ce980fa6-65cf-46d7-9343-d08c800914fd

Change summary

assets/settings/default.json             |  1 
crates/title_bar/src/title_bar.rs        | 28 +++++++++++++++++--------
crates/workspace/src/workspace.rs        | 17 +++++++++++++-
crates/worktree/src/worktree_settings.rs |  9 ++++++++
4 files changed, 44 insertions(+), 11 deletions(-)

Detailed changes

crates/title_bar/src/title_bar.rs 🔗

@@ -31,10 +31,10 @@ use gpui::{
 };
 use keymap_editor;
 use onboarding_banner::OnboardingBanner;
-use project::Project;
+use project::{Project, WorktreeSettings};
 use remote::RemoteConnectionOptions;
-use settings::Settings as _;
-use std::sync::Arc;
+use settings::{Settings, SettingsLocation};
+use std::{path::Path, sync::Arc};
 use theme::ActiveTheme;
 use title_bar_settings::TitleBarSettings;
 use ui::{
@@ -433,14 +433,24 @@ impl TitleBar {
     }
 
     pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
-        let name = {
-            let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
+        let name = self
+            .project
+            .read(cx)
+            .visible_worktrees(cx)
+            .map(|worktree| {
                 let worktree = worktree.read(cx);
-                worktree.root_name()
-            });
+                let settings_location = SettingsLocation {
+                    worktree_id: worktree.id(),
+                    path: Path::new(""),
+                };
 
-            names.next()
-        };
+                let settings = WorktreeSettings::get(Some(settings_location), cx);
+                match &settings.project_name {
+                    Some(name) => name.as_str(),
+                    None => worktree.root_name(),
+                }
+            })
+            .next();
         let is_project_selected = name.is_some();
         let name = if let Some(name) = name {
             util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)

crates/workspace/src/workspace.rs 🔗

@@ -72,6 +72,7 @@ pub use persistence::{
 use postage::stream::Stream;
 use project::{
     DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
+    WorktreeSettings,
     debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
     toolchain_store::ToolchainStoreEvent,
 };
@@ -79,7 +80,7 @@ use remote::{RemoteClientDelegate, RemoteConnectionOptions, remote_client::Conne
 use schemars::JsonSchema;
 use serde::Deserialize;
 use session::AppSession;
-use settings::{Settings, update_settings_file};
+use settings::{Settings, SettingsLocation, update_settings_file};
 use shared_screen::SharedScreen;
 use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
@@ -4376,7 +4377,19 @@ impl Workspace {
         let project = self.project().read(cx);
         let mut title = String::new();
 
-        for (i, name) in project.worktree_root_names(cx).enumerate() {
+        for (i, worktree) in project.worktrees(cx).enumerate() {
+            let name = {
+                let settings_location = SettingsLocation {
+                    worktree_id: worktree.read(cx).id(),
+                    path: Path::new(""),
+                };
+
+                let settings = WorktreeSettings::get(Some(settings_location), cx);
+                match &settings.project_name {
+                    Some(name) => name.as_str(),
+                    None => worktree.read(cx).root_name(),
+                }
+            };
             if i > 0 {
                 title.push_str(", ");
             }

crates/worktree/src/worktree_settings.rs 🔗

@@ -9,6 +9,7 @@ use util::paths::PathMatcher;
 
 #[derive(Clone, PartialEq, Eq)]
 pub struct WorktreeSettings {
+    pub project_name: Option<String>,
     pub file_scan_inclusions: PathMatcher,
     pub file_scan_exclusions: PathMatcher,
     pub private_files: PathMatcher,
@@ -34,6 +35,13 @@ impl WorktreeSettings {
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
 #[settings_key(None)]
 pub struct WorktreeSettingsContent {
+    /// The displayed name of this project. If not set, the root directory name
+    /// will be displayed.
+    ///
+    /// Default: none
+    #[serde(default)]
+    pub project_name: Option<String>,
+
     /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
     /// `file_scan_inclusions`.
     ///
@@ -93,6 +101,7 @@ impl Settings for WorktreeSettings {
                 &parsed_file_scan_inclusions,
                 "file_scan_inclusions",
             )?,
+            project_name: result.project_name,
         })
     }