Improve diff hunks (#18283)

This PR improves the display of diff hunks:

- Deleted hunks now show a regular line indicator in the gutter when
expanded
- The rounding on the diff indicators in the gutter has been removed.

We also did some refactoring to ensure the sizing of the diff indicators
in the gutter were consistent.

#### Collapsed

<img width="1624" alt="Screenshot 2024-09-24 at 11 13 26 AM"
src="https://github.com/user-attachments/assets/f864dc83-cbbc-4d81-a62b-65c406ed310a">

#### Expanded

<img width="1624" alt="Screenshot 2024-09-24 at 11 13 35 AM"
src="https://github.com/user-attachments/assets/04d382ca-e0e6-4f1c-92eb-cd1e3a031c2c">


Release Notes:

- Improved the appearance of diff hunks in the editor.

---------

Co-authored-by: Max <max@zed.dev>
This commit is contained in:
Marshall Bowers 2024-09-24 11:40:08 -04:00 committed by GitHub
parent 2470db4901
commit 21be70f278
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 123 additions and 81 deletions

View file

@ -12473,11 +12473,6 @@ impl Editor {
Some(gpui::Point::new(source_x, source_y)) Some(gpui::Point::new(source_x, source_y))
} }
fn gutter_bounds(&self) -> Option<Bounds<Pixels>> {
let bounds = self.last_bounds?;
Some(element::gutter_bounds(bounds, self.gutter_dimensions))
}
pub fn has_active_completions_menu(&self) -> bool { pub fn has_active_completions_menu(&self) -> bool {
self.context_menu.read().as_ref().map_or(false, |menu| { self.context_menu.read().as_ref().map_or(false, |menu| {
menu.visible() && matches!(menu, ContextMenu::Completions(_)) menu.visible() && matches!(menu, ContextMenu::Completions(_))

View file

@ -1269,6 +1269,7 @@ impl EditorElement {
line_height: Pixels, line_height: Pixels,
gutter_hitbox: &Hitbox, gutter_hitbox: &Hitbox,
display_rows: Range<DisplayRow>, display_rows: Range<DisplayRow>,
anchor_range: Range<Anchor>,
snapshot: &EditorSnapshot, snapshot: &EditorSnapshot,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> { ) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
@ -1289,30 +1290,84 @@ impl EditorElement {
.git .git
.git_gutter .git_gutter
.unwrap_or_default(); .unwrap_or_default();
let display_hunks = buffer_snapshot
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row) self.editor.update(cx, |editor, cx| {
.map(|hunk| diff_hunk_to_display(&hunk, snapshot)) let expanded_hunks = &editor.expanded_hunks.hunks;
.dedup() let expanded_hunks_start_ix = expanded_hunks
.map(|hunk| match git_gutter_setting { .binary_search_by(|hunk| {
GitGutterSetting::TrackedFiles => { hunk.hunk_range
let hitbox = match hunk { .end
DisplayDiffHunk::Unfolded { .. } => { .cmp(&anchor_range.start, &buffer_snapshot)
let hunk_bounds = Self::diff_hunk_bounds( .then(Ordering::Less)
snapshot, })
line_height, .unwrap_err();
gutter_hitbox.bounds, let mut expanded_hunks = expanded_hunks[expanded_hunks_start_ix..].iter().peekable();
&hunk,
); let display_hunks = buffer_snapshot
Some(cx.insert_hitbox(hunk_bounds, true)) .git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
.filter_map(|hunk| {
let mut display_hunk = diff_hunk_to_display(&hunk, snapshot);
if let DisplayDiffHunk::Unfolded {
multi_buffer_range,
status,
..
} = &mut display_hunk
{
let mut is_expanded = false;
while let Some(expanded_hunk) = expanded_hunks.peek() {
match expanded_hunk
.hunk_range
.start
.cmp(&multi_buffer_range.start, &buffer_snapshot)
{
Ordering::Less => {
expanded_hunks.next();
}
Ordering::Equal => {
is_expanded = true;
break;
}
Ordering::Greater => {
break;
}
}
} }
DisplayDiffHunk::Folded { .. } => None, match status {
}; DiffHunkStatus::Added => {}
(hunk, hitbox) DiffHunkStatus::Modified => {}
} DiffHunkStatus::Removed => {
GitGutterSetting::Hide => (hunk, None), if is_expanded {
}) return None;
.collect(); }
display_hunks }
}
}
Some(display_hunk)
})
.dedup()
.map(|hunk| match git_gutter_setting {
GitGutterSetting::TrackedFiles => {
let hitbox = match hunk {
DisplayDiffHunk::Unfolded { .. } => {
let hunk_bounds = Self::diff_hunk_bounds(
snapshot,
line_height,
gutter_hitbox.bounds,
&hunk,
);
Some(cx.insert_hitbox(hunk_bounds, true))
}
DisplayDiffHunk::Folded { .. } => None,
};
(hunk, hitbox)
}
GitGutterSetting::Hide => (hunk, None),
})
.collect();
display_hunks
})
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -3187,7 +3242,7 @@ impl EditorElement {
Some(( Some((
hunk_bounds, hunk_bounds,
cx.theme().status().modified, cx.theme().status().modified,
Corners::all(1. * line_height), Corners::all(px(0.)),
)) ))
} }
DisplayDiffHunk::Unfolded { status, .. } => { DisplayDiffHunk::Unfolded { status, .. } => {
@ -3195,12 +3250,12 @@ impl EditorElement {
DiffHunkStatus::Added => ( DiffHunkStatus::Added => (
hunk_hitbox.bounds, hunk_hitbox.bounds,
cx.theme().status().created, cx.theme().status().created,
Corners::all(0.05 * line_height), Corners::all(px(0.)),
), ),
DiffHunkStatus::Modified => ( DiffHunkStatus::Modified => (
hunk_hitbox.bounds, hunk_hitbox.bounds,
cx.theme().status().modified, cx.theme().status().modified,
Corners::all(0.05 * line_height), Corners::all(px(0.)),
), ),
DiffHunkStatus::Removed => ( DiffHunkStatus::Removed => (
Bounds::new( Bounds::new(
@ -3244,7 +3299,7 @@ impl EditorElement {
let start_y = display_row.as_f32() * line_height - scroll_top; let start_y = display_row.as_f32() * line_height - scroll_top;
let end_y = start_y + line_height; let end_y = start_y + line_height;
let width = 0.275 * line_height; let width = Self::diff_hunk_strip_width(line_height);
let highlight_origin = gutter_bounds.origin + point(px(0.), start_y); let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
let highlight_size = size(width, end_y - start_y); let highlight_size = size(width, end_y - start_y);
Bounds::new(highlight_origin, highlight_size) Bounds::new(highlight_origin, highlight_size)
@ -3277,7 +3332,7 @@ impl EditorElement {
let start_y = start_row.as_f32() * line_height - scroll_top; let start_y = start_row.as_f32() * line_height - scroll_top;
let end_y = end_row_in_current_excerpt.as_f32() * line_height - scroll_top; let end_y = end_row_in_current_excerpt.as_f32() * line_height - scroll_top;
let width = 0.275 * line_height; let width = Self::diff_hunk_strip_width(line_height);
let highlight_origin = gutter_bounds.origin + point(px(0.), start_y); let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
let highlight_size = size(width, end_y - start_y); let highlight_size = size(width, end_y - start_y);
Bounds::new(highlight_origin, highlight_size) Bounds::new(highlight_origin, highlight_size)
@ -3289,7 +3344,7 @@ impl EditorElement {
let start_y = row.as_f32() * line_height - offset - scroll_top; let start_y = row.as_f32() * line_height - offset - scroll_top;
let end_y = start_y + line_height; let end_y = start_y + line_height;
let width = 0.35 * line_height; let width = (0.35 * line_height).floor();
let highlight_origin = gutter_bounds.origin + point(px(0.), start_y); let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
let highlight_size = size(width, end_y - start_y); let highlight_size = size(width, end_y - start_y);
Bounds::new(highlight_origin, highlight_size) Bounds::new(highlight_origin, highlight_size)
@ -3298,6 +3353,12 @@ impl EditorElement {
} }
} }
/// Returns the width of the diff strip that will be displayed in the gutter.
pub(super) fn diff_hunk_strip_width(line_height: Pixels) -> Pixels {
// We floor the value to prevent pixel rounding.
(0.275 * line_height).floor()
}
fn paint_gutter_indicators(&self, layout: &mut EditorLayout, cx: &mut WindowContext) { fn paint_gutter_indicators(&self, layout: &mut EditorLayout, cx: &mut WindowContext) {
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| { cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
cx.with_element_namespace("gutter_fold_toggles", |cx| { cx.with_element_namespace("gutter_fold_toggles", |cx| {
@ -5158,6 +5219,7 @@ impl Element for EditorElement {
line_height, line_height,
&gutter_hitbox, &gutter_hitbox,
start_row..end_row, start_row..end_row,
start_anchor..end_anchor,
&snapshot, &snapshot,
cx, cx,
); );

View file

@ -90,8 +90,8 @@ pub fn diff_hunk_to_display(
let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start); let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start);
let hunk_end_point = Point::new(hunk_end_row.0, 0); let hunk_end_point = Point::new(hunk_end_row.0, 0);
let multi_buffer_start = snapshot.buffer_snapshot.anchor_after(hunk_start_point); let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point);
let multi_buffer_end = snapshot.buffer_snapshot.anchor_before(hunk_end_point); let multi_buffer_end = snapshot.buffer_snapshot.anchor_after(hunk_end_point);
let end = hunk_end_point.to_display_point(snapshot).row(); let end = hunk_end_point.to_display_point(snapshot).row();
DisplayDiffHunk::Unfolded { DisplayDiffHunk::Unfolded {

View file

@ -14,8 +14,8 @@ use multi_buffer::{
use settings::SettingsStore; use settings::SettingsStore;
use text::{BufferId, Point}; use text::{BufferId, Point};
use ui::{ use ui::{
div, h_flex, rems, v_flex, ActiveTheme, Context as _, ContextMenu, InteractiveElement, prelude::*, ActiveTheme, ContextMenu, InteractiveElement, IntoElement, ParentElement, Pixels,
IntoElement, ParentElement, Pixels, Styled, ViewContext, VisualContext, Styled, ViewContext, VisualContext,
}; };
use util::{debug_panic, RangeExt}; use util::{debug_panic, RangeExt};
@ -38,7 +38,7 @@ pub(super) struct HoveredHunk {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(super) struct ExpandedHunks { pub(super) struct ExpandedHunks {
hunks: Vec<ExpandedHunk>, pub(crate) hunks: Vec<ExpandedHunk>,
diff_base: HashMap<BufferId, DiffBaseBuffer>, diff_base: HashMap<BufferId, DiffBaseBuffer>,
hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>, hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
} }
@ -414,39 +414,22 @@ impl Editor {
style: BlockStyle::Flex, style: BlockStyle::Flex,
disposition: BlockDisposition::Above, disposition: BlockDisposition::Above,
render: Box::new(move |cx| { render: Box::new(move |cx| {
let Some(gutter_bounds) = editor.read(cx).gutter_bounds() else { let width = EditorElement::diff_hunk_strip_width(cx.line_height());
return div().into_any_element(); let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
};
let (gutter_dimensions, hunk_bounds, close_button) = let close_button = editor.update(cx.context, |editor, cx| {
editor.update(cx.context, |editor, cx| { let editor_snapshot = editor.snapshot(cx);
let editor_snapshot = editor.snapshot(cx); let hunk_display_range = hunk
let hunk_display_range = hunk .multi_buffer_range
.multi_buffer_range .clone()
.clone() .to_display_points(&editor_snapshot);
.to_display_points(&editor_snapshot); editor.close_hunk_diff_button(
let gutter_dimensions = editor.gutter_dimensions; hunk.clone(),
let hunk_bounds = EditorElement::diff_hunk_bounds( hunk_display_range.start.row(),
&editor_snapshot, cx,
cx.line_height(), )
gutter_bounds, });
&DisplayDiffHunk::Unfolded {
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
multi_buffer_range: hunk.multi_buffer_range.clone(),
display_row_range: hunk_display_range.start.row()
..hunk_display_range.end.row(),
status: hunk.status,
},
);
let close_button = editor.close_hunk_diff_button(
hunk.clone(),
hunk_display_range.start.row(),
cx,
);
(gutter_dimensions, hunk_bounds, close_button)
});
let click_editor = editor.clone();
let clicked_hunk = hunk.clone();
h_flex() h_flex()
.id("gutter with editor") .id("gutter with editor")
.bg(deleted_hunk_color) .bg(deleted_hunk_color)
@ -461,27 +444,29 @@ impl Editor {
.child( .child(
h_flex() h_flex()
.id("gutter hunk") .id("gutter hunk")
.bg(cx.theme().status().deleted)
.pl(gutter_dimensions.margin .pl(gutter_dimensions.margin
+ gutter_dimensions + gutter_dimensions
.git_blame_entries_width .git_blame_entries_width
.unwrap_or_default()) .unwrap_or_default())
.max_w(hunk_bounds.size.width) .max_w(width)
.min_w(hunk_bounds.size.width) .min_w(width)
.size_full() .size_full()
.cursor(CursorStyle::PointingHand) .cursor(CursorStyle::PointingHand)
.on_mouse_down(MouseButton::Left, { .on_mouse_down(MouseButton::Left, {
let click_hunk = hunk.clone(); let editor = editor.clone();
move |e, cx| { let hunk = hunk.clone();
let modifiers = e.modifiers; move |event, cx| {
let modifiers = event.modifiers;
if modifiers.control || modifiers.platform { if modifiers.control || modifiers.platform {
click_editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.toggle_hovered_hunk(&click_hunk, cx); editor.toggle_hovered_hunk(&hunk, cx);
}); });
} else { } else {
click_editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.open_hunk_context_menu( editor.open_hunk_context_menu(
clicked_hunk.clone(), hunk.clone(),
e.position, event.position,
cx, cx,
); );
}); });