wip

Cole Miller created

Change summary

crates/buffer_diff/src/buffer_diff.rs        | 1896 +++++++++++----------
crates/collab/src/tests/integration_tests.rs |   34 
crates/multi_buffer/src/multi_buffer.rs      |    9 
crates/project/src/git_store.rs              |    8 
crates/project/src/project_tests.rs          |   24 
5 files changed, 986 insertions(+), 985 deletions(-)

Detailed changes

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -237,17 +237,19 @@ impl BufferDiffSnapshot {
             base_text_pair = Some((text.clone(), base_text_rope.clone()));
             base_text_buffer.update(cx, |base_text_buffer, cx| {
                 // TODO(split-diff) arguably an unnecessary allocation
-                base_text_buffer.set_text(text.clone(), cx);
+                base_text_buffer.set_text(dbg!(text.clone()), cx);
             });
             base_text_exists = true;
         } else {
             base_text_pair = None;
             base_text_buffer.update(cx, |base_text_buffer, cx| {
+                dbg!("SET TO EMPTY");
                 base_text_buffer.set_text("", cx);
             });
             base_text_exists = false;
         };
         base_text_snapshot = base_text_buffer.read(cx).snapshot();
+        dbg!(base_text_snapshot.text());
 
         let hunks = cx
             .background_executor()
@@ -310,11 +312,11 @@ impl BufferDiffSnapshot {
         cx: &mut gpui::TestAppContext,
     ) -> BufferDiffSnapshot {
         cx.executor().block(cx.update(|cx| {
-            let base_text_buffer = cx.new(|cx| language::Buffer::local("", cx));
+            let base_text_buffer = cx.new(|cx| language::Buffer::local(diff_base.clone(), cx));
             Self::new_with_base_text(
                 base_text_buffer,
                 buffer,
-                Some(diff_base.as_str().into()),
+                Some(dbg!(diff_base.as_str().into())),
                 None,
                 None,
                 cx,
@@ -484,7 +486,7 @@ impl BufferDiffInner<Entity<language::Buffer>> {
 
         // If the file doesn't exist in either HEAD or the index, then the
         // entire file must be either created or deleted in the index.
-        let (index_text, head_text) = match (index_text, head_text) {
+        let (index_text, head_text) = match (dbg!(index_text), head_text) {
             (Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
             (index_text, head_text) => {
                 let (new_index_text, new_status) = if stage {
@@ -663,11 +665,12 @@ impl BufferDiffInner<Entity<language::Buffer>> {
         }
 
         let mut new_index_text = Rope::new();
+        dbg!(index_text.len());
         let mut index_cursor = index_text.cursor(0);
 
         for (old_range, replacement_text) in edits {
-            new_index_text.append(index_cursor.slice(old_range.start));
-            index_cursor.seek_forward(old_range.end);
+            new_index_text.append(index_cursor.slice(dbg!(old_range.start)));
+            index_cursor.seek_forward(dbg!(old_range.end));
             new_index_text.push(&replacement_text);
         }
         new_index_text.append(index_cursor.suffix());
@@ -1194,15 +1197,15 @@ impl BufferDiff {
     #[cfg(any(test, feature = "test-support"))]
     pub fn new_with_base_text(
         base_text: &str,
-        buffer: &Entity<language::Buffer>,
+        buffer: &text::BufferSnapshot,
         cx: &mut App,
     ) -> Self {
-        let base_buffer = cx.new(|cx| language::Buffer::local("", cx));
+        let base_buffer = cx.new(|cx| language::Buffer::local(base_text, cx));
         let mut base_text = base_text.to_owned();
         text::LineEnding::normalize(&mut base_text);
         let snapshot = BufferDiffSnapshot::new_with_base_text(
             base_buffer.clone(),
-            buffer.read(cx).text_snapshot(),
+            buffer.clone(),
             Some(base_text.into()),
             None,
             None,
@@ -1210,7 +1213,7 @@ impl BufferDiff {
         );
         let snapshot = cx.background_executor().block(snapshot);
         Self {
-            buffer_id: buffer.read(cx).remote_id(),
+            buffer_id: buffer.remote_id(),
             inner: BufferDiffInner {
                 hunks: snapshot.inner.hunks.clone(),
                 pending_hunks: snapshot.inner.pending_hunks.clone(),
@@ -1254,6 +1257,7 @@ impl BufferDiff {
             .secondary_diff
             .as_ref()?
             .update(cx, |secondary_diff, cx| {
+                dbg!(secondary_diff.base_text_string(cx));
                 self.inner.stage_or_unstage_hunks_impl(
                     &secondary_diff.inner,
                     stage,
@@ -1386,6 +1390,7 @@ impl BufferDiff {
 
         let state = &mut self.inner;
         state.base_text_exists = new_state.base_text_exists;
+        // FIXME state.base_text = ...;
         state.hunks = new_state.hunks;
         if base_text_changed || clear_pending_hunks {
             if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
@@ -1632,938 +1637,939 @@ pub fn assert_hunks<ExpectedText, HunkIter>(
     pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
 }
 
-// FIXME
-// #[cfg(test)]
-// mod tests {
-//     use std::fmt::Write as _;
-//
-//     use super::*;
-//     use gpui::TestAppContext;
-//     use pretty_assertions::{assert_eq, assert_ne};
-//     use rand::{Rng as _, rngs::StdRng};
-//     use text::{Buffer, BufferId, ReplicaId, Rope};
-//     use unindent::Unindent as _;
-//     use util::test::marked_text_ranges;
-//
-//     #[ctor::ctor]
-//     fn init_logger() {
-//         zlog::init_test();
-//     }
-//
-//     #[gpui::test]
-//     async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
-//         let diff_base = "
-//             one
-//             two
-//             three
-//         "
-//         .unindent();
-//
-//         let buffer_text = "
-//             one
-//             HELLO
-//             three
-//         "
-//         .unindent();
-//
-//         let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
-//         let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
-//         assert_hunks(
-//             diff.hunks_intersecting_range(
-//                 Anchor::min_max_range_for_buffer(buffer.remote_id()),
-//                 &buffer,
-//             ),
-//             &buffer,
-//             &diff_base,
-//             &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
-//         );
-//
-//         buffer.edit([(0..0, "point five\n")]);
-//         diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
-//         assert_hunks(
-//             diff.hunks_intersecting_range(
-//                 Anchor::min_max_range_for_buffer(buffer.remote_id()),
-//                 &buffer,
-//             ),
-//             &buffer,
-//             &diff_base,
-//             &[
-//                 (0..1, "", "point five\n", DiffHunkStatus::added_none()),
-//                 (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
-//             ],
-//         );
-//
-//         diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
-//         assert_hunks::<&str, _>(
-//             diff.hunks_intersecting_range(
-//                 Anchor::min_max_range_for_buffer(buffer.remote_id()),
-//                 &buffer,
-//             ),
-//             &buffer,
-//             &diff_base,
-//             &[],
-//         );
-//     }
-//
-//     #[gpui::test]
-//     async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
-//         let head_text = "
-//             zero
-//             one
-//             two
-//             three
-//             four
-//             five
-//             six
-//             seven
-//             eight
-//             nine
-//         "
-//         .unindent();
-//
-//         let index_text = "
-//             zero
-//             one
-//             TWO
-//             three
-//             FOUR
-//             five
-//             six
-//             seven
-//             eight
-//             NINE
-//         "
-//         .unindent();
-//
-//         let buffer_text = "
-//             zero
-//             one
-//             TWO
-//             three
-//             FOUR
-//             FIVE
-//             six
-//             SEVEN
-//             eight
-//             nine
-//         "
-//         .unindent();
-//
-//         let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
-//         let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
-//         let mut uncommitted_diff =
-//             BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
-//         uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
-//
-//         let expected_hunks = vec![
-//             (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
-//             (
-//                 4..6,
-//                 "four\nfive\n",
-//                 "FOUR\nFIVE\n",
-//                 DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
-//             ),
-//             (
-//                 7..8,
-//                 "seven\n",
-//                 "SEVEN\n",
-//                 DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
-//             ),
-//         ];
-//
-//         assert_hunks(
-//             uncommitted_diff.hunks_intersecting_range(
-//                 Anchor::min_max_range_for_buffer(buffer.remote_id()),
-//                 &buffer,
-//             ),
-//             &buffer,
-//             &head_text,
-//             &expected_hunks,
-//         );
-//     }
-//
-//     #[gpui::test]
-//     async fn test_buffer_diff_range(cx: &mut TestAppContext) {
-//         let diff_base = Arc::new(
-//             "
-//             one
-//             two
-//             three
-//             four
-//             five
-//             six
-//             seven
-//             eight
-//             nine
-//             ten
-//         "
-//             .unindent(),
-//         );
-//
-//         let buffer_text = "
-//             A
-//             one
-//             B
-//             two
-//             C
-//             three
-//             HELLO
-//             four
-//             five
-//             SIXTEEN
-//             seven
-//             eight
-//             WORLD
-//             nine
-//
-//             ten
-//
-//         "
-//         .unindent();
-//
-//         let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
-//         let diff = cx
-//             .update(|cx| {
-//                 BufferDiffSnapshot::new_with_base_text(
-//                     buffer.snapshot(),
-//                     Some(diff_base.clone()),
-//                     None,
-//                     None,
-//                     cx,
-//                 )
-//             })
-//             .await;
-//         assert_eq!(
-//             diff.hunks_intersecting_range(
-//                 Anchor::min_max_range_for_buffer(buffer.remote_id()),
-//                 &buffer
-//             )
-//             .count(),
-//             8
-//         );
-//
-//         assert_hunks(
-//             diff.hunks_intersecting_range(
-//                 buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
-//                 &buffer,
-//             ),
-//             &buffer,
-//             &diff_base,
-//             &[
-//                 (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
-//                 (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
-//                 (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
-//             ],
-//         );
-//     }
-//
-//     #[gpui::test]
-//     async fn test_stage_hunk(cx: &mut TestAppContext) {
-//         struct Example {
-//             name: &'static str,
-//             head_text: String,
-//             index_text: String,
-//             buffer_marked_text: String,
-//             final_index_text: String,
-//         }
-//
-//         let table = [
-//             Example {
-//                 name: "uncommitted hunk straddles end of unstaged hunk",
-//                 head_text: "
-//                     one
-//                     two
-//                     three
-//                     four
-//                     five
-//                 "
-//                 .unindent(),
-//                 index_text: "
-//                     one
-//                     TWO_HUNDRED
-//                     three
-//                     FOUR_HUNDRED
-//                     five
-//                 "
-//                 .unindent(),
-//                 buffer_marked_text: "
-//                     ZERO
-//                     one
-//                     two
-//                     «THREE_HUNDRED
-//                     FOUR_HUNDRED»
-//                     five
-//                     SIX
-//                 "
-//                 .unindent(),
-//                 final_index_text: "
-//                     one
-//                     two
-//                     THREE_HUNDRED
-//                     FOUR_HUNDRED
-//                     five
-//                 "
-//                 .unindent(),
-//             },
-//             Example {
-//                 name: "uncommitted hunk straddles start of unstaged hunk",
-//                 head_text: "
-//                     one
-//                     two
-//                     three
-//                     four
-//                     five
-//                 "
-//                 .unindent(),
-//                 index_text: "
-//                     one
-//                     TWO_HUNDRED
-//                     three
-//                     FOUR_HUNDRED
-//                     five
-//                 "
-//                 .unindent(),
-//                 buffer_marked_text: "
-//                     ZERO
-//                     one
-//                     «TWO_HUNDRED
-//                     THREE_HUNDRED»
-//                     four
-//                     five
-//                     SIX
-//                 "
-//                 .unindent(),
-//                 final_index_text: "
-//                     one
-//                     TWO_HUNDRED
-//                     THREE_HUNDRED
-//                     four
-//                     five
-//                 "
-//                 .unindent(),
-//             },
-//             Example {
-//                 name: "uncommitted hunk strictly contains unstaged hunks",
-//                 head_text: "
-//                     one
-//                     two
-//                     three
-//                     four
-//                     five
-//                     six
-//                     seven
-//                 "
-//                 .unindent(),
-//                 index_text: "
-//                     one
-//                     TWO
-//                     THREE
-//                     FOUR
-//                     FIVE
-//                     SIX
-//                     seven
-//                 "
-//                 .unindent(),
-//                 buffer_marked_text: "
-//                     one
-//                     TWO
-//                     «THREE_HUNDRED
-//                     FOUR
-//                     FIVE_HUNDRED»
-//                     SIX
-//                     seven
-//                 "
-//                 .unindent(),
-//                 final_index_text: "
-//                     one
-//                     TWO
-//                     THREE_HUNDRED
-//                     FOUR
-//                     FIVE_HUNDRED
-//                     SIX
-//                     seven
-//                 "
-//                 .unindent(),
-//             },
-//             Example {
-//                 name: "uncommitted deletion hunk",
-//                 head_text: "
-//                     one
-//                     two
-//                     three
-//                     four
-//                     five
-//                 "
-//                 .unindent(),
-//                 index_text: "
-//                     one
-//                     two
-//                     three
-//                     four
-//                     five
-//                 "
-//                 .unindent(),
-//                 buffer_marked_text: "
-//                     one
-//                     ˇfive
-//                 "
-//                 .unindent(),
-//                 final_index_text: "
-//                     one
-//                     five
-//                 "
-//                 .unindent(),
-//             },
-//             Example {
-//                 name: "one unstaged hunk that contains two uncommitted hunks",
-//                 head_text: "
-//                     one
-//                     two
-//
-//                     three
-//                     four
-//                 "
-//                 .unindent(),
-//                 index_text: "
-//                     one
-//                     two
-//                     three
-//                     four
-//                 "
-//                 .unindent(),
-//                 buffer_marked_text: "
-//                     «one
-//
-//                     three // modified
-//                     four»
-//                 "
-//                 .unindent(),
-//                 final_index_text: "
-//                     one
-//
-//                     three // modified
-//                     four
-//                 "
-//                 .unindent(),
-//             },
-//             Example {
-//                 name: "one uncommitted hunk that contains two unstaged hunks",
-//                 head_text: "
-//                     one
-//                     two
-//                     three
-//                     four
-//                     five
-//                 "
-//                 .unindent(),
-//                 index_text: "
-//                     ZERO
-//                     one
-//                     TWO
-//                     THREE
-//                     FOUR
-//                     five
-//                 "
-//                 .unindent(),
-//                 buffer_marked_text: "
-//                     «one
-//                     TWO_HUNDRED
-//                     THREE
-//                     FOUR_HUNDRED
-//                     five»
-//                 "
-//                 .unindent(),
-//                 final_index_text: "
-//                     ZERO
-//                     one
-//                     TWO_HUNDRED
-//                     THREE
-//                     FOUR_HUNDRED
-//                     five
-//                 "
-//                 .unindent(),
-//             },
-//         ];
-//
-//         for example in table {
-//             let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
-//             let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
-//             let hunk_range =
-//                 buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
-//
-//             let unstaged =
-//                 BufferDiffSnapshot::new_sync(buffer.clone(), example.index_text.clone(), cx);
-//             let uncommitted =
-//                 BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx);
-//
-//             let unstaged_diff = cx.new(|cx| {
-//                 let mut diff = BufferDiff::new(&buffer, cx);
-//                 diff.set_snapshot(unstaged, &buffer, cx);
-//                 diff
-//             });
-//
-//             let uncommitted_diff = cx.new(|cx| {
-//                 let mut diff = BufferDiff::new(&buffer, cx);
-//                 diff.set_snapshot(uncommitted, &buffer, cx);
-//                 diff.set_secondary_diff(unstaged_diff);
-//                 diff
-//             });
-//
-//             uncommitted_diff.update(cx, |diff, cx| {
-//                 let hunks = diff
-//                     .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
-//                     .collect::<Vec<_>>();
-//                 for hunk in &hunks {
-//                     assert_ne!(
-//                         hunk.secondary_status,
-//                         DiffHunkSecondaryStatus::NoSecondaryHunk
-//                     )
-//                 }
-//
-//                 let new_index_text = diff
-//                     .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
-//                     .unwrap()
-//                     .to_string();
-//
-//                 let hunks = diff
-//                     .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
-//                     .collect::<Vec<_>>();
-//                 for hunk in &hunks {
-//                     assert_eq!(
-//                         hunk.secondary_status,
-//                         DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
-//                     )
-//                 }
-//
-//                 pretty_assertions::assert_eq!(
-//                     new_index_text,
-//                     example.final_index_text,
-//                     "example: {}",
-//                     example.name
-//                 );
-//             });
-//         }
-//     }
-//
-//     #[gpui::test]
-//     async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
-//         let head_text = "
-//             one
-//             two
-//             three
-//         "
-//         .unindent();
-//         let index_text = head_text.clone();
-//         let buffer_text = "
-//             one
-//             three
-//         "
-//         .unindent();
-//
-//         let buffer = Buffer::new(
-//             ReplicaId::LOCAL,
-//             BufferId::new(1).unwrap(),
-//             buffer_text.clone(),
-//         );
-//         let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
-//         let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
-//         let unstaged_diff = cx.new(|cx| {
-//             let mut diff = BufferDiff::new(&buffer, cx);
-//             diff.set_snapshot(unstaged, &buffer, cx);
-//             diff
-//         });
-//         let uncommitted_diff = cx.new(|cx| {
-//             let mut diff = BufferDiff::new(&buffer, cx);
-//             diff.set_snapshot(uncommitted, &buffer, cx);
-//             diff.set_secondary_diff(unstaged_diff.clone());
-//             diff
-//         });
-//
-//         uncommitted_diff.update(cx, |diff, cx| {
-//             let hunk = diff.hunks(&buffer, cx).next().unwrap();
-//
-//             let new_index_text = diff
-//                 .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
-//                 .unwrap()
-//                 .to_string();
-//             assert_eq!(new_index_text, buffer_text);
-//
-//             let hunk = diff.hunks(&buffer, cx).next().unwrap();
-//             assert_eq!(
-//                 hunk.secondary_status,
-//                 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
-//             );
-//
-//             let index_text = diff
-//                 .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
-//                 .unwrap()
-//                 .to_string();
-//             assert_eq!(index_text, head_text);
-//
-//             let hunk = diff.hunks(&buffer, cx).next().unwrap();
-//             // optimistically unstaged (fine, could also be HasSecondaryHunk)
-//             assert_eq!(
-//                 hunk.secondary_status,
-//                 DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
-//             );
-//         });
-//     }
-//
-//     #[gpui::test]
-//     async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
-//         let base_text = "
-//             zero
-//             one
-//             two
-//             three
-//             four
-//             five
-//             six
-//             seven
-//             eight
-//             nine
-//         "
-//         .unindent();
-//
-//         let buffer_text_1 = "
-//             one
-//             three
-//             four
-//             five
-//             SIX
-//             seven
-//             eight
-//             NINE
-//         "
-//         .unindent();
-//
-//         let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
-//
-//         let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
-//         let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
-//         let range = diff_1.inner.compare(&empty_diff.inner, &buffer).0.unwrap();
-//         assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
-//
-//         // Edit does not affect the diff.
-//         buffer.edit_via_marked_text(
-//             &"
-//                 one
-//                 three
-//                 four
-//                 five
-//                 «SIX.5»
-//                 seven
-//                 eight
-//                 NINE
-//             "
-//             .unindent(),
-//         );
-//         let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
-//         assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer).0);
-//
-//         // Edit turns a deletion hunk into a modification.
-//         buffer.edit_via_marked_text(
-//             &"
-//                 one
-//                 «THREE»
-//                 four
-//                 five
-//                 SIX.5
-//                 seven
-//                 eight
-//                 NINE
-//             "
-//             .unindent(),
-//         );
-//         let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
-//         let range = diff_3.inner.compare(&diff_2.inner, &buffer).0.unwrap();
-//         assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
-//
-//         // Edit turns a modification hunk into a deletion.
-//         buffer.edit_via_marked_text(
-//             &"
-//                 one
-//                 THREE
-//                 four
-//                 five«»
-//                 seven
-//                 eight
-//                 NINE
-//             "
-//             .unindent(),
-//         );
-//         let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
-//         let range = diff_4.inner.compare(&diff_3.inner, &buffer).0.unwrap();
-//         assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
-//
-//         // Edit introduces a new insertion hunk.
-//         buffer.edit_via_marked_text(
-//             &"
-//                 one
-//                 THREE
-//                 four«
-//                 FOUR.5
-//                 »five
-//                 seven
-//                 eight
-//                 NINE
-//             "
-//             .unindent(),
-//         );
-//         let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
-//         let range = diff_5.inner.compare(&diff_4.inner, &buffer).0.unwrap();
-//         assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
-//
-//         // Edit removes a hunk.
-//         buffer.edit_via_marked_text(
-//             &"
-//                 one
-//                 THREE
-//                 four
-//                 FOUR.5
-//                 five
-//                 seven
-//                 eight
-//                 «nine»
-//             "
-//             .unindent(),
-//         );
-//         let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
-//         let range = diff_6.inner.compare(&diff_5.inner, &buffer).0.unwrap();
-//         assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
-//     }
-//
-//     #[gpui::test(iterations = 100)]
-//     async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
-//         fn gen_line(rng: &mut StdRng) -> String {
-//             if rng.random_bool(0.2) {
-//                 "\n".to_owned()
-//             } else {
-//                 let c = rng.random_range('A'..='Z');
-//                 format!("{c}{c}{c}\n")
-//             }
-//         }
-//
-//         fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
-//             let mut old_lines = {
-//                 let mut old_lines = Vec::new();
-//                 let old_lines_iter = head.lines();
-//                 for line in old_lines_iter {
-//                     assert!(!line.ends_with("\n"));
-//                     old_lines.push(line.to_owned());
-//                 }
-//                 if old_lines.last().is_some_and(|line| line.is_empty()) {
-//                     old_lines.pop();
-//                 }
-//                 old_lines.into_iter()
-//             };
-//             let mut result = String::new();
-//             let unchanged_count = rng.random_range(0..=old_lines.len());
-//             result +=
-//                 &old_lines
-//                     .by_ref()
-//                     .take(unchanged_count)
-//                     .fold(String::new(), |mut s, line| {
-//                         writeln!(&mut s, "{line}").unwrap();
-//                         s
-//                     });
-//             while old_lines.len() > 0 {
-//                 let deleted_count = rng.random_range(0..=old_lines.len());
-//                 let _advance = old_lines
-//                     .by_ref()
-//                     .take(deleted_count)
-//                     .map(|line| line.len() + 1)
-//                     .sum::<usize>();
-//                 let minimum_added = if deleted_count == 0 { 1 } else { 0 };
-//                 let added_count = rng.random_range(minimum_added..=5);
-//                 let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
-//                 result += &addition;
-//
-//                 if old_lines.len() > 0 {
-//                     let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
-//                     if blank_lines == old_lines.len() {
-//                         break;
-//                     };
-//                     let unchanged_count =
-//                         rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
-//                     result += &old_lines.by_ref().take(unchanged_count).fold(
-//                         String::new(),
-//                         |mut s, line| {
-//                             writeln!(&mut s, "{line}").unwrap();
-//                             s
-//                         },
-//                     );
-//                 }
-//             }
-//             result
-//         }
-//
-//         fn uncommitted_diff(
-//             working_copy: &language::BufferSnapshot,
-//             index_text: &Rope,
-//             head_text: String,
-//             cx: &mut TestAppContext,
-//         ) -> Entity<BufferDiff> {
-//             let inner =
-//                 BufferDiffSnapshot::new_sync(working_copy.text.clone(), head_text, cx).inner;
-//             let secondary = BufferDiff {
-//                 buffer_id: working_copy.remote_id(),
-//                 inner: BufferDiffSnapshot::new_sync(
-//                     working_copy.text.clone(),
-//                     index_text.to_string(),
-//                     cx,
-//                 )
-//                 .inner,
-//                 secondary_diff: None,
-//             };
-//             let secondary = cx.new(|_| secondary);
-//             cx.new(|_| BufferDiff {
-//                 buffer_id: working_copy.remote_id(),
-//                 inner,
-//                 secondary_diff: Some(secondary),
-//             })
-//         }
-//
-//         let operations = std::env::var("OPERATIONS")
-//             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-//             .unwrap_or(10);
-//
-//         let rng = &mut rng;
-//         let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
-//             writeln!(&mut s, "{c}{c}{c}").unwrap();
-//             s
-//         });
-//         let working_copy = gen_working_copy(rng, &head_text);
-//         let working_copy = cx.new(|cx| {
-//             language::Buffer::local_normalized(
-//                 Rope::from(working_copy.as_str()),
-//                 text::LineEnding::default(),
-//                 cx,
-//             )
-//         });
-//         let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
-//         let mut index_text = if rng.random() {
-//             Rope::from(head_text.as_str())
-//         } else {
-//             working_copy.as_rope().clone()
-//         };
-//
-//         let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
-//         let mut hunks = diff.update(cx, |diff, cx| {
-//             diff.hunks_intersecting_range(
-//                 Anchor::min_max_range_for_buffer(diff.buffer_id),
-//                 &working_copy,
-//                 cx,
-//             )
-//             .collect::<Vec<_>>()
-//         });
-//         if hunks.is_empty() {
-//             return;
-//         }
-//
-//         for _ in 0..operations {
-//             let i = rng.random_range(0..hunks.len());
-//             let hunk = &mut hunks[i];
-//             let hunk_to_change = hunk.clone();
-//             let stage = match hunk.secondary_status {
-//                 DiffHunkSecondaryStatus::HasSecondaryHunk => {
-//                     hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
-//                     true
-//                 }
-//                 DiffHunkSecondaryStatus::NoSecondaryHunk => {
-//                     hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
-//                     false
-//                 }
-//                 _ => unreachable!(),
-//             };
-//
-//             index_text = diff.update(cx, |diff, cx| {
-//                 diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
-//                     .unwrap()
-//             });
-//
-//             diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
-//             let found_hunks = diff.update(cx, |diff, cx| {
-//                 diff.hunks_intersecting_range(
-//                     Anchor::min_max_range_for_buffer(diff.buffer_id),
-//                     &working_copy,
-//                     cx,
-//                 )
-//                 .collect::<Vec<_>>()
-//             });
-//             assert_eq!(hunks.len(), found_hunks.len());
-//
-//             for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
-//                 assert_eq!(
-//                     expected_hunk.buffer_range.to_point(&working_copy),
-//                     found_hunk.buffer_range.to_point(&working_copy)
-//                 );
-//                 assert_eq!(
-//                     expected_hunk.diff_base_byte_range,
-//                     found_hunk.diff_base_byte_range
-//                 );
-//                 assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
-//             }
-//             hunks = found_hunks;
-//         }
-//     }
-//
-//     #[gpui::test]
-//     async fn test_row_to_base_text_row(cx: &mut TestAppContext) {
-//         let base_text = "
-//             zero
-//             one
-//             two
-//             three
-//             four
-//             five
-//             six
-//             seven
-//             eight
-//         "
-//         .unindent();
-//         let buffer_text = "
-//             zero
-//             ONE
-//             two
-//             NINE
-//             five
-//             seven
-//         "
-//         .unindent();
-//
-//         //   zero
-//         // - one
-//         // + ONE
-//         //   two
-//         // - three
-//         // - four
-//         // + NINE
-//         //   five
-//         // - six
-//         //   seven
-//         // + eight
-//
-//         let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
-//         let buffer_snapshot = buffer.snapshot();
-//         let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx);
-//         let expected_results = [
-//             // don't format me
-//             (0, 0),
-//             (1, 2),
-//             (2, 2),
-//             (3, 5),
-//             (4, 5),
-//             (5, 7),
-//             (6, 9),
-//         ];
-//         for (buffer_row, expected) in expected_results {
-//             assert_eq!(
-//                 diff.row_to_base_text_row(buffer_row, &buffer_snapshot),
-//                 expected,
-//                 "{buffer_row}"
-//             );
-//         }
-//     }
-// }
-//
+#[cfg(test)]
+mod tests {
+    use std::fmt::Write as _;
+
+    use super::*;
+    use gpui::TestAppContext;
+    use pretty_assertions::{assert_eq, assert_ne};
+    use rand::{Rng as _, rngs::StdRng};
+    use text::{Buffer, BufferId, ReplicaId, Rope};
+    use unindent::Unindent as _;
+    use util::test::marked_text_ranges;
+
+    #[ctor::ctor]
+    fn init_logger() {
+        zlog::init_test();
+    }
+
+    #[gpui::test]
+    async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
+        let diff_base = "
+            one
+            two
+            three
+        "
+        .unindent();
+
+        let buffer_text = "
+            one
+            HELLO
+            three
+        "
+        .unindent();
+
+        let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+        let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
+        assert_hunks(
+            diff.hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(buffer.remote_id()),
+                &buffer,
+            ),
+            &buffer,
+            &diff_base,
+            &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
+        );
+
+        buffer.edit([(0..0, "point five\n")]);
+        diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
+        assert_hunks(
+            diff.hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(buffer.remote_id()),
+                &buffer,
+            ),
+            &buffer,
+            &diff_base,
+            &[
+                (0..1, "", "point five\n", DiffHunkStatus::added_none()),
+                (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified_none()),
+            ],
+        );
+
+        diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
+        assert_hunks::<&str, _>(
+            diff.hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(buffer.remote_id()),
+                &buffer,
+            ),
+            &buffer,
+            &diff_base,
+            &[],
+        );
+    }
+
+    #[gpui::test]
+    async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) {
+        let head_text = "
+            zero
+            one
+            two
+            three
+            four
+            five
+            six
+            seven
+            eight
+            nine
+        "
+        .unindent();
+
+        let index_text = "
+            zero
+            one
+            TWO
+            three
+            FOUR
+            five
+            six
+            seven
+            eight
+            NINE
+        "
+        .unindent();
+
+        let buffer_text = "
+            zero
+            one
+            TWO
+            three
+            FOUR
+            FIVE
+            six
+            SEVEN
+            eight
+            nine
+        "
+        .unindent();
+
+        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+        let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
+        let mut uncommitted_diff =
+            BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
+        uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
+
+        let expected_hunks = vec![
+            (2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
+            (
+                4..6,
+                "four\nfive\n",
+                "FOUR\nFIVE\n",
+                DiffHunkStatus::modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk),
+            ),
+            (
+                7..8,
+                "seven\n",
+                "SEVEN\n",
+                DiffHunkStatus::modified(DiffHunkSecondaryStatus::HasSecondaryHunk),
+            ),
+        ];
+
+        assert_hunks(
+            uncommitted_diff.hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(buffer.remote_id()),
+                &buffer,
+            ),
+            &buffer,
+            &head_text,
+            &expected_hunks,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_buffer_diff_range(cx: &mut TestAppContext) {
+        let diff_base = "
+            one
+            two
+            three
+            four
+            five
+            six
+            seven
+            eight
+            nine
+            ten
+        "
+        .unindent();
+
+        let buffer_text = "
+            A
+            one
+            B
+            two
+            C
+            three
+            HELLO
+            four
+            five
+            SIXTEEN
+            seven
+            eight
+            WORLD
+            nine
+
+            ten
+
+        "
+        .unindent();
+
+        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+        let base_text_buffer = cx.new(|cx| language::Buffer::local(diff_base.clone(), cx));
+        let diff = cx
+            .update(|cx| {
+                BufferDiffSnapshot::new_with_base_text(
+                    base_text_buffer,
+                    buffer.snapshot(),
+                    Some(diff_base.as_str().into()),
+                    None,
+                    None,
+                    cx,
+                )
+            })
+            .await;
+        assert_eq!(
+            diff.hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(buffer.remote_id()),
+                &buffer
+            )
+            .count(),
+            8
+        );
+
+        assert_hunks(
+            diff.hunks_intersecting_range(
+                buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
+                &buffer,
+            ),
+            &buffer,
+            &diff_base,
+            &[
+                (6..7, "", "HELLO\n", DiffHunkStatus::added_none()),
+                (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified_none()),
+                (12..13, "", "WORLD\n", DiffHunkStatus::added_none()),
+            ],
+        );
+    }
+
+    #[gpui::test]
+    async fn test_stage_hunk(cx: &mut TestAppContext) {
+        struct Example {
+            name: &'static str,
+            head_text: String,
+            index_text: String,
+            buffer_marked_text: String,
+            final_index_text: String,
+        }
+
+        let table = [
+            Example {
+                name: "uncommitted hunk straddles end of unstaged hunk",
+                head_text: "
+                    one
+                    two
+                    three
+                    four
+                    five
+                "
+                .unindent(),
+                index_text: "
+                    one
+                    TWO_HUNDRED
+                    three
+                    FOUR_HUNDRED
+                    five
+                "
+                .unindent(),
+                buffer_marked_text: "
+                    ZERO
+                    one
+                    two
+                    «THREE_HUNDRED
+                    FOUR_HUNDRED»
+                    five
+                    SIX
+                "
+                .unindent(),
+                final_index_text: "
+                    one
+                    two
+                    THREE_HUNDRED
+                    FOUR_HUNDRED
+                    five
+                "
+                .unindent(),
+            },
+            Example {
+                name: "uncommitted hunk straddles start of unstaged hunk",
+                head_text: "
+                    one
+                    two
+                    three
+                    four
+                    five
+                "
+                .unindent(),
+                index_text: "
+                    one
+                    TWO_HUNDRED
+                    three
+                    FOUR_HUNDRED
+                    five
+                "
+                .unindent(),
+                buffer_marked_text: "
+                    ZERO
+                    one
+                    «TWO_HUNDRED
+                    THREE_HUNDRED»
+                    four
+                    five
+                    SIX
+                "
+                .unindent(),
+                final_index_text: "
+                    one
+                    TWO_HUNDRED
+                    THREE_HUNDRED
+                    four
+                    five
+                "
+                .unindent(),
+            },
+            Example {
+                name: "uncommitted hunk strictly contains unstaged hunks",
+                head_text: "
+                    one
+                    two
+                    three
+                    four
+                    five
+                    six
+                    seven
+                "
+                .unindent(),
+                index_text: "
+                    one
+                    TWO
+                    THREE
+                    FOUR
+                    FIVE
+                    SIX
+                    seven
+                "
+                .unindent(),
+                buffer_marked_text: "
+                    one
+                    TWO
+                    «THREE_HUNDRED
+                    FOUR
+                    FIVE_HUNDRED»
+                    SIX
+                    seven
+                "
+                .unindent(),
+                final_index_text: "
+                    one
+                    TWO
+                    THREE_HUNDRED
+                    FOUR
+                    FIVE_HUNDRED
+                    SIX
+                    seven
+                "
+                .unindent(),
+            },
+            Example {
+                name: "uncommitted deletion hunk",
+                head_text: "
+                    one
+                    two
+                    three
+                    four
+                    five
+                "
+                .unindent(),
+                index_text: "
+                    one
+                    two
+                    three
+                    four
+                    five
+                "
+                .unindent(),
+                buffer_marked_text: "
+                    one
+                    ˇfive
+                "
+                .unindent(),
+                final_index_text: "
+                    one
+                    five
+                "
+                .unindent(),
+            },
+            Example {
+                name: "one unstaged hunk that contains two uncommitted hunks",
+                head_text: "
+                    one
+                    two
+
+                    three
+                    four
+                "
+                .unindent(),
+                index_text: "
+                    one
+                    two
+                    three
+                    four
+                "
+                .unindent(),
+                buffer_marked_text: "
+                    «one
+
+                    three // modified
+                    four»
+                "
+                .unindent(),
+                final_index_text: "
+                    one
+
+                    three // modified
+                    four
+                "
+                .unindent(),
+            },
+            Example {
+                name: "one uncommitted hunk that contains two unstaged hunks",
+                head_text: "
+                    one
+                    two
+                    three
+                    four
+                    five
+                "
+                .unindent(),
+                index_text: "
+                    ZERO
+                    one
+                    TWO
+                    THREE
+                    FOUR
+                    five
+                "
+                .unindent(),
+                buffer_marked_text: "
+                    «one
+                    TWO_HUNDRED
+                    THREE
+                    FOUR_HUNDRED
+                    five»
+                "
+                .unindent(),
+                final_index_text: "
+                    ZERO
+                    one
+                    TWO_HUNDRED
+                    THREE
+                    FOUR_HUNDRED
+                    five
+                "
+                .unindent(),
+            },
+        ];
+
+        for example in table {
+            dbg!("----------------------------------");
+            let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
+            let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+            let hunk_range =
+                buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
+
+            dbg!();
+
+            let unstaged =
+                BufferDiffSnapshot::new_sync(buffer.clone(), dbg!(example.index_text.clone()), cx);
+            dbg!(unstaged.base_text().text());
+            let uncommitted =
+                BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx);
+            dbg!();
+
+            let unstaged_diff = cx.new(|cx| {
+                let mut diff = BufferDiff::new(&buffer, cx);
+                diff.set_snapshot(unstaged, &buffer, cx);
+                dbg!(diff.base_text(cx).text());
+                diff
+            });
+            dbg!();
+
+            let uncommitted_diff = cx.new(|cx| {
+                let mut diff = BufferDiff::new(&buffer, cx);
+                diff.set_snapshot(uncommitted, &buffer, cx);
+                dbg!(unstaged_diff.read(cx).base_text_string(cx));
+                diff.set_secondary_diff(unstaged_diff);
+                diff
+            });
+            dbg!();
+
+            uncommitted_diff.update(cx, |diff, cx| {
+                let hunks = diff
+                    .snapshot(cx)
+                    .hunks_intersecting_range(hunk_range.clone(), &buffer)
+                    .collect::<Vec<_>>();
+                for hunk in &hunks {
+                    assert_ne!(
+                        hunk.secondary_status,
+                        DiffHunkSecondaryStatus::NoSecondaryHunk
+                    )
+                }
+
+                dbg!();
+                let new_index_text = diff
+                    .stage_or_unstage_hunks(true, &hunks, &buffer, true, cx)
+                    .unwrap()
+                    .to_string();
+                dbg!();
+
+                let hunks = diff
+                    .snapshot(cx)
+                    .hunks_intersecting_range(hunk_range.clone(), &buffer)
+                    .collect::<Vec<_>>();
+                for hunk in &hunks {
+                    assert_eq!(
+                        hunk.secondary_status,
+                        DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
+                    )
+                }
+
+                pretty_assertions::assert_eq!(
+                    new_index_text,
+                    example.final_index_text,
+                    "example: {}",
+                    example.name
+                );
+            });
+        }
+    }
+
+    #[gpui::test]
+    async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
+        let head_text = "
+            one
+            two
+            three
+        "
+        .unindent();
+        let index_text = head_text.clone();
+        let buffer_text = "
+            one
+            three
+        "
+        .unindent();
+
+        let buffer = Buffer::new(
+            ReplicaId::LOCAL,
+            BufferId::new(1).unwrap(),
+            buffer_text.clone(),
+        );
+        let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
+        let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
+        let unstaged_diff = cx.new(|cx| {
+            let mut diff = BufferDiff::new(&buffer, cx);
+            diff.set_snapshot(unstaged, &buffer, cx);
+            diff
+        });
+        let uncommitted_diff = cx.new(|cx| {
+            let mut diff = BufferDiff::new(&buffer, cx);
+            diff.set_snapshot(uncommitted, &buffer, cx);
+            diff.set_secondary_diff(unstaged_diff.clone());
+            diff
+        });
+
+        uncommitted_diff.update(cx, |diff, cx| {
+            let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
+
+            let new_index_text = diff
+                .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
+                .unwrap()
+                .to_string();
+            assert_eq!(new_index_text, buffer_text);
+
+            let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
+            assert_eq!(
+                hunk.secondary_status,
+                DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
+            );
+
+            let index_text = diff
+                .stage_or_unstage_hunks(false, &[hunk], &buffer, true, cx)
+                .unwrap()
+                .to_string();
+            assert_eq!(index_text, head_text);
+
+            let hunk = diff.snapshot(cx).hunks(&buffer).next().unwrap();
+            // optimistically unstaged (fine, could also be HasSecondaryHunk)
+            assert_eq!(
+                hunk.secondary_status,
+                DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
+        let base_text = "
+            zero
+            one
+            two
+            three
+            four
+            five
+            six
+            seven
+            eight
+            nine
+        "
+        .unindent();
+
+        let buffer_text_1 = "
+            one
+            three
+            four
+            five
+            SIX
+            seven
+            eight
+            NINE
+        "
+        .unindent();
+
+        let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
+
+        let empty_diff = cx.update(|cx| BufferDiff::new(&buffer, cx).snapshot(cx));
+        let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+        let range = diff_1.inner.compare(&empty_diff.inner, &buffer).0.unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
+
+        // Edit does not affect the diff.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                three
+                four
+                five
+                «SIX.5»
+                seven
+                eight
+                NINE
+            "
+            .unindent(),
+        );
+        let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+        assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer).0);
+
+        // Edit turns a deletion hunk into a modification.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                «THREE»
+                four
+                five
+                SIX.5
+                seven
+                eight
+                NINE
+            "
+            .unindent(),
+        );
+        let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+        let range = diff_3.inner.compare(&diff_2.inner, &buffer).0.unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
+
+        // Edit turns a modification hunk into a deletion.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                THREE
+                four
+                five«»
+                seven
+                eight
+                NINE
+            "
+            .unindent(),
+        );
+        let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+        let range = diff_4.inner.compare(&diff_3.inner, &buffer).0.unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
+
+        // Edit introduces a new insertion hunk.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                THREE
+                four«
+                FOUR.5
+                »five
+                seven
+                eight
+                NINE
+            "
+            .unindent(),
+        );
+        let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
+        let range = diff_5.inner.compare(&diff_4.inner, &buffer).0.unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
+
+        // Edit removes a hunk.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                THREE
+                four
+                FOUR.5
+                five
+                seven
+                eight
+                «nine»
+            "
+            .unindent(),
+        );
+        let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
+        let range = diff_6.inner.compare(&diff_5.inner, &buffer).0.unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
+    }
+
+    #[gpui::test(iterations = 100)]
+    async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
+        fn gen_line(rng: &mut StdRng) -> String {
+            if rng.random_bool(0.2) {
+                "\n".to_owned()
+            } else {
+                let c = rng.random_range('A'..='Z');
+                format!("{c}{c}{c}\n")
+            }
+        }
+
+        fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
+            let mut old_lines = {
+                let mut old_lines = Vec::new();
+                let old_lines_iter = head.lines();
+                for line in old_lines_iter {
+                    assert!(!line.ends_with("\n"));
+                    old_lines.push(line.to_owned());
+                }
+                if old_lines.last().is_some_and(|line| line.is_empty()) {
+                    old_lines.pop();
+                }
+                old_lines.into_iter()
+            };
+            let mut result = String::new();
+            let unchanged_count = rng.random_range(0..=old_lines.len());
+            result +=
+                &old_lines
+                    .by_ref()
+                    .take(unchanged_count)
+                    .fold(String::new(), |mut s, line| {
+                        writeln!(&mut s, "{line}").unwrap();
+                        s
+                    });
+            while old_lines.len() > 0 {
+                let deleted_count = rng.random_range(0..=old_lines.len());
+                let _advance = old_lines
+                    .by_ref()
+                    .take(deleted_count)
+                    .map(|line| line.len() + 1)
+                    .sum::<usize>();
+                let minimum_added = if deleted_count == 0 { 1 } else { 0 };
+                let added_count = rng.random_range(minimum_added..=5);
+                let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
+                result += &addition;
+
+                if old_lines.len() > 0 {
+                    let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
+                    if blank_lines == old_lines.len() {
+                        break;
+                    };
+                    let unchanged_count =
+                        rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
+                    result += &old_lines.by_ref().take(unchanged_count).fold(
+                        String::new(),
+                        |mut s, line| {
+                            writeln!(&mut s, "{line}").unwrap();
+                            s
+                        },
+                    );
+                }
+            }
+            result
+        }
+
+        fn uncommitted_diff(
+            working_copy: &language::BufferSnapshot,
+            index_text: &Rope,
+            head_text: String,
+            cx: &mut TestAppContext,
+        ) -> Entity<BufferDiff> {
+            let secondary = cx.new(|cx| {
+                BufferDiff::new_with_base_text(&index_text.to_string(), &working_copy.text, cx)
+            });
+            cx.new(|cx| {
+                let mut diff = BufferDiff::new_with_base_text(&head_text, &working_copy.text, cx);
+                diff.secondary_diff = Some(secondary);
+                diff
+            })
+        }
+
+        let operations = std::env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let rng = &mut rng;
+        let head_text = ('a'..='z').fold(String::new(), |mut s, c| {
+            writeln!(&mut s, "{c}{c}{c}").unwrap();
+            s
+        });
+        let working_copy = gen_working_copy(rng, &head_text);
+        let working_copy = cx.new(|cx| {
+            language::Buffer::local_normalized(
+                Rope::from(working_copy.as_str()),
+                text::LineEnding::default(),
+                cx,
+            )
+        });
+        let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
+        let mut index_text = if rng.random() {
+            Rope::from(head_text.as_str())
+        } else {
+            working_copy.as_rope().clone()
+        };
+
+        let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
+        let mut hunks = diff.update(cx, |diff, cx| {
+            diff.snapshot(cx)
+                .hunks_intersecting_range(
+                    Anchor::min_max_range_for_buffer(diff.buffer_id),
+                    &working_copy,
+                )
+                .collect::<Vec<_>>()
+        });
+        if hunks.is_empty() {
+            return;
+        }
+
+        for _ in 0..operations {
+            let i = rng.random_range(0..hunks.len());
+            let hunk = &mut hunks[i];
+            let hunk_to_change = hunk.clone();
+            let stage = match hunk.secondary_status {
+                DiffHunkSecondaryStatus::HasSecondaryHunk => {
+                    hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
+                    true
+                }
+                DiffHunkSecondaryStatus::NoSecondaryHunk => {
+                    hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
+                    false
+                }
+                _ => unreachable!(),
+            };
+
+            index_text = diff.update(cx, |diff, cx| {
+                diff.stage_or_unstage_hunks(stage, &[hunk_to_change], &working_copy, true, cx)
+                    .unwrap()
+            });
+
+            diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
+            let found_hunks = diff.update(cx, |diff, cx| {
+                diff.snapshot(cx)
+                    .hunks_intersecting_range(
+                        Anchor::min_max_range_for_buffer(diff.buffer_id),
+                        &working_copy,
+                    )
+                    .collect::<Vec<_>>()
+            });
+            assert_eq!(hunks.len(), found_hunks.len());
+
+            for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
+                assert_eq!(
+                    expected_hunk.buffer_range.to_point(&working_copy),
+                    found_hunk.buffer_range.to_point(&working_copy)
+                );
+                assert_eq!(
+                    expected_hunk.diff_base_byte_range,
+                    found_hunk.diff_base_byte_range
+                );
+                assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
+            }
+            hunks = found_hunks;
+        }
+    }
+
+    #[gpui::test]
+    async fn test_row_to_base_text_row(cx: &mut TestAppContext) {
+        let base_text = "
+            zero
+            one
+            two
+            three
+            four
+            five
+            six
+            seven
+            eight
+        "
+        .unindent();
+        let buffer_text = "
+            zero
+            ONE
+            two
+            NINE
+            five
+            seven
+        "
+        .unindent();
+
+        //   zero
+        // - one
+        // + ONE
+        //   two
+        // - three
+        // - four
+        // + NINE
+        //   five
+        // - six
+        //   seven
+        // + eight
+
+        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+        let buffer_snapshot = buffer.snapshot();
+        let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx);
+        let expected_results = [
+            // don't format me
+            (0, 0),
+            (1, 2),
+            (2, 2),
+            (3, 5),
+            (4, 5),
+            (5, 7),
+            (6, 9),
+        ];
+        for (buffer_row, expected) in expected_results {
+            assert_eq!(
+                diff.row_to_base_text_row(buffer_row, &buffer_snapshot),
+                expected,
+                "{buffer_row}"
+            );
+        }
+    }
+}

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

@@ -2647,13 +2647,13 @@ async fn test_git_diff_base_change(
     local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
         let buffer = buffer_local_a.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(staged_text.as_str())
         );
         assert_hunks(
             diff.hunks_in_row_range(0..4, buffer, cx),
             buffer,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[(1..2, "", "two\n", DiffHunkStatus::added_none())],
         );
     });
@@ -2677,13 +2677,13 @@ async fn test_git_diff_base_change(
     remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
         let buffer = remote_buffer_a.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(staged_text.as_str())
         );
         assert_hunks(
             diff.hunks_in_row_range(0..4, buffer, cx),
             buffer,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[(1..2, "", "two\n", DiffHunkStatus::added_none())],
         );
     });
@@ -2699,13 +2699,13 @@ async fn test_git_diff_base_change(
     remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
         let buffer = remote_buffer_a.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(committed_text.as_str())
         );
         assert_hunks(
             diff.hunks_in_row_range(0..4, buffer, cx),
             buffer,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[(
                 1..2,
                 "TWO\n",
@@ -2731,13 +2731,13 @@ async fn test_git_diff_base_change(
     local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
         let buffer = buffer_local_a.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(new_staged_text.as_str())
         );
         assert_hunks(
             diff.hunks_in_row_range(0..4, buffer, cx),
             buffer,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[(2..3, "", "three\n", DiffHunkStatus::added_none())],
         );
     });
@@ -2746,13 +2746,13 @@ async fn test_git_diff_base_change(
     remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
         let buffer = remote_buffer_a.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(new_staged_text.as_str())
         );
         assert_hunks(
             diff.hunks_in_row_range(0..4, buffer, cx),
             buffer,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[(2..3, "", "three\n", DiffHunkStatus::added_none())],
         );
     });
@@ -2760,13 +2760,13 @@ async fn test_git_diff_base_change(
     remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
         let buffer = remote_buffer_a.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(new_committed_text.as_str())
         );
         assert_hunks(
             diff.hunks_in_row_range(0..4, buffer, cx),
             buffer,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[(
                 1..2,
                 "TWO_HUNDRED\n",
@@ -2813,13 +2813,13 @@ async fn test_git_diff_base_change(
     local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
         let buffer = buffer_local_b.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(staged_text.as_str())
         );
         assert_hunks(
             diff.hunks_in_row_range(0..4, buffer, cx),
             buffer,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[(1..2, "", "two\n", DiffHunkStatus::added_none())],
         );
     });
@@ -2842,7 +2842,7 @@ async fn test_git_diff_base_change(
     remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
         let buffer = remote_buffer_b.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(staged_text.as_str())
         );
         assert_hunks(
@@ -2864,7 +2864,7 @@ async fn test_git_diff_base_change(
     local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
         let buffer = buffer_local_b.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(new_staged_text.as_str())
         );
         assert_hunks(
@@ -2878,7 +2878,7 @@ async fn test_git_diff_base_change(
     remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
         let buffer = remote_buffer_b.read(cx);
         assert_eq!(
-            diff.base_text_string().as_deref(),
+            diff.base_text_string(cx).as_deref(),
             Some(new_staged_text.as_str())
         );
         assert_hunks(

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -506,12 +506,7 @@ struct BufferState {
     _subscriptions: [gpui::Subscription; 2],
 }
 
-fn foo(buffer: Entity<Buffer>, cx: &mut App) {
-    buffer.update(cx, |buffer, cx| {
-        let new_buffer = Buffer::new();
-        std::mem::replace(buffer, new_buffer);
-    })
-}
+
 
 struct DiffState {
     diff: Entity<BufferDiff>,
@@ -2644,7 +2639,7 @@ impl MultiBuffer {
     ) {
         debug_assert!(self.diffs.values().all(|diff| diff.main_buffer.is_some()));
 
-        let diff_change_range = 0..diff.read(cx).base_text().len();
+        let diff_change_range = 0..diff.read(cx).base_text(cx).len();
         self.inverted_buffer_diff_changed(
             diff.clone(),
             diff_change_range,

crates/project/src/git_store.rs 🔗

@@ -708,7 +708,7 @@ impl GitStore {
             buffer_diff
                 .update(cx, |buffer_diff, cx| {
                     buffer_diff.set_base_text(
-                        content.map(Arc::new),
+                        content.map(|s| s.as_ref().into()),
                         buffer_snapshot.language().cloned(),
                         Some(languages.clone()),
                         buffer_snapshot.text,
@@ -2611,7 +2611,7 @@ impl GitStore {
                 .or_default();
             shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone());
         })?;
-        let staged_text = diff.read_with(&cx, |diff, _| diff.base_text_string())?;
+        let staged_text = diff.read_with(&cx, |diff, _| diff.base_text_string(cx))?;
         Ok(proto::OpenUnstagedDiffResponse { staged_text })
     }
 
@@ -2641,14 +2641,14 @@ impl GitStore {
             let unstaged_diff = diff.secondary_diff();
             let index_snapshot = unstaged_diff.and_then(|diff| {
                 let diff = diff.read(cx);
-                diff.base_text_exists().then(|| diff.base_text())
+                diff.base_text_exists().then(|| diff.base_text(cx))
             });
 
             let mode;
             let staged_text;
             let committed_text;
             if diff.base_text_exists() {
-                let committed_snapshot = diff.base_text();
+                let committed_snapshot = diff.base_text(cx);
                 committed_text = Some(committed_snapshot.text());
                 if let Some(index_text) = index_snapshot {
                     if index_text.remote_id() == committed_snapshot.remote_id() {

crates/project/src/project_tests.rs 🔗

@@ -7849,9 +7849,9 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext)
     // The hunks are initially unstaged.
     uncommitted_diff.read_with(cx, |diff, cx| {
         assert_hunks(
-            diff.hunks(&snapshot, cx),
+            diff.snapshot(cx).hunks(&snapshot),
             &snapshot,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[
                 (
                     0..0,
@@ -7880,12 +7880,12 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext)
 
     // Stage the first hunk.
     uncommitted_diff.update(cx, |diff, cx| {
-        let hunk = diff.hunks(&snapshot, cx).next().unwrap();
+        let hunk = diff.snapshot(cx).hunks(&snapshot).next().unwrap();
         diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
         assert_hunks(
-            diff.hunks(&snapshot, cx),
+            diff.snapshot(cx).hunks(&snapshot),
             &snapshot,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[
                 (
                     0..0,
@@ -7912,12 +7912,12 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext)
     // Stage the second hunk *before* receiving the FS event for the first hunk.
     cx.run_until_parked();
     uncommitted_diff.update(cx, |diff, cx| {
-        let hunk = diff.hunks(&snapshot, cx).nth(1).unwrap();
+        let hunk = diff.snapshot(cx).hunks(&snapshot).nth(1).unwrap();
         diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
         assert_hunks(
             diff.hunks(&snapshot, cx),
             &snapshot,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[
                 (
                     0..0,
@@ -7961,7 +7961,7 @@ async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext)
         assert_hunks(
             diff.hunks(&snapshot, cx),
             &snapshot,
-            &diff.base_text_string().unwrap(),
+            &diff.base_text_string(cx).unwrap(),
             &[
                 (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
                 (
@@ -8164,7 +8164,7 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
         assert_hunks(
             uncommitted_diff.hunks(&snapshot, cx),
             &snapshot,
-            &uncommitted_diff.base_text_string().unwrap(),
+            &uncommitted_diff.base_text_string(cx).unwrap(),
             &[(
                 1..2,
                 "    println!(\"hello from HEAD\");\n",
@@ -10345,7 +10345,7 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo
     cx.run_until_parked();
 
     unstaged_diff.update(cx, |unstaged_diff, _cx| {
-        let base_text = unstaged_diff.base_text_string().unwrap();
+        let base_text = unstaged_diff.base_text_string(cx).unwrap();
         assert_eq!(base_text, file_1_staged, "Should start with file_1 staged");
     });
 
@@ -10369,7 +10369,7 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo
     // the `BufferChangedFilePath` event being handled.
     unstaged_diff.update(cx, |unstaged_diff, cx| {
         let snapshot = buffer.read(cx).snapshot();
-        let base_text = unstaged_diff.base_text_string().unwrap();
+        let base_text = unstaged_diff.base_text_string(cx).unwrap();
         assert_eq!(
             base_text, file_2_staged,
             "Diff bases should be automatically updated to file_2 staged content"
@@ -10389,7 +10389,7 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo
     cx.run_until_parked();
 
     uncommitted_diff.update(cx, |uncommitted_diff, _cx| {
-        let base_text = uncommitted_diff.base_text_string().unwrap();
+        let base_text = uncommitted_diff.base_text_string(cx).unwrap();
         assert_eq!(
             base_text, file_2_committed,
             "Uncommitted diff should compare against file_2 committed content"