made git diff rendering respect line wrap

Mikayla Maki created

Change summary

crates/editor/src/element.rs   | 207 +++++++++++++++++++++++++----------
crates/git/src/diff.rs         |   2 
crates/project/src/fs.rs       |  10 +
crates/theme/src/theme.rs      |  17 +-
styles/src/styleTree/editor.ts |  14 +-
5 files changed, 170 insertions(+), 80 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -46,6 +46,7 @@ use std::{
     ops::Range,
     sync::Arc,
 };
+use theme::DiffStyle;
 
 struct SelectionLayout {
     head: DisplayPoint,
@@ -525,98 +526,156 @@ impl EditorElement {
         layout: &mut LayoutState,
         cx: &mut PaintContext,
     ) {
-        let line_height = layout.position_map.line_height;
-        let scroll_position = layout.position_map.snapshot.scroll_position();
-        let scroll_top = scroll_position.y() * line_height;
-        for (ix, line) in layout.line_number_layouts.iter().enumerate() {
-            if let Some(line) = line {
-                let line_origin = bounds.origin()
-                    + vec2f(
-                        bounds.width() - line.width() - layout.gutter_padding,
-                        ix as f32 * layout.position_map.line_height
-                            - (scroll_top % layout.position_map.line_height),
-                    );
-                line.paint(
-                    line_origin,
-                    visible_bounds,
-                    layout.position_map.line_height,
-                    cx,
-                );
-            }
+        struct GutterLayout {
+            line_height: f32,
+            // scroll_position: Vector2F,
+            scroll_top: f32,
+            bounds: RectF,
         }
 
-        let (
-            inserted_color,
-            modified_color,
-            deleted_color,
-            width_multiplier,
-            corner_radius,
-            removed_width_mult,
-        ) = {
-            let editor = &cx.global::<Settings>().theme.editor;
-            (
-                editor.diff_background_inserted,
-                editor.diff_background_modified,
-                editor.diff_background_deleted,
-                editor.diff_indicator_width_multiplier,
-                editor.diff_indicator_corner_radius,
-                editor.removed_diff_width_multiplier,
-            )
-        };
+        struct DiffLayout<'a> {
+            buffer_line: usize,
+            last_diff: Option<(&'a DiffHunk<u32>, usize)>,
+        }
 
-        for hunk in &layout.diff_hunks {
-            let color = match hunk.status() {
-                DiffHunkStatus::Added => inserted_color,
-                DiffHunkStatus::Modified => modified_color,
+        fn diff_quad(
+            status: DiffHunkStatus,
+            layout_range: Range<usize>,
+            gutter_layout: &GutterLayout,
+            diff_style: &DiffStyle,
+        ) -> Quad {
+            let color = match status {
+                DiffHunkStatus::Added => diff_style.inserted,
+                DiffHunkStatus::Modified => diff_style.modified,
 
                 //TODO: This rendering is entirely a horrible hack
                 DiffHunkStatus::Removed => {
-                    let row = hunk.buffer_range.start;
+                    let row = layout_range.start;
 
-                    let offset = line_height / 2.;
-                    let start_y = row as f32 * line_height + offset - scroll_top;
-                    let end_y = start_y + line_height;
+                    let offset = gutter_layout.line_height / 2.;
+                    let start_y =
+                        row as f32 * gutter_layout.line_height + offset - gutter_layout.scroll_top;
+                    let end_y = start_y + gutter_layout.line_height;
 
-                    let width = removed_width_mult * line_height;
-                    let highlight_origin = bounds.origin() + vec2f(-width, start_y);
+                    let width = diff_style.removed_width_em * gutter_layout.line_height;
+                    let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
                     let highlight_size = vec2f(width * 2., end_y - start_y);
                     let highlight_bounds = RectF::new(highlight_origin, highlight_size);
 
-                    cx.scene.push_quad(Quad {
+                    return Quad {
                         bounds: highlight_bounds,
-                        background: Some(deleted_color),
+                        background: Some(diff_style.deleted),
                         border: Border::new(0., Color::transparent_black()),
-                        corner_radius: 1. * line_height,
-                    });
-
-                    continue;
+                        corner_radius: 1. * gutter_layout.line_height,
+                    };
                 }
             };
 
-            let start_row = hunk.buffer_range.start;
-            let end_row = hunk.buffer_range.end;
+            let start_row = layout_range.start;
+            let end_row = layout_range.end;
 
-            let start_y = start_row as f32 * line_height - scroll_top;
-            let end_y = end_row as f32 * line_height - scroll_top;
+            let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
+            let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
 
-            let width = width_multiplier * line_height;
-            let highlight_origin = bounds.origin() + vec2f(-width, start_y);
+            let width = diff_style.width_em * gutter_layout.line_height;
+            let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
             let highlight_size = vec2f(width * 2., end_y - start_y);
             let highlight_bounds = RectF::new(highlight_origin, highlight_size);
 
-            cx.scene.push_quad(Quad {
+            Quad {
                 bounds: highlight_bounds,
                 background: Some(color),
                 border: Border::new(0., Color::transparent_black()),
-                corner_radius: corner_radius * line_height,
-            });
+                corner_radius: diff_style.corner_radius * gutter_layout.line_height,
+            }
+        }
+
+        let gutter_layout = {
+            let scroll_position = layout.position_map.snapshot.scroll_position();
+            let line_height = layout.position_map.line_height;
+            GutterLayout {
+                scroll_top: scroll_position.y() * line_height,
+                // scroll_position,
+                line_height,
+                bounds,
+            }
+        };
+
+        let mut diff_layout = DiffLayout {
+            buffer_line: 0,
+            last_diff: None,
+        };
+
+        let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
+        // dbg!("***************");
+        // dbg!(&layout.diff_hunks);
+        // dbg!("***************");
+
+        // line is `None` when there's a line wrap
+        for (ix, line) in layout.line_number_layouts.iter().enumerate() {
+            // dbg!(ix);
+            if let Some(line) = line {
+                let line_origin = bounds.origin()
+                    + vec2f(
+                        bounds.width() - line.width() - layout.gutter_padding,
+                        ix as f32 * gutter_layout.line_height
+                            - (gutter_layout.scroll_top % gutter_layout.line_height),
+                    );
+
+                line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx);
+
+                //This line starts a buffer line, so let's do the diff calculation
+                let new_hunk = get_hunk(diff_layout.buffer_line, &layout.diff_hunks);
+
+                // This + the unwraps are annoying, but at least it's legible
+                let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) {
+                    (None, None) => (false, false),
+                    (None, Some(_)) => (false, true),
+                    (Some(_), None) => (true, false),
+                    (Some((old_hunk, _)), Some(new_hunk)) if new_hunk == old_hunk => (false, false),
+                    (Some(_), Some(_)) => (true, true),
+                };
+
+                // dbg!(diff_layout.buffer_line, is_starting);
+
+                if is_ending {
+                    let (last_hunk, start_line) = diff_layout.last_diff.take().unwrap();
+                    // dbg!("ending");
+                    // dbg!(start_line..ix);
+                    cx.scene.push_quad(diff_quad(
+                        last_hunk.status(),
+                        start_line..ix,
+                        &gutter_layout,
+                        diff_style,
+                    ));
+                }
+
+                if is_starting {
+                    let new_hunk = new_hunk.unwrap();
+
+                    diff_layout.last_diff = Some((new_hunk, ix));
+                };
+
+                diff_layout.buffer_line += 1;
+            }
+        }
+
+        // If we ran out  with a diff hunk still being prepped, paint it now
+        if let Some((last_hunk, start_line)) = diff_layout.last_diff {
+            let end_line = layout.line_number_layouts.len();
+            cx.scene.push_quad(diff_quad(
+                last_hunk.status(),
+                start_line..end_line,
+                &gutter_layout,
+                diff_style,
+            ))
         }
 
         if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
             let mut x = bounds.width() - layout.gutter_padding;
-            let mut y = *row as f32 * line_height - scroll_top;
+            let mut y = *row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
             x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
-            y += (line_height - indicator.size().y()) / 2.;
+            y += (gutter_layout.line_height - indicator.size().y()) / 2.;
             indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
         }
     }
@@ -1321,6 +1380,28 @@ impl EditorElement {
     }
 }
 
+/// Get the hunk that contains buffer_line, starting from start_idx
+/// Returns none if there is none found, and
+fn get_hunk(buffer_line: usize, hunks: &[DiffHunk<u32>]) -> Option<&DiffHunk<u32>> {
+    for i in 0..hunks.len() {
+        // Safety: Index out of bounds is handled by the check above
+        let hunk = hunks.get(i).unwrap();
+        if hunk.buffer_range.contains(&(buffer_line as u32)) {
+            return Some(hunk);
+        } else if hunk.status() == DiffHunkStatus::Removed
+            && buffer_line == hunk.buffer_range.start as usize
+        {
+            return Some(hunk);
+        } else if hunk.buffer_range.start > buffer_line as u32 {
+            // If we've passed the buffer_line, just stop
+            return None;
+        }
+    }
+
+    // We reached the end of the array without finding a hunk, just return none.
+    return None;
+}
+
 impl Element for EditorElement {
     type LayoutState = LayoutState;
     type PaintState = ();

crates/git/src/diff.rs 🔗

@@ -6,7 +6,7 @@ use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
 pub use git2 as libgit;
 use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum DiffHunkStatus {
     Added,
     Modified,

crates/project/src/fs.rs 🔗

@@ -1,10 +1,11 @@
 use anyhow::{anyhow, Result};
 use fsevent::EventStream;
 use futures::{future::BoxFuture, Stream, StreamExt};
-use git::repository::{FakeGitRepositoryState, GitRepository, LibGitRepository};
+use git::repository::{GitRepository, LibGitRepository};
 use language::LineEnding;
 use parking_lot::Mutex as SyncMutex;
 use smol::io::{AsyncReadExt, AsyncWriteExt};
+use std::sync::Arc;
 use std::{
     io,
     os::unix::fs::MetadataExt,
@@ -12,16 +13,17 @@ use std::{
     pin::Pin,
     time::{Duration, SystemTime},
 };
-use util::ResultExt;
-
 use text::Rope;
+use util::ResultExt;
 
 #[cfg(any(test, feature = "test-support"))]
 use collections::{btree_map, BTreeMap};
 #[cfg(any(test, feature = "test-support"))]
 use futures::lock::Mutex;
 #[cfg(any(test, feature = "test-support"))]
-use std::sync::{Arc, Weak};
+use git::repository::FakeGitRepositoryState;
+#[cfg(any(test, feature = "test-support"))]
+use std::sync::Weak;
 
 #[async_trait::async_trait]
 pub trait Fs: Send + Sync {

crates/theme/src/theme.rs 🔗

@@ -488,12 +488,7 @@ pub struct Editor {
     pub rename_fade: f32,
     pub document_highlight_read_background: Color,
     pub document_highlight_write_background: Color,
-    pub diff_background_deleted: Color,
-    pub diff_background_inserted: Color,
-    pub diff_background_modified: Color,
-    pub removed_diff_width_multiplier: f32,
-    pub diff_indicator_width_multiplier: f32,
-    pub diff_indicator_corner_radius: f32,
+    pub diff: DiffStyle,
     pub line_number: Color,
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
@@ -577,6 +572,16 @@ pub struct CodeActions {
     pub vertical_scale: f32,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct DiffStyle {
+    pub inserted: Color,
+    pub modified: Color,
+    pub deleted: Color,
+    pub removed_width_em: f32,
+    pub width_em: f32,
+    pub corner_radius: f32,
+}
+
 #[derive(Debug, Default, Clone, Copy)]
 pub struct Interactive<T> {
     pub default: T,

styles/src/styleTree/editor.ts 🔗

@@ -60,12 +60,14 @@ export default function editor(theme: Theme) {
       indicator: iconColor(theme, "secondary"),
       verticalScale: 0.618
     },
-    diffBackgroundDeleted: theme.iconColor.error,
-    diffBackgroundInserted: theme.iconColor.ok,
-    diffBackgroundModified: theme.iconColor.warning,
-    removedDiffWidthMultiplier: 0.275,
-    diffIndicatorWidthMultiplier: 0.16,
-    diffIndicatorCornerRadius: 0.05,
+    diff: {
+      deleted: theme.iconColor.error,
+      inserted: theme.iconColor.ok,
+      modified: theme.iconColor.warning,
+      removedWidthEm: 0.275,
+      widthEm: 0.16,
+      cornerRadius: 0.05,
+    },
     documentHighlightReadBackground: theme.editor.highlight.occurrence,
     documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
     errorColor: theme.textColor.error,