From a5eb666951fb8999fe4181d175a8c7fcdba4e6b0 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 22 Jan 2026 16:17:23 -0500 Subject: [PATCH] git: Fix panic when committing from side-by-side view (#47425) When committing, it was possible for the left-hand side multibuffer to get the updated base text (via the `buffer_changed_since_sync` mechanism) without updating its diff snapshot for that buffer (via the diff subscription), causing a mismatch between that multibuffer's diff state and its buffer state. The fix is to ensure for inverted diffs that we always update pull an updated diff snapshot as part of `sync_from_buffer_changes`. This also removes some code that we added in #44838 to sync the left-hand side multibuffer when edits on the right-hand side invalided diff hunks. Instead, the left-hand side will just sync the next time the diff recalculates when this happens, and will always consider hunks from the last diff calculation as valid--so there will be a short window where the diff transforms and `diff_hunks_in_range` don't match between the two sides. That's okay because we don't rely on this in the display map--the code that translates positions between the two sides accesses the diff's `InternalDiffHunk`s directly rather than going through a multibuffer API. Release Notes: - N/A --- crates/editor/src/display_map/block_map.rs | 2 +- crates/editor/src/split.rs | 6 +- crates/multi_buffer/src/multi_buffer.rs | 232 +++--------------- crates/multi_buffer/src/multi_buffer_tests.rs | 153 ++++++------ 4 files changed, 104 insertions(+), 289 deletions(-) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 9479230aa2e0cfdbbd63752ca26e3f2cebbfc716..ea3fb11099ff5f5ded7e303ac7ef09f81d959c72 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -4222,7 +4222,7 @@ mod tests { [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], cx, ); - mb.add_inverted_diff(diff.clone(), rhs_buffer.clone(), cx); + mb.add_inverted_diff(diff.clone(), cx); mb }); let rhs_multibuffer = cx.new(|cx| { diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 13c6ee2f014eef197f92eec4f53003ec585dba93..a0aa32155f1e14b1af7be893ec76288db04da447 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -1577,10 +1577,6 @@ impl SecondaryEditor { }) .collect(); - let main_buffer = primary_multibuffer_ref - .buffer(main_buffer.remote_id()) - .unwrap(); - self.editor.update(cx, |editor, cx| { editor.buffer().update(cx, |buffer, cx| { let (ids, _) = buffer.update_path_excerpts( @@ -1595,7 +1591,7 @@ impl SecondaryEditor { .diff_for(base_text_buffer.read(cx).remote_id()) .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id()) { - buffer.add_inverted_diff(diff, main_buffer, cx); + buffer.add_inverted_diff(diff, cx); } }) }); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index ad877482bbbc8a29ba77d747941c4c92c30e0c8f..283d99db4bd722d7e8a6eaf9d86d2622a191d8f8 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -15,7 +15,7 @@ use buffer_diff::{ }; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; -use gpui::{App, Context, Entity, EntityId, EventEmitter, WeakEntity}; +use gpui::{App, Context, Entity, EntityId, EventEmitter}; use itertools::Itertools; use language::{ AutoindentMode, BracketMatch, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, @@ -499,10 +499,7 @@ struct BufferState { struct DiffState { diff: Entity, - /// If set, this diff is "inverted" (i.e. showing additions as deletions). - /// This is used in the side-by-side diff view. The main_buffer is the - /// editable buffer, while the excerpt shows the base text. - main_buffer: Option>, + is_inverted: bool, _subscription: gpui::Subscription, } @@ -510,11 +507,7 @@ impl DiffState { fn snapshot(&self, cx: &App) -> DiffStateSnapshot { DiffStateSnapshot { diff: self.diff.read(cx).snapshot(cx), - main_buffer: self.main_buffer.as_ref().and_then(|main_buffer| { - main_buffer - .read_with(cx, |main_buffer, _| main_buffer.text_snapshot()) - .ok() - }), + is_inverted: self.is_inverted, } } } @@ -522,7 +515,7 @@ impl DiffState { #[derive(Clone)] struct DiffStateSnapshot { diff: BufferDiffSnapshot, - main_buffer: Option, + is_inverted: bool, } impl std::ops::Deref for DiffStateSnapshot { @@ -557,19 +550,13 @@ impl DiffState { _ => {} }), diff, - main_buffer: None, + is_inverted: false, } } - fn new_inverted( - diff: Entity, - main_buffer: Entity, - cx: &mut Context, - ) -> Self { - let main_buffer = main_buffer.downgrade(); + fn new_inverted(diff: Entity, cx: &mut Context) -> Self { DiffState { _subscription: cx.subscribe(&diff, { - let main_buffer = main_buffer.clone(); move |this, diff, event, cx| match event { BufferDiffEvent::DiffChanged(DiffChanged { changed_range: _, @@ -577,23 +564,18 @@ impl DiffState { extended_range: _, }) => { if let Some(base_text_changed_range) = base_text_changed_range.clone() { - this.inverted_buffer_diff_changed( - diff, - base_text_changed_range, - main_buffer.clone(), - cx, - ) + this.inverted_buffer_diff_changed(diff, base_text_changed_range, cx) } cx.emit(Event::BufferDiffChanged); } BufferDiffEvent::LanguageChanged => { - this.inverted_buffer_diff_language_changed(diff, main_buffer.clone(), cx) + this.inverted_buffer_diff_language_changed(diff, cx) } _ => {} } }), diff, - main_buffer: Some(main_buffer), + is_inverted: true, } } } @@ -2223,10 +2205,7 @@ impl MultiBuffer { // Recalculate has_inverted_diff after removing diffs if !removed_buffer_ids.is_empty() { - snapshot.has_inverted_diff = snapshot - .diffs - .iter() - .any(|(_, diff)| diff.main_buffer.is_some()); + snapshot.has_inverted_diff = snapshot.diffs.iter().any(|(_, diff)| diff.is_inverted); } if changed_trailing_excerpt { @@ -2328,7 +2307,7 @@ impl MultiBuffer { let buffer_id = diff.buffer_id; let diff = DiffStateSnapshot { diff: diff.snapshot(cx), - main_buffer: None, + is_inverted: false, }; self.snapshot.get_mut().diffs.insert(buffer_id, diff); } @@ -2336,16 +2315,13 @@ impl MultiBuffer { fn inverted_buffer_diff_language_changed( &mut self, diff: Entity, - main_buffer: WeakEntity, cx: &mut Context, ) { let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id(); let diff = diff.read(cx); let diff = DiffStateSnapshot { diff: diff.snapshot(cx), - main_buffer: main_buffer - .update(cx, |main_buffer, _| main_buffer.text_snapshot()) - .ok(), + is_inverted: true, }; self.snapshot .get_mut() @@ -2369,7 +2345,7 @@ impl MultiBuffer { }; let new_diff = DiffStateSnapshot { diff: diff.snapshot(cx), - main_buffer: None, + is_inverted: false, }; let mut snapshot = self.snapshot.get_mut(); let base_text_changed = snapshot @@ -2401,7 +2377,6 @@ impl MultiBuffer { &mut self, diff: Entity, diff_change_range: Range, - main_buffer: WeakEntity, cx: &mut Context, ) { self.sync_mut(cx); @@ -2411,13 +2386,10 @@ impl MultiBuffer { let Some(buffer_state) = self.buffers.get(&base_text_buffer_id) else { return; }; - self.buffer_changed_since_sync.replace(true); let new_diff = DiffStateSnapshot { diff: diff.snapshot(cx), - main_buffer: main_buffer - .update(cx, |main_buffer, _| main_buffer.text_snapshot()) - .ok(), + is_inverted: true, }; let mut snapshot = self.snapshot.get_mut(); snapshot @@ -2429,7 +2401,7 @@ impl MultiBuffer { &mut snapshot, excerpt_edits, DiffChangeKind::DiffUpdated { - // We don't use read this field for inverted diffs. + // We don't read this field for inverted diffs. base_changed: false, }, ); @@ -2602,28 +2574,13 @@ impl MultiBuffer { self.diffs.insert(buffer_id, DiffState::new(diff, cx)); } - pub fn add_inverted_diff( - &mut self, - diff: Entity, - main_buffer: Entity, - cx: &mut Context, - ) { + pub fn add_inverted_diff(&mut self, diff: Entity, cx: &mut Context) { let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id(); let diff_change_range = 0..diff.read(cx).base_text(cx).len(); self.snapshot.get_mut().has_inverted_diff = true; - main_buffer.update(cx, |buffer, _| { - buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync)); - }); - self.inverted_buffer_diff_changed( - diff.clone(), - diff_change_range, - main_buffer.downgrade(), - cx, - ); - self.diffs.insert( - base_text_buffer_id, - DiffState::new_inverted(diff, main_buffer, cx), - ); + self.inverted_buffer_diff_changed(diff.clone(), diff_change_range, cx); + self.diffs + .insert(base_text_buffer_id, DiffState::new_inverted(diff, cx)); } pub fn diff_for(&self, buffer_id: BufferId) -> Option> { @@ -3048,71 +3005,12 @@ impl MultiBuffer { } for (id, diff) in diffs.iter() { - if buffer_diff.get(id).is_none() { + if buffer_diff.get(id).is_none() || diff.is_inverted { buffer_diff.insert(*id, diff.snapshot(cx)); } } - // Check for main buffer changes in inverted diffs - let mut main_buffer_changed_diffs = Vec::new(); - for (id, diff_state) in diffs.iter() { - if let Some(main_buffer) = &diff_state.main_buffer { - if let Ok(current_main_buffer) = - main_buffer.read_with(cx, |buffer, _| buffer.text_snapshot()) - { - if let Some(stored_diff) = buffer_diff.get(id) { - if let Some(stored_main_buffer) = &stored_diff.main_buffer { - if current_main_buffer - .version() - .changed_since(stored_main_buffer.version()) - { - main_buffer_changed_diffs.push(( - *id, - stored_diff.clone(), - current_main_buffer, - )); - } - } - } - } - } - } - - let mut inverted_diff_touch_info: HashMap< - Locator, - ( - BufferDiffSnapshot, - text::BufferSnapshot, - text::BufferSnapshot, - ), - > = HashMap::default(); - for (buffer_id, old_diff_snapshot, new_main_buffer) in &main_buffer_changed_diffs { - if let Some(old_main_buffer) = &old_diff_snapshot.main_buffer { - if let Some(buffer_state) = buffers.get(buffer_id) { - for locator in &buffer_state.excerpts { - inverted_diff_touch_info.insert( - locator.clone(), - ( - old_diff_snapshot.diff.clone(), - old_main_buffer.clone(), - new_main_buffer.clone(), - ), - ); - excerpts_to_edit.push((locator, buffer_state.buffer.clone(), false)); - } - } - } - } - excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator); - excerpts_to_edit.dedup_by(|a, b| { - if a.0 == b.0 { - b.2 |= a.2; - true - } else { - false - } - }); let mut edits = Vec::new(); let mut new_excerpts = SumTree::default(); @@ -3124,59 +3022,6 @@ impl MultiBuffer { let buffer = buffer.read(cx); let buffer_id = buffer.remote_id(); - let excerpt_old_start = cursor.start().1; - let excerpt_new_start = ExcerptDimension(new_excerpts.summary().text.len); - - if !buffer_edited - && let Some((old_diff, old_main_buffer, new_main_buffer)) = - inverted_diff_touch_info.get(locator) - { - // TODO(split-diff) this iterates over all excerpts for all edited buffers; - // it would be nice to be able to skip excerpts that weren't edited using - // new_main_buffer.has_edits_since_in_range. - let excerpt_buffer_start = old_excerpt - .range - .context - .start - .to_offset(&old_excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + old_excerpt.text_summary.len; - - let mut hunks = old_diff - .hunks_intersecting_base_text_range( - excerpt_buffer_start..excerpt_buffer_end, - old_main_buffer, - ) - .chain(old_diff.hunks_intersecting_base_text_range( - excerpt_buffer_start..excerpt_buffer_end, - new_main_buffer, - )) - .filter(|hunk| { - hunk.buffer_range.start.is_valid(&old_main_buffer) - != hunk.buffer_range.start.is_valid(&new_main_buffer) - }) - .collect::>(); - hunks.sort_by(|l, r| { - l.diff_base_byte_range - .start - .cmp(&r.diff_base_byte_range.start) - }); - - for hunk in hunks { - let hunk_buffer_start = hunk.diff_base_byte_range.start; - if hunk_buffer_start >= excerpt_buffer_start - && hunk_buffer_start <= excerpt_buffer_end - { - let hunk_offset = hunk_buffer_start - excerpt_buffer_start; - let old_hunk_pos = excerpt_old_start + hunk_offset; - let new_hunk_pos = excerpt_new_start + hunk_offset; - edits.push(Edit { - old: old_hunk_pos..old_hunk_pos, - new: new_hunk_pos..new_hunk_pos, - }); - } - } - } - let mut new_excerpt; if buffer_edited { edits.extend( @@ -3221,14 +3066,6 @@ impl MultiBuffer { drop(cursor); *excerpts = new_excerpts; - for (buffer_id, _, new_main_buffer) in main_buffer_changed_diffs { - if let Some(stored) = buffer_diff.get(&buffer_id) { - let mut updated = stored.clone(); - updated.main_buffer = Some(new_main_buffer); - buffer_diff.insert(buffer_id, updated); - } - } - Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited) } @@ -3320,9 +3157,9 @@ impl MultiBuffer { .. }) => excerpts.item().is_some_and(|excerpt| { if let Some(diff) = snapshot.diffs.get(&excerpt.buffer_id) - && let Some(main_buffer) = &diff.main_buffer + && diff.is_inverted { - return hunk.hunk_start_anchor.is_valid(main_buffer); + return true; } hunk.hunk_start_anchor.is_valid(&excerpt.buffer) }), @@ -3432,10 +3269,10 @@ impl MultiBuffer { excerpt_buffer_start + edit.new.end.saturating_sub(excerpt_start); let edit_buffer_end = edit_buffer_end.min(excerpt_buffer_end); - if let Some(main_buffer) = &diff.main_buffer { + if diff.is_inverted { for hunk in diff.hunks_intersecting_base_text_range( edit_buffer_start..edit_buffer_end, - main_buffer, + diff.original_buffer_snapshot(), ) { did_expand_hunks = true; let hunk_buffer_range = hunk.diff_base_byte_range.clone(); @@ -3974,13 +3811,13 @@ impl MultiBufferSnapshot { self.lift_buffer_metadata(query_range.clone(), move |buffer, buffer_range| { let diff = self.diffs.get(&buffer.remote_id())?; let iter: Box> = - if let Some(main_buffer) = &diff.main_buffer { + if diff.is_inverted { let buffer_start = buffer.point_to_offset(buffer_range.start); let buffer_end = buffer.point_to_offset(buffer_range.end); Box::new( diff.hunks_intersecting_base_text_range( buffer_start..buffer_end, - main_buffer, + diff.original_buffer_snapshot(), ) .map(move |hunk| (hunk, buffer, true)), ) @@ -4433,10 +4270,10 @@ impl MultiBufferSnapshot { .to_offset(&excerpt.buffer); if let Some(diff) = self.diffs.get(&excerpt.buffer_id) { - if let Some(main_buffer) = diff.main_buffer.as_ref() { + if diff.is_inverted { for hunk in diff.hunks_intersecting_base_text_range_rev( excerpt_start..excerpt_end, - &main_buffer, + diff.original_buffer_snapshot(), ) { if hunk.diff_base_byte_range.end >= current_position { continue; @@ -4471,11 +4308,11 @@ impl MultiBufferSnapshot { let Some(diff) = self.diffs.get(&excerpt.buffer_id) else { continue; }; - if let Some(main_buffer) = diff.main_buffer.as_ref() { + if diff.is_inverted { let Some(hunk) = diff .hunks_intersecting_base_text_range_rev( excerpt.range.context.to_offset(&excerpt.buffer), - main_buffer, + diff.original_buffer_snapshot(), ) .next() else { @@ -6811,15 +6648,6 @@ impl MultiBufferSnapshot { self.diffs.get(&buffer_id).map(|diff| &diff.diff) } - /// For inverted diffs (used in side-by-side diff view), returns the main buffer - /// snapshot that the diff's anchors refer to. Returns `None` if the diff is not - /// inverted or if there's no diff for the given buffer ID. - pub fn inverted_diff_main_buffer(&self, buffer_id: BufferId) -> Option<&text::BufferSnapshot> { - self.diffs - .get(&buffer_id) - .and_then(|diff| diff.main_buffer.as_ref()) - } - /// Visually annotates a position or range with the `Debug` representation of a value. The /// callsite of this function is used as a key - previous annotations will be removed. #[cfg(debug_assertions)] diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 0c5cefb53ee30b8814cb4721e38da5b09479223c..0ab14a2739257d72b1882ceb38466f7886ac1c46 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -487,7 +487,7 @@ async fn test_inverted_diff_hunks_in_range(cx: &mut TestAppContext) { }); multibuffer.update(cx, |multibuffer, cx| { - multibuffer.add_inverted_diff(diff, buffer.clone(), cx); + multibuffer.add_inverted_diff(diff, cx); }); assert_new_snapshot( @@ -2295,7 +2295,7 @@ async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { struct ReferenceMultibuffer { excerpts: Vec, diffs: HashMap>, - inverted_diffs: HashMap, WeakEntity)>, + inverted_diffs: HashMap>, } #[derive(Debug)] @@ -2442,16 +2442,14 @@ impl ReferenceMultibuffer { let buffer_id = buffer.remote_id(); let buffer_range = excerpt.range.to_offset(buffer); - if let Some((diff, main_buffer)) = self.inverted_diffs.get(&buffer_id) { + if let Some(diff) = self.inverted_diffs.get(&buffer_id) { let diff_snapshot = diff.read(cx).snapshot(cx); - let main_buffer_snapshot = main_buffer - .read_with(cx, |main_buffer, _| main_buffer.snapshot()) - .unwrap(); let mut offset = buffer_range.start; - for hunk in diff_snapshot - .hunks_intersecting_base_text_range(buffer_range.clone(), &main_buffer_snapshot) - { + for hunk in diff_snapshot.hunks_intersecting_base_text_range( + buffer_range.clone(), + diff_snapshot.original_buffer_snapshot(), + ) { let mut hunk_base_range = hunk.diff_base_byte_range.clone(); hunk_base_range.end = hunk_base_range.end.min(buffer_range.end); @@ -2461,10 +2459,6 @@ impl ReferenceMultibuffer { continue; } - if !hunk.buffer_range.start.is_valid(&main_buffer_snapshot) { - continue; - } - // Add the text before the hunk if hunk_base_range.start >= offset { let len = text.len(); @@ -2753,15 +2747,9 @@ impl ReferenceMultibuffer { self.diffs.insert(buffer_id, diff); } - fn add_inverted_diff( - &mut self, - diff: Entity, - main_buffer: Entity, - cx: &App, - ) { + fn add_inverted_diff(&mut self, diff: Entity, cx: &App) { let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id(); - self.inverted_diffs - .insert(base_text_buffer_id, (diff, main_buffer.downgrade())); + self.inverted_diffs.insert(base_text_buffer_id, diff); } } @@ -3133,9 +3121,9 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { excerpt_buffer.read_with(cx, |buffer, _| buffer.remote_id()); multibuffer.update(cx, |multibuffer, cx| { if multibuffer.diff_for(excerpt_buffer_id).is_none() { - if let Some(main_buffer) = inverted_main_buffer { - reference.add_inverted_diff(diff.clone(), main_buffer.clone(), cx); - multibuffer.add_inverted_diff(diff, main_buffer, cx); + if inverted_main_buffer.is_some() { + reference.add_inverted_diff(diff.clone(), cx); + multibuffer.add_inverted_diff(diff, cx); } else { reference.add_diff(diff.clone(), cx); multibuffer.add_diff(diff, cx); @@ -3877,62 +3865,6 @@ fn format_diff( // .join("\n") // } -#[gpui::test] -async fn test_inverted_diff_hunk_invalidation_on_main_buffer_edit(cx: &mut TestAppContext) { - let text = "one\ntwo\nthree\n"; - let base_text = "one\nTWO\nthree\n"; - - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx - .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); - cx.run_until_parked(); - - let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer()); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx); - multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - - TWO - three - " - ), - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(3..5, "")], None, cx); - }); - cx.run_until_parked(); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - TWO - three - " - ), - ); -} - #[gpui::test] async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) { let text = indoc!( @@ -3965,7 +3897,7 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) { let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx); multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx); + multibuffer.add_inverted_diff(diff.clone(), cx); multibuffer }); @@ -4102,6 +4034,65 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_inverted_diff_base_text_change(cx: &mut TestAppContext) { + let base_text = "aaa\nbbb\nccc\n"; + let text = "ddd\n"; + let buffer = cx.new(|cx| Buffer::local(text, cx)); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)); + cx.run_until_parked(); + + let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer()); + + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer.add_inverted_diff(diff.clone(), cx); + multibuffer + }); + + let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { + (multibuffer.snapshot(cx), multibuffer.subscribe()) + }); + + assert_eq!(snapshot.text(), base_text); + assert_new_snapshot( + &multibuffer, + &mut snapshot, + &mut subscription, + cx, + indoc!( + " + - aaa + - bbb + - ccc + " + ), + ); + + let update = diff + .update(cx, |diff, cx| { + diff.update_diff( + buffer.read(cx).text_snapshot(), + Some("ddd\n".into()), + Some(true), + None, + cx, + ) + }) + .await; + diff.update(cx, |diff, cx| { + diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx) + }) + .detach(); + + let _hunks: Vec<_> = multibuffer + .read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)) + .diff_hunks() + .collect(); +} + #[track_caller] fn assert_excerpts_match( multibuffer: &Entity,