Gutter-highlight removed and added portions of expanded diff hunks separately (#24834)

Cole Miller and Conrad created

cc @iamnbutler 

Release Notes:

- Color gutter highlights separately for removed and deleted portions of
git modification hunks

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

Change summary

crates/editor/src/element.rs | 138 +++++++++++++++++++++++++++----------
1 file changed, 100 insertions(+), 38 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -79,17 +79,18 @@ use workspace::{item::Item, notifications::NotifyTaskExt, Workspace};
 
 const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
 
+/// Note that for a "modified" MultiBufferDiffHunk, there are two DisplayDiffHunks,
+/// one for the deleted portion and one for the added portion.
 #[derive(Debug, Clone, PartialEq, Eq)]
 enum DisplayDiffHunk {
     Folded {
         display_row: DisplayRow,
     },
     Unfolded {
-        diff_base_byte_range: Range<usize>,
         display_row_range: Range<DisplayRow>,
         multi_buffer_range: Range<Anchor>,
         status: DiffHunkStatus,
-        contains_expanded: bool,
+        expanded: bool,
     },
 }
 
@@ -104,7 +105,7 @@ struct SelectionLayout {
 }
 
 impl SelectionLayout {
-    fn new<T: ToPoint + ToDisplayPoint + Clone>(
+    fn new<T: multi_buffer::ToPoint + ToDisplayPoint + Clone>(
         selection: Selection<T>,
         line_mode: bool,
         cursor_shape: CursorShape,
@@ -1557,36 +1558,97 @@ impl EditorElement {
             let hunk_end_point = Point::new(hunk.row_range.end.0, 0);
 
             let hunk_display_start = snapshot.point_to_display_point(hunk_start_point, Bias::Left);
+            let hunk_added_start_at =
+                Anchor::in_buffer(hunk.excerpt_id, hunk.buffer_id, hunk.buffer_range.start);
+            let hunk_deleted_to_added_break = snapshot.point_to_display_point(
+                hunk_added_start_at.to_point(&snapshot.buffer_snapshot),
+                Bias::Right,
+            );
+
             let hunk_display_end = snapshot.point_to_display_point(hunk_end_point, Bias::Right);
 
-            let display_hunk = if hunk_display_start.column() != 0 {
-                DisplayDiffHunk::Folded {
-                    display_row: hunk_display_start.row(),
-                }
+            if hunk_display_start.column() != 0 {
+                display_hunks.push((
+                    DisplayDiffHunk::Folded {
+                        display_row: hunk_display_start.row(),
+                    },
+                    None,
+                ));
             } else {
                 let mut end_row = hunk_display_end.row();
                 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,
-                    display_row_range: hunk_display_start.row()..end_row,
-                    multi_buffer_range: Anchor::range_in_buffer(
-                        hunk.excerpt_id,
-                        hunk.buffer_id,
-                        hunk.buffer_range,
-                    ),
-                    contains_expanded,
+                let deleted_count = snapshot
+                    .buffer_snapshot
+                    .row_infos(hunk.row_range.start)
+                    .take(hunk.row_range.end.0 as usize - hunk.row_range.start.0 as usize)
+                    .take_while(|row_info| {
+                        matches!(row_info.diff_status, Some(DiffHunkStatus::Removed(_)))
+                    })
+                    .count();
+                let has_added = snapshot
+                    .buffer_snapshot
+                    .row_infos(hunk.row_range.start)
+                    .take(hunk.row_range.end.0 as usize - hunk.row_range.start.0 as usize)
+                    .any(|row_info| matches!(row_info.diff_status, Some(DiffHunkStatus::Added(_))));
+                let expanded = deleted_count > 0 || has_added;
+                if deleted_count > 0 && has_added {
+                    display_hunks.push((
+                        DisplayDiffHunk::Unfolded {
+                            status: DiffHunkStatus::Removed(hunk.secondary_status),
+                            display_row_range: hunk_display_start.row()
+                                ..hunk_display_start.row() + DisplayRow(deleted_count as u32),
+                            multi_buffer_range: Anchor::range_in_buffer(
+                                hunk.excerpt_id,
+                                hunk.buffer_id,
+                                hunk.buffer_range.clone(),
+                            ),
+                            expanded,
+                        },
+                        None,
+                    ));
+                    display_hunks.push((
+                        DisplayDiffHunk::Unfolded {
+                            status: DiffHunkStatus::Added(hunk.secondary_status),
+                            display_row_range: hunk_display_start.row()
+                                + DisplayRow(deleted_count as u32)
+                                ..end_row,
+                            multi_buffer_range: Anchor::range_in_buffer(
+                                hunk.excerpt_id,
+                                hunk.buffer_id,
+                                hunk.buffer_range,
+                            ),
+                            expanded,
+                        },
+                        None,
+                    ));
+                } else {
+                    let status = if expanded && matches!(hunk.status(), DiffHunkStatus::Modified(_))
+                    {
+                        if hunk_display_start.row() < hunk_deleted_to_added_break.row() {
+                            DiffHunkStatus::Removed(hunk.secondary_status)
+                        } else {
+                            DiffHunkStatus::Added(hunk.secondary_status)
+                        }
+                    } else {
+                        hunk.status()
+                    };
+                    display_hunks.push((
+                        DisplayDiffHunk::Unfolded {
+                            status,
+                            display_row_range: hunk_display_start.row()..end_row,
+                            multi_buffer_range: Anchor::range_in_buffer(
+                                hunk.excerpt_id,
+                                hunk.buffer_id,
+                                hunk.buffer_range,
+                            ),
+                            expanded,
+                        },
+                        None,
+                    ));
                 }
             };
-
-            display_hunks.push((display_hunk, None));
         }
 
         let git_gutter_setting = ProjectSettings::get_global(cx)
@@ -1924,7 +1986,8 @@ impl EditorElement {
                     if tasks.offset.0 < offset_range_start || tasks.offset.0 >= offset_range_end {
                         return None;
                     }
-                    let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
+                    let multibuffer_point =
+                        multi_buffer::ToPoint::to_point(&tasks.offset.0, &snapshot.buffer_snapshot);
                     let multibuffer_row = MultiBufferRow(multibuffer_point.row);
                     let buffer_folded = snapshot
                         .buffer_snapshot
@@ -4579,7 +4642,7 @@ impl EditorElement {
                     DisplayDiffHunk::Unfolded {
                         status,
                         display_row_range,
-                        contains_expanded,
+                        expanded,
                         ..
                     } => hitbox.as_ref().map(|hunk_hitbox| match status {
                         DiffHunkStatus::Added(secondary_status) => (
@@ -4587,14 +4650,14 @@ impl EditorElement {
                             cx.theme().colors().version_control_added.opacity(0.7),
                             corners,
                             secondary_status,
-                            *contains_expanded,
+                            *expanded,
                         ),
                         DiffHunkStatus::Modified(secondary_status) => (
                             hunk_hitbox.bounds,
                             cx.theme().colors().version_control_modified.opacity(0.7),
                             corners,
                             secondary_status,
-                            *contains_expanded,
+                            *expanded,
                         ),
                         DiffHunkStatus::Removed(secondary_status)
                             if !display_row_range.is_empty() =>
@@ -4604,7 +4667,7 @@ impl EditorElement {
                                 cx.theme().colors().version_control_deleted.opacity(0.7),
                                 corners,
                                 secondary_status,
-                                *contains_expanded,
+                                *expanded,
                             )
                         }
                         DiffHunkStatus::Removed(secondary_status) => (
@@ -4618,7 +4681,7 @@ impl EditorElement {
                             cx.theme().colors().version_control_deleted.opacity(0.7),
                             Corners::all(1. * line_height),
                             secondary_status,
-                            *contains_expanded,
+                            *expanded,
                         ),
                     }),
                 };
@@ -4628,16 +4691,15 @@ impl EditorElement {
                     background_color,
                     corner_radii,
                     secondary_status,
-                    contains_expanded,
+                    expanded,
                 )) = hunk_to_paint
                 {
-                    let background = if *secondary_status != DiffHunkSecondaryStatus::None
-                        && contains_expanded
-                    {
-                        pattern_slash(background_color, line_height.0 / 2.5)
-                    } else {
-                        solid_color(background_color)
-                    };
+                    let background =
+                        if *secondary_status != DiffHunkSecondaryStatus::None && expanded {
+                            pattern_slash(background_color, line_height.0 / 2.5)
+                        } else {
+                            solid_color(background_color)
+                        };
 
                     window.paint_quad(quad(
                         hunk_bounds,