Add staged status information to diff hunks (#24475)

Release Notes:

- Render unstaged hunks in the project diff editor with a slashed
background

---------

Co-authored-by: maxbrunsfeld <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Cole Miller 2025-02-10 21:43:25 -05:00 committed by GitHub
parent a9de9e3cb4
commit 8f75fe25e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1132 additions and 753 deletions

View file

@ -38,7 +38,7 @@ clock.workspace = true
collections.workspace = true
convert_case.workspace = true
db.workspace = true
diff.workspace = true
buffer_diff.workspace = true
emojis.workspace = true
file_icons.workspace = true
futures.workspace = true

View file

@ -73,17 +73,16 @@ use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionsMenu, ContextMenuOrigin,
};
use diff::DiffHunkStatus;
use git::blame::GitBlame;
use gpui::{
div, impl_actions, linear_color_stop, linear_gradient, point, prelude::*, pulsating_between,
px, relative, size, Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext,
AvailableSpace, Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId,
Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId,
FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton,
MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled,
StyledText, Subscription, Task, TextRun, TextStyle, TextStyleRefinement, UTF16Selection,
UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
AvailableSpace, Background, Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase,
ElementId, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable,
FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers,
MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size,
Styled, StyledText, Subscription, Task, TextRun, TextStyle, TextStyleRefinement,
UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@ -722,6 +721,7 @@ pub struct Editor {
show_git_blame_gutter: bool,
show_git_blame_inline: bool,
show_git_blame_inline_delay_task: Option<Task<()>>,
distinguish_unstaged_diff_hunks: bool,
git_blame_inline_enabled: bool,
serialize_dirty_buffers: bool,
show_selection_menu: Option<bool>,
@ -1418,6 +1418,7 @@ impl Editor {
custom_context_menu: None,
show_git_blame_gutter: false,
show_git_blame_inline: false,
distinguish_unstaged_diff_hunks: false,
show_selection_menu: None,
show_git_blame_inline_delay_task: None,
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
@ -6878,8 +6879,7 @@ impl Editor {
let buffer = buffer.read(cx);
let original_text = diff
.read(cx)
.snapshot
.base_text
.base_text()
.as_ref()?
.as_rope()
.slice(hunk.diff_base_byte_range.clone());
@ -12290,6 +12290,10 @@ impl Editor {
});
}
pub fn set_distinguish_unstaged_diff_hunks(&mut self) {
self.distinguish_unstaged_diff_hunks = true;
}
pub fn expand_all_diff_hunks(
&mut self,
_: &ExpandAllHunkDiffs,
@ -13332,14 +13336,14 @@ impl Editor {
&self,
window: &mut Window,
cx: &mut App,
) -> BTreeMap<DisplayRow, Hsla> {
) -> BTreeMap<DisplayRow, Background> {
let snapshot = self.snapshot(window, cx);
let mut used_highlight_orders = HashMap::default();
self.highlighted_rows
.iter()
.flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
.fold(
BTreeMap::<DisplayRow, Hsla>::new(),
BTreeMap::<DisplayRow, Background>::new(),
|mut unique_rows, highlight| {
let start = highlight.range.start.to_display_point(&snapshot);
let end = highlight.range.end.to_display_point(&snapshot);
@ -13356,7 +13360,7 @@ impl Editor {
used_highlight_orders.entry(row).or_insert(highlight.index);
if highlight.index >= *used_index {
*used_index = highlight.index;
unique_rows.insert(DisplayRow(row), highlight.color);
unique_rows.insert(DisplayRow(row), highlight.color.into());
}
}
unique_rows
@ -15518,7 +15522,7 @@ impl EditorSnapshot {
) {
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
// when the caret is just above or just below the deleted hunk.
let allow_adjacent = hunk.status() == DiffHunkStatus::Removed;
let allow_adjacent = hunk.status().is_removed();
let related_to_selection = if allow_adjacent {
hunk.row_range.overlaps(&query_rows)
|| hunk.row_range.start == query_rows.end

View file

@ -7,7 +7,7 @@ use crate::{
},
JoinLines,
};
use diff::{BufferDiff, DiffHunkStatus};
use buffer_diff::{BufferDiff, DiffHunkStatus};
use futures::StreamExt;
use gpui::{
div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
@ -11989,7 +11989,7 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
struct Row9.2;
struct Row9.3;
struct Row10;"#},
vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
indoc! {r#"struct Row;
struct Row1;
struct Row1.1;
@ -12027,7 +12027,7 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
struct Row8;
struct Row9;
struct Row10;"#},
vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
vec![DiffHunkStatus::added(), DiffHunkStatus::added()],
indoc! {r#"struct Row;
struct Row1;
struct Row2;
@ -12074,11 +12074,11 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
«ˇ// something on bottom»
struct Row10;"#},
vec![
DiffHunkStatus::Added,
DiffHunkStatus::Added,
DiffHunkStatus::Added,
DiffHunkStatus::Added,
DiffHunkStatus::Added,
DiffHunkStatus::added(),
DiffHunkStatus::added(),
DiffHunkStatus::added(),
DiffHunkStatus::added(),
DiffHunkStatus::added(),
],
indoc! {r#"struct Row;
ˇstruct Row1;
@ -12126,7 +12126,7 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
struct Row99;
struct Row9;
struct Row10;"#},
vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
indoc! {r#"struct Row;
struct Row1;
struct Row33;
@ -12153,7 +12153,7 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
struct Row99;
struct Row9;
struct Row10;"#},
vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()],
indoc! {r#"struct Row;
struct Row1;
struct Row33;
@ -12182,12 +12182,12 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
struct Row9;
struct Row1011;ˇ"#},
vec![
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::Modified,
DiffHunkStatus::modified(),
DiffHunkStatus::modified(),
DiffHunkStatus::modified(),
DiffHunkStatus::modified(),
DiffHunkStatus::modified(),
DiffHunkStatus::modified(),
],
indoc! {r#"struct Row;
ˇstruct Row1;
@ -12265,7 +12265,7 @@ struct Row10;"#};
ˇ
struct Row8;
struct Row10;"#},
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
indoc! {r#"struct Row;
struct Row2;
@ -12288,7 +12288,7 @@ struct Row10;"#};
ˇ»
struct Row8;
struct Row10;"#},
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
indoc! {r#"struct Row;
struct Row2;
@ -12313,7 +12313,7 @@ struct Row10;"#};
struct Row8;ˇ
struct Row10;"#},
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()],
indoc! {r#"struct Row;
struct Row1;
ˇstruct Row2;
@ -12338,9 +12338,9 @@ struct Row10;"#};
struct Row8;ˇ»
struct Row10;"#},
vec![
DiffHunkStatus::Removed,
DiffHunkStatus::Removed,
DiffHunkStatus::Removed,
DiffHunkStatus::removed(),
DiffHunkStatus::removed(),
DiffHunkStatus::removed(),
],
indoc! {r#"struct Row;
struct Row1;

View file

@ -25,21 +25,21 @@ use crate::{
EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
use diff::DiffHunkStatus;
use file_icons::FileIcons;
use git::{blame::BlameEntry, Oid};
use gpui::{
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
Hsla, InteractiveElement, IntoElement, KeyBindingContextPredicate, Keystroke, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement,
WeakEntity, Window,
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
point, px, quad, relative, size, svg, transparent_black, Action, AnyElement, App,
AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners,
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _,
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement,
KeyBindingContextPredicate, Keystroke, Length, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
Subscription, TextRun, TextStyleRefinement, WeakEntity, Window,
};
use itertools::Itertools;
use language::{
@ -85,7 +85,6 @@ enum DisplayDiffHunk {
Folded {
display_row: DisplayRow,
},
Unfolded {
diff_base_byte_range: Range<usize>,
display_row_range: Range<DisplayRow>,
@ -2116,7 +2115,7 @@ impl EditorElement {
.get(&display_row)
.unwrap_or(&non_relative_number);
write!(&mut line_number, "{number}").unwrap();
if row_info.diff_status == Some(DiffHunkStatus::Removed) {
if matches!(row_info.diff_status, Some(DiffHunkStatus::Removed(_))) {
return None;
}
@ -4007,8 +4006,10 @@ impl EditorElement {
if row_infos[row_ix].diff_status.is_none() {
continue;
}
if row_infos[row_ix].diff_status == Some(DiffHunkStatus::Added)
&& *status != DiffHunkStatus::Added
if matches!(
row_infos[row_ix].diff_status,
Some(DiffHunkStatus::Added(_))
) && !matches!(*status, DiffHunkStatus::Added(_))
{
continue;
}
@ -4191,26 +4192,26 @@ impl EditorElement {
window.paint_quad(fill(Bounds { origin, size }, color));
};
let mut current_paint: Option<(Hsla, Range<DisplayRow>)> = None;
for (&new_row, &new_color) in &layout.highlighted_rows {
let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
for (&new_row, &new_background) in &layout.highlighted_rows {
match &mut current_paint {
Some((current_color, current_range)) => {
let current_color = *current_color;
let new_range_started = current_color != new_color
Some((current_background, current_range)) => {
let current_background = *current_background;
let new_range_started = current_background != new_background
|| current_range.end.next_row() != new_row;
if new_range_started {
paint_highlight(
current_range.start,
current_range.end,
current_color,
current_background,
);
current_paint = Some((new_color, new_row..new_row));
current_paint = Some((new_background, new_row..new_row));
continue;
} else {
current_range.end = current_range.end.next_row();
}
}
None => current_paint = Some((new_color, new_row..new_row)),
None => current_paint = Some((new_background, new_row..new_row)),
};
}
if let Some((color, range)) = current_paint {
@ -4409,6 +4410,7 @@ impl EditorElement {
hunk_bounds,
cx.theme().status().modified,
Corners::all(px(0.)),
&DiffHunkSecondaryStatus::None,
))
}
DisplayDiffHunk::Unfolded {
@ -4416,22 +4418,29 @@ impl EditorElement {
display_row_range,
..
} => hitbox.as_ref().map(|hunk_hitbox| match status {
DiffHunkStatus::Added => (
DiffHunkStatus::Added(secondary_status) => (
hunk_hitbox.bounds,
cx.theme().status().created,
Corners::all(px(0.)),
secondary_status,
),
DiffHunkStatus::Modified => (
DiffHunkStatus::Modified(secondary_status) => (
hunk_hitbox.bounds,
cx.theme().status().modified,
Corners::all(px(0.)),
secondary_status,
),
DiffHunkStatus::Removed if !display_row_range.is_empty() => (
hunk_hitbox.bounds,
cx.theme().status().deleted,
Corners::all(px(0.)),
),
DiffHunkStatus::Removed => (
DiffHunkStatus::Removed(secondary_status)
if !display_row_range.is_empty() =>
{
(
hunk_hitbox.bounds,
cx.theme().status().deleted,
Corners::all(px(0.)),
secondary_status,
)
}
DiffHunkStatus::Removed(secondary_status) => (
Bounds::new(
point(
hunk_hitbox.origin.x - hunk_hitbox.size.width,
@ -4441,11 +4450,17 @@ impl EditorElement {
),
cx.theme().status().deleted,
Corners::all(1. * line_height),
secondary_status,
),
}),
};
if let Some((hunk_bounds, background_color, corner_radii)) = hunk_to_paint {
if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) =
hunk_to_paint
{
if *secondary_status != DiffHunkSecondaryStatus::None {
background_color.a *= 0.6;
}
window.paint_quad(quad(
hunk_bounds,
corner_radii,
@ -4481,7 +4496,7 @@ impl EditorElement {
status,
..
} => {
if *status == DiffHunkStatus::Removed && display_row_range.is_empty() {
if status.is_removed() && display_row_range.is_empty() {
let row = display_row_range.start;
let offset = line_height / 2.;
@ -5128,9 +5143,9 @@ impl EditorElement {
end_display_row.0 -= 1;
}
let color = match &hunk.status() {
DiffHunkStatus::Added => theme.status().created,
DiffHunkStatus::Modified => theme.status().modified,
DiffHunkStatus::Removed => theme.status().deleted,
DiffHunkStatus::Added(_) => theme.status().created,
DiffHunkStatus::Modified(_) => theme.status().modified,
DiffHunkStatus::Removed(_) => theme.status().deleted,
};
ColoredRange {
start: start_display_row,
@ -6798,19 +6813,46 @@ impl Element for EditorElement {
)
};
let mut highlighted_rows = self
.editor
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
let (mut highlighted_rows, distinguish_unstaged_hunks) =
self.editor.update(cx, |editor, cx| {
(
editor.highlighted_display_rows(window, cx),
editor.distinguish_unstaged_diff_hunks,
)
});
for (ix, row_info) in row_infos.iter().enumerate() {
let color = match row_info.diff_status {
Some(DiffHunkStatus::Added) => style.status.created_background,
Some(DiffHunkStatus::Removed) => style.status.deleted_background,
let background = match row_info.diff_status {
Some(DiffHunkStatus::Added(secondary_status)) => {
let color = style.status.created_background;
match secondary_status {
DiffHunkSecondaryStatus::HasSecondaryHunk
| DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
if distinguish_unstaged_hunks =>
{
pattern_slash(color, line_height.0 / 4.0)
}
_ => color.into(),
}
}
Some(DiffHunkStatus::Removed(secondary_status)) => {
let color = style.status.deleted_background;
match secondary_status {
DiffHunkSecondaryStatus::HasSecondaryHunk
| DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
if distinguish_unstaged_hunks =>
{
pattern_slash(color, line_height.0 / 4.0)
}
_ => color.into(),
}
}
_ => continue,
};
highlighted_rows
.entry(start_row + DisplayRow(ix as u32))
.or_insert(color);
.or_insert(background);
}
let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
@ -7643,7 +7685,7 @@ pub struct EditorLayout {
indent_guides: Option<Vec<IndentGuideLayout>>,
visible_display_row_range: Range<DisplayRow>,
active_rows: BTreeMap<DisplayRow, bool>,
highlighted_rows: BTreeMap<DisplayRow, Hsla>,
highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
line_elements: SmallVec<[AnyElement; 1]>,
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,

View file

@ -1,6 +1,6 @@
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
use buffer_diff::BufferDiff;
use collections::HashSet;
use diff::BufferDiff;
use futures::{channel::mpsc, future::join_all};
use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
use language::{Buffer, BufferEvent, Capability};
@ -185,7 +185,7 @@ impl ProposedChangesEditor {
} else {
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
new_diffs.push(cx.new(|cx| {
let mut diff = BufferDiff::new(&branch_buffer, cx);
let mut diff = BufferDiff::new(branch_buffer.read(cx));
let _ = diff.set_base_text(
location.buffer.clone(),
branch_buffer.read(cx).text_snapshot(),

View file

@ -2,8 +2,8 @@ use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
RowExt,
};
use buffer_diff::DiffHunkStatus;
use collections::BTreeMap;
use diff::DiffHunkStatus;
use futures::Future;
use gpui::{
@ -459,9 +459,9 @@ pub fn assert_state_with_diff(
.zip(line_infos)
.map(|(line, info)| {
let mut marker = match info.diff_status {
Some(DiffHunkStatus::Added) => "+ ",
Some(DiffHunkStatus::Removed) => "- ",
Some(DiffHunkStatus::Modified) => unreachable!(),
Some(DiffHunkStatus::Added(_)) => "+ ",
Some(DiffHunkStatus::Removed(_)) => "- ",
Some(DiffHunkStatus::Modified(_)) => unreachable!(),
None => {
if has_diff {
" "