wip

Cole Miller and Neel created

Co-authored-by: Neel <neel@zed.dev>

Change summary

crates/buffer_diff/src/buffer_diff.rs | 155 +++++++++++++++++++++++++++-
1 file changed, 145 insertions(+), 10 deletions(-)

Detailed changes

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -109,8 +109,8 @@ pub struct DiffHunk {
     // Offsets relative to the start of the deleted diff that represent word diff locations
     pub base_word_diffs: Vec<Range<usize>>,
     // These fields are nonempty only if the secondary status is OverlapsWithSecondaryHunk
-    pub buffer_staged_lines: Vec<Range<Anchor>>,
-    pub base_staged_lines: Vec<Range<Point>>,
+    pub buffer_staged_ranges: Vec<Range<Anchor>>,
+    pub base_staged_ranges: Vec<Range<usize>>,
 }
 
 /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
@@ -544,6 +544,56 @@ impl BufferDiffSnapshot {
             edit.new.start
         }
     }
+
+    #[cfg(test)]
+    fn debug_text(&self, buffer: &text::BufferSnapshot) -> String {
+        let mut text = String::new();
+        let mut last_hunk_end = text::Anchor::MIN;
+
+        for hunk in self.hunks(buffer) {
+            text.extend(buffer.text_for_range(last_hunk_end..hunk.buffer_range.start));
+            {
+                let deleted_text = self
+                    .base_text()
+                    .text_for_range(hunk.diff_base_byte_range.clone())
+                    .collect::<String>();
+                let mut start_of_line = hunk.diff_base_byte_range.start;
+                text.extend(deleted_text.lines().map(|line| {
+                    let is_staged = hunk.secondary_status
+                        == DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
+                        || hunk.base_staged_ranges.iter().any(|range| {
+                            range.start <= start_of_line && start_of_line + line.len() <= range.end
+                        });
+                    let modifier = if is_staged { "*" } else { "" };
+                    start_of_line += line.len() + 1;
+                    format!("[-{modifier}] {line}\n")
+                }));
+            }
+            {
+                let inserted_text = buffer
+                    .text_for_range(hunk.buffer_range.clone())
+                    .collect::<String>();
+                let mut start_of_line = hunk.buffer_range.start.to_offset(buffer);
+                text.extend(inserted_text.lines().map(|line| {
+                    let start_of_line_anchor = buffer.anchor_after(start_of_line);
+                    let end_of_line_anchor = buffer.anchor_before(start_of_line + line.len());
+                    let is_staged = hunk.secondary_status
+                        == DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
+                        || hunk.buffer_staged_ranges.iter().any(|range| {
+                            range.start.cmp(&start_of_line_anchor, buffer).is_le()
+                                && end_of_line_anchor.cmp(&range.end, buffer).is_le()
+                        });
+                    let modifier = if is_staged { "*" } else { "" };
+                    start_of_line += line.len() + 1;
+                    format!("[+{modifier}] {line}\n")
+                }));
+            }
+            last_hunk_end = hunk.buffer_range.end;
+        }
+
+        text.extend(buffer.text_for_range(last_hunk_end..text::Anchor::MAX));
+        text
+    }
 }
 
 impl BufferDiffInner<Entity<language::Buffer>> {
@@ -757,6 +807,15 @@ impl BufferDiffInner<Entity<language::Buffer>> {
         new_index_text.append(index_cursor.suffix());
         Some(new_index_text)
     }
+    
+    // Updates the index text to stage the given range from the buffer
+    fn stage_or_unstage_buffer_range(&mut self, stage: bool, range: Range<text::Anchor>) {
+        todo!()
+    }
+    
+    fn stage_or_unstage_base_text_range(&mut self, stage: bool, range: Range<usize>) {
+        todo!()
+    }
 }
 
 impl BufferDiffInner<language::BufferSnapshot> {
@@ -825,8 +884,8 @@ impl BufferDiffInner<language::BufferSnapshot> {
                 let base_word_diffs = hunk.base_word_diffs.clone();
                 let buffer_word_diffs = hunk.buffer_word_diffs.clone();
 
-                let mut buffer_staged_lines = Vec::new();
-                let mut base_staged_lines = Vec::new();
+                let mut buffer_staged_ranges = Vec::new();
+                let mut base_staged_ranges = Vec::new();
 
                 if !start_anchor.is_valid(buffer) {
                     continue;
@@ -887,6 +946,9 @@ impl BufferDiffInner<language::BufferSnapshot> {
                         } else if secondary_range == (start_point..end_point) {
                             secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
                         } else if secondary_range.start <= end_point {
+                            // primary (uncommitted) diff: current buffer vs. HEAD
+                            // secondary (unstaged) diff: current buffer vs. index
+
                             // FIXME this should be a background computation that only happens when either the diff or the secondary diff changes
                             let (buffer, base) = compute_staged_lines(
                                 &hunk,
@@ -895,8 +957,8 @@ impl BufferDiffInner<language::BufferSnapshot> {
                                 &self.base_text,
                                 &secondary.unwrap().base_text,
                             );
-                            buffer_staged_lines = buffer;
-                            base_staged_lines = base;
+                            buffer_staged_ranges = buffer;
+                            base_staged_ranges = base;
 
                             secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
                         }
@@ -910,8 +972,8 @@ impl BufferDiffInner<language::BufferSnapshot> {
                     base_word_diffs,
                     buffer_word_diffs,
                     secondary_status,
-                    buffer_staged_lines,
-                    base_staged_lines,
+                    buffer_staged_ranges,
+                    base_staged_ranges,
                 });
             }
         })
@@ -938,8 +1000,8 @@ impl BufferDiffInner<language::BufferSnapshot> {
                 secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
                 base_word_diffs: hunk.base_word_diffs.clone(),
                 buffer_word_diffs: hunk.buffer_word_diffs.clone(),
-                base_staged_lines: Vec::new(),
-                buffer_staged_lines: Vec::new(),
+                base_staged_ranges: Vec::new(),
+                buffer_staged_ranges: Vec::new(),
             })
         })
     }
@@ -2553,6 +2615,79 @@ mod tests {
         }
     }
 
+    #[gpui::test]
+    async fn test_partially_staged_hunks(cx: &mut TestAppContext) {
+        let head_text = "
+            aaa
+            bbb
+            ccc
+            ddd
+            eee
+            fff
+            ggg
+            hhh
+            iii
+        "
+        .unindent();
+
+        let buffer_text = "
+            aaa
+            bbb
+            XXX
+            YYY
+            ggg
+            hhh
+            iii
+        "
+        .unindent();
+
+        // Initially, we have one hunk that's fully unstaged.
+        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
+        let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(&head_text, &buffer, cx));
+        let uncommitted_diff = cx.new(|cx| {
+            let mut diff = BufferDiff::new_with_base_text(&head_text, &buffer, cx);
+            diff.set_secondary_diff(unstaged_diff.clone());
+            diff
+        });
+
+        let buffer_snapshot = buffer.snapshot();
+        let diff_snapshot = uncommitted_diff.read_with(cx, |diff, cx| diff.snapshot(cx));
+        let hunks = diff_snapshot.hunks(&buffer_snapshot).collect::<Vec<_>>();
+
+        eprintln!("{}", diff_snapshot.debug_text(&buffer_snapshot));
+
+        // Now stage a couple of lines
+        let new_index_text = "
+            aaa
+            bbb
+            ccc
+            ddd
+            eee
+            XXX
+            ggg
+            hhh
+            iii
+        "
+        .unindent();
+
+        unstaged_diff
+            .update(cx, |diff, cx| {
+                diff.set_base_text(Some(new_index_text.into()), None, buffer_snapshot, cx)
+            })
+            .await
+            .unwrap();
+
+        uncommitted_diff.update(cx, |diff, cx| {
+            diff.set_snapshot_with_secondary(
+                update,
+                buffer,
+                secondary_diff_change,
+                clear_pending_hunks,
+                cx,
+            )
+        })
+    }
+
     #[gpui::test]
     async fn test_stage_all_with_nested_hunks(cx: &mut TestAppContext) {
         // This test reproduces a crash where staging all hunks would cause an underflow