Expand git diffs when clicking the gutter strip, display their controls in a block above (#18313)

Max Brunsfeld , Marshall , and Marshall Bowers created

Todo:

* [x] Tooltips for hunk buttons
* [x] Buttons to go to next and previous hunk
* [x] Ellipsis button that opens a context menu with `Revert all`

/cc @iamnbutler @danilo-leal for design 👀 

Release Notes:

- Changed the behavior of the git gutter so that diff hunk are expanded
immediately when clicking the gutter, and hunk controls are displayed
above the hunk.

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

crates/collab/src/tests/editor_tests.rs | 289 ------------
crates/editor/src/editor.rs             | 126 +++--
crates/editor/src/editor_tests.rs       | 211 ++++----
crates/editor/src/element.rs            |  95 ---
crates/editor/src/hunk_diff.rs          | 597 +++++++++++++++++---------
5 files changed, 570 insertions(+), 748 deletions(-)

Detailed changes

crates/collab/src/tests/editor_tests.rs 🔗

@@ -7,18 +7,12 @@ use collections::HashMap;
 use editor::{
     actions::{
         ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
-        RevertSelectedHunks, ToggleCodeActions, Undo,
-    },
-    display_map::DisplayRow,
-    test::{
-        editor_hunks,
-        editor_test_context::{AssertionContextManager, EditorTestContext},
-        expanded_hunks, expanded_hunks_background_highlights,
+        ToggleCodeActions, Undo,
     },
+    test::editor_test_context::{AssertionContextManager, EditorTestContext},
     Editor,
 };
 use futures::StreamExt;
-use git::diff::DiffHunkStatus;
 use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
 use indoc::indoc;
 use language::{
@@ -1970,285 +1964,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
     });
 }
 
-#[gpui::test]
-async fn test_multiple_hunk_types_revert(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
-    let mut server = TestServer::start(cx_a.executor()).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-    server
-        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-        .await;
-    let active_call_a = cx_a.read(ActiveCall::global);
-    let active_call_b = cx_b.read(ActiveCall::global);
-
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
-    client_a.language_registry().add(rust_lang());
-    client_b.language_registry().add(rust_lang());
-
-    let base_text = indoc! {r#"struct Row;
-struct Row1;
-struct Row2;
-
-struct Row4;
-struct Row5;
-struct Row6;
-
-struct Row8;
-struct Row9;
-struct Row10;"#};
-
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "main.rs": base_text,
-            }),
-        )
-        .await;
-    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-    active_call_a
-        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-        .await
-        .unwrap();
-    let project_id = active_call_a
-        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-        .await
-        .unwrap();
-
-    let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
-    active_call_b
-        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-        .await
-        .unwrap();
-
-    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
-    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
-
-    let editor_a = workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    let editor_b = workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    let mut editor_cx_a = EditorTestContext {
-        cx: cx_a.clone(),
-        window: cx_a.handle(),
-        editor: editor_a,
-        assertion_cx: AssertionContextManager::new(),
-    };
-    let mut editor_cx_b = EditorTestContext {
-        cx: cx_b.clone(),
-        window: cx_b.handle(),
-        editor: editor_b,
-        assertion_cx: AssertionContextManager::new(),
-    };
-
-    // host edits the file, that differs from the base text, producing diff hunks
-    editor_cx_a.set_state(indoc! {r#"struct Row;
-        struct Row0.1;
-        struct Row0.2;
-        struct Row1;
-
-        struct Row4;
-        struct Row5444;
-        struct Row6;
-
-        struct Row9;
-        struct Row1220;ˇ"#});
-    editor_cx_a.update_editor(|editor, cx| {
-        editor
-            .buffer()
-            .read(cx)
-            .as_singleton()
-            .unwrap()
-            .update(cx, |buffer, cx| {
-                buffer.set_diff_base(Some(base_text.into()), cx);
-            });
-    });
-    editor_cx_b.update_editor(|editor, cx| {
-        editor
-            .buffer()
-            .read(cx)
-            .as_singleton()
-            .unwrap()
-            .update(cx, |buffer, cx| {
-                buffer.set_diff_base(Some(base_text.into()), cx);
-            });
-    });
-    cx_a.executor().run_until_parked();
-    cx_b.executor().run_until_parked();
-
-    // the client selects a range in the updated buffer, expands it to see the diff for each hunk in the selection
-    // the host does not see the diffs toggled
-    editor_cx_b.set_selections_state(indoc! {r#"«ˇstruct Row;
-        struct Row0.1;
-        struct Row0.2;
-        struct Row1;
-
-        struct Row4;
-        struct Row5444;
-        struct Row6;
-
-        struct R»ow9;
-        struct Row1220;"#});
-    editor_cx_b
-        .update_editor(|editor, cx| editor.toggle_hunk_diff(&editor::actions::ToggleHunkDiff, cx));
-    cx_a.executor().run_until_parked();
-    cx_b.executor().run_until_parked();
-    editor_cx_a.update_editor(|editor, cx| {
-        let snapshot = editor.snapshot(cx);
-        let all_hunks = editor_hunks(editor, &snapshot, cx);
-        let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
-        assert_eq!(
-            all_hunks,
-            vec![
-                (
-                    "".to_string(),
-                    DiffHunkStatus::Added,
-                    DisplayRow(1)..DisplayRow(3)
-                ),
-                (
-                    "struct Row2;\n".to_string(),
-                    DiffHunkStatus::Removed,
-                    DisplayRow(4)..DisplayRow(4)
-                ),
-                (
-                    "struct Row5;\n".to_string(),
-                    DiffHunkStatus::Modified,
-                    DisplayRow(6)..DisplayRow(7)
-                ),
-                (
-                    "struct Row8;\n".to_string(),
-                    DiffHunkStatus::Removed,
-                    DisplayRow(9)..DisplayRow(9)
-                ),
-                (
-                    "struct Row10;".to_string(),
-                    DiffHunkStatus::Modified,
-                    DisplayRow(10)..DisplayRow(10),
-                ),
-            ]
-        );
-        assert_eq!(all_expanded_hunks, Vec::new());
-    });
-    editor_cx_b.update_editor(|editor, cx| {
-        let snapshot = editor.snapshot(cx);
-        let all_hunks = editor_hunks(editor, &snapshot, cx);
-        let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(
-            expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(1)..=DisplayRow(2), DisplayRow(8)..=DisplayRow(8)],
-        );
-        assert_eq!(
-            all_hunks,
-            vec![
-                (
-                    "".to_string(),
-                    DiffHunkStatus::Added,
-                    DisplayRow(1)..DisplayRow(3)
-                ),
-                (
-                    "struct Row2;\n".to_string(),
-                    DiffHunkStatus::Removed,
-                    DisplayRow(5)..DisplayRow(5)
-                ),
-                (
-                    "struct Row5;\n".to_string(),
-                    DiffHunkStatus::Modified,
-                    DisplayRow(8)..DisplayRow(9)
-                ),
-                (
-                    "struct Row8;\n".to_string(),
-                    DiffHunkStatus::Removed,
-                    DisplayRow(12)..DisplayRow(12)
-                ),
-                (
-                    "struct Row10;".to_string(),
-                    DiffHunkStatus::Modified,
-                    DisplayRow(13)..DisplayRow(13),
-                ),
-            ]
-        );
-        assert_eq!(all_expanded_hunks, &all_hunks[..all_hunks.len() - 1]);
-    });
-
-    // the client reverts the hunks, removing the expanded diffs too
-    // both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
-    editor_cx_b.update_editor(|editor, cx| {
-        editor.revert_selected_hunks(&RevertSelectedHunks, cx);
-    });
-    cx_a.executor().run_until_parked();
-    cx_b.executor().run_until_parked();
-    editor_cx_a.update_editor(|editor, cx| {
-        let snapshot = editor.snapshot(cx);
-        let all_hunks = editor_hunks(editor, &snapshot, cx);
-        let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
-        assert_eq!(
-            all_hunks,
-            vec![(
-                "struct Row10;".to_string(),
-                DiffHunkStatus::Modified,
-                DisplayRow(10)..DisplayRow(10),
-            )]
-        );
-        assert_eq!(all_expanded_hunks, Vec::new());
-    });
-    editor_cx_b.update_editor(|editor, cx| {
-        let snapshot = editor.snapshot(cx);
-        let all_hunks = editor_hunks(editor, &snapshot, cx);
-        let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
-        assert_eq!(
-            all_hunks,
-            vec![(
-                "struct Row10;".to_string(),
-                DiffHunkStatus::Modified,
-                DisplayRow(10)..DisplayRow(10),
-            )]
-        );
-        assert_eq!(all_expanded_hunks, Vec::new());
-    });
-    editor_cx_a.assert_editor_state(indoc! {r#"struct Row;
-        struct Row1;
-        struct Row2;
-
-        struct Row4;
-        struct Row5;
-        struct Row6;
-
-        struct Row8;
-        struct Row9;
-        struct Row1220;ˇ"#});
-    editor_cx_b.assert_editor_state(indoc! {r#"«ˇstruct Row;
-        struct Row1;
-        struct Row2;
-
-        struct Row4;
-        struct Row5;
-        struct Row6;
-
-        struct Row8;
-        struct R»ow9;
-        struct Row1220;"#});
-}
-
 #[gpui::test(iterations = 10)]
 async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
     let mut server = TestServer::start(cx_a.executor()).await;

crates/editor/src/editor.rs 🔗

@@ -154,7 +154,7 @@ use theme::{
 };
 use ui::{
     h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
-    ListItem, Popover, Tooltip,
+    ListItem, Popover, PopoverMenuHandle, Tooltip,
 };
 use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::item::{ItemHandle, PreviewTabsSettings};
@@ -562,6 +562,7 @@ pub struct Editor {
     nav_history: Option<ItemNavHistory>,
     context_menu: RwLock<Option<ContextMenu>>,
     mouse_context_menu: Option<MouseContextMenu>,
+    hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
     signature_help_state: SignatureHelpState,
     auto_signature_help: Option<bool>,
@@ -1938,6 +1939,7 @@ impl Editor {
             nav_history: None,
             context_menu: RwLock::new(None),
             mouse_context_menu: None,
+            hunk_controls_menu_handle: PopoverMenuHandle::default(),
             completion_tasks: Default::default(),
             signature_help_state: SignatureHelpState::default(),
             auto_signature_help: None,
@@ -5383,23 +5385,6 @@ impl Editor {
             }))
     }
 
-    fn close_hunk_diff_button(
-        &self,
-        hunk: HoveredHunk,
-        row: DisplayRow,
-        cx: &mut ViewContext<Self>,
-    ) -> IconButton {
-        IconButton::new(
-            ("close_hunk_diff_indicator", row.0 as usize),
-            ui::IconName::Close,
-        )
-        .shape(ui::IconButtonShape::Square)
-        .icon_size(IconSize::XSmall)
-        .icon_color(Color::Muted)
-        .tooltip(|cx| Tooltip::for_action("Close hunk diff", &ToggleHunkDiff, cx))
-        .on_click(cx.listener(move |editor, _e, cx| editor.toggle_hovered_hunk(&hunk, cx)))
-    }
-
     pub fn context_menu_visible(&self) -> bool {
         self.context_menu
             .read()
@@ -9335,32 +9320,42 @@ impl Editor {
         }
     }
 
-    fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
+    fn go_to_next_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
         let snapshot = self
             .display_map
             .update(cx, |display_map, cx| display_map.snapshot(cx));
         let selection = self.selections.newest::<Point>(cx);
+        self.go_to_hunk_after_position(&snapshot, selection.head(), cx);
+    }
 
-        if !self.seek_in_direction(
-            &snapshot,
-            selection.head(),
+    fn go_to_hunk_after_position(
+        &mut self,
+        snapshot: &DisplaySnapshot,
+        position: Point,
+        cx: &mut ViewContext<'_, Editor>,
+    ) -> Option<MultiBufferDiffHunk> {
+        if let Some(hunk) = self.go_to_next_hunk_in_direction(
+            snapshot,
+            position,
             false,
-            snapshot.buffer_snapshot.git_diff_hunks_in_range(
-                MultiBufferRow(selection.head().row + 1)..MultiBufferRow::MAX,
-            ),
+            snapshot
+                .buffer_snapshot
+                .git_diff_hunks_in_range(MultiBufferRow(position.row + 1)..MultiBufferRow::MAX),
             cx,
         ) {
-            let wrapped_point = Point::zero();
-            self.seek_in_direction(
-                &snapshot,
-                wrapped_point,
-                true,
-                snapshot.buffer_snapshot.git_diff_hunks_in_range(
-                    MultiBufferRow(wrapped_point.row + 1)..MultiBufferRow::MAX,
-                ),
-                cx,
-            );
+            return Some(hunk);
         }
+
+        let wrapped_point = Point::zero();
+        self.go_to_next_hunk_in_direction(
+            snapshot,
+            wrapped_point,
+            true,
+            snapshot.buffer_snapshot.git_diff_hunks_in_range(
+                MultiBufferRow(wrapped_point.row + 1)..MultiBufferRow::MAX,
+            ),
+            cx,
+        )
     }
 
     fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
@@ -9369,52 +9364,65 @@ impl Editor {
             .update(cx, |display_map, cx| display_map.snapshot(cx));
         let selection = self.selections.newest::<Point>(cx);
 
-        if !self.seek_in_direction(
-            &snapshot,
-            selection.head(),
+        self.go_to_hunk_before_position(&snapshot, selection.head(), cx);
+    }
+
+    fn go_to_hunk_before_position(
+        &mut self,
+        snapshot: &DisplaySnapshot,
+        position: Point,
+        cx: &mut ViewContext<'_, Editor>,
+    ) -> Option<MultiBufferDiffHunk> {
+        if let Some(hunk) = self.go_to_next_hunk_in_direction(
+            snapshot,
+            position,
             false,
-            snapshot.buffer_snapshot.git_diff_hunks_in_range_rev(
-                MultiBufferRow(0)..MultiBufferRow(selection.head().row),
-            ),
+            snapshot
+                .buffer_snapshot
+                .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(position.row)),
             cx,
         ) {
-            let wrapped_point = snapshot.buffer_snapshot.max_point();
-            self.seek_in_direction(
-                &snapshot,
-                wrapped_point,
-                true,
-                snapshot.buffer_snapshot.git_diff_hunks_in_range_rev(
-                    MultiBufferRow(0)..MultiBufferRow(wrapped_point.row),
-                ),
-                cx,
-            );
+            return Some(hunk);
         }
+
+        let wrapped_point = snapshot.buffer_snapshot.max_point();
+        self.go_to_next_hunk_in_direction(
+            snapshot,
+            wrapped_point,
+            true,
+            snapshot
+                .buffer_snapshot
+                .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(wrapped_point.row)),
+            cx,
+        )
     }
 
-    fn seek_in_direction(
+    fn go_to_next_hunk_in_direction(
         &mut self,
         snapshot: &DisplaySnapshot,
         initial_point: Point,
         is_wrapped: bool,
         hunks: impl Iterator<Item = MultiBufferDiffHunk>,
         cx: &mut ViewContext<Editor>,
-    ) -> bool {
+    ) -> Option<MultiBufferDiffHunk> {
         let display_point = initial_point.to_display_point(snapshot);
         let mut hunks = hunks
-            .map(|hunk| diff_hunk_to_display(&hunk, snapshot))
-            .filter(|hunk| is_wrapped || !hunk.contains_display_row(display_point.row()))
+            .map(|hunk| (diff_hunk_to_display(&hunk, snapshot), hunk))
+            .filter(|(display_hunk, _)| {
+                is_wrapped || !display_hunk.contains_display_row(display_point.row())
+            })
             .dedup();
 
-        if let Some(hunk) = hunks.next() {
+        if let Some((display_hunk, hunk)) = hunks.next() {
             self.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                let row = hunk.start_display_row();
+                let row = display_hunk.start_display_row();
                 let point = DisplayPoint::new(row, 0);
                 s.select_display_ranges([point..point]);
             });
 
-            true
+            Some(hunk)
         } else {
-            false
+            None
         }
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -9623,7 +9623,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext)
     cx.update_editor(|editor, cx| {
         //Wrap around the bottom of the buffer
         for _ in 0..3 {
-            editor.go_to_hunk(&GoToHunk, cx);
+            editor.go_to_next_hunk(&GoToHunk, cx);
         }
     });
 
@@ -9709,7 +9709,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext)
 
         //Make sure that the fold only gets one hunk
         for _ in 0..4 {
-            editor.go_to_hunk(&GoToHunk, cx);
+            editor.go_to_next_hunk(&GoToHunk, cx);
         }
     });
 
@@ -11226,7 +11226,7 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
 
     cx.update_editor(|editor, cx| {
         for _ in 0..4 {
-            editor.go_to_hunk(&GoToHunk, cx);
+            editor.go_to_next_hunk(&GoToHunk, cx);
             editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
         }
     });
@@ -11249,18 +11249,13 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
         let snapshot = editor.snapshot(cx);
         let all_hunks = editor_hunks(editor, &snapshot, cx);
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(
-            expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(1)..=DisplayRow(1), DisplayRow(7)..=DisplayRow(7), DisplayRow(9)..=DisplayRow(9)],
-            "After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
-        );
         assert_eq!(
             all_hunks,
             vec![
-                ("use some::mod;\n".to_string(), DiffHunkStatus::Modified, DisplayRow(1)..DisplayRow(2)),
-                ("const A: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(4)..DisplayRow(4)),
-                ("    println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, DisplayRow(7)..DisplayRow(8)),
-                ("".to_string(), DiffHunkStatus::Added, DisplayRow(9)..DisplayRow(10)),
+                ("use some::mod;\n".to_string(), DiffHunkStatus::Modified, DisplayRow(2)..DisplayRow(3)),
+                ("const A: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(6)..DisplayRow(6)),
+                ("    println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, DisplayRow(10)..DisplayRow(11)),
+                ("".to_string(), DiffHunkStatus::Added, DisplayRow(13)..DisplayRow(14)),
             ],
             "After expanding, all hunks' display rows should have shifted by the amount of deleted lines added \
             (from modified and removed hunks)"
@@ -11269,6 +11264,11 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
             all_hunks, all_expanded_hunks,
             "Editor hunks should not change and all be expanded"
         );
+        assert_eq!(
+            expanded_hunks_background_highlights(editor, cx),
+            vec![DisplayRow(2)..=DisplayRow(2), DisplayRow(10)..=DisplayRow(10), DisplayRow(13)..=DisplayRow(13)],
+            "After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
+        );
     });
 
     cx.update_editor(|editor, cx| {
@@ -11311,7 +11311,7 @@ async fn test_toggled_diff_base_change(
         const B: u32 = 42;
         const C: u32 = 42;
 
-        fn main(ˇ) {
+        fn main() {
             println!("hello");
 
             println!("world");
@@ -11356,9 +11356,9 @@ async fn test_toggled_diff_base_change(
                     DisplayRow(3)..DisplayRow(3)
                 ),
                 (
-                    "fn main(ˇ) {\n    println!(\"hello\");\n".to_string(),
+                    "    println!(\"hello\");\n".to_string(),
                     DiffHunkStatus::Modified,
-                    DisplayRow(5)..DisplayRow(7)
+                    DisplayRow(6)..DisplayRow(7)
                 ),
                 (
                     "".to_string(),
@@ -11390,22 +11390,18 @@ async fn test_toggled_diff_base_change(
         "#
         .unindent(),
     );
+
     cx.update_editor(|editor, cx| {
         let snapshot = editor.snapshot(cx);
         let all_hunks = editor_hunks(editor, &snapshot, cx);
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(
-            expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(9)..=DisplayRow(10), DisplayRow(13)..=DisplayRow(14)],
-            "After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
-        );
         assert_eq!(
             all_hunks,
             vec![
-                ("use some::mod1;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(1)..DisplayRow(1)),
-                ("const B: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(5)..DisplayRow(5)),
-                ("fn main(ˇ) {\n    println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, DisplayRow(9)..DisplayRow(11)),
-                ("".to_string(), DiffHunkStatus::Added, DisplayRow(13)..DisplayRow(15)),
+                ("use some::mod1;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(2)..DisplayRow(2)),
+                ("const B: u32 = 42;\n".to_string(), DiffHunkStatus::Removed, DisplayRow(7)..DisplayRow(7)),
+                ("    println!(\"hello\");\n".to_string(), DiffHunkStatus::Modified, DisplayRow(12)..DisplayRow(13)),
+                ("".to_string(), DiffHunkStatus::Added, DisplayRow(16)..DisplayRow(18)),
             ],
             "After expanding, all hunks' display rows should have shifted by the amount of deleted lines added \
             (from modified and removed hunks)"
@@ -11414,6 +11410,11 @@ async fn test_toggled_diff_base_change(
             all_hunks, all_expanded_hunks,
             "Editor hunks should not change and all be expanded"
         );
+        assert_eq!(
+            expanded_hunks_background_highlights(editor, cx),
+            vec![DisplayRow(12)..=DisplayRow(12), DisplayRow(16)..=DisplayRow(17)],
+            "After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
+        );
     });
 
     cx.set_diff_base(Some("new diff base!"));
@@ -11459,7 +11460,7 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
         const B: u32 = 42;
         const C: u32 = 42;
 
-        fn main(ˇ) {
+        fn main() {
             println!("hello");
 
             println!("world");
@@ -11520,9 +11521,9 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
                     DisplayRow(3)..DisplayRow(3)
                 ),
                 (
-                    "fn main(ˇ) {\n    println!(\"hello\");\n".to_string(),
+                    "    println!(\"hello\");\n".to_string(),
                     DiffHunkStatus::Modified,
-                    DisplayRow(5)..DisplayRow(7)
+                    DisplayRow(6)..DisplayRow(7)
                 ),
                 (
                     "".to_string(),
@@ -11576,50 +11577,50 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
         let snapshot = editor.snapshot(cx);
         let all_hunks = editor_hunks(editor, &snapshot, cx);
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(
-            expanded_hunks_background_highlights(editor, cx),
-            vec![
-                DisplayRow(9)..=DisplayRow(10),
-                DisplayRow(13)..=DisplayRow(14),
-                DisplayRow(19)..=DisplayRow(19)
-            ]
-        );
         assert_eq!(
             all_hunks,
             vec![
                 (
                     "use some::mod1;\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(1)..DisplayRow(1)
+                    DisplayRow(2)..DisplayRow(2)
                 ),
                 (
                     "const B: u32 = 42;\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(5)..DisplayRow(5)
+                    DisplayRow(7)..DisplayRow(7)
                 ),
                 (
-                    "fn main(ˇ) {\n    println!(\"hello\");\n".to_string(),
+                    "    println!(\"hello\");\n".to_string(),
                     DiffHunkStatus::Modified,
-                    DisplayRow(9)..DisplayRow(11)
+                    DisplayRow(12)..DisplayRow(13)
                 ),
                 (
                     "".to_string(),
                     DiffHunkStatus::Added,
-                    DisplayRow(13)..DisplayRow(15)
+                    DisplayRow(16)..DisplayRow(18)
                 ),
                 (
                     "".to_string(),
                     DiffHunkStatus::Added,
-                    DisplayRow(19)..DisplayRow(20)
+                    DisplayRow(23)..DisplayRow(24)
                 ),
                 (
                     "fn another2() {\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(23)..DisplayRow(23)
+                    DisplayRow(28)..DisplayRow(28)
                 ),
             ],
         );
         assert_eq!(all_hunks, all_expanded_hunks);
+        assert_eq!(
+            expanded_hunks_background_highlights(editor, cx),
+            vec![
+                DisplayRow(12)..=DisplayRow(12),
+                DisplayRow(16)..=DisplayRow(17),
+                DisplayRow(23)..=DisplayRow(23)
+            ]
+        );
     });
 
     cx.update_editor(|editor, cx| editor.fold_selected_ranges(&FoldSelectedRanges, cx));
@@ -11653,11 +11654,6 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
         let snapshot = editor.snapshot(cx);
         let all_hunks = editor_hunks(editor, &snapshot, cx);
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(
-            expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(0)..=DisplayRow(0), DisplayRow(5)..=DisplayRow(5)],
-            "Only one hunk is left not folded, its highlight should be visible"
-        );
         assert_eq!(
             all_hunks,
             vec![
@@ -11672,7 +11668,7 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
                     DisplayRow(0)..DisplayRow(0)
                 ),
                 (
-                    "fn main(ˇ) {\n    println!(\"hello\");\n".to_string(),
+                    "    println!(\"hello\");\n".to_string(),
                     DiffHunkStatus::Modified,
                     DisplayRow(0)..DisplayRow(0)
                 ),
@@ -11684,12 +11680,12 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
                 (
                     "".to_string(),
                     DiffHunkStatus::Added,
-                    DisplayRow(5)..DisplayRow(6)
+                    DisplayRow(6)..DisplayRow(7)
                 ),
                 (
                     "fn another2() {\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(9)..DisplayRow(9)
+                    DisplayRow(11)..DisplayRow(11)
                 ),
             ],
             "Hunk list should still return shifted folded hunks"
@@ -11700,16 +11696,21 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
                 (
                     "".to_string(),
                     DiffHunkStatus::Added,
-                    DisplayRow(5)..DisplayRow(6)
+                    DisplayRow(6)..DisplayRow(7)
                 ),
                 (
                     "fn another2() {\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(9)..DisplayRow(9)
+                    DisplayRow(11)..DisplayRow(11)
                 ),
             ],
             "Only non-folded hunks should be left expanded"
         );
+        assert_eq!(
+            expanded_hunks_background_highlights(editor, cx),
+            vec![DisplayRow(0)..=DisplayRow(0), DisplayRow(6)..=DisplayRow(6)],
+            "Only one hunk is left not folded, its highlight should be visible"
+        );
     });
 
     cx.update_editor(|editor, cx| {
@@ -11746,51 +11747,51 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
         let snapshot = editor.snapshot(cx);
         let all_hunks = editor_hunks(editor, &snapshot, cx);
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
-        assert_eq!(
-            expanded_hunks_background_highlights(editor, cx),
-            vec![
-                DisplayRow(9)..=DisplayRow(10),
-                DisplayRow(13)..=DisplayRow(14),
-                DisplayRow(19)..=DisplayRow(19)
-            ],
-            "After unfolding, all hunk diffs should be visible again"
-        );
         assert_eq!(
             all_hunks,
             vec![
                 (
                     "use some::mod1;\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(1)..DisplayRow(1)
+                    DisplayRow(2)..DisplayRow(2)
                 ),
                 (
                     "const B: u32 = 42;\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(5)..DisplayRow(5)
+                    DisplayRow(7)..DisplayRow(7)
                 ),
                 (
-                    "fn main(ˇ) {\n    println!(\"hello\");\n".to_string(),
+                    "    println!(\"hello\");\n".to_string(),
                     DiffHunkStatus::Modified,
-                    DisplayRow(9)..DisplayRow(11)
+                    DisplayRow(12)..DisplayRow(13)
                 ),
                 (
                     "".to_string(),
                     DiffHunkStatus::Added,
-                    DisplayRow(13)..DisplayRow(15)
+                    DisplayRow(16)..DisplayRow(18)
                 ),
                 (
                     "".to_string(),
                     DiffHunkStatus::Added,
-                    DisplayRow(19)..DisplayRow(20)
+                    DisplayRow(23)..DisplayRow(24)
                 ),
                 (
                     "fn another2() {\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(23)..DisplayRow(23)
+                    DisplayRow(28)..DisplayRow(28)
                 ),
             ],
         );
         assert_eq!(all_hunks, all_expanded_hunks);
+        assert_eq!(
+            expanded_hunks_background_highlights(editor, cx),
+            vec![
+                DisplayRow(12)..=DisplayRow(12),
+                DisplayRow(16)..=DisplayRow(17),
+                DisplayRow(23)..=DisplayRow(23)
+            ],
+            "After unfolding, all hunk diffs should be visible again"
+        );
     });
 }
 
@@ -11940,17 +11941,17 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
         (
             "bbbb\n".to_string(),
             DiffHunkStatus::Removed,
-            DisplayRow(5)..DisplayRow(5),
+            DisplayRow(6)..DisplayRow(6),
         ),
         (
             "nnnn\n".to_string(),
             DiffHunkStatus::Modified,
-            DisplayRow(23)..DisplayRow(24),
+            DisplayRow(25)..DisplayRow(26),
         ),
         (
             "".to_string(),
             DiffHunkStatus::Added,
-            DisplayRow(43)..DisplayRow(44),
+            DisplayRow(46)..DisplayRow(47),
         ),
     ];
 
@@ -11975,8 +11976,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
             vec![
-                DisplayRow(23)..=DisplayRow(23),
-                DisplayRow(43)..=DisplayRow(43)
+                DisplayRow(25)..=DisplayRow(25),
+                DisplayRow(46)..=DisplayRow(46)
             ],
         );
         assert_eq!(all_hunks, expected_all_hunks_shifted);
@@ -12007,8 +12008,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
             vec![
-                DisplayRow(23)..=DisplayRow(23),
-                DisplayRow(43)..=DisplayRow(43)
+                DisplayRow(25)..=DisplayRow(25),
+                DisplayRow(46)..=DisplayRow(46)
             ],
         );
         assert_eq!(all_hunks, expected_all_hunks_shifted);
@@ -12116,12 +12117,12 @@ async fn test_edits_around_toggled_additions(
             vec![(
                 "".to_string(),
                 DiffHunkStatus::Added,
-                DisplayRow(4)..DisplayRow(7)
+                DisplayRow(5)..DisplayRow(8)
             )]
         );
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(4)..=DisplayRow(6)]
+            vec![DisplayRow(5)..=DisplayRow(7)]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
     });
@@ -12156,12 +12157,12 @@ async fn test_edits_around_toggled_additions(
             vec![(
                 "".to_string(),
                 DiffHunkStatus::Added,
-                DisplayRow(4)..DisplayRow(8)
+                DisplayRow(5)..DisplayRow(9)
             )]
         );
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(4)..=DisplayRow(6)],
+            vec![DisplayRow(5)..=DisplayRow(7)],
             "Edited hunk should have one more line added"
         );
         assert_eq!(
@@ -12201,12 +12202,12 @@ async fn test_edits_around_toggled_additions(
             vec![(
                 "".to_string(),
                 DiffHunkStatus::Added,
-                DisplayRow(4)..DisplayRow(9)
+                DisplayRow(5)..DisplayRow(10)
             )]
         );
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(4)..=DisplayRow(6)],
+            vec![DisplayRow(5)..=DisplayRow(7)],
             "Edited hunk should have one more line added"
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12245,12 +12246,12 @@ async fn test_edits_around_toggled_additions(
             vec![(
                 "".to_string(),
                 DiffHunkStatus::Added,
-                DisplayRow(4)..DisplayRow(8)
+                DisplayRow(5)..DisplayRow(9)
             )]
         );
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(4)..=DisplayRow(6)],
+            vec![DisplayRow(5)..=DisplayRow(7)],
             "Deleting a line should shrint the hunk"
         );
         assert_eq!(
@@ -12293,12 +12294,12 @@ async fn test_edits_around_toggled_additions(
             vec![(
                 "".to_string(),
                 DiffHunkStatus::Added,
-                DisplayRow(5)..DisplayRow(6)
+                DisplayRow(6)..DisplayRow(7)
             )]
         );
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(5)..=DisplayRow(5)]
+            vec![DisplayRow(6)..=DisplayRow(6)]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
     });
@@ -12335,7 +12336,7 @@ async fn test_edits_around_toggled_additions(
                 (
                     "const A: u32 = 42;\n".to_string(),
                     DiffHunkStatus::Removed,
-                    DisplayRow(2)..DisplayRow(2)
+                    DisplayRow(3)..DisplayRow(3)
                 )
             ]
         );
@@ -12349,7 +12350,7 @@ async fn test_edits_around_toggled_additions(
             vec![(
                 "const A: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Removed,
-                DisplayRow(2)..DisplayRow(2)
+                DisplayRow(3)..DisplayRow(3)
             )],
             "Should open hunks that were adjacent to the stale addition one"
         );
@@ -12445,7 +12446,7 @@ async fn test_edits_around_toggled_deletions(
             vec![(
                 "const A: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Removed,
-                DisplayRow(4)..DisplayRow(4)
+                DisplayRow(5)..DisplayRow(5)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12485,7 +12486,7 @@ async fn test_edits_around_toggled_deletions(
             vec![(
                 "const A: u32 = 42;\nconst B: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Removed,
-                DisplayRow(5)..DisplayRow(5)
+                DisplayRow(6)..DisplayRow(6)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12520,7 +12521,7 @@ async fn test_edits_around_toggled_deletions(
             vec![(
                 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Removed,
-                DisplayRow(6)..DisplayRow(6)
+                DisplayRow(7)..DisplayRow(7)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12554,12 +12555,12 @@ async fn test_edits_around_toggled_deletions(
             vec![(
                 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n\n".to_string(),
                 DiffHunkStatus::Modified,
-                DisplayRow(7)..DisplayRow(8)
+                DisplayRow(8)..DisplayRow(9)
             )]
         );
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(7)..=DisplayRow(7)],
+            vec![DisplayRow(8)..=DisplayRow(8)],
             "Modified expanded hunks should display additions and highlight their background"
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12653,14 +12654,14 @@ async fn test_edits_around_toggled_modifications(
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(6)..=DisplayRow(6)],
+            vec![DisplayRow(7)..=DisplayRow(7)],
         );
         assert_eq!(
             all_hunks,
             vec![(
                 "const C: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Modified,
-                DisplayRow(6)..DisplayRow(7)
+                DisplayRow(7)..DisplayRow(8)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12696,7 +12697,7 @@ async fn test_edits_around_toggled_modifications(
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(6)..=DisplayRow(6)],
+            vec![DisplayRow(7)..=DisplayRow(7)],
             "Modified hunk should grow highlighted lines on more text additions"
         );
         assert_eq!(
@@ -12704,7 +12705,7 @@ async fn test_edits_around_toggled_modifications(
             vec![(
                 "const C: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Modified,
-                DisplayRow(6)..DisplayRow(9)
+                DisplayRow(7)..DisplayRow(10)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12742,14 +12743,14 @@ async fn test_edits_around_toggled_modifications(
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(6)..=DisplayRow(8)],
+            vec![DisplayRow(7)..=DisplayRow(9)],
         );
         assert_eq!(
             all_hunks,
             vec![(
                 "const B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Modified,
-                DisplayRow(6)..DisplayRow(9)
+                DisplayRow(7)..DisplayRow(10)
             )],
             "Modified hunk should grow deleted lines on text deletions above"
         );
@@ -12786,7 +12787,7 @@ async fn test_edits_around_toggled_modifications(
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(6)..=DisplayRow(9)],
+            vec![DisplayRow(7)..=DisplayRow(10)],
             "Modified hunk should grow deleted lines on text modifications above"
         );
         assert_eq!(
@@ -12794,7 +12795,7 @@ async fn test_edits_around_toggled_modifications(
             vec![(
                 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Modified,
-                DisplayRow(6)..DisplayRow(10)
+                DisplayRow(7)..DisplayRow(11)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12830,7 +12831,7 @@ async fn test_edits_around_toggled_modifications(
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(6)..=DisplayRow(8)],
+            vec![DisplayRow(7)..=DisplayRow(9)],
             "Modified hunk should grow shrink lines on modification lines removal"
         );
         assert_eq!(
@@ -12838,7 +12839,7 @@ async fn test_edits_around_toggled_modifications(
             vec![(
                 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Modified,
-                DisplayRow(6)..DisplayRow(9)
+                DisplayRow(7)..DisplayRow(10)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12880,7 +12881,7 @@ async fn test_edits_around_toggled_modifications(
                 "const A: u32 = 42;\nconst B: u32 = 42;\nconst C: u32 = 42;\nconst D: u32 = 42;\n"
                     .to_string(),
                 DiffHunkStatus::Removed,
-                DisplayRow(7)..DisplayRow(7)
+                DisplayRow(8)..DisplayRow(8)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);
@@ -12974,14 +12975,14 @@ async fn test_multiple_expanded_hunks_merge(
         let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
         assert_eq!(
             expanded_hunks_background_highlights(editor, cx),
-            vec![DisplayRow(6)..=DisplayRow(6)],
+            vec![DisplayRow(7)..=DisplayRow(7)],
         );
         assert_eq!(
             all_hunks,
             vec![(
                 "const C: u32 = 42;\n".to_string(),
                 DiffHunkStatus::Modified,
-                DisplayRow(6)..DisplayRow(7)
+                DisplayRow(7)..DisplayRow(8)
             )]
         );
         assert_eq!(all_hunks, all_expanded_hunks);

crates/editor/src/element.rs 🔗

@@ -11,7 +11,7 @@ use crate::{
     hover_popover::{
         self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
     },
-    hunk_diff::{diff_hunk_to_display, DisplayDiffHunk, ExpandedHunk},
+    hunk_diff::{diff_hunk_to_display, DisplayDiffHunk},
     hunk_status,
     items::BufferSearchHighlights,
     mouse_context_menu::{self, MenuPosition, MouseContextMenu},
@@ -20,8 +20,8 @@ use crate::{
     DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
     EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
     HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown,
-    PageUp, Point, RangeToAnchorExt, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap,
-    ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
+    PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
+    CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
 };
 use client::ParticipantIndex;
 use collections::{BTreeMap, HashMap};
@@ -302,7 +302,7 @@ impl EditorElement {
         }
         register_action(view, cx, Editor::go_to_diagnostic);
         register_action(view, cx, Editor::go_to_prev_diagnostic);
-        register_action(view, cx, Editor::go_to_hunk);
+        register_action(view, cx, Editor::go_to_next_hunk);
         register_action(view, cx, Editor::go_to_prev_hunk);
         register_action(view, cx, |editor, a, cx| {
             editor.go_to_definition(a, cx).detach_and_log_err(cx);
@@ -489,28 +489,7 @@ impl EditorElement {
         let mut modifiers = event.modifiers;
 
         if let Some(hovered_hunk) = hovered_hunk {
-            if modifiers.control || modifiers.platform {
-                editor.toggle_hovered_hunk(&hovered_hunk, cx);
-            } else {
-                let display_range = hovered_hunk
-                    .multi_buffer_range
-                    .clone()
-                    .to_display_points(&position_map.snapshot);
-                let hunk_bounds = Self::diff_hunk_bounds(
-                    &position_map.snapshot,
-                    position_map.line_height,
-                    gutter_hitbox.bounds,
-                    &DisplayDiffHunk::Unfolded {
-                        diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
-                        display_row_range: display_range.start.row()..display_range.end.row(),
-                        multi_buffer_range: hovered_hunk.multi_buffer_range.clone(),
-                        status: hovered_hunk.status,
-                    },
-                );
-                if hunk_bounds.contains(&event.position) {
-                    editor.open_hunk_context_menu(hovered_hunk, event.position, cx);
-                }
-            }
+            editor.toggle_hovered_hunk(&hovered_hunk, cx);
             cx.notify();
             return;
         } else if gutter_hitbox.is_hovered(cx) {
@@ -1303,13 +1282,13 @@ impl EditorElement {
             let display_hunks = buffer_snapshot
                 .git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
                 .filter_map(|hunk| {
-                    let mut display_hunk = diff_hunk_to_display(&hunk, snapshot);
+                    let display_hunk = diff_hunk_to_display(&hunk, snapshot);
 
                     if let DisplayDiffHunk::Unfolded {
                         multi_buffer_range,
                         status,
                         ..
-                    } = &mut display_hunk
+                    } = &display_hunk
                     {
                         let mut is_expanded = false;
                         while let Some(expanded_hunk) = expanded_hunks.peek() {
@@ -1332,11 +1311,7 @@ impl EditorElement {
                         }
                         match status {
                             DiffHunkStatus::Added => {}
-                            DiffHunkStatus::Modified => {
-                                if is_expanded {
-                                    *status = DiffHunkStatus::Added;
-                                }
-                            }
+                            DiffHunkStatus::Modified => {}
                             DiffHunkStatus::Removed => {
                                 if is_expanded {
                                     return None;
@@ -3371,9 +3346,6 @@ impl EditorElement {
             for test_indicator in layout.test_indicators.iter_mut() {
                 test_indicator.paint(cx);
             }
-            for close_indicator in layout.close_indicators.iter_mut() {
-                close_indicator.paint(cx);
-            }
 
             if let Some(indicator) = layout.code_actions_indicator.as_mut() {
                 indicator.paint(cx);
@@ -4159,46 +4131,6 @@ impl EditorElement {
             + 1;
         self.column_pixels(digit_count, cx)
     }
-
-    #[allow(clippy::too_many_arguments)]
-    fn layout_hunk_diff_close_indicators(
-        &self,
-        line_height: Pixels,
-        scroll_pixel_position: gpui::Point<Pixels>,
-        gutter_dimensions: &GutterDimensions,
-        gutter_hitbox: &Hitbox,
-        rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
-        expanded_hunks_by_rows: HashMap<DisplayRow, ExpandedHunk>,
-        cx: &mut WindowContext,
-    ) -> Vec<AnyElement> {
-        self.editor.update(cx, |editor, cx| {
-            expanded_hunks_by_rows
-                .into_iter()
-                .map(|(display_row, hunk)| {
-                    let button = editor.close_hunk_diff_button(
-                        HoveredHunk {
-                            multi_buffer_range: hunk.hunk_range,
-                            status: hunk.status,
-                            diff_base_byte_range: hunk.diff_base_byte_range,
-                        },
-                        display_row,
-                        cx,
-                    );
-
-                    prepaint_gutter_button(
-                        button,
-                        display_row,
-                        line_height,
-                        gutter_dimensions,
-                        scroll_pixel_position,
-                        gutter_hitbox,
-                        rows_with_hunk_bounds,
-                        cx,
-                    )
-                })
-                .collect()
-        })
-    }
 }
 
 #[allow(clippy::too_many_arguments)]
@@ -5549,15 +5481,6 @@ impl Element for EditorElement {
                     } else {
                         Vec::new()
                     };
-                    let close_indicators = self.layout_hunk_diff_close_indicators(
-                        line_height,
-                        scroll_pixel_position,
-                        &gutter_dimensions,
-                        &gutter_hitbox,
-                        &rows_with_hunk_bounds,
-                        expanded_add_hunks_by_rows,
-                        cx,
-                    );
 
                     self.layout_signature_help(
                         &hitbox,
@@ -5670,7 +5593,6 @@ impl Element for EditorElement {
                         selections,
                         mouse_context_menu,
                         test_indicators,
-                        close_indicators,
                         code_actions_indicator,
                         gutter_fold_toggles,
                         crease_trailers,
@@ -5812,7 +5734,6 @@ pub struct EditorLayout {
     selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
     code_actions_indicator: Option<AnyElement>,
     test_indicators: Vec<AnyElement>,
-    close_indicators: Vec<AnyElement>,
     gutter_fold_toggles: Vec<Option<AnyElement>>,
     crease_trailers: Vec<Option<CreaseTrailerLayout>>,
     mouse_context_menu: Option<AnyElement>,

crates/editor/src/hunk_diff.rs 🔗

@@ -1,28 +1,26 @@
 use collections::{hash_map, HashMap, HashSet};
 use git::diff::DiffHunkStatus;
-use gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View};
+use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Task, View};
 use language::{Buffer, BufferId, Point};
 use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
     MultiBufferSnapshot, ToPoint,
 };
-use settings::SettingsStore;
 use std::{
     ops::{Range, RangeInclusive},
     sync::Arc,
 };
 use ui::{
-    prelude::*, ActiveTheme, ContextMenu, InteractiveElement, IntoElement, ParentElement, Pixels,
-    Styled, ViewContext, VisualContext,
+    prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement,
+    ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext,
 };
-use util::{debug_panic, RangeExt};
+use util::RangeExt;
 
 use crate::{
-    editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections,
-    mouse_context_menu::MouseContextMenu, BlockDisposition, BlockProperties, BlockStyle,
-    CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, Editor, EditorElement,
-    EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertFile, RevertSelectedHunks,
-    ToDisplayPoint, ToggleHunkDiff,
+    editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, BlockDisposition,
+    BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot,
+    Editor, EditorElement, EditorSnapshot, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk,
+    RangeToAnchorExt, RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
 };
 
 #[derive(Debug, Clone)]
@@ -41,7 +39,7 @@ pub(super) struct ExpandedHunks {
 
 #[derive(Debug, Clone)]
 pub(super) struct ExpandedHunk {
-    pub block: Option<CustomBlockId>,
+    pub blocks: Vec<CustomBlockId>,
     pub hunk_range: Range<Anchor>,
     pub diff_base_byte_range: Range<usize>,
     pub status: DiffHunkStatus,
@@ -77,85 +75,6 @@ impl ExpandedHunks {
 }
 
 impl Editor {
-    pub(super) fn open_hunk_context_menu(
-        &mut self,
-        hovered_hunk: HoveredHunk,
-        clicked_point: gpui::Point<Pixels>,
-        cx: &mut ViewContext<Editor>,
-    ) {
-        let focus_handle = self.focus_handle.clone();
-        let expanded = self
-            .expanded_hunks
-            .hunks(false)
-            .any(|expanded_hunk| expanded_hunk.hunk_range == hovered_hunk.multi_buffer_range);
-        let editor_handle = cx.view().clone();
-        let editor_snapshot = self.snapshot(cx);
-        let start_point = self
-            .to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx)
-            .unwrap_or(clicked_point);
-        let end_point = self
-            .to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx)
-            .unwrap_or(clicked_point);
-        let norm =
-            |a: gpui::Point<Pixels>, b: gpui::Point<Pixels>| (a.x - b.x).abs() + (a.y - b.y).abs();
-        let closest_source = if norm(start_point, clicked_point) < norm(end_point, clicked_point) {
-            hovered_hunk.multi_buffer_range.start
-        } else {
-            hovered_hunk.multi_buffer_range.end
-        };
-
-        self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
-            self,
-            closest_source,
-            clicked_point,
-            ContextMenu::build(cx, move |menu, _| {
-                menu.on_blur_subscription(Subscription::new(|| {}))
-                    .context(focus_handle)
-                    .entry(
-                        if expanded {
-                            "Collapse Hunk"
-                        } else {
-                            "Expand Hunk"
-                        },
-                        Some(ToggleHunkDiff.boxed_clone()),
-                        {
-                            let editor = editor_handle.clone();
-                            let hunk = hovered_hunk.clone();
-                            move |cx| {
-                                editor.update(cx, |editor, cx| {
-                                    editor.toggle_hovered_hunk(&hunk, cx);
-                                });
-                            }
-                        },
-                    )
-                    .entry("Revert Hunk", Some(RevertSelectedHunks.boxed_clone()), {
-                        let editor = editor_handle.clone();
-                        let hunk = hovered_hunk.clone();
-                        move |cx| {
-                            let multi_buffer = editor.read(cx).buffer().clone();
-                            let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
-                            let mut revert_changes = HashMap::default();
-                            if let Some(hunk) =
-                                crate::hunk_diff::to_diff_hunk(&hunk, &multi_buffer_snapshot)
-                            {
-                                Editor::prepare_revert_change(
-                                    &mut revert_changes,
-                                    &multi_buffer,
-                                    &hunk,
-                                    cx,
-                                );
-                            }
-                            if !revert_changes.is_empty() {
-                                editor.update(cx, |editor, cx| editor.revert(revert_changes, cx));
-                            }
-                        }
-                    })
-                    .action("Revert File", RevertFile.boxed_clone())
-            }),
-            cx,
-        )
-    }
-
     pub(super) fn toggle_hovered_hunk(
         &mut self,
         hovered_hunk: &HoveredHunk,
@@ -264,7 +183,8 @@ impl Editor {
                                         break;
                                     } else if expanded_hunk_row_range == hunk_to_toggle_row_range {
                                         highlights_to_remove.push(expanded_hunk.hunk_range.clone());
-                                        blocks_to_remove.extend(expanded_hunk.block);
+                                        blocks_to_remove
+                                            .extend(expanded_hunk.blocks.iter().copied());
                                         hunks_to_toggle.next();
                                         retain = false;
                                         break;
@@ -371,9 +291,17 @@ impl Editor {
             Err(ix) => ix,
         };
 
-        let block = match hunk.status {
+        let blocks;
+        match hunk.status {
             DiffHunkStatus::Removed => {
-                self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, hunk, cx)
+                blocks = self.insert_blocks(
+                    [
+                        self.hunk_header_block(&hunk, cx),
+                        Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx),
+                    ],
+                    None,
+                    cx,
+                );
             }
             DiffHunkStatus::Added => {
                 self.highlight_rows::<DiffRowHighlight>(
@@ -382,7 +310,7 @@ impl Editor {
                     false,
                     cx,
                 );
-                None
+                blocks = self.insert_blocks([self.hunk_header_block(&hunk, cx)], None, cx);
             }
             DiffHunkStatus::Modified => {
                 self.highlight_rows::<DiffRowHighlight>(
@@ -391,13 +319,20 @@ impl Editor {
                     false,
                     cx,
                 );
-                self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, hunk, cx)
+                blocks = self.insert_blocks(
+                    [
+                        self.hunk_header_block(&hunk, cx),
+                        Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx),
+                    ],
+                    None,
+                    cx,
+                );
             }
         };
         self.expanded_hunks.hunks.insert(
             block_insert_index,
             ExpandedHunk {
-                block,
+                blocks,
                 hunk_range: hunk_start..hunk_end,
                 status: hunk.status,
                 folded: false,
@@ -408,109 +343,368 @@ impl Editor {
         Some(())
     }
 
-    fn insert_deleted_text_block(
-        &mut self,
-        diff_base_buffer: Model<Buffer>,
-        deleted_text_height: u32,
+    fn hunk_header_block(
+        &self,
         hunk: &HoveredHunk,
-        cx: &mut ViewContext<'_, Self>,
-    ) -> Option<CustomBlockId> {
-        let deleted_hunk_color = deleted_hunk_color(cx);
-        let (editor_height, editor_with_deleted_text) =
-            editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
-        let editor = cx.view().clone();
-        let hunk = hunk.clone();
-        let height = editor_height.max(deleted_text_height);
-        let mut new_block_ids = self.insert_blocks(
-            Some(BlockProperties {
-                position: hunk.multi_buffer_range.start,
-                height,
-                style: BlockStyle::Flex,
-                disposition: BlockDisposition::Above,
-                render: Box::new(move |cx| {
-                    let width = EditorElement::diff_hunk_strip_width(cx.line_height());
-                    let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
-
-                    let close_button = editor.update(cx.context, |editor, cx| {
-                        let editor_snapshot = editor.snapshot(cx);
-                        let hunk_display_range = hunk
-                            .multi_buffer_range
-                            .clone()
-                            .to_display_points(&editor_snapshot);
-                        editor.close_hunk_diff_button(
-                            hunk.clone(),
-                            hunk_display_range.start.row(),
-                            cx,
-                        )
-                    });
+        cx: &mut ViewContext<'_, Editor>,
+    ) -> BlockProperties<Anchor> {
+        let border_color = cx.theme().colors().border_disabled;
+        let gutter_color = match hunk.status {
+            DiffHunkStatus::Added => cx.theme().status().created,
+            DiffHunkStatus::Modified => cx.theme().status().modified,
+            DiffHunkStatus::Removed => cx.theme().status().deleted,
+        };
+
+        BlockProperties {
+            position: hunk.multi_buffer_range.start,
+            height: 1,
+            style: BlockStyle::Sticky,
+            disposition: BlockDisposition::Above,
+            priority: 0,
+            render: Box::new({
+                let editor = cx.view().clone();
+                let hunk = hunk.clone();
+                move |cx| {
+                    let hunk_controls_menu_handle =
+                        editor.read(cx).hunk_controls_menu_handle.clone();
 
                     h_flex()
-                        .id("gutter with editor")
-                        .bg(deleted_hunk_color)
-                        .h(height as f32 * cx.line_height())
+                        .id(cx.block_id)
                         .w_full()
+                        .h(cx.line_height())
+                        .child(
+                            div()
+                                .id("gutter-strip")
+                                .w(EditorElement::diff_hunk_strip_width(cx.line_height()))
+                                .h_full()
+                                .bg(gutter_color)
+                                .cursor(CursorStyle::PointingHand)
+                                .on_click({
+                                    let editor = editor.clone();
+                                    let hunk = hunk.clone();
+                                    move |_event, cx| {
+                                        editor.update(cx, |editor, cx| {
+                                            editor.toggle_hovered_hunk(&hunk, cx);
+                                        });
+                                    }
+                                }),
+                        )
                         .child(
                             h_flex()
-                                .id("gutter")
-                                .max_w(gutter_dimensions.full_width())
-                                .min_w(gutter_dimensions.full_width())
                                 .size_full()
+                                .justify_between()
+                                .border_t_1()
+                                .border_color(border_color)
                                 .child(
                                     h_flex()
-                                        .id("gutter hunk")
-                                        .bg(cx.theme().status().deleted)
-                                        .pl(gutter_dimensions.margin
-                                            + gutter_dimensions
-                                                .git_blame_entries_width
-                                                .unwrap_or_default())
-                                        .max_w(width)
-                                        .min_w(width)
-                                        .size_full()
-                                        .cursor(CursorStyle::PointingHand)
-                                        .on_mouse_down(MouseButton::Left, {
-                                            let editor = editor.clone();
-                                            let hunk = hunk.clone();
-                                            move |event, cx| {
-                                                let modifiers = event.modifiers;
-                                                if modifiers.control || modifiers.platform {
-                                                    editor.update(cx, |editor, cx| {
-                                                        editor.toggle_hovered_hunk(&hunk, cx);
-                                                    });
-                                                } else {
-                                                    editor.update(cx, |editor, cx| {
-                                                        editor.open_hunk_context_menu(
-                                                            hunk.clone(),
-                                                            event.position,
+                                        .gap_2()
+                                        .pl_6()
+                                        .child(
+                                            IconButton::new("next-hunk", IconName::ArrowDown)
+                                                .shape(IconButtonShape::Square)
+                                                .icon_size(IconSize::Small)
+                                                .tooltip({
+                                                    let focus_handle = editor.focus_handle(cx);
+                                                    move |cx| {
+                                                        Tooltip::for_action_in(
+                                                            "Next Hunk",
+                                                            &GoToHunk,
+                                                            &focus_handle,
                                                             cx,
-                                                        );
-                                                    });
-                                                }
-                                            }
-                                        }),
+                                                        )
+                                                    }
+                                                })
+                                                .on_click({
+                                                    let editor = editor.clone();
+                                                    let hunk = hunk.clone();
+                                                    move |_event, cx| {
+                                                        editor.update(cx, |editor, cx| {
+                                                            let snapshot = editor.snapshot(cx);
+                                                            let position = hunk
+                                                                .multi_buffer_range
+                                                                .end
+                                                                .to_point(
+                                                                    &snapshot.buffer_snapshot,
+                                                                );
+                                                            if let Some(hunk) = editor
+                                                                .go_to_hunk_after_position(
+                                                                    &snapshot, position, cx,
+                                                                )
+                                                            {
+                                                                let multi_buffer_start = snapshot
+                                                                    .buffer_snapshot
+                                                                    .anchor_before(Point::new(
+                                                                        hunk.row_range.start.0,
+                                                                        0,
+                                                                    ));
+                                                                let multi_buffer_end = snapshot
+                                                                    .buffer_snapshot
+                                                                    .anchor_after(Point::new(
+                                                                        hunk.row_range.end.0,
+                                                                        0,
+                                                                    ));
+                                                                editor.expand_diff_hunk(
+                                                                    None,
+                                                                    &HoveredHunk {
+                                                                        multi_buffer_range:
+                                                                            multi_buffer_start
+                                                                                ..multi_buffer_end,
+                                                                        status: hunk_status(&hunk),
+                                                                        diff_base_byte_range: hunk
+                                                                            .diff_base_byte_range,
+                                                                    },
+                                                                    cx,
+                                                                );
+                                                            }
+                                                        });
+                                                    }
+                                                }),
+                                        )
+                                        .child(
+                                            IconButton::new("prev-hunk", IconName::ArrowUp)
+                                                .shape(IconButtonShape::Square)
+                                                .icon_size(IconSize::Small)
+                                                .tooltip({
+                                                    let focus_handle = editor.focus_handle(cx);
+                                                    move |cx| {
+                                                        Tooltip::for_action_in(
+                                                            "Previous Hunk",
+                                                            &GoToPrevHunk,
+                                                            &focus_handle,
+                                                            cx,
+                                                        )
+                                                    }
+                                                })
+                                                .on_click({
+                                                    let editor = editor.clone();
+                                                    let hunk = hunk.clone();
+                                                    move |_event, cx| {
+                                                        editor.update(cx, |editor, cx| {
+                                                            let snapshot = editor.snapshot(cx);
+                                                            let position = hunk
+                                                                .multi_buffer_range
+                                                                .start
+                                                                .to_point(
+                                                                    &snapshot.buffer_snapshot,
+                                                                );
+                                                            let hunk = editor
+                                                                .go_to_hunk_before_position(
+                                                                    &snapshot, position, cx,
+                                                                );
+                                                            if let Some(hunk) = hunk {
+                                                                let multi_buffer_start = snapshot
+                                                                    .buffer_snapshot
+                                                                    .anchor_before(Point::new(
+                                                                        hunk.row_range.start.0,
+                                                                        0,
+                                                                    ));
+                                                                let multi_buffer_end = snapshot
+                                                                    .buffer_snapshot
+                                                                    .anchor_after(Point::new(
+                                                                        hunk.row_range.end.0,
+                                                                        0,
+                                                                    ));
+                                                                editor.expand_diff_hunk(
+                                                                    None,
+                                                                    &HoveredHunk {
+                                                                        multi_buffer_range:
+                                                                            multi_buffer_start
+                                                                                ..multi_buffer_end,
+                                                                        status: hunk_status(&hunk),
+                                                                        diff_base_byte_range: hunk
+                                                                            .diff_base_byte_range,
+                                                                    },
+                                                                    cx,
+                                                                );
+                                                            }
+                                                        });
+                                                    }
+                                                }),
+                                        ),
                                 )
                                 .child(
-                                    v_flex()
-                                        .size_full()
-                                        .pt(rems(0.25))
-                                        .justify_start()
-                                        .child(close_button),
+                                    h_flex()
+                                        .gap_2()
+                                        .pr_6()
+                                        .child({
+                                            let focus = editor.focus_handle(cx);
+                                            PopoverMenu::new("hunk-controls-dropdown")
+                                                .trigger(
+                                                    IconButton::new(
+                                                        "toggle_editor_selections_icon",
+                                                        IconName::EllipsisVertical,
+                                                    )
+                                                    .shape(IconButtonShape::Square)
+                                                    .icon_size(IconSize::Small)
+                                                    .style(ButtonStyle::Subtle)
+                                                    .selected(
+                                                        hunk_controls_menu_handle.is_deployed(),
+                                                    )
+                                                    .when(
+                                                        !hunk_controls_menu_handle.is_deployed(),
+                                                        |this| {
+                                                            this.tooltip(|cx| {
+                                                                Tooltip::text("Hunk Controls", cx)
+                                                            })
+                                                        },
+                                                    ),
+                                                )
+                                                .anchor(AnchorCorner::TopRight)
+                                                .with_handle(hunk_controls_menu_handle)
+                                                .menu(move |cx| {
+                                                    let focus = focus.clone();
+                                                    let menu =
+                                                        ContextMenu::build(cx, move |menu, _| {
+                                                            menu.context(focus.clone()).action(
+                                                                "Discard All",
+                                                                RevertFile.boxed_clone(),
+                                                            )
+                                                        });
+                                                    Some(menu)
+                                                })
+                                        })
+                                        .child(
+                                            IconButton::new("discard", IconName::RotateCcw)
+                                                .shape(IconButtonShape::Square)
+                                                .icon_size(IconSize::Small)
+                                                .tooltip({
+                                                    let focus_handle = editor.focus_handle(cx);
+                                                    move |cx| {
+                                                        Tooltip::for_action_in(
+                                                            "Discard Hunk",
+                                                            &RevertSelectedHunks,
+                                                            &focus_handle,
+                                                            cx,
+                                                        )
+                                                    }
+                                                })
+                                                .on_click({
+                                                    let editor = editor.clone();
+                                                    let hunk = hunk.clone();
+                                                    move |_event, cx| {
+                                                        let multi_buffer =
+                                                            editor.read(cx).buffer().clone();
+                                                        let multi_buffer_snapshot =
+                                                            multi_buffer.read(cx).snapshot(cx);
+                                                        let mut revert_changes = HashMap::default();
+                                                        if let Some(hunk) =
+                                                            crate::hunk_diff::to_diff_hunk(
+                                                                &hunk,
+                                                                &multi_buffer_snapshot,
+                                                            )
+                                                        {
+                                                            Editor::prepare_revert_change(
+                                                                &mut revert_changes,
+                                                                &multi_buffer,
+                                                                &hunk,
+                                                                cx,
+                                                            );
+                                                        }
+                                                        if !revert_changes.is_empty() {
+                                                            editor.update(cx, |editor, cx| {
+                                                                editor.revert(revert_changes, cx)
+                                                            });
+                                                        }
+                                                    }
+                                                }),
+                                        )
+                                        .child(
+                                            IconButton::new("collapse", IconName::Close)
+                                                .shape(IconButtonShape::Square)
+                                                .icon_size(IconSize::Small)
+                                                .tooltip({
+                                                    let focus_handle = editor.focus_handle(cx);
+                                                    move |cx| {
+                                                        Tooltip::for_action_in(
+                                                            "Collapse Hunk",
+                                                            &ToggleHunkDiff,
+                                                            &focus_handle,
+                                                            cx,
+                                                        )
+                                                    }
+                                                })
+                                                .on_click({
+                                                    let editor = editor.clone();
+                                                    let hunk = hunk.clone();
+                                                    move |_event, cx| {
+                                                        editor.update(cx, |editor, cx| {
+                                                            editor.toggle_hovered_hunk(&hunk, cx);
+                                                        });
+                                                    }
+                                                }),
+                                        ),
                                 ),
                         )
-                        .child(editor_with_deleted_text.clone())
                         .into_any_element()
-                }),
-                priority: 0,
+                }
+            }),
+        }
+    }
+
+    fn deleted_text_block(
+        hunk: &HoveredHunk,
+        diff_base_buffer: Model<Buffer>,
+        deleted_text_height: u32,
+        cx: &mut ViewContext<'_, Editor>,
+    ) -> BlockProperties<Anchor> {
+        let gutter_color = match hunk.status {
+            DiffHunkStatus::Added => unreachable!(),
+            DiffHunkStatus::Modified => cx.theme().status().modified,
+            DiffHunkStatus::Removed => cx.theme().status().deleted,
+        };
+        let deleted_hunk_color = deleted_hunk_color(cx);
+        let (editor_height, editor_with_deleted_text) =
+            editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
+        let editor = cx.view().clone();
+        let hunk = hunk.clone();
+        let height = editor_height.max(deleted_text_height);
+        BlockProperties {
+            position: hunk.multi_buffer_range.start,
+            height,
+            style: BlockStyle::Flex,
+            disposition: BlockDisposition::Above,
+            priority: 0,
+            render: Box::new(move |cx| {
+                let width = EditorElement::diff_hunk_strip_width(cx.line_height());
+                let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
+
+                h_flex()
+                    .id(cx.block_id)
+                    .bg(deleted_hunk_color)
+                    .h(height as f32 * cx.line_height())
+                    .w_full()
+                    .child(
+                        h_flex()
+                            .id("gutter")
+                            .max_w(gutter_dimensions.full_width())
+                            .min_w(gutter_dimensions.full_width())
+                            .size_full()
+                            .child(
+                                h_flex()
+                                    .id("gutter hunk")
+                                    .bg(gutter_color)
+                                    .pl(gutter_dimensions.margin
+                                        + gutter_dimensions
+                                            .git_blame_entries_width
+                                            .unwrap_or_default())
+                                    .max_w(width)
+                                    .min_w(width)
+                                    .size_full()
+                                    .cursor(CursorStyle::PointingHand)
+                                    .on_mouse_down(MouseButton::Left, {
+                                        let editor = editor.clone();
+                                        let hunk = hunk.clone();
+                                        move |_event, cx| {
+                                            editor.update(cx, |editor, cx| {
+                                                editor.toggle_hovered_hunk(&hunk, cx);
+                                            });
+                                        }
+                                    }),
+                            ),
+                    )
+                    .child(editor_with_deleted_text.clone())
+                    .into_any_element()
             }),
-            None,
-            cx,
-        );
-        if new_block_ids.len() == 1 {
-            new_block_ids.pop()
-        } else {
-            debug_panic!(
-                "Inserted one editor block but did not receive exactly one block id: {new_block_ids:?}"
-            );
-            None
         }
     }
 
@@ -521,7 +715,7 @@ impl Editor {
             .expanded_hunks
             .hunks
             .drain(..)
-            .filter_map(|expanded_hunk| expanded_hunk.block)
+            .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
             .collect::<HashSet<_>>();
         if to_remove.is_empty() {
             false
@@ -603,7 +797,7 @@ impl Editor {
                                             expanded_hunk.folded = true;
                                             highlights_to_remove
                                                 .push(expanded_hunk.hunk_range.clone());
-                                            if let Some(block) = expanded_hunk.block.take() {
+                                            for block in expanded_hunk.blocks.drain(..) {
                                                 blocks_to_remove.insert(block);
                                             }
                                             break;
@@ -650,7 +844,7 @@ impl Editor {
                             }
                         }
                         if !retain {
-                            blocks_to_remove.extend(expanded_hunk.block);
+                            blocks_to_remove.extend(expanded_hunk.blocks.drain(..));
                             highlights_to_remove.push(expanded_hunk.hunk_range.clone());
                         }
                         retain
@@ -749,7 +943,7 @@ fn added_hunk_color(cx: &AppContext) -> Hsla {
 }
 
 fn deleted_hunk_color(cx: &AppContext) -> Hsla {
-    let mut deleted_color = cx.theme().status().git().deleted;
+    let mut deleted_color = cx.theme().status().deleted;
     deleted_color.fade_out(0.7);
     deleted_color
 }
@@ -788,32 +982,15 @@ fn editor_with_deleted_text(
             false,
             cx,
         );
-
-        let subscription_editor = parent_editor.clone();
-        editor._subscriptions.extend([
-            cx.on_blur(&editor.focus_handle, |editor, cx| {
-                editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
+        editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
+        editor
+            ._subscriptions
+            .extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
                 editor.change_selections(None, cx, |s| {
                     s.try_cancel();
                 });
-                cx.notify();
-            }),
-            cx.on_focus(&editor.focus_handle, move |editor, cx| {
-                let restored_highlight = if let Some(parent_editor) = subscription_editor.upgrade()
-                {
-                    parent_editor.read(cx).current_line_highlight
-                } else {
-                    None
-                };
-                editor.set_current_line_highlight(restored_highlight);
-                cx.notify();
-            }),
-            cx.observe_global::<SettingsStore>(|editor, cx| {
-                if !editor.is_focused(cx) {
-                    editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
-                }
-            }),
-        ]);
+            })]);
+
         let parent_editor_for_reverts = parent_editor.clone();
         let original_multi_buffer_range = hunk.multi_buffer_range.clone();
         let diff_base_range = hunk.diff_base_byte_range.clone();