git: Compute and synchronize diffs from HEAD (#23626)
This PR builds on #21258 to make it possible to use HEAD as a diff base. The buffer store is extended to support holding multiple change sets, and collab gains support for synchronizing the committed text of files when any collaborator requires it. Not implemented in this PR: - Exposing the diff from HEAD to the user - Decorating the diff from HEAD with information about which hunks are staged `test_random_multibuffer` now fails first at `SEED=13277`, similar to the previous high-water mark, but with various bugs in the multibuffer logic now shaken out. Release Notes: - N/A --------- Co-authored-by: Max <max@zed.dev> Co-authored-by: Ben <ben@zed.dev> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
parent
871f98bc4d
commit
5704b50fb1
29 changed files with 2799 additions and 603 deletions
|
@ -19,12 +19,14 @@ fn init_logger() {
|
|||
#[gpui::test]
|
||||
fn test_empty_singleton(cx: &mut App) {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
assert_eq!(snapshot.text(), "");
|
||||
assert_eq!(
|
||||
snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>(),
|
||||
[RowInfo {
|
||||
buffer_id: Some(buffer_id),
|
||||
buffer_row: Some(0),
|
||||
multibuffer_row: Some(MultiBufferRow(0)),
|
||||
diff_status: None
|
||||
|
@ -359,13 +361,7 @@ fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
|
|||
let base_text = "one\ntwo\nthree\n";
|
||||
let text = "one\nthree\n";
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let change_set = cx.new(|cx| {
|
||||
let mut change_set = BufferChangeSet::new(&buffer, cx);
|
||||
let _ = change_set.set_base_text(base_text.into(), snapshot.text, cx);
|
||||
change_set
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
|
||||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.add_change_set(change_set, cx)
|
||||
|
@ -382,7 +378,7 @@ fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
|
|||
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||||
let actual_text = snapshot.text();
|
||||
let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
|
||||
let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default());
|
||||
let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default(), None);
|
||||
pretty_assertions::assert_eq!(
|
||||
actual_diff,
|
||||
indoc! {
|
||||
|
@ -409,13 +405,7 @@ fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
|
|||
let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
|
||||
let text = "one\nfour\nseven\n";
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
let change_set = cx.new(|cx| {
|
||||
let mut change_set = BufferChangeSet::new(&buffer, cx);
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let _ = change_set.set_base_text(base_text.into(), snapshot.text, cx);
|
||||
change_set
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
|
||||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||||
|
@ -508,13 +498,7 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
|
|||
let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n";
|
||||
let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n";
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
let change_set = cx.new(|cx| {
|
||||
let mut change_set = BufferChangeSet::new(&buffer, cx);
|
||||
let snapshot = buffer.read(cx).text_snapshot();
|
||||
let _ = change_set.set_base_text(base_text.into(), snapshot, cx);
|
||||
change_set
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(&base_text, &buffer, cx));
|
||||
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||||
|
||||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
|
@ -995,12 +979,7 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
|
|||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let base_text = "a\nb\nc";
|
||||
|
||||
let change_set = cx.new(|cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let mut change_set = BufferChangeSet::new(&buffer, cx);
|
||||
let _ = change_set.set_base_text(base_text.into(), snapshot.text, cx);
|
||||
change_set
|
||||
});
|
||||
let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||
multibuffer.add_change_set(change_set.clone(), cx);
|
||||
|
@ -1040,7 +1019,7 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
|
|||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "a\nb\nc")], None, cx);
|
||||
change_set.update(cx, |change_set, cx| {
|
||||
let _ = change_set.recalculate_diff(buffer.snapshot().text, cx);
|
||||
change_set.recalculate_diff_sync(buffer.snapshot().text, cx);
|
||||
});
|
||||
assert_eq!(buffer.text(), "a\nb\nc")
|
||||
});
|
||||
|
@ -1052,7 +1031,7 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
|
|||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.undo(cx);
|
||||
change_set.update(cx, |change_set, cx| {
|
||||
let _ = change_set.recalculate_diff(buffer.snapshot().text, cx);
|
||||
change_set.recalculate_diff_sync(buffer.snapshot().text, cx);
|
||||
});
|
||||
assert_eq!(buffer.text(), "")
|
||||
});
|
||||
|
@ -1294,8 +1273,7 @@ fn test_basic_diff_hunks(cx: &mut TestAppContext) {
|
|||
);
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
let change_set =
|
||||
cx.new(|cx| BufferChangeSet::new_with_base_text(base_text.to_string(), &buffer, cx));
|
||||
let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let multibuffer = cx.new(|cx| {
|
||||
|
@ -1485,8 +1463,8 @@ fn test_basic_diff_hunks(cx: &mut TestAppContext) {
|
|||
assert_line_indents(&snapshot);
|
||||
|
||||
// Recalculate the diff, changing the first diff hunk.
|
||||
let _ = change_set.update(cx, |change_set, cx| {
|
||||
change_set.recalculate_diff(buffer.read(cx).text_snapshot(), cx)
|
||||
change_set.update(cx, |change_set, cx| {
|
||||
change_set.recalculate_diff_sync(buffer.read(cx).text_snapshot(), cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_new_snapshot(
|
||||
|
@ -1538,8 +1516,7 @@ fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
|
|||
);
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
let change_set =
|
||||
cx.new(|cx| BufferChangeSet::new_with_base_text(base_text.to_string(), &buffer, cx));
|
||||
let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let multibuffer = cx.new(|cx| {
|
||||
|
@ -1840,10 +1817,8 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
|
|||
|
||||
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
|
||||
let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx));
|
||||
let change_set_1 =
|
||||
cx.new(|cx| BufferChangeSet::new_with_base_text(base_text_1.to_string(), &buffer_1, cx));
|
||||
let change_set_2 =
|
||||
cx.new(|cx| BufferChangeSet::new_with_base_text(base_text_2.to_string(), &buffer_2, cx));
|
||||
let change_set_1 = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text_1, &buffer_1, cx));
|
||||
let change_set_2 = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text_2, &buffer_2, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let multibuffer = cx.new(|cx| {
|
||||
|
@ -2028,6 +2003,7 @@ struct ReferenceMultibuffer {
|
|||
change_sets: HashMap<BufferId, Entity<BufferChangeSet>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ReferenceExcerpt {
|
||||
id: ExcerptId,
|
||||
buffer: Entity<Buffer>,
|
||||
|
@ -2037,6 +2013,7 @@ struct ReferenceExcerpt {
|
|||
|
||||
#[derive(Debug)]
|
||||
struct ReferenceRegion {
|
||||
buffer_id: Option<BufferId>,
|
||||
range: Range<usize>,
|
||||
buffer_start: Option<Point>,
|
||||
status: Option<DiffHunkStatus>,
|
||||
|
@ -2117,37 +2094,26 @@ impl ReferenceMultibuffer {
|
|||
};
|
||||
let diff = change_set.read(cx).diff_to_buffer.clone();
|
||||
let excerpt_range = excerpt.range.to_offset(&buffer);
|
||||
if excerpt_range.is_empty() {
|
||||
return;
|
||||
}
|
||||
for hunk in diff.hunks_intersecting_range(range, &buffer) {
|
||||
let hunk_range = hunk.buffer_range.to_offset(&buffer);
|
||||
let hunk_precedes_excerpt = hunk
|
||||
.buffer_range
|
||||
.end
|
||||
.cmp(&excerpt.range.start, &buffer)
|
||||
.is_lt();
|
||||
let hunk_follows_excerpt = hunk
|
||||
.buffer_range
|
||||
.start
|
||||
.cmp(&excerpt.range.end, &buffer)
|
||||
.is_ge();
|
||||
if hunk_precedes_excerpt || hunk_follows_excerpt {
|
||||
if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(ix) = excerpt
|
||||
.expanded_diff_hunks
|
||||
.binary_search_by(|anchor| anchor.cmp(&hunk.buffer_range.start, &buffer))
|
||||
{
|
||||
log::info!(
|
||||
"expanding diff hunk {:?}. excerpt: {:?}",
|
||||
"expanding diff hunk {:?}. excerpt:{:?}, excerpt range:{:?}",
|
||||
hunk_range,
|
||||
excerpt_id,
|
||||
excerpt_range
|
||||
);
|
||||
excerpt
|
||||
.expanded_diff_hunks
|
||||
.insert(ix, hunk.buffer_range.start);
|
||||
} else {
|
||||
log::trace!("hunk {hunk_range:?} already expanded in excerpt {excerpt_id:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2170,17 +2136,12 @@ impl ReferenceMultibuffer {
|
|||
.peekable();
|
||||
|
||||
while let Some(hunk) = hunks.next() {
|
||||
if !hunk.buffer_range.start.is_valid(&buffer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore hunks that are outside the excerpt range.
|
||||
let mut hunk_range = hunk.buffer_range.to_offset(buffer);
|
||||
|
||||
hunk_range.end = hunk_range.end.min(buffer_range.end);
|
||||
if hunk_range.start > buffer_range.end
|
||||
|| hunk_range.end < buffer_range.start
|
||||
|| buffer_range.is_empty()
|
||||
{
|
||||
if hunk_range.start > buffer_range.end || hunk_range.start < buffer_range.start {
|
||||
log::trace!("skipping hunk outside excerpt range");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -2188,6 +2149,12 @@ impl ReferenceMultibuffer {
|
|||
expanded_anchor.to_offset(&buffer).max(buffer_range.start)
|
||||
== hunk_range.start.max(buffer_range.start)
|
||||
}) {
|
||||
log::trace!("skipping a hunk that's not marked as expanded");
|
||||
continue;
|
||||
}
|
||||
|
||||
if !hunk.buffer_range.start.is_valid(&buffer) {
|
||||
log::trace!("skipping hunk with deleted start: {:?}", hunk.row_range);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -2196,6 +2163,7 @@ impl ReferenceMultibuffer {
|
|||
let len = text.len();
|
||||
text.extend(buffer.text_for_range(offset..hunk_range.start));
|
||||
regions.push(ReferenceRegion {
|
||||
buffer_id: Some(buffer.remote_id()),
|
||||
range: len..text.len(),
|
||||
buffer_start: Some(buffer.offset_to_point(offset)),
|
||||
status: None,
|
||||
|
@ -2212,6 +2180,7 @@ impl ReferenceMultibuffer {
|
|||
let len = text.len();
|
||||
text.push_str(&base_text);
|
||||
regions.push(ReferenceRegion {
|
||||
buffer_id: Some(base_buffer.remote_id()),
|
||||
range: len..text.len(),
|
||||
buffer_start: Some(
|
||||
base_buffer.offset_to_point(hunk.diff_base_byte_range.start),
|
||||
|
@ -2228,6 +2197,7 @@ impl ReferenceMultibuffer {
|
|||
let len = text.len();
|
||||
text.extend(buffer.text_for_range(offset..hunk_range.end));
|
||||
regions.push(ReferenceRegion {
|
||||
buffer_id: Some(buffer.remote_id()),
|
||||
range: len..text.len(),
|
||||
buffer_start: Some(buffer.offset_to_point(offset)),
|
||||
status: Some(DiffHunkStatus::Added),
|
||||
|
@ -2241,6 +2211,7 @@ impl ReferenceMultibuffer {
|
|||
text.extend(buffer.text_for_range(offset..buffer_range.end));
|
||||
text.push('\n');
|
||||
regions.push(ReferenceRegion {
|
||||
buffer_id: Some(buffer.remote_id()),
|
||||
range: len..text.len(),
|
||||
buffer_start: Some(buffer.offset_to_point(offset)),
|
||||
status: None,
|
||||
|
@ -2250,6 +2221,7 @@ impl ReferenceMultibuffer {
|
|||
// Remove final trailing newline.
|
||||
if self.excerpts.is_empty() {
|
||||
regions.push(ReferenceRegion {
|
||||
buffer_id: None,
|
||||
range: 0..1,
|
||||
buffer_start: Some(Point::new(0, 0)),
|
||||
status: None,
|
||||
|
@ -2273,6 +2245,7 @@ impl ReferenceMultibuffer {
|
|||
+ text[region.range.start..ix].matches('\n').count() as u32
|
||||
});
|
||||
RowInfo {
|
||||
buffer_id: region.buffer_id,
|
||||
diff_status: region.status,
|
||||
buffer_row,
|
||||
multibuffer_row: Some(MultiBufferRow(
|
||||
|
@ -2348,6 +2321,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
|||
buffer.update(cx, |buf, cx| {
|
||||
let edit_count = rng.gen_range(1..5);
|
||||
buf.randomly_edit(&mut rng, edit_count, cx);
|
||||
log::info!("buffer text:\n{}", buf.text());
|
||||
needs_diff_calculation = true;
|
||||
});
|
||||
cx.update(|cx| reference.diffs_updated(cx));
|
||||
|
@ -2440,7 +2414,11 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
|||
let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap()
|
||||
..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap();
|
||||
|
||||
log::info!("expanding diff hunks for excerpt {:?}", excerpt_ix);
|
||||
log::info!(
|
||||
"expanding diff hunks in range {:?} (excerpt id {:?}) index {excerpt_ix:?})",
|
||||
range.to_offset(&snapshot),
|
||||
excerpt.id
|
||||
);
|
||||
reference.expand_diff_hunks(excerpt.id, start..end, cx);
|
||||
multibuffer.expand_diff_hunks(vec![range], cx);
|
||||
});
|
||||
|
@ -2457,7 +2435,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
|||
"recalculating diff for buffer {:?}",
|
||||
snapshot.remote_id(),
|
||||
);
|
||||
change_set.recalculate_diff(snapshot.text, cx)
|
||||
change_set.recalculate_diff_sync(snapshot.text, cx);
|
||||
});
|
||||
}
|
||||
reference.diffs_updated(cx);
|
||||
|
@ -2471,14 +2449,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
|||
.collect::<String>();
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
|
||||
let change_set = cx.new(|cx| BufferChangeSet::new(&buffer, cx));
|
||||
change_set
|
||||
.update(cx, |change_set, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
change_set.set_base_text(base_text, snapshot.text, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let change_set =
|
||||
cx.new(|cx| BufferChangeSet::new_with_base_text(&base_text, &buffer, cx));
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
reference.add_change_set(change_set.clone(), cx);
|
||||
|
@ -2553,12 +2525,28 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
|||
.filter_map(|b| if b.next.is_some() { Some(b.row) } else { None })
|
||||
.collect::<HashSet<_>>();
|
||||
let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
|
||||
let actual_diff = format_diff(&actual_text, &actual_row_infos, &actual_boundary_rows);
|
||||
|
||||
let (expected_text, expected_row_infos, expected_boundary_rows) =
|
||||
cx.update(|cx| reference.expected_content(cx));
|
||||
let expected_diff =
|
||||
format_diff(&expected_text, &expected_row_infos, &expected_boundary_rows);
|
||||
|
||||
let has_diff = actual_row_infos
|
||||
.iter()
|
||||
.any(|info| info.diff_status.is_some())
|
||||
|| expected_row_infos
|
||||
.iter()
|
||||
.any(|info| info.diff_status.is_some());
|
||||
let actual_diff = format_diff(
|
||||
&actual_text,
|
||||
&actual_row_infos,
|
||||
&actual_boundary_rows,
|
||||
Some(has_diff),
|
||||
);
|
||||
let expected_diff = format_diff(
|
||||
&expected_text,
|
||||
&expected_row_infos,
|
||||
&expected_boundary_rows,
|
||||
Some(has_diff),
|
||||
);
|
||||
|
||||
log::info!("Multibuffer content:\n{}", actual_diff);
|
||||
|
||||
|
@ -2569,8 +2557,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
|||
actual_text.split('\n').count()
|
||||
);
|
||||
pretty_assertions::assert_eq!(actual_diff, expected_diff);
|
||||
pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos);
|
||||
pretty_assertions::assert_eq!(actual_text, expected_text);
|
||||
pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos);
|
||||
|
||||
for _ in 0..5 {
|
||||
let start_row = rng.gen_range(0..=expected_row_infos.len());
|
||||
|
@ -2937,8 +2925,10 @@ fn format_diff(
|
|||
text: &str,
|
||||
row_infos: &Vec<RowInfo>,
|
||||
boundary_rows: &HashSet<MultiBufferRow>,
|
||||
has_diff: Option<bool>,
|
||||
) -> String {
|
||||
let has_diff = row_infos.iter().any(|info| info.diff_status.is_some());
|
||||
let has_diff =
|
||||
has_diff.unwrap_or_else(|| row_infos.iter().any(|info| info.diff_status.is_some()));
|
||||
text.split('\n')
|
||||
.enumerate()
|
||||
.zip(row_infos)
|
||||
|
@ -3002,7 +2992,7 @@ fn assert_new_snapshot(
|
|||
let line_infos = new_snapshot
|
||||
.row_infos(MultiBufferRow(0))
|
||||
.collect::<Vec<_>>();
|
||||
let actual_diff = format_diff(&actual_text, &line_infos, &Default::default());
|
||||
let actual_diff = format_diff(&actual_text, &line_infos, &Default::default(), None);
|
||||
pretty_assertions::assert_eq!(actual_diff, expected_diff);
|
||||
check_edits(
|
||||
snapshot,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue