editor: Update git hunk indicators to show staging status when hunk is expanded (#24818)

Nate Butler and cole-miller created

- Update git hunk indicators to show staging status when hunk is
expanded
- Updates uses of status colors to the new version control theme colors
- Adds new version control theme colors to included themes

Before:

![CleanShot 2025-02-13 at 14 42
48@2x](https://github.com/user-attachments/assets/ccca147e-0de2-4e69-9cd4-01b010bf06d0)

After:

![CleanShot 2025-02-13 at 14 42
04@2x](https://github.com/user-attachments/assets/1ab49174-bde5-43b2-83c5-d217533df49a)

(Colors here are from before theme colors were added)


Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: cole-miller <m@cole-miller.net>

Change summary

assets/themes/gruvbox/gruvbox.json |  60 ++++++++++++++++
assets/themes/one/one.json         |  10 ++
crates/editor/src/editor.rs        |  20 ++--
crates/editor/src/element.rs       | 117 +++++++++++++++++--------------
crates/gpui/src/color.rs           |  19 +++++
crates/theme/src/default_colors.rs |   6 
6 files changed, 167 insertions(+), 65 deletions(-)

Detailed changes

assets/themes/gruvbox/gruvbox.json 🔗

@@ -105,6 +105,16 @@
         "terminal.ansi.bright_white": "#fbf1c7ff",
         "terminal.ansi.dim_white": "#b0a189ff",
         "link_text.hover": "#83a598ff",
+        "version_control.added": "#b7bb26ff",
+        "version_control.added_background": "#b7bb2614",
+        "version_control.deleted": "#fb4a35ff",
+        "version_control.deleted_background": "#fb4a3514",
+        "version_control.modified": "#f9bd2fff",
+        "version_control.modified_background": "#f9bd2f14",
+        "version_control.renamed": "#83a598ff",
+        "version_control.conflict": "#f9bd2fff",
+        "version_control.conflict_background": "#f9bd2f14",
+        "version_control.ignored": "#998b78ff",
         "conflict": "#f9bd2fff",
         "conflict.background": "#572e10ff",
         "conflict.border": "#754916ff",
@@ -490,6 +500,16 @@
         "terminal.ansi.bright_white": "#fbf1c7ff",
         "terminal.ansi.dim_white": "#b0a189ff",
         "link_text.hover": "#83a598ff",
+        "version_control.added": "#b7bb26ff",
+        "version_control.added_background": "#b7bb2614",
+        "version_control.deleted": "#fb4a35ff",
+        "version_control.deleted_background": "#fb4a3514",
+        "version_control.modified": "#f9bd2fff",
+        "version_control.modified_background": "#f9bd2f14",
+        "version_control.renamed": "#83a598ff",
+        "version_control.conflict": "#f9bd2fff",
+        "version_control.conflict_background": "#f9bd2f14",
+        "version_control.ignored": "#998b78ff",
         "conflict": "#f9bd2fff",
         "conflict.background": "#572e10ff",
         "conflict.border": "#754916ff",
@@ -875,6 +895,16 @@
         "terminal.ansi.bright_white": "#fbf1c7ff",
         "terminal.ansi.dim_white": "#b0a189ff",
         "link_text.hover": "#83a598ff",
+        "version_control.added": "#b7bb26ff",
+        "version_control.added_background": "#b7bb2614",
+        "version_control.deleted": "#fb4a35ff",
+        "version_control.deleted_background": "#fb4a3514",
+        "version_control.modified": "#f9bd2fff",
+        "version_control.modified_background": "#f9bd2f14",
+        "version_control.renamed": "#83a598ff",
+        "version_control.conflict": "#f9bd2fff",
+        "version_control.conflict_background": "#f9bd2f14",
+        "version_control.ignored": "#998b78ff",
         "conflict": "#f9bd2fff",
         "conflict.background": "#572e10ff",
         "conflict.border": "#754916ff",
@@ -1260,6 +1290,16 @@
         "terminal.ansi.bright_white": "#282828ff",
         "terminal.ansi.dim_white": "#73675eff",
         "link_text.hover": "#0b6678ff",
+        "version_control.added": "#79740eff",
+        "version_control.added_background": "#79740e14",
+        "version_control.deleted": "#9d0006ff",
+        "version_control.deleted_background": "#9d000614",
+        "version_control.modified": "#b57614ff",
+        "version_control.modified_background": "#b5761414",
+        "version_control.renamed": "#076678ff",
+        "version_control.conflict": "#b57614ff",
+        "version_control.conflict_background": "#b5761414",
+        "version_control.ignored": "#928374ff",
         "conflict": "#b57615ff",
         "conflict.background": "#f5e2d0ff",
         "conflict.border": "#ebccabff",
@@ -1645,6 +1685,16 @@
         "terminal.ansi.bright_white": "#282828ff",
         "terminal.ansi.dim_white": "#73675eff",
         "link_text.hover": "#0b6678ff",
+        "version_control.added": "#79740eff",
+        "version_control.added_background": "#79740e14",
+        "version_control.deleted": "#9d0006ff",
+        "version_control.deleted_background": "#9d000614",
+        "version_control.modified": "#b57614ff",
+        "version_control.modified_background": "#b5761414",
+        "version_control.renamed": "#076678ff",
+        "version_control.conflict": "#b57614ff",
+        "version_control.conflict_background": "#b5761414",
+        "version_control.ignored": "#928374ff",
         "conflict": "#b57615ff",
         "conflict.background": "#f5e2d0ff",
         "conflict.border": "#ebccabff",
@@ -2030,6 +2080,16 @@
         "terminal.ansi.bright_white": "#282828ff",
         "terminal.ansi.dim_white": "#73675eff",
         "link_text.hover": "#0b6678ff",
+        "version_control.added": "#79740eff",
+        "version_control.added_background": "#79740e14",
+        "version_control.deleted": "#9d0006ff",
+        "version_control.deleted_background": "#9d000614",
+        "version_control.modified": "#b57614ff",
+        "version_control.modified_background": "#b5761414",
+        "version_control.renamed": "#076678ff",
+        "version_control.conflict": "#b57614ff",
+        "version_control.conflict_background": "#b5761414",
+        "version_control.ignored": "#928374ff",
         "conflict": "#b57615ff",
         "conflict.background": "#f5e2d0ff",
         "conflict.border": "#ebccabff",

assets/themes/one/one.json 🔗

@@ -96,6 +96,16 @@
         "terminal.ansi.bright_white": "#dce0e5ff",
         "terminal.ansi.dim_white": "#575d65ff",
         "link_text.hover": "#74ade8ff",
+        "version_control.added": "#a1c181ff",
+        "version_control.added_background": "#a1c18114",
+        "version_control.deleted": "#d07277ff",
+        "version_control.deleted_background": "#d0727714",
+        "version_control.modified": "#dec184ff",
+        "version_control.modified_background": "#dec18414",
+        "version_control.renamed": "#74ade8ff",
+        "version_control.conflict": "#dec184ff",
+        "version_control.conflict_background": "#dec18414",
+        "version_control.ignored": "#878a98ff",
         "conflict": "#dec184ff",
         "conflict.background": "#dec1841a",
         "conflict.border": "#5d4c2fff",

crates/editor/src/editor.rs 🔗

@@ -80,13 +80,13 @@ use code_context_menus::{
 use git::blame::GitBlame;
 use gpui::{
     div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
-    AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds,
-    ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler,
-    EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
-    HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton, MouseDownEvent,
-    PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription,
-    Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
-    WeakEntity, WeakFocusHandle, Window,
+    AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Bounds, ClipboardEntry,
+    ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter,
+    FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
+    InteractiveText, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement,
+    Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task, TextStyle,
+    TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
+    WeakFocusHandle, Window,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -13645,14 +13645,14 @@ impl Editor {
         &self,
         window: &mut Window,
         cx: &mut App,
-    ) -> BTreeMap<DisplayRow, Background> {
+    ) -> BTreeMap<DisplayRow, Hsla> {
         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, Hsla>::new(),
                 |mut unique_rows, highlight| {
                     let start = highlight.range.start.to_display_point(&snapshot);
                     let end = highlight.range.end.to_display_point(&snapshot);
@@ -13669,7 +13669,7 @@ impl Editor {
                             used_highlight_orders.entry(row).or_insert(highlight.index);
                         if highlight.index >= *used_index {
                             *used_index = highlight.index;
-                            unique_rows.insert(DisplayRow(row), highlight.color.into());
+                            unique_rows.insert(DisplayRow(row), highlight.color);
                         }
                     }
                     unique_rows

crates/editor/src/element.rs 🔗

@@ -31,7 +31,7 @@ use file_icons::FileIcons;
 use git::{blame::BlameEntry, Oid};
 use gpui::{
     anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
-    point, px, quad, relative, size, svg, transparent_black, Action, AnyElement, App,
+    point, px, quad, relative, size, solid_color, svg, transparent_black, Action, AnyElement, App,
     AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners,
     CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable, FontId,
     GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
@@ -89,6 +89,7 @@ enum DisplayDiffHunk {
         display_row_range: Range<DisplayRow>,
         multi_buffer_range: Range<Anchor>,
         status: DiffHunkStatus,
+        contains_expanded: bool,
     },
 }
 
@@ -1567,6 +1568,11 @@ impl EditorElement {
                 if hunk_display_end.column() > 0 {
                     end_row.0 += 1;
                 }
+                let start_row = hunk_display_start.row();
+                let contains_expanded = snapshot
+                    .row_infos(start_row)
+                    .take(end_row.0 as usize - start_row.0 as usize)
+                    .any(|row_info| row_info.diff_status.is_some());
                 DisplayDiffHunk::Unfolded {
                     status: hunk.status(),
                     diff_base_byte_range: hunk.diff_base_byte_range,
@@ -1576,6 +1582,7 @@ impl EditorElement {
                         hunk.buffer_id,
                         hunk.buffer_range,
                     ),
+                    contains_expanded,
                 }
             };
 
@@ -4341,7 +4348,7 @@ impl EditorElement {
                         window.paint_quad(fill(Bounds { origin, size }, color));
                     };
 
-                let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
+                let mut current_paint: Option<(Hsla, Range<DisplayRow>)> = None;
                 for (&new_row, &new_background) in &layout.highlighted_rows {
                     match &mut current_paint {
                         Some((current_background, current_range)) => {
@@ -4539,11 +4546,17 @@ impl EditorElement {
         }
     }
 
-    fn paint_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
+    fn paint_diff_hunk_gutter_indicators(
+        layout: &mut EditorLayout,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
         if layout.display_hunks.is_empty() {
             return;
         }
 
+        let corners = Corners::all(px(0.));
+
         let line_height = layout.position_map.line_height;
         window.paint_layer(layout.gutter_hitbox.bounds, |window| {
             for (hunk, hitbox) in &layout.display_hunks {
@@ -4557,36 +4570,41 @@ impl EditorElement {
                         );
                         Some((
                             hunk_bounds,
-                            cx.theme().status().modified,
-                            Corners::all(px(0.)),
+                            cx.theme().colors().version_control_modified.opacity(0.7),
+                            corners,
                             &DiffHunkSecondaryStatus::None,
+                            false,
                         ))
                     }
                     DisplayDiffHunk::Unfolded {
                         status,
                         display_row_range,
+                        contains_expanded,
                         ..
                     } => hitbox.as_ref().map(|hunk_hitbox| match status {
                         DiffHunkStatus::Added(secondary_status) => (
                             hunk_hitbox.bounds,
-                            cx.theme().status().created,
-                            Corners::all(px(0.)),
+                            cx.theme().colors().version_control_added.opacity(0.7),
+                            corners,
                             secondary_status,
+                            *contains_expanded,
                         ),
                         DiffHunkStatus::Modified(secondary_status) => (
                             hunk_hitbox.bounds,
-                            cx.theme().status().modified,
-                            Corners::all(px(0.)),
+                            cx.theme().colors().version_control_modified.opacity(0.7),
+                            corners,
                             secondary_status,
+                            *contains_expanded,
                         ),
                         DiffHunkStatus::Removed(secondary_status)
                             if !display_row_range.is_empty() =>
                         {
                             (
                                 hunk_hitbox.bounds,
-                                cx.theme().status().deleted,
-                                Corners::all(px(0.)),
+                                cx.theme().colors().version_control_deleted.opacity(0.7),
+                                corners,
                                 secondary_status,
+                                *contains_expanded,
                             )
                         }
                         DiffHunkStatus::Removed(secondary_status) => (
@@ -4597,23 +4615,34 @@ impl EditorElement {
                                 ),
                                 size(hunk_hitbox.size.width * px(2.), hunk_hitbox.size.height),
                             ),
-                            cx.theme().status().deleted,
+                            cx.theme().colors().version_control_deleted.opacity(0.7),
                             Corners::all(1. * line_height),
                             secondary_status,
+                            *contains_expanded,
                         ),
                     }),
                 };
 
-                if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) =
-                    hunk_to_paint
+                if let Some((
+                    hunk_bounds,
+                    background_color,
+                    corner_radii,
+                    secondary_status,
+                    contains_expanded,
+                )) = hunk_to_paint
                 {
-                    if *secondary_status != DiffHunkSecondaryStatus::None {
-                        background_color.a *= 0.6;
-                    }
+                    let background = if *secondary_status != DiffHunkSecondaryStatus::None
+                        && contains_expanded
+                    {
+                        pattern_slash(background_color, line_height.0 / 2.5)
+                    } else {
+                        solid_color(background_color)
+                    };
+
                     window.paint_quad(quad(
                         hunk_bounds,
                         corner_radii,
-                        background_color,
+                        background,
                         Edges::default(),
                         transparent_black(),
                     ));
@@ -4733,7 +4762,7 @@ impl EditorElement {
                 )
             });
         if show_git_gutter {
-            Self::paint_diff_hunks(layout, window, cx)
+            Self::paint_diff_hunk_gutter_indicators(layout, window, cx)
         }
 
         let highlight_width = 0.275 * layout.position_map.line_height;
@@ -5292,9 +5321,15 @@ impl EditorElement {
                                             end_display_row.0 -= 1;
                                         }
                                         let color = match &hunk.status() {
-                                            DiffHunkStatus::Added(_) => theme.status().created,
-                                            DiffHunkStatus::Modified(_) => theme.status().modified,
-                                            DiffHunkStatus::Removed(_) => theme.status().deleted,
+                                            DiffHunkStatus::Added(_) => {
+                                                theme.colors().version_control_added
+                                            }
+                                            DiffHunkStatus::Modified(_) => {
+                                                theme.colors().version_control_modified
+                                            }
+                                            DiffHunkStatus::Removed(_) => {
+                                                theme.colors().version_control_deleted
+                                            }
                                         };
                                         ColoredRange {
                                             start: start_display_row,
@@ -6875,39 +6910,17 @@ impl Element for EditorElement {
                         )
                     };
 
-                    let (mut highlighted_rows, distinguish_unstaged_hunks) =
-                        self.editor.update(cx, |editor, cx| {
-                            (
-                                editor.highlighted_display_rows(window, cx),
-                                editor.distinguish_unstaged_diff_hunks,
-                            )
-                        });
+                    let mut highlighted_rows = self
+                        .editor
+                        .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
 
                     for (ix, row_info) in row_infos.iter().enumerate() {
                         let background = match row_info.diff_status {
-                            Some(DiffHunkStatus::Added(secondary_status)) => {
-                                let color = style.status.created_background;
-                                match secondary_status {
-                                    DiffHunkSecondaryStatus::HasSecondaryHunk
-                                    | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
-                                        if distinguish_unstaged_hunks =>
-                                    {
-                                        pattern_slash(color, line_height.0 / 4.0)
-                                    }
-                                    _ => color.into(),
-                                }
+                            Some(DiffHunkStatus::Added(_)) => {
+                                cx.theme().colors().version_control_added_background
                             }
-                            Some(DiffHunkStatus::Removed(secondary_status)) => {
-                                let color = style.status.deleted_background;
-                                match secondary_status {
-                                    DiffHunkSecondaryStatus::HasSecondaryHunk
-                                    | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
-                                        if distinguish_unstaged_hunks =>
-                                    {
-                                        pattern_slash(color, line_height.0 / 4.0)
-                                    }
-                                    _ => color.into(),
-                                }
+                            Some(DiffHunkStatus::Removed(_)) => {
+                                cx.theme().colors().version_control_deleted_background
                             }
                             _ => continue,
                         };
@@ -7755,7 +7768,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, Hsla>,
     line_elements: SmallVec<[AnyElement; 1]>,
     line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
     display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,

crates/gpui/src/color.rs 🔗

@@ -607,6 +607,25 @@ impl Default for Background {
     }
 }
 
+impl Background {
+    /// Gets the color of the background if there is one.
+    pub fn color(&self) -> Option<Hsla> {
+        match self.tag {
+            BackgroundTag::Solid => Some(self.solid),
+            BackgroundTag::LinearGradient => None,
+            BackgroundTag::PatternSlash => Some(self.solid),
+        }
+    }
+}
+
+/// Creates a background with a solid color
+pub fn solid_color(color: impl Into<Hsla>) -> Background {
+    Background {
+        solid: color.into(),
+        ..Default::default()
+    }
+}
+
 /// Creates a hash pattern background
 pub fn pattern_slash(color: Hsla, thickness: f32) -> Background {
     Background {

crates/theme/src/default_colors.rs 🔗

@@ -136,11 +136,11 @@ impl ThemeColors {
             terminal_ansi_dim_white: neutral().light().step_11(),
             link_text_hover: orange().light().step_10(),
             version_control_added: ADDED_COLOR,
-            version_control_added_background: ADDED_COLOR.opacity(0.1),
+            version_control_added_background: ADDED_COLOR.opacity(0.08),
             version_control_deleted: REMOVED_COLOR,
-            version_control_deleted_background: REMOVED_COLOR.opacity(0.1),
+            version_control_deleted_background: REMOVED_COLOR.opacity(0.08),
             version_control_modified: MODIFIED_COLOR,
-            version_control_modified_background: MODIFIED_COLOR.opacity(0.1),
+            version_control_modified_background: MODIFIED_COLOR.opacity(0.08),
             version_control_renamed: MODIFIED_COLOR,
             version_control_conflict: orange().light().step_12(),
             version_control_conflict_background: orange().light().step_12().opacity(0.1),