git: Side-by-side diff UI (#47349)

This PR implements a UI for the side-by-side diff, using blocks to align
the two sides and adding a coherent `SplitEditorElement`.

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-01-22 02:31:14 -05:00
1 parent 900419fde3
commit 6b8eb8fe07
30 files changed
+7882 -888

No files matched your search

+121 -2
View File
@@ -4012,7 +4012,7 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
diff.update_diff(
buffer.read(cx).text_snapshot(),
Some(base_text.into()),
false,
None,
None,
cx,
)
@@ -4050,7 +4050,7 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
diff.update_diff(
buffer.read(cx).text_snapshot(),
Some(base_text.into()),
false,
None,
None,
cx,
)
@@ -4890,3 +4890,122 @@ fn test_excerpts_containment_functions(cx: &mut App) {
"excerpt_containing should return None for ranges spanning multiple excerpts"
);
}
#[gpui::test]
fn test_range_to_buffer_ranges_with_range_bounds(cx: &mut App) {
use std::ops::Bound;
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb", cx));
let buffer_2 = cx.new(|cx| Buffer::local("ccc", cx));
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let (excerpt_1_id, excerpt_2_id) = multibuffer.update(cx, |multibuffer, cx| {
let excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))],
cx,
)[0];
let excerpt_2_id = multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(0, 3))],
cx,
)[0];
(excerpt_1_id, excerpt_2_id)
});
let snapshot = multibuffer.read(cx).snapshot(cx);
assert_eq!(snapshot.text(), "aaa\nbbb\nccc");
let excerpt_2_start = Point::new(2, 0);
let ranges_half_open = snapshot.range_to_buffer_ranges(Point::zero()..excerpt_2_start);
assert_eq!(
ranges_half_open.len(),
1,
"Half-open range ending at excerpt start should EXCLUDE that excerpt"
);
assert_eq!(ranges_half_open[0].2, excerpt_1_id);
let ranges_inclusive = snapshot.range_to_buffer_ranges(Point::zero()..=excerpt_2_start);
assert_eq!(
ranges_inclusive.len(),
2,
"Inclusive range ending at excerpt start should INCLUDE that excerpt"
);
assert_eq!(ranges_inclusive[0].2, excerpt_1_id);
assert_eq!(ranges_inclusive[1].2, excerpt_2_id);
let ranges_unbounded =
snapshot.range_to_buffer_ranges((Bound::Included(Point::zero()), Bound::Unbounded));
assert_eq!(
ranges_unbounded.len(),
2,
"Unbounded end should include all excerpts"
);
assert_eq!(ranges_unbounded[0].2, excerpt_1_id);
assert_eq!(ranges_unbounded[1].2, excerpt_2_id);
let ranges_excluded_end = snapshot.range_to_buffer_ranges((
Bound::Included(Point::zero()),
Bound::Excluded(excerpt_2_start),
));
assert_eq!(
ranges_excluded_end.len(),
1,
"Excluded end bound should exclude excerpt starting at that point"
);
assert_eq!(ranges_excluded_end[0].2, excerpt_1_id);
let buffer_empty = cx.new(|cx| Buffer::local("", cx));
let multibuffer_trailing_empty = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let (te_excerpt_1_id, te_excerpt_2_id) =
multibuffer_trailing_empty.update(cx, |multibuffer, cx| {
let excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))],
cx,
)[0];
let excerpt_2_id = multibuffer.push_excerpts(
buffer_empty.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(0, 0))],
cx,
)[0];
(excerpt_1_id, excerpt_2_id)
});
let snapshot_trailing = multibuffer_trailing_empty.read(cx).snapshot(cx);
assert_eq!(snapshot_trailing.text(), "aaa\nbbb\n");
let max_point = snapshot_trailing.max_point();
let ranges_half_open_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..max_point);
assert_eq!(
ranges_half_open_max.len(),
1,
"Half-open range to max_point should EXCLUDE trailing empty excerpt at max_point"
);
assert_eq!(ranges_half_open_max[0].2, te_excerpt_1_id);
let ranges_inclusive_max = snapshot_trailing.range_to_buffer_ranges(Point::zero()..=max_point);
assert_eq!(
ranges_inclusive_max.len(),
2,
"Inclusive range to max_point should INCLUDE trailing empty excerpt"
);
assert_eq!(ranges_inclusive_max[0].2, te_excerpt_1_id);
assert_eq!(ranges_inclusive_max[1].2, te_excerpt_2_id);
let ranges_unbounded_trailing = snapshot_trailing
.range_to_buffer_ranges((Bound::Included(Point::zero()), Bound::Unbounded));
assert_eq!(
ranges_unbounded_trailing.len(),
2,
"Unbounded end should include trailing empty excerpt"
);
assert_eq!(ranges_unbounded_trailing[0].2, te_excerpt_1_id);
assert_eq!(ranges_unbounded_trailing[1].2, te_excerpt_2_id);
}