Improve readability of files in the git changes panel (#41857)

Aaron Saunders created

Closes _unknown_

<img width="1212" height="463" alt="image"
src="https://github.com/user-attachments/assets/ec00fcf0-7eb9-4291-b1e2-66e014dc30ac"
/>


This PR places the file_name before the file_path so that when the panel
is slim it is still usable, mirrors the behaviour of the file picker
(cmd+P)

Release Notes:
-  Improved readability of files in the git changes panel

Change summary

assets/settings/default.json                    |  5 
crates/git_ui/src/git_panel.rs                  | 81 ++++++++++++++----
crates/project/src/project_settings.rs          | 21 ++++
crates/settings/src/settings_content/project.rs | 26 ++++++
crates/settings_ui/src/page_data.rs             | 13 +++
crates/settings_ui/src/settings_ui.rs           |  1 
6 files changed, 127 insertions(+), 20 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1316,7 +1316,10 @@
     //    "hunk_style": "staged_hollow"
     // 2. Show unstaged hunks hollow and staged hunks filled:
     //    "hunk_style": "unstaged_hollow"
-    "hunk_style": "staged_hollow"
+    "hunk_style": "staged_hollow",
+    // Should the name or path be displayed first in the git view.
+    // "path_style": "file_name_first" or "file_path_first"
+    "path_style": "file_name_first"
   },
   // The list of custom Git hosting providers.
   "git_hosting_providers": [

crates/git_ui/src/git_panel.rs 🔗

@@ -51,6 +51,7 @@ use panel::{
 use project::{
     Fs, Project, ProjectPath,
     git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
+    project_settings::{GitPathStyle, ProjectSettings},
 };
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore, StatusStyle};
@@ -3954,6 +3955,7 @@ impl GitPanel {
         cx: &Context<Self>,
     ) -> AnyElement {
         let path_style = self.project.read(cx).path_style(cx);
+        let git_path_style = ProjectSettings::get_global(cx).git.path_style;
         let display_name = entry.display_name(path_style);
 
         let selected = self.selected_entry == Some(ix);
@@ -4053,7 +4055,6 @@ impl GitPanel {
         } else {
             cx.theme().colors().ghost_element_active
         };
-
         h_flex()
             .id(id)
             .h(self.list_item_height())
@@ -4151,28 +4152,70 @@ impl GitPanel {
                 h_flex()
                     .items_center()
                     .flex_1()
-                    // .overflow_hidden()
-                    .when_some(entry.parent_dir(path_style), |this, parent| {
-                        if !parent.is_empty() {
-                            this.child(
-                                self.entry_label(
-                                    format!("{parent}{}", path_style.separator()),
-                                    path_color,
-                                )
-                                .when(status.is_deleted(), |this| this.strikethrough()),
-                            )
-                        } else {
-                            this
-                        }
-                    })
-                    .child(
-                        self.entry_label(display_name, label_color)
-                            .when(status.is_deleted(), |this| this.strikethrough()),
-                    ),
+                    .child(h_flex().items_center().flex_1().map(|this| {
+                        self.path_formatted(
+                            this,
+                            entry.parent_dir(path_style),
+                            path_color,
+                            display_name,
+                            label_color,
+                            path_style,
+                            git_path_style,
+                            status.is_deleted(),
+                        )
+                    })),
             )
             .into_any_element()
     }
 
+    fn path_formatted(
+        &self,
+        parent: Div,
+        directory: Option<String>,
+        path_color: Color,
+        file_name: String,
+        label_color: Color,
+        path_style: PathStyle,
+        git_path_style: GitPathStyle,
+        strikethrough: bool,
+    ) -> Div {
+        parent
+            .when(git_path_style == GitPathStyle::FileNameFirst, |this| {
+                this.child(
+                    self.entry_label(
+                        match directory.as_ref().is_none_or(|d| d.is_empty()) {
+                            true => file_name.clone(),
+                            false => format!("{file_name} "),
+                        },
+                        label_color,
+                    )
+                    .when(strikethrough, Label::strikethrough),
+                )
+            })
+            .when_some(directory, |this, dir| {
+                match (
+                    !dir.is_empty(),
+                    git_path_style == GitPathStyle::FileNameFirst,
+                ) {
+                    (true, true) => this.child(
+                        self.entry_label(dir, path_color)
+                            .when(strikethrough, Label::strikethrough),
+                    ),
+                    (true, false) => this.child(
+                        self.entry_label(format!("{dir}{}", path_style.separator()), path_color)
+                            .when(strikethrough, Label::strikethrough),
+                    ),
+                    _ => this,
+                }
+            })
+            .when(git_path_style == GitPathStyle::FilePathFirst, |this| {
+                this.child(
+                    self.entry_label(file_name, label_color)
+                        .when(strikethrough, Label::strikethrough),
+                )
+            })
+    }
+
     fn has_write_access(&self, cx: &App) -> bool {
         !self.project.read(cx).is_read_only(cx)
     }

crates/project/src/project_settings.rs 🔗

@@ -348,6 +348,26 @@ pub struct GitSettings {
     ///
     /// Default: staged_hollow
     pub hunk_style: settings::GitHunkStyleSetting,
+    /// How file paths are displayed in the git gutter.
+    ///
+    /// Default: file_name_first
+    pub path_style: GitPathStyle,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Default)]
+pub enum GitPathStyle {
+    #[default]
+    FileNameFirst,
+    FilePathFirst,
+}
+
+impl From<settings::GitPathStyle> for GitPathStyle {
+    fn from(style: settings::GitPathStyle) -> Self {
+        match style {
+            settings::GitPathStyle::FileNameFirst => GitPathStyle::FileNameFirst,
+            settings::GitPathStyle::FilePathFirst => GitPathStyle::FilePathFirst,
+        }
+    }
 }
 
 #[derive(Clone, Copy, Debug)]
@@ -501,6 +521,7 @@ impl Settings for ProjectSettings {
                 }
             },
             hunk_style: git.hunk_style.unwrap(),
+            path_style: git.path_style.unwrap().into(),
         };
         Self {
             context_servers: project

crates/settings/src/settings_content/project.rs 🔗

@@ -311,6 +311,10 @@ pub struct GitSettings {
     ///
     /// Default: staged_hollow
     pub hunk_style: Option<GitHunkStyleSetting>,
+    /// How file paths are displayed in the git gutter.
+    ///
+    /// Default: file_name_first
+    pub path_style: Option<GitPathStyle>,
 }
 
 #[derive(
@@ -406,6 +410,28 @@ pub enum GitHunkStyleSetting {
     UnstagedHollow,
 }
 
+#[derive(
+    Copy,
+    Clone,
+    Debug,
+    PartialEq,
+    Default,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    strum::VariantArray,
+    strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum GitPathStyle {
+    /// Show file name first, then path
+    #[default]
+    FileNameFirst,
+    /// Show full path first
+    FilePathFirst,
+}
+
 #[skip_serializing_none]
 #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct DiagnosticsSettingsContent {

crates/settings_ui/src/page_data.rs 🔗

@@ -5494,6 +5494,19 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Path Style",
+                    description: "Should the name or path be displayed first in the git view.",
+                    field: Box::new(SettingField {
+                        json_path: Some("git.path_style"),
+                        pick: |settings_content| settings_content.git.as_ref()?.path_style.as_ref(),
+                        write: |settings_content, value| {
+                            settings_content.git.get_or_insert_default().path_style = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
             ],
         },
         SettingsPage {

crates/settings_ui/src/settings_ui.rs 🔗

@@ -452,6 +452,7 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::DockPosition>(render_dropdown)
         .add_basic_renderer::<settings::GitGutterSetting>(render_dropdown)
         .add_basic_renderer::<settings::GitHunkStyleSetting>(render_dropdown)
+        .add_basic_renderer::<settings::GitPathStyle>(render_dropdown)
         .add_basic_renderer::<settings::DiagnosticSeverityContent>(render_dropdown)
         .add_basic_renderer::<settings::SeedQuerySetting>(render_dropdown)
         .add_basic_renderer::<settings::DoubleClickInMultibuffer>(render_dropdown)