git: Fix misalignment in the split diff when inlays fall at the end of an excerpt (#49078) (cherry-pick to preview) (#49124)

zed-zippy[bot] and Cole Miller created

Cherry-pick of #49078 to preview

----
- [x] Tests or screenshots needed?
- [ ] Code Reviewed
- [x] Manual QA

Release Notes:

- Fixed misalignment of lines in the split diff when using inlay hints.

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

crates/editor/src/display_map/block_map.rs | 39 ++++++---
crates/editor/src/display_map/inlay_map.rs |  8 +-
crates/editor/src/split.rs                 | 93 ++++++++++++++++++++++++
3 files changed, 121 insertions(+), 19 deletions(-)

Detailed changes

crates/editor/src/display_map/block_map.rs 🔗

@@ -1017,8 +1017,9 @@ impl BlockMap {
                 |point, bias| {
                     wrap_point_cursor
                         .map(
-                            tab_point_cursor
-                                .map(fold_point_cursor.map(inlay_point_cursor.map(point), bias)),
+                            tab_point_cursor.map(
+                                fold_point_cursor.map(inlay_point_cursor.map(point, bias), bias),
+                            ),
                         )
                         .row()
                 },
@@ -1232,32 +1233,33 @@ impl BlockMap {
         let mut companion_tab_point_cursor = companion_snapshot.tab_point_cursor();
         let mut companion_wrap_point_cursor = companion_snapshot.wrap_point_cursor();
 
-        let mut our_wrapper = |our_point: Point| {
+        let mut our_wrapper = |our_point: Point, bias: Bias| {
             our_wrap_point_cursor
                 .map(our_tab_point_cursor.map(
-                    our_fold_point_cursor.map(our_inlay_point_cursor.map(our_point), Bias::Left),
+                    our_fold_point_cursor.map(our_inlay_point_cursor.map(our_point, bias), bias),
                 ))
                 .row()
         };
-        let mut companion_wrapper = |their_point: Point| {
+        let mut companion_wrapper = |their_point: Point, bias: Bias| {
             companion_wrap_point_cursor
                 .map(
                     companion_tab_point_cursor.map(
                         companion_fold_point_cursor
-                            .map(companion_inlay_point_cursor.map(their_point), Bias::Left),
+                            .map(companion_inlay_point_cursor.map(their_point, bias), bias),
                     ),
                 )
                 .row()
         };
         fn determine_spacer(
-            our_wrapper: &mut impl FnMut(Point) -> WrapRow,
-            companion_wrapper: &mut impl FnMut(Point) -> WrapRow,
+            our_wrapper: &mut impl FnMut(Point, Bias) -> WrapRow,
+            companion_wrapper: &mut impl FnMut(Point, Bias) -> WrapRow,
             our_point: Point,
             their_point: Point,
             delta: i32,
+            bias: Bias,
         ) -> (i32, Option<(WrapRow, u32)>) {
-            let our_wrap = our_wrapper(our_point);
-            let companion_wrap = companion_wrapper(their_point);
+            let our_wrap = our_wrapper(our_point, bias);
+            let companion_wrap = companion_wrapper(their_point, bias);
             let new_delta = companion_wrap.0 as i32 - our_wrap.0 as i32;
 
             let spacer = if new_delta > delta {
@@ -1313,9 +1315,11 @@ impl BlockMap {
                 // Case 3: We are at the start of the excerpt--no previous row to use as the baseline.
                 (first_point, edit_for_first_point.new.start)
             };
-            let our_baseline = our_wrapper(our_baseline);
-            let their_baseline =
-                companion_wrapper(their_baseline.min(excerpt.target_excerpt_range.end));
+            let our_baseline = our_wrapper(our_baseline, Bias::Left);
+            let their_baseline = companion_wrapper(
+                their_baseline.min(excerpt.target_excerpt_range.end),
+                Bias::Left,
+            );
 
             let mut delta = their_baseline.0 as i32 - our_baseline.0 as i32;
 
@@ -1338,6 +1342,7 @@ impl BlockMap {
                     current_boundary,
                     current_range.end.min(excerpt.target_excerpt_range.end),
                     delta,
+                    Bias::Left,
                 );
 
                 delta = new_delta;
@@ -1371,6 +1376,7 @@ impl BlockMap {
                     current_boundary,
                     current_range.start.min(excerpt.target_excerpt_range.end),
                     delta,
+                    Bias::Left,
                 );
                 delta = delta_at_start;
 
@@ -1411,6 +1417,7 @@ impl BlockMap {
                         current_boundary,
                         current_range.end.min(excerpt.target_excerpt_range.end),
                         delta,
+                        Bias::Left,
                     );
                     delta = delta_at_end;
                     spacer_at_end
@@ -1449,6 +1456,7 @@ impl BlockMap {
                     last_source_point,
                     excerpt.target_excerpt_range.end,
                     delta,
+                    Bias::Right,
                 );
                 if let Some((wrap_row, height)) = spacer {
                     result.push((
@@ -4066,8 +4074,9 @@ mod tests {
                 |point, bias| {
                     wrap_point_cursor
                         .map(
-                            tab_point_cursor
-                                .map(fold_point_cursor.map(inlay_point_cursor.map(point), bias)),
+                            tab_point_cursor.map(
+                                fold_point_cursor.map(inlay_point_cursor.map(point, bias), bias),
+                            ),
                         )
                         .row()
                 },

crates/editor/src/display_map/inlay_map.rs 🔗

@@ -935,7 +935,7 @@ impl InlaySnapshot {
 
     #[ztracing::instrument(skip_all)]
     pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
-        self.inlay_point_cursor().map(point)
+        self.inlay_point_cursor().map(point, Bias::Left)
     }
 
     #[ztracing::instrument(skip_all)]
@@ -1212,7 +1212,7 @@ pub struct InlayPointCursor<'transforms> {
 
 impl InlayPointCursor<'_> {
     #[ztracing::instrument(skip_all)]
-    pub fn map(&mut self, point: Point) -> InlayPoint {
+    pub fn map(&mut self, point: Point, bias: Bias) -> InlayPoint {
         let cursor = &mut self.cursor;
         if cursor.did_seek() {
             cursor.seek_forward(&point, Bias::Left);
@@ -1224,7 +1224,7 @@ impl InlayPointCursor<'_> {
                 Some(Transform::Isomorphic(_)) => {
                     if point == cursor.end().0 {
                         while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
-                            if inlay.position.bias() == Bias::Right {
+                            if bias == Bias::Left && inlay.position.bias() == Bias::Right {
                                 break;
                             } else {
                                 cursor.next();
@@ -1237,7 +1237,7 @@ impl InlayPointCursor<'_> {
                     }
                 }
                 Some(Transform::Inlay(inlay)) => {
-                    if inlay.position.bias() == Bias::Left {
+                    if inlay.position.bias() == Bias::Left || bias == Bias::Right {
                         cursor.next();
                     } else {
                         return cursor.start().1;

crates/editor/src/split.rs 🔗

@@ -2091,6 +2091,7 @@ mod tests {
 
     use crate::SplittableEditor;
     use crate::display_map::{BlockPlacement, BlockProperties, BlockStyle};
+    use crate::inlays::Inlay;
     use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
 
     async fn init_test(
@@ -5450,4 +5451,96 @@ mod tests {
             Some(5)
         );
     }
+
+    #[gpui::test]
+    async fn test_multiline_inlays_create_spacers(cx: &mut gpui::TestAppContext) {
+        use rope::Point;
+        use unindent::Unindent as _;
+
+        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
+
+        let base_text = "
+            aaa
+            bbb
+            ccc
+            ddd
+        "
+        .unindent();
+        let current_text = base_text.clone();
+
+        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
+
+        editor.update(cx, |editor, cx| {
+            let path = PathKey::for_buffer(&buffer, cx);
+            editor.set_excerpts_for_path(
+                path,
+                buffer.clone(),
+                vec![Point::new(0, 0)..Point::new(3, 3)],
+                0,
+                diff.clone(),
+                cx,
+            );
+        });
+
+        cx.run_until_parked();
+
+        let rhs_editor = editor.read_with(cx, |e, _| e.rhs_editor.clone());
+        rhs_editor.update(cx, |rhs_editor, cx| {
+            let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
+            rhs_editor.splice_inlays(
+                &[],
+                vec![
+                    Inlay::edit_prediction(
+                        0,
+                        snapshot.anchor_after(Point::new(0, 3)),
+                        "\nINLAY_WITHIN",
+                    ),
+                    Inlay::edit_prediction(
+                        1,
+                        snapshot.anchor_after(Point::new(1, 3)),
+                        "\nINLAY_MID_1\nINLAY_MID_2",
+                    ),
+                    Inlay::edit_prediction(
+                        2,
+                        snapshot.anchor_after(Point::new(3, 3)),
+                        "\nINLAY_END_1\nINLAY_END_2",
+                    ),
+                ],
+                cx,
+            );
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            § <no file>
+            § -----
+            aaa
+            INLAY_WITHIN
+            bbb
+            INLAY_MID_1
+            INLAY_MID_2
+            ccc
+            ddd
+            INLAY_END_1
+            INLAY_END_2"
+                .unindent(),
+            "
+            § <no file>
+            § -----
+            aaa
+            § spacer
+            bbb
+            § spacer
+            § spacer
+            ccc
+            ddd
+            § spacer
+            § spacer"
+                .unindent(),
+            &mut cx,
+        );
+    }
 }