Change the default staging and unstaging state display (#26299)

Mikayla Maki , Conrad , and Nate created

This adds a setting for the "border" hunk display mode, as discussed,
and makes it the default.

Here's how it looks in light mode:

<img width="1512" alt="Screenshot 2025-03-07 at 11 39 25 AM"
src="https://github.com/user-attachments/assets/a934faa3-ec69-47e1-ad46-535e48b98e9f"
/>

And dark mode: 

<img width="1511" alt="Screenshot 2025-03-07 at 11 39 56 AM"
src="https://github.com/user-attachments/assets/43c9afd1-22bb-4bd8-96ce-82702a6cbc80"
/>


Release Notes:

- Git Beta: Adjusted the default hunk styling for staged and unstaged
changes

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Nate <nate@zed.dev>

Change summary

assets/settings/default.json           |   2 
crates/editor/src/editor.rs            |  28 +++++
crates/editor/src/element.rs           | 128 ++++++++++++++++++++-------
crates/gpui/src/color.rs               |  24 +++++
crates/project/src/project_settings.rs |   5 +
5 files changed, 149 insertions(+), 38 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -845,7 +845,7 @@
     //    "hunk_style": "transparent"
     // 2. Show unstaged hunks with a pattern background:
     //    "hunk_style": "pattern"
-    "hunk_style": "transparent"
+    "hunk_style": "staged_border"
   },
   // Configuration for how direnv configuration should be loaded. May take 2 values:
   // 1. Load direnv configuration using `direnv export json` directly.

crates/editor/src/editor.rs 🔗

@@ -14882,14 +14882,14 @@ impl Editor {
         &self,
         window: &mut Window,
         cx: &mut App,
-    ) -> BTreeMap<DisplayRow, Background> {
+    ) -> BTreeMap<DisplayRow, LineHighlight> {
         let snapshot = self.snapshot(window, cx);
         let mut used_highlight_orders = HashMap::default();
         self.highlighted_rows
             .iter()
             .flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
             .fold(
-                BTreeMap::<DisplayRow, Background>::new(),
+                BTreeMap::<DisplayRow, LineHighlight>::new(),
                 |mut unique_rows, highlight| {
                     let start = highlight.range.start.to_display_point(&snapshot);
                     let end = highlight.range.end.to_display_point(&snapshot);
@@ -18426,3 +18426,27 @@ impl Render for MissingEditPredictionKeybindingTooltip {
         })
     }
 }
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct LineHighlight {
+    pub background: Background,
+    pub border: Option<gpui::Hsla>,
+}
+
+impl From<Hsla> for LineHighlight {
+    fn from(hsla: Hsla) -> Self {
+        Self {
+            background: hsla.into(),
+            border: None,
+        }
+    }
+}
+
+impl From<Background> for LineHighlight {
+    fn from(background: Background) -> Self {
+        Self {
+            background,
+            border: None,
+        }
+    }
+}

crates/editor/src/element.rs 🔗

@@ -20,10 +20,10 @@ use crate::{
     DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
     EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
     GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
-    InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown,
-    PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
-    StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
-    FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
+    InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
+    OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
+    Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
+    CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
     MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
 };
 use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
@@ -4132,46 +4132,74 @@ impl EditorElement {
                     }
                 }
 
-                let mut paint_highlight =
-                    |highlight_row_start: DisplayRow, highlight_row_end: DisplayRow, color| {
-                        let origin = point(
-                            layout.hitbox.origin.x,
-                            layout.hitbox.origin.y
-                                + (highlight_row_start.as_f32() - scroll_top)
-                                    * layout.position_map.line_height,
-                        );
-                        let size = size(
-                            layout.hitbox.size.width,
-                            layout.position_map.line_height
-                                * highlight_row_end.next_row().minus(highlight_row_start) as f32,
-                        );
-                        window.paint_quad(fill(Bounds { origin, size }, color));
-                    };
+                let mut paint_highlight = |highlight_row_start: DisplayRow,
+                                           highlight_row_end: DisplayRow,
+                                           highlight: crate::LineHighlight,
+                                           edges| {
+                    let origin = point(
+                        layout.hitbox.origin.x,
+                        layout.hitbox.origin.y
+                            + (highlight_row_start.as_f32() - scroll_top)
+                                * layout.position_map.line_height,
+                    );
+                    let size = size(
+                        layout.hitbox.size.width,
+                        layout.position_map.line_height
+                            * highlight_row_end.next_row().minus(highlight_row_start) as f32,
+                    );
+                    let mut quad = fill(Bounds { origin, size }, highlight.background);
+                    if let Some(border_color) = highlight.border {
+                        quad.border_color = border_color;
+                        quad.border_widths = edges
+                    }
+                    window.paint_quad(quad);
+                };
 
-                let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
+                let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
+                    None;
                 for (&new_row, &new_background) in &layout.highlighted_rows {
                     match &mut current_paint {
-                        Some((current_background, current_range)) => {
+                        Some((current_background, current_range, mut edges)) => {
                             let current_background = *current_background;
                             let new_range_started = current_background != new_background
                                 || current_range.end.next_row() != new_row;
                             if new_range_started {
+                                if current_range.end.next_row() == new_row {
+                                    edges.bottom = px(0.);
+                                };
                                 paint_highlight(
                                     current_range.start,
                                     current_range.end,
                                     current_background,
+                                    edges,
                                 );
-                                current_paint = Some((new_background, new_row..new_row));
+                                let edges = Edges {
+                                    top: if current_range.end.next_row() != new_row {
+                                        px(1.)
+                                    } else {
+                                        px(0.)
+                                    },
+                                    bottom: px(1.),
+                                    ..Default::default()
+                                };
+                                current_paint = Some((new_background, new_row..new_row, edges));
                                 continue;
                             } else {
                                 current_range.end = current_range.end.next_row();
                             }
                         }
-                        None => current_paint = Some((new_background, new_row..new_row)),
+                        None => {
+                            let edges = Edges {
+                                top: px(1.),
+                                bottom: px(1.),
+                                ..Default::default()
+                            };
+                            current_paint = Some((new_background, new_row..new_row, edges))
+                        }
                     };
                 }
-                if let Some((color, range)) = current_paint {
-                    paint_highlight(range.start, range.end, color);
+                if let Some((color, range, edges)) = current_paint {
+                    paint_highlight(range.start, range.end, color, edges);
                 }
 
                 let scroll_left =
@@ -4431,6 +4459,9 @@ impl EditorElement {
                                     background_color.opacity(if is_light { 0.2 } else { 0.32 });
                             }
                         }
+                        GitHunkStyleSetting::StagedBorder | GitHunkStyleSetting::Border => {
+                            // Don't change the background color
+                        }
                     }
 
                     // Flatten the background color with the editor color to prevent
@@ -6775,12 +6806,15 @@ impl Element for EditorElement {
                         let hunk_opacity = if is_light { 0.16 } else { 0.12 };
                         let slash_width = line_height.0 / 1.5; // ~16 by default
 
-                        let staged_background = match hunk_style {
-                            GitHunkStyleSetting::Transparent | GitHunkStyleSetting::Pattern => {
-                                solid_background(background_color.opacity(hunk_opacity))
+                        let staged_highlight: LineHighlight = match hunk_style {
+                            GitHunkStyleSetting::Transparent
+                            | GitHunkStyleSetting::Pattern
+                            | GitHunkStyleSetting::Border => {
+                                solid_background(background_color.opacity(hunk_opacity)).into()
                             }
                             GitHunkStyleSetting::StagedPattern => {
                                 pattern_slash(background_color.opacity(hunk_opacity), slash_width)
+                                    .into()
                             }
                             GitHunkStyleSetting::StagedTransparent => {
                                 solid_background(background_color.opacity(if is_light {
@@ -6788,30 +6822,56 @@ impl Element for EditorElement {
                                 } else {
                                     0.04
                                 }))
+                                .into()
                             }
+                            GitHunkStyleSetting::StagedBorder => LineHighlight {
+                                background: (background_color.opacity(if is_light {
+                                    0.08
+                                } else {
+                                    0.06
+                                }))
+                                .into(),
+                                border: Some(if is_light {
+                                    background_color.opacity(0.48)
+                                } else {
+                                    background_color.opacity(0.36)
+                                }),
+                            },
                         };
 
-                        let unstaged_background = match hunk_style {
+                        let unstaged_highlight = match hunk_style {
                             GitHunkStyleSetting::Transparent => {
                                 solid_background(background_color.opacity(if is_light {
                                     0.08
                                 } else {
                                     0.04
                                 }))
+                                .into()
                             }
                             GitHunkStyleSetting::Pattern => {
                                 pattern_slash(background_color.opacity(hunk_opacity), slash_width)
+                                    .into()
                             }
+                            GitHunkStyleSetting::Border => LineHighlight {
+                                background: (background_color.opacity(if is_light {
+                                    0.08
+                                } else {
+                                    0.02
+                                }))
+                                .into(),
+                                border: Some(background_color.opacity(0.5)),
+                            },
                             GitHunkStyleSetting::StagedPattern
-                            | GitHunkStyleSetting::StagedTransparent => {
-                                solid_background(background_color.opacity(hunk_opacity))
+                            | GitHunkStyleSetting::StagedTransparent
+                            | GitHunkStyleSetting::StagedBorder => {
+                                solid_background(background_color.opacity(hunk_opacity)).into()
                             }
                         };
 
                         let background = if unstaged {
-                            unstaged_background
+                            unstaged_highlight
                         } else {
-                            staged_background
+                            staged_highlight
                         };
 
                         highlighted_rows
@@ -7660,7 +7720,7 @@ pub struct EditorLayout {
     indent_guides: Option<Vec<IndentGuideLayout>>,
     visible_display_row_range: Range<DisplayRow>,
     active_rows: BTreeMap<DisplayRow, bool>,
-    highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
+    highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
     line_elements: SmallVec<[AnyElement; 1]>,
     line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
     display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,

crates/gpui/src/color.rs 🔗

@@ -634,7 +634,7 @@ impl Display for ColorSpace {
 }
 
 /// A background color, which can be either a solid color or a linear gradient.
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Clone, Copy, PartialEq)]
 #[repr(C)]
 pub struct Background {
     pub(crate) tag: BackgroundTag,
@@ -646,6 +646,28 @@ pub struct Background {
     pad: u32,
 }
 
+impl std::fmt::Debug for Background {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self.tag {
+            BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
+            BackgroundTag::LinearGradient => {
+                write!(
+                    f,
+                    "LinearGradient({}, {:?}, {:?})",
+                    self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
+                )
+            }
+            BackgroundTag::PatternSlash => {
+                write!(
+                    f,
+                    "PatternSlash({:?}, {})",
+                    self.solid, self.gradient_angle_or_pattern_height
+                )
+            }
+        }
+    }
+}
+
 impl Eq for Background {}
 impl Default for Background {
     fn default() -> Self {

crates/project/src/project_settings.rs 🔗

@@ -212,10 +212,15 @@ pub enum GitHunkStyleSetting {
     Transparent,
     /// Show unstaged hunks with a pattern background
     Pattern,
+    /// Show unstaged hunks with a border background
+    Border,
+
     /// Show staged hunks with a pattern background
     StagedPattern,
     /// Show staged hunks with a pattern background
     StagedTransparent,
+    /// Show staged hunks with a pattern background
+    StagedBorder,
 }
 
 #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]