project_panel: Add Git status indicators (#50216)

Davide Scaccia and Smit Barmase created

This PR adds Git status badges next to file names in the Project Panel,
following my older PR #49802
These are enabled by having "git_status" true.

Screenshot
<img width="343" height="320" alt="image"
src="https://github.com/user-attachments/assets/b2c208bf-5027-4947-a5ee-eeb74fadb02b"
/>

I'd love to hear feedback about any of this :)
Especially feedback on these:

- File name colour is determined only by Git status, the diagnostic
badges remain separate. Should diagnostics also affect the filename
colour?
- (Unstaged) Modified files and staged modifications share the same
colour, in vscode staged modifications use a brownish colour by default
which I could not find the colours. I think differentiating them is
definetely something to add.

Release Notes

- Added git status indicators in Project Panel. It can be enabled by
setting `git_status_indicator` to `true` in `project_panel` settings.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

assets/settings/default.json                       |  2 
crates/project_panel/src/project_panel.rs          | 53 ++++++++++++++-
crates/project_panel/src/project_panel_settings.rs |  2 
crates/settings/src/vscode_import.rs               |  1 
crates/settings_content/src/workspace.rs           |  6 +
crates/settings_ui/src/page_data.rs                | 26 +++++++
6 files changed, 83 insertions(+), 7 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -782,6 +782,8 @@
     "sort_mode": "directories_first",
     // Whether to show error and warning count badges next to file names in the project panel.
     "diagnostic_badges": false,
+    // Whether to show the git status indicator next to file names in the project panel.
+    "git_status_indicator": false,
     // Whether to enable drag-and-drop operations in the project panel.
     "drag_and_drop": true,
     // Whether to hide the root entry when only one folder is open in the window;

crates/project_panel/src/project_panel.rs 🔗

@@ -61,9 +61,9 @@ use std::{
 use theme::ThemeSettings;
 use ui::{
     Color, ContextMenu, ContextMenuEntry, DecoratedIcon, Divider, Icon, IconDecoration,
-    IconDecorationKind, IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize,
-    ListItem, ListItemSpacing, ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip,
-    WithScrollbar, prelude::*, v_flex,
+    IconDecorationKind, IndentGuideColors, IndentGuideLayout, Indicator, KeyBinding, Label,
+    LabelSize, ListItem, ListItemSpacing, ScrollAxes, ScrollableHandle, Scrollbars,
+    StickyCandidate, Tooltip, WithScrollbar, prelude::*, v_flex,
 };
 use util::{
     ResultExt, TakeUntilExt, TryFutureExt, maybe,
@@ -5352,6 +5352,10 @@ impl ProjectPanel {
                 false
             }
         };
+        let git_indicator = settings
+            .git_status_indicator
+            .then(|| git_status_indicator(details.git_status))
+            .flatten();
 
         let id: ElementId = if is_sticky {
             SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
@@ -5696,7 +5700,9 @@ impl ProjectPanel {
                     })
                     .selectable(false)
                     .when(
-                        canonical_path.is_some() || diagnostic_count.is_some(),
+                        canonical_path.is_some()
+                            || diagnostic_count.is_some()
+                            || git_indicator.is_some(),
                         |this| {
                             let symlink_element = canonical_path.map(|path| {
                                 div()
@@ -5739,6 +5745,20 @@ impl ProjectPanel {
                                             },
                                         )
                                     })
+                                    .when_some(git_indicator, |this, (label, color)| {
+                                        let git_indicator = if kind.is_dir() {
+                                            Indicator::dot()
+                                                .color(Color::Custom(color.color(cx).opacity(0.5)))
+                                                .into_any_element()
+                                        } else {
+                                            Label::new(label)
+                                                .size(LabelSize::Small)
+                                                .color(color)
+                                                .into_any_element()
+                                        };
+
+                                        this.child(git_indicator)
+                                    })
                                     .when_some(symlink_element, |this, el| this.child(el))
                                     .into_any_element(),
                             )
@@ -7286,5 +7306,30 @@ pub fn par_sort_worktree_entries_with_mode(
     entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode));
 }
 
+fn git_status_indicator(git_status: GitSummary) -> Option<(&'static str, Color)> {
+    if git_status.conflict > 0 {
+        return Some(("!", Color::Conflict));
+    }
+    if git_status.untracked > 0 {
+        return Some(("U", Color::Created));
+    }
+    if git_status.worktree.deleted > 0 {
+        return Some(("D", Color::Deleted));
+    }
+    if git_status.worktree.modified > 0 {
+        return Some(("M", Color::Warning));
+    }
+    if git_status.index.deleted > 0 {
+        return Some(("D", Color::Deleted));
+    }
+    if git_status.index.modified > 0 {
+        return Some(("M", Color::Modified));
+    }
+    if git_status.index.added > 0 {
+        return Some(("A", Color::Created));
+    }
+    None
+}
+
 #[cfg(test)]
 mod project_panel_tests;

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -36,6 +36,7 @@ pub struct ProjectPanelSettings {
     pub auto_open: AutoOpenSettings,
     pub sort_mode: ProjectPanelSortMode,
     pub diagnostic_badges: bool,
+    pub git_status_indicator: bool,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -137,6 +138,7 @@ impl Settings for ProjectPanelSettings {
             },
             sort_mode: project_panel.sort_mode.unwrap(),
             diagnostic_badges: project_panel.diagnostic_badges.unwrap(),
+            git_status_indicator: project_panel.git_status_indicator.unwrap(),
         }
     }
 }

crates/settings/src/vscode_import.rs 🔗

@@ -808,6 +808,7 @@ impl VsCodeSettings {
             sticky_scroll: None,
             auto_open: None,
             diagnostic_badges: None,
+            git_status_indicator: None,
         };
 
         if let (Some(false), Some(false)) = (

crates/settings_content/src/workspace.rs 🔗

@@ -741,8 +741,12 @@ pub struct ProjectPanelSettingsContent {
     pub sort_mode: Option<ProjectPanelSortMode>,
     /// Whether to show error and warning count badges next to file names in the project panel.
     ///
-    /// Default: true
+    /// Default: false
     pub diagnostic_badges: Option<bool>,
+    /// Whether to show a git status indicator next to file names in the project panel.
+    ///
+    /// Default: false
+    pub git_status_indicator: Option<bool>,
 }
 
 #[derive(

crates/settings_ui/src/page_data.rs 🔗

@@ -4349,7 +4349,7 @@ fn window_and_layout_page() -> SettingsPage {
 }
 
 fn panels_page() -> SettingsPage {
-    fn project_panel_section() -> [SettingsPageItem; 23] {
+    fn project_panel_section() -> [SettingsPageItem; 24] {
         [
             SettingsPageItem::SectionHeader("Project Panel"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -4697,6 +4697,28 @@ fn panels_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Git Status Indicator",
+                description: "Show a git status indicator next to file names in the project panel.",
+                field: Box::new(SettingField {
+                    json_path: Some("project_panel.git_status_indicator"),
+                    pick: |settings_content| {
+                        settings_content
+                            .project_panel
+                            .as_ref()?
+                            .git_status_indicator
+                            .as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content
+                            .project_panel
+                            .get_or_insert_default()
+                            .git_status_indicator = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
             SettingsPageItem::SettingItem(SettingItem {
                 title: "Sticky Scroll",
                 description: "Whether to stick parent directories at top of the project panel.",
@@ -4771,7 +4793,7 @@ fn panels_page() -> SettingsPage {
                 title: "Hide Root",
                 description: "Whether to hide the root entry when only one folder is open in the window.",
                 field: Box::new(SettingField {
-                    json_path: Some("project_panel.drag_and_drop"),
+                    json_path: Some("project_panel.hide_root"),
                     pick: |settings_content| {
                         settings_content.project_panel.as_ref()?.hide_root.as_ref()
                     },