very basic per-line staging visualization

Cole Miller and Smit Barmase created

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

crates/buffer_diff/src/buffer_diff.rs   | 78 ++++++++++++++++++++++++++
crates/editor/src/editor.rs             |  2 
crates/editor/src/element.rs            | 24 ++++++-
crates/language/src/language.rs         |  4 
crates/language/src/text_diff.rs        | 36 +++++++-----
crates/multi_buffer/src/multi_buffer.rs | 14 ++++
6 files changed, 136 insertions(+), 22 deletions(-)

Detailed changes

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -3,7 +3,7 @@ use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as
 use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
 use language::{
     Capability, Diff, DiffOptions, File, Language, LanguageName, LanguageRegistry,
-    language_settings::language_settings, word_diff_ranges,
+    language_settings::language_settings, text_diff, word_diff_ranges,
 };
 use rope::Rope;
 use std::{
@@ -108,12 +108,17 @@ pub struct DiffHunk {
     pub buffer_word_diffs: Vec<Range<Anchor>>,
     // 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>>,
 }
 
 /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
 #[derive(Debug, Clone, PartialEq, Eq)]
 struct InternalDiffHunk {
+    // Range of text that has been added to the main buffer
     buffer_range: Range<Anchor>,
+    // Range of text that has been deleted from the diff base
     diff_base_byte_range: Range<usize>,
     base_word_diffs: Vec<Range<usize>>,
     buffer_word_diffs: Vec<Range<Anchor>>,
@@ -820,6 +825,9 @@ 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();
+
                 if !start_anchor.is_valid(buffer) {
                     continue;
                 }
@@ -879,6 +887,17 @@ impl BufferDiffInner<language::BufferSnapshot> {
                         } else if secondary_range == (start_point..end_point) {
                             secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
                         } else if secondary_range.start <= end_point {
+                            // 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,
+                                &secondary_hunk,
+                                buffer,
+                                &self.base_text,
+                                &secondary.unwrap().base_text,
+                            );
+                            buffer_staged_lines = buffer;
+                            base_staged_lines = base;
+
                             secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
                         }
                     }
@@ -891,6 +910,8 @@ impl BufferDiffInner<language::BufferSnapshot> {
                     base_word_diffs,
                     buffer_word_diffs,
                     secondary_status,
+                    buffer_staged_lines,
+                    base_staged_lines,
                 });
             }
         })
@@ -917,11 +938,66 @@ 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(),
             })
         })
     }
 }
 
+fn compute_staged_lines(
+    hunk: &InternalDiffHunk,
+    secondary_hunk: &InternalDiffHunk,
+    buffer: &text::BufferSnapshot,
+    base_text: &language::BufferSnapshot,
+    secondary_base_text: &language::BufferSnapshot,
+) -> (Vec<Range<Anchor>>, Vec<Range<Point>>) {
+    let primary_hunk_buffer_text = buffer
+        .text_for_range(hunk.buffer_range.clone())
+        .collect::<String>();
+    let secondary_hunk_buffer_text = buffer
+        .text_for_range(secondary_hunk.buffer_range.clone())
+        .collect::<String>();
+    let primary_hunk_base_text = base_text
+        .text_for_range(hunk.diff_base_byte_range.clone())
+        .collect::<String>();
+    let secondary_hunk_base_text = secondary_base_text
+        .text_for_range(secondary_hunk.diff_base_byte_range.clone())
+        .collect::<String>();
+
+    let primary_hunk_start_in_buffer = hunk.buffer_range.start.to_offset(buffer);
+    // No word diffs
+    let buffer_staged_lines = language::text_diff_with_options_internal(
+        dbg!(&secondary_hunk_buffer_text),
+        dbg!(&primary_hunk_buffer_text),
+        DiffOptions {
+            language_scope: None,
+            max_word_diff_len: 0,
+            max_word_diff_line_count: 0,
+        },
+    )
+    .into_iter()
+    .map(|edit| {
+        buffer.anchor_before(edit.new.start + primary_hunk_start_in_buffer)
+            ..buffer.anchor_after(edit.new.end + primary_hunk_start_in_buffer)
+    })
+    .collect::<Vec<_>>();
+
+    // FIXME
+    if !buffer_staged_lines.is_empty() {
+        eprintln!(
+            "staged lines: {:?}",
+            buffer_staged_lines
+                .iter()
+                .map(|range| buffer.text_for_range(range.clone()).collect::<String>())
+                .collect::<Vec<_>>()
+        );
+    }
+
+    // FIXME
+    (buffer_staged_lines, Vec::new())
+}
+
 fn build_diff_options(
     file: Option<&Arc<dyn File>>,
     language: Option<LanguageName>,

crates/editor/src/editor.rs 🔗

@@ -20622,6 +20622,8 @@ impl Editor {
                             ..hunk.diff_base_byte_range.end.0,
                         secondary_status: hunk.status.secondary,
                         range: Point::zero()..Point::zero(), // unused
+                        buffer_staged_lines: Vec::new(),     // unused
+                        base_staged_lines: Vec::new(),       // unused
                     })
                     .collect::<Vec<_>>(),
                 &buffer_snapshot,

crates/editor/src/element.rs 🔗

@@ -9682,6 +9682,20 @@ impl Element for EditorElement {
                         .row_infos(start_row)
                         .take((start_row..end_row).len())
                         .collect::<Vec<RowInfo>>();
+
+                    // FIXME
+                    let all_staged_lines = snapshot
+                        .buffer_snapshot()
+                        .diff_hunks_in_range(Anchor::min()..Anchor::max())
+                        .flat_map(|hunk| hunk.staged_rows)
+                        .map(|row| {
+                            snapshot
+                                .display_snapshot
+                                .point_to_display_point(Point::new(row.0, 0), Bias::Left)
+                                .row()
+                        })
+                        .collect::<Vec<_>>();
+
                     let is_row_soft_wrapped = |row: usize| {
                         row_infos
                             .get(row)
@@ -9761,15 +9775,17 @@ impl Element for EditorElement {
                             type_id: None,
                         };
 
-                        let background = if Self::diff_hunk_hollow(diff_status, cx) {
+                        let base_display_point =
+                            DisplayPoint::new(start_row + DisplayRow(ix as u32), 0);
+
+                        let background = if Self::diff_hunk_hollow(diff_status, cx)
+                            || all_staged_lines.contains(&base_display_point.row())
+                        {
                             hollow_highlight
                         } else {
                             filled_highlight
                         };
 
-                        let base_display_point =
-                            DisplayPoint::new(start_row + DisplayRow(ix as u32), 0);
-
                         highlighted_rows
                             .entry(base_display_point.row())
                             .or_insert(background);

crates/language/src/language.rs 🔗

@@ -67,8 +67,8 @@ use task::RunnableTag;
 pub use task_context::{ContextLocation, ContextProvider, RunnableRange};
 pub use text_diff::{
     DiffOptions, apply_diff_patch, apply_reversed_diff_patch, line_diff, text_diff,
-    text_diff_with_options, unified_diff, unified_diff_with_context, unified_diff_with_offsets,
-    word_diff_ranges,
+    text_diff_with_options, text_diff_with_options_internal, unified_diff,
+    unified_diff_with_context, unified_diff_with_offsets, word_diff_ranges,
 };
 use theme::SyntaxTheme;
 pub use toolchain::{

crates/language/src/text_diff.rs 🔗

@@ -237,12 +237,11 @@ impl Default for DiffOptions {
 
 /// Computes a diff between two strings, using a specific language scope's
 /// word characters for word-level diffing.
-pub fn text_diff_with_options(
+pub fn text_diff_with_options_internal(
     old_text: &str,
     new_text: &str,
     options: DiffOptions,
-) -> Vec<(Range<usize>, Arc<str>)> {
-    let empty: Arc<str> = Arc::default();
+) -> Vec<text::Edit<usize>> {
     let mut edits = Vec::new();
     let mut hunk_input = InternedInput::default();
     let input = InternedInput::new(
@@ -275,26 +274,33 @@ pub fn text_diff_with_options(
                         old_offset + old_byte_range.start..old_offset + old_byte_range.end;
                     let new_byte_range =
                         new_offset + new_byte_range.start..new_offset + new_byte_range.end;
-                    let replacement_text = if new_byte_range.is_empty() {
-                        empty.clone()
-                    } else {
-                        new_text[new_byte_range].into()
-                    };
-                    edits.push((old_byte_range, replacement_text));
+                    edits.push(text::Edit {
+                        old: old_byte_range,
+                        new: new_byte_range,
+                    });
                 });
             } else {
-                let replacement_text = if new_byte_range.is_empty() {
-                    empty.clone()
-                } else {
-                    new_text[new_byte_range].into()
-                };
-                edits.push((old_byte_range, replacement_text));
+                edits.push(text::Edit {
+                    old: old_byte_range,
+                    new: new_byte_range,
+                });
             }
         },
     );
     edits
 }
 
+pub fn text_diff_with_options(
+    old_text: &str,
+    new_text: &str,
+    options: DiffOptions,
+) -> Vec<(Range<usize>, Arc<str>)> {
+    text_diff_with_options_internal(old_text, new_text, options)
+        .into_iter()
+        .map(|edit| (edit.old, new_text[edit.new].into()))
+        .collect()
+}
+
 pub fn apply_diff_patch(base_text: &str, patch: &str) -> Result<String, anyhow::Error> {
     let patch = diffy::Patch::from_str(patch).context("Failed to parse patch")?;
     let result = diffy::apply(base_text, &patch);

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -150,6 +150,7 @@ pub struct MultiBufferDiffHunk {
     pub status: DiffHunkStatus,
     /// The word diffs for this hunk.
     pub word_diffs: Vec<Range<MultiBufferOffset>>,
+    pub staged_rows: Vec<MultiBufferRow>,
 }
 
 impl MultiBufferDiffHunk {
@@ -3933,6 +3934,18 @@ impl MultiBufferSnapshot {
                     })
                     .unwrap_or_default();
 
+            let staged_rows = (!hunk.buffer_staged_lines.is_empty())
+                .then(|| {
+                    let mut rows = Vec::new();
+                    for range in &hunk.buffer_staged_lines {
+                        let range =
+                            Anchor::range_in_buffer(excerpt.id, range.clone()).to_point(self);
+                        rows.extend((range.start.row..range.end.row).map(MultiBufferRow));
+                    }
+                    rows
+                })
+                .unwrap_or_default();
+
             let buffer_range = if is_inverted {
                 excerpt.buffer.anchor_after(hunk.diff_base_byte_range.start)
                     ..excerpt.buffer.anchor_before(hunk.diff_base_byte_range.end)
@@ -3958,6 +3971,7 @@ impl MultiBufferSnapshot {
                     kind: status_kind,
                     secondary: hunk.secondary_status,
                 },
+                staged_rows,
             })
         })
     }