git_ui: Add Git Panel settings (#23132)

Nate Butler created

This PR adds settings for the Git Panel.

The new settings include:

| Setting | Description | Default |
|---------|-------------|---------|
| `git_panel.button` | Toggle visibility of the Git Panel button in the
status bar | `true` |
| `git_panel.dock` | Choose where to dock the Git Panel | `"left"` |
| `git_panel.default_width` | Set the default width of the Git Panel in
pixels | `360` |
| `git_panel.status_style` | Select how Git status is displayed |
`"icon"` |
| `git_panel.scrollbar.show` | Configure scrollbar behavior | Inherits
from editor settings |

Example usage:

```json
"git_panel": {
  "button": true,
  "dock": "left",
  "default_width": 360,
  "status_style": "icon",
  "scrollbar": {
    "show": "auto"
  }
}
```

Release Notes:

- N/A

Change summary

assets/settings/default.json            | 12 +++
crates/git_ui/src/git_panel.rs          | 99 +++++++++++++++++++-------
crates/git_ui/src/git_panel_settings.rs | 83 ++++++++++++++++++++++
crates/git_ui/src/git_ui.rs             |  4 
crates/git_ui/src/settings.rs           | 41 -----------
5 files changed, 166 insertions(+), 73 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -503,7 +503,17 @@
     // Where to the git panel. Can be 'left' or 'right'.
     "dock": "left",
     // Default width of the git panel.
-    "default_width": 360
+    "default_width": 360,
+    // Style of the git status indicator in the panel.
+    //
+    // Default: icon
+    "status_style": "icon",
+    "scrollbar": {
+      // When to show the scrollbar in the git panel.
+      //
+      // Default: inherits editor scrollbar settings
+      "show": null
+    }
   },
   "message_editor": {
     // Whether to automatically replace emoji shortcodes with emoji characters.

crates/git_ui/src/git_panel.rs 🔗

@@ -1,11 +1,13 @@
+use crate::git_panel_settings::StatusStyle;
 use crate::{first_repository_in_project, first_worktree_repository};
 use crate::{
-    git_status_icon, settings::GitPanelSettings, CommitAllChanges, CommitChanges, GitState,
-    GitViewMode, RevertAll, StageAll, ToggleStaged, UnstageAll,
+    git_panel_settings::GitPanelSettings, git_status_icon, CommitAllChanges, CommitChanges,
+    GitState, GitViewMode, RevertAll, StageAll, ToggleStaged, UnstageAll,
 };
 use anyhow::{Context as _, Result};
 use db::kvp::KEY_VALUE_STORE;
-use editor::Editor;
+use editor::scroll::ScrollbarAutoHide;
+use editor::{Editor, EditorSettings, ShowScrollbar};
 use git::repository::{GitFileStatus, RepoPath};
 use git::status::GitStatusPair;
 use gpui::*;
@@ -313,7 +315,7 @@ impl GitPanel {
                 scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
                 scroll_handle,
                 selected_entry: None,
-                show_scrollbar: !Self::should_autohide_scrollbar(cx),
+                show_scrollbar: false,
                 hide_scrollbar_task: None,
                 rebuild_requested,
                 commit_editor,
@@ -322,6 +324,7 @@ impl GitPanel {
                 project,
             };
             git_panel.schedule_update();
+            git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
             git_panel
         });
 
@@ -376,19 +379,38 @@ impl GitPanel {
         }
     }
 
-    fn should_show_scrollbar(_cx: &AppContext) -> bool {
-        // TODO: plug into settings
-        true
+    fn show_scrollbar(&self, cx: &mut ViewContext<Self>) -> ShowScrollbar {
+        GitPanelSettings::get_global(cx)
+            .scrollbar
+            .show
+            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
     }
 
-    fn should_autohide_scrollbar(_cx: &AppContext) -> bool {
-        // TODO: plug into settings
-        true
+    fn should_show_scrollbar(&self, cx: &mut ViewContext<Self>) -> bool {
+        let show = self.show_scrollbar(cx);
+        match show {
+            ShowScrollbar::Auto => true,
+            ShowScrollbar::System => true,
+            ShowScrollbar::Always => true,
+            ShowScrollbar::Never => false,
+        }
+    }
+
+    fn should_autohide_scrollbar(&self, cx: &mut ViewContext<Self>) -> bool {
+        let show = self.show_scrollbar(cx);
+        match show {
+            ShowScrollbar::Auto => true,
+            ShowScrollbar::System => cx
+                .try_global::<ScrollbarAutoHide>()
+                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
+            ShowScrollbar::Always => false,
+            ShowScrollbar::Never => true,
+        }
     }
 
     fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
         const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
-        if !Self::should_autohide_scrollbar(cx) {
+        if !self.should_autohide_scrollbar(cx) {
             return;
         }
         self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
@@ -960,15 +982,26 @@ impl GitPanel {
     }
 
     fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
-        if !Self::should_show_scrollbar(cx)
+        let scroll_bar_style = self.show_scrollbar(cx);
+        let show_container = matches!(scroll_bar_style, ShowScrollbar::Always);
+
+        if !self.should_show_scrollbar(cx)
             || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
         {
             return None;
         }
+
         Some(
             div()
+                .id("git-panel-vertical-scroll")
                 .occlude()
-                .id("project-panel-vertical-scroll")
+                .flex_none()
+                .h_full()
+                .cursor_default()
+                .when(show_container, |this| this.pl_1().px_1p5())
+                .when(!show_container, |this| {
+                    this.absolute().right_1().top_1().bottom_1().w(px(12.))
+                })
                 .on_mouse_move(cx.listener(|_, _, cx| {
                     cx.notify();
                     cx.stop_propagation()
@@ -995,13 +1028,6 @@ impl GitPanel {
                 .on_scroll_wheel(cx.listener(|_, _, cx| {
                     cx.notify();
                 }))
-                .h_full()
-                .absolute()
-                .right_1()
-                .top_1()
-                .bottom_1()
-                .w(px(12.))
-                .cursor_default()
                 .children(Scrollbar::vertical(
                     // percentage as f32..end_offset as f32,
                     self.scrollbar_state.clone(),
@@ -1042,9 +1068,26 @@ impl GitPanel {
         let state = self.git_state.clone();
         let repo_path = entry_details.repo_path.clone();
         let selected = self.selected_entry == Some(ix);
-
+        let status_style = GitPanelSettings::get_global(cx).status_style;
         // TODO revisit, maybe use a different status here?
         let status = entry_details.status.combined();
+
+        let mut label_color = cx.theme().colors().text;
+        if status_style == StatusStyle::LabelColor {
+            label_color = match status {
+                GitFileStatus::Added => cx.theme().status().created,
+                GitFileStatus::Modified => cx.theme().status().modified,
+                GitFileStatus::Conflict => cx.theme().status().conflict,
+                GitFileStatus::Deleted => cx.theme().colors().text_disabled,
+                // TODO: Should we even have this here?
+                GitFileStatus::Untracked => cx.theme().colors().text_placeholder,
+            }
+        }
+
+        let path_color = matches!(status, GitFileStatus::Deleted)
+            .then_some(cx.theme().colors().text_disabled)
+            .unwrap_or(cx.theme().colors().text_muted);
+
         let entry_id = ElementId::Name(format!("entry_{}", entry_details.display_name).into());
         let checkbox_id =
             ElementId::Name(format!("checkbox_{}", entry_details.display_name).into());
@@ -1125,21 +1168,19 @@ impl GitPanel {
                     }
                 }),
             )
-            .child(git_status_icon(status))
+            .when(status_style == StatusStyle::Icon, |this| {
+                this.child(git_status_icon(status))
+            })
             .child(
                 h_flex()
-                    .when(status == GitFileStatus::Deleted, |this| {
-                        this.text_color(cx.theme().colors().text_disabled)
-                            .line_through()
-                    })
+                    .text_color(label_color)
+                    .when(status == GitFileStatus::Deleted, |this| this.line_through())
                     .when_some(repo_path.parent(), |this, parent| {
                         let parent_str = parent.to_string_lossy();
                         if !parent_str.is_empty() {
                             this.child(
                                 div()
-                                    .when(status != GitFileStatus::Deleted, |this| {
-                                        this.text_color(cx.theme().colors().text_muted)
-                                    })
+                                    .text_color(path_color)
                                     .child(format!("{}/", parent_str)),
                             )
                         } else {

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -0,0 +1,83 @@
+use editor::ShowScrollbar;
+use gpui::Pixels;
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources};
+use workspace::dock::DockPosition;
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ScrollbarSettingsContent {
+    /// When to show the scrollbar in the git panel.
+    ///
+    /// Default: inherits editor scrollbar settings
+    pub show: Option<Option<ShowScrollbar>>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ScrollbarSettings {
+    pub show: Option<ShowScrollbar>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+// Style of the git status indicator in the panel.
+//
+// Default: icon
+pub enum StatusStyleContent {
+    Icon,
+    LabelColor,
+}
+
+#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum StatusStyle {
+    #[default]
+    Icon,
+    LabelColor,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct GitPanelSettingsContent {
+    /// Whether to show the panel button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    /// Where to dock the panel.
+    ///
+    /// Default: left
+    pub dock: Option<DockPosition>,
+    /// Default width of the panel in pixels.
+    ///
+    /// Default: 360
+    pub default_width: Option<f32>,
+    /// How entry statuses are displayed.
+    ///
+    /// Default: icon
+    pub status_style: Option<StatusStyle>,
+    /// How and when the scrollbar should be displayed.
+    ///
+    /// Default: inherits editor scrollbar settings
+    pub scrollbar: Option<ScrollbarSettings>,
+}
+
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
+pub struct GitPanelSettings {
+    pub button: bool,
+    pub dock: DockPosition,
+    pub default_width: Pixels,
+    pub status_style: StatusStyle,
+    pub scrollbar: ScrollbarSettings,
+}
+
+impl Settings for GitPanelSettings {
+    const KEY: Option<&'static str> = Some("git_panel");
+
+    type FileContent = GitPanelSettingsContent;
+
+    fn load(
+        sources: SettingsSources<Self::FileContent>,
+        _: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        sources.json_merge()
+    }
+}

crates/git_ui/src/git_ui.rs 🔗

@@ -2,9 +2,9 @@ use ::settings::Settings;
 use collections::HashMap;
 use futures::{future::FusedFuture, select, FutureExt};
 use git::repository::{GitFileStatus, GitRepository, RepoPath};
+use git_panel_settings::GitPanelSettings;
 use gpui::{actions, AppContext, Context, Global, Hsla, Model, ModelContext};
 use project::{Project, WorktreeId};
-use settings::GitPanelSettings;
 use std::sync::mpsc;
 use std::{
     pin::{pin, Pin},
@@ -16,7 +16,7 @@ use ui::{Color, Icon, IconName, IntoElement, SharedString};
 use worktree::RepositoryEntry;
 
 pub mod git_panel;
-mod settings;
+mod git_panel_settings;
 
 const GIT_TASK_DEBOUNCE: Duration = Duration::from_millis(50);
 

crates/git_ui/src/settings.rs 🔗

@@ -1,41 +0,0 @@
-use gpui::Pixels;
-use schemars::JsonSchema;
-use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
-use workspace::dock::DockPosition;
-
-#[derive(Deserialize, Debug)]
-pub struct GitPanelSettings {
-    pub button: bool,
-    pub dock: DockPosition,
-    pub default_width: Pixels,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct PanelSettingsContent {
-    /// Whether to show the panel button in the status bar.
-    ///
-    /// Default: true
-    pub button: Option<bool>,
-    /// Where to dock the panel.
-    ///
-    /// Default: left
-    pub dock: Option<DockPosition>,
-    /// Default width of the panel in pixels.
-    ///
-    /// Default: 360
-    pub default_width: Option<f32>,
-}
-
-impl Settings for GitPanelSettings {
-    const KEY: Option<&'static str> = Some("git_panel");
-
-    type FileContent = PanelSettingsContent;
-
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self> {
-        sources.json_merge()
-    }
-}