diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index c3c74d965f3739b674c1def4051517de021121f7..725535017723ae470dcc7ded333f5b9e15973cdc 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -508,12 +508,9 @@ struct BufferState { struct DiffState { diff: Entity, - /// Whether the [`MultiBuffer`] this is associated with is inverted (i.e. - /// showing additions as deletions). This is used in the side-by-side diff - /// view. - /// - /// The [`DiffState`]s in a [`MultiBuffer`] must either be all inverted, or - // all not inverted. + /// 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>, _subscription: gpui::Subscription, } @@ -613,6 +610,7 @@ pub struct MultiBufferSnapshot { is_dirty: bool, has_deleted_file: bool, has_conflict: bool, + has_inverted_diff: bool, /// immutable fields singleton: bool, excerpt_ids: SumTree, @@ -1870,6 +1868,7 @@ impl MultiBuffer { is_dirty, has_deleted_file, has_conflict, + has_inverted_diff, singleton: _, excerpt_ids: _, replaced_excerpts, @@ -1885,6 +1884,7 @@ impl MultiBuffer { *is_dirty = false; *has_deleted_file = false; *has_conflict = false; + *has_inverted_diff = false; replaced_excerpts.clear(); let edits = Self::sync_diff_transforms( @@ -2182,6 +2182,14 @@ impl MultiBuffer { snapshot.diffs.remove(buffer_id); } + // 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()); + } + if changed_trailing_excerpt { snapshot.trailing_excerpt_update_count += 1; } @@ -2544,8 +2552,6 @@ impl MultiBuffer { } pub fn add_diff(&mut self, diff: Entity, cx: &mut Context) { - debug_assert!(self.diffs.values().all(|diff| diff.main_buffer.is_none())); - let buffer_id = diff.read(cx).buffer_id; self.buffer_diff_changed( diff.clone(), @@ -2561,10 +2567,9 @@ impl MultiBuffer { main_buffer: Entity, cx: &mut Context, ) { - debug_assert!(self.diffs.values().all(|diff| diff.main_buffer.is_some())); - 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; self.inverted_buffer_diff_changed( diff.clone(), diff_change_range, @@ -2940,6 +2945,7 @@ impl MultiBuffer { is_dirty, has_deleted_file, has_conflict, + has_inverted_diff: _, singleton: _, excerpt_ids: _, replaced_excerpts: _, @@ -3228,10 +3234,12 @@ impl MultiBuffer { } // Avoid querying diff hunks if there's no possibility of hunks being expanded. + // For inverted diffs, hunks are always shown, so we can't skip this. let all_diff_hunks_expanded = snapshot.all_diff_hunks_expanded; if old_expanded_hunks.is_empty() && change_kind == DiffChangeKind::BufferEdited && !all_diff_hunks_expanded + && !snapshot.has_inverted_diff { return false; } diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 3cb60eb9d2cb2ff79caeb5393b23b882876e6a86..975ea41edafc4abde63e815cf3440c8f47f92206 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -2295,6 +2295,7 @@ async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { struct ReferenceMultibuffer { excerpts: Vec, diffs: HashMap>, + inverted_diffs: HashMap, text::BufferSnapshot)>, } #[derive(Debug)] @@ -2355,6 +2356,7 @@ impl ReferenceMultibuffer { .any(|excerpt| excerpt.buffer.read(cx).remote_id() == id) { self.diffs.remove(&id); + self.inverted_diffs.remove(&id); } } @@ -2392,6 +2394,12 @@ impl ReferenceMultibuffer { .unwrap(); let buffer = excerpt.buffer.read(cx).snapshot(); let buffer_id = buffer.remote_id(); + + // Skip inverted excerpts - hunks are always expanded + if self.inverted_diffs.contains_key(&buffer_id) { + return; + } + let Some(diff) = self.diffs.get(&buffer_id) else { return; }; @@ -2431,107 +2439,186 @@ impl ReferenceMultibuffer { for excerpt in &self.excerpts { excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32)); let buffer = excerpt.buffer.read(cx); + let buffer_id = buffer.remote_id(); let buffer_range = excerpt.range.to_offset(buffer); - let diff = self - .diffs - .get(&buffer.remote_id()) - .unwrap() - .read(cx) - .snapshot(cx); - let base_buffer = diff.base_text(); - - let mut offset = buffer_range.start; - let hunks = diff - .hunks_intersecting_range(excerpt.range.clone(), buffer) - .peekable(); - - for hunk in hunks { - // 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.start < buffer_range.start { - log::trace!("skipping hunk outside excerpt range"); - continue; - } - if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { - 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 let Some((diff, main_buffer_snapshot)) = self.inverted_diffs.get(&buffer_id) { + // INVERTED DIFF: Show base text with deleted regions marked + let diff_snapshot = diff.read(cx).snapshot(cx); - if !hunk.buffer_range.start.is_valid(buffer) { - log::trace!("skipping hunk with deleted start: {:?}", hunk.range); - continue; - } + let mut offset = buffer_range.start; + for hunk in diff_snapshot + .hunks_intersecting_base_text_range(buffer_range.clone(), main_buffer_snapshot) + { + let hunk_base_range = hunk.diff_base_byte_range.clone(); - if hunk_range.start >= offset { - // Add the buffer text before the hunk - let len = text.len(); - text.extend(buffer.text_for_range(offset..hunk_range.start)); - if text.len() > len { - regions.push(ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_range: Some((offset..hunk_range.start).to_point(&buffer)), - status: None, - excerpt_id: Some(excerpt.id), - }); + // Match actual multibuffer behavior: skip hunks that start before excerpt + if hunk_base_range.start < buffer_range.start { + continue; } - // Add the deleted text for the hunk. - if !hunk.diff_base_byte_range.is_empty() { - let mut base_text = base_buffer - .text_for_range(hunk.diff_base_byte_range.clone()) - .collect::(); - if !base_text.ends_with('\n') { - base_text.push('\n'); + // Clamp to excerpt range + let hunk_start = hunk_base_range.start.max(buffer_range.start); + let hunk_end = hunk_base_range.end.min(buffer_range.end); + + if hunk_start > buffer_range.end { + continue; + } + + // Add buffer text before the hunk (no status) + if hunk_start > offset { + let len = text.len(); + text.extend(buffer.text_for_range(offset..hunk_start)); + if text.len() > len { + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: len..text.len(), + buffer_range: Some((offset..hunk_start).to_point(&buffer)), + status: None, + excerpt_id: Some(excerpt.id), + }); } + } + + // Add the "deleted" region (base text that's not in main) + if hunk_end > hunk_start { let len = text.len(); - text.push_str(&base_text); + text.extend(buffer.text_for_range(hunk_start..hunk_end)); regions.push(ReferenceRegion { - buffer_id: Some(base_buffer.remote_id()), + buffer_id: Some(buffer_id), range: len..text.len(), - buffer_range: Some(hunk.diff_base_byte_range.to_point(&base_buffer)), + buffer_range: Some((hunk_start..hunk_end).to_point(&buffer)), status: Some(DiffHunkStatus::deleted(hunk.secondary_status)), excerpt_id: Some(excerpt.id), }); } - offset = hunk_range.start; + offset = hunk_end.max(offset); } - // Add the inserted text for the hunk. - if hunk_range.end > offset { + // Add remaining buffer text + if offset < buffer_range.end { let len = text.len(); - text.extend(buffer.text_for_range(offset..hunk_range.end)); - let range = len..text.len(); - let region = ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range, - buffer_range: Some((offset..hunk_range.end).to_point(&buffer)), - status: Some(DiffHunkStatus::added(hunk.secondary_status)), + text.extend(buffer.text_for_range(offset..buffer_range.end)); + text.push('\n'); + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: len..text.len(), + buffer_range: Some((offset..buffer_range.end).to_point(&buffer)), + status: None, excerpt_id: Some(excerpt.id), - }; - offset = hunk_range.end; - regions.push(region); + }); + } else { + text.push('\n'); + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: text.len() - 1..text.len(), + buffer_range: Some((buffer_range.end..buffer_range.end).to_point(&buffer)), + status: None, + excerpt_id: Some(excerpt.id), + }); } - } + } else { + // NON-INVERTED: Existing logic (show main buffer, insert deleted content) + let diff = self.diffs.get(&buffer_id).unwrap().read(cx).snapshot(cx); + let base_buffer = diff.base_text(); - // Add the buffer text for the rest of the excerpt. - let len = text.len(); - 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_range: Some((offset..buffer_range.end).to_point(&buffer)), - status: None, - excerpt_id: Some(excerpt.id), - }); + let mut offset = buffer_range.start; + let hunks = diff + .hunks_intersecting_range(excerpt.range.clone(), buffer) + .peekable(); + + for hunk in hunks { + // 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.start < buffer_range.start + { + log::trace!("skipping hunk outside excerpt range"); + continue; + } + + if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { + 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.range); + continue; + } + + if hunk_range.start >= offset { + // Add the buffer text before the hunk + let len = text.len(); + text.extend(buffer.text_for_range(offset..hunk_range.start)); + if text.len() > len { + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: len..text.len(), + buffer_range: Some((offset..hunk_range.start).to_point(&buffer)), + status: None, + excerpt_id: Some(excerpt.id), + }); + } + + // Add the deleted text for the hunk. + if !hunk.diff_base_byte_range.is_empty() { + let mut base_text = base_buffer + .text_for_range(hunk.diff_base_byte_range.clone()) + .collect::(); + if !base_text.ends_with('\n') { + base_text.push('\n'); + } + let len = text.len(); + text.push_str(&base_text); + regions.push(ReferenceRegion { + buffer_id: Some(base_buffer.remote_id()), + range: len..text.len(), + buffer_range: Some( + hunk.diff_base_byte_range.to_point(&base_buffer), + ), + status: Some(DiffHunkStatus::deleted(hunk.secondary_status)), + excerpt_id: Some(excerpt.id), + }); + } + + offset = hunk_range.start; + } + + // Add the inserted text for the hunk. + if hunk_range.end > offset { + let len = text.len(); + text.extend(buffer.text_for_range(offset..hunk_range.end)); + let range = len..text.len(); + let region = ReferenceRegion { + buffer_id: Some(buffer_id), + range, + buffer_range: Some((offset..hunk_range.end).to_point(&buffer)), + status: Some(DiffHunkStatus::added(hunk.secondary_status)), + excerpt_id: Some(excerpt.id), + }; + offset = hunk_range.end; + regions.push(region); + } + } + + // Add the buffer text for the rest of the excerpt. + let len = text.len(); + text.extend(buffer.text_for_range(offset..buffer_range.end)); + text.push('\n'); + regions.push(ReferenceRegion { + buffer_id: Some(buffer_id), + range: len..text.len(), + buffer_range: Some((offset..buffer_range.end).to_point(&buffer)), + status: None, + excerpt_id: Some(excerpt.id), + }); + } } // Remove final trailing newline. @@ -2635,9 +2722,18 @@ impl ReferenceMultibuffer { fn diffs_updated(&mut self, cx: &App) { for excerpt in &mut self.excerpts { let buffer = excerpt.buffer.read(cx).snapshot(); - let excerpt_range = excerpt.range.to_offset(&buffer); let buffer_id = buffer.remote_id(); - let diff = self.diffs.get(&buffer_id).unwrap().read(cx).snapshot(cx); + + // Skip inverted diff excerpts - hunks are always expanded + if self.inverted_diffs.contains_key(&buffer_id) { + continue; + } + + let excerpt_range = excerpt.range.to_offset(&buffer); + let Some(diff) = self.diffs.get(&buffer_id) else { + continue; + }; + let diff = diff.read(cx).snapshot(cx); let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer).peekable(); excerpt.expanded_diff_hunks.retain(|hunk_anchor| { if !hunk_anchor.is_valid(&buffer) { @@ -2665,6 +2761,29 @@ impl ReferenceMultibuffer { let buffer_id = diff.read(cx).buffer_id; self.diffs.insert(buffer_id, diff); } + + fn add_inverted_diff( + &mut self, + diff: Entity, + main_buffer_snapshot: text::BufferSnapshot, + 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_snapshot)); + } + + fn update_inverted_diff_snapshot( + &mut self, + diff: &Entity, + main_buffer_snapshot: text::BufferSnapshot, + cx: &App, + ) { + let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id(); + if let Some((_, stored_snapshot)) = self.inverted_diffs.get_mut(&base_text_buffer_id) { + *stored_snapshot = main_buffer_snapshot; + } + } } #[gpui::test(iterations = 100)] @@ -2754,6 +2873,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let mut anchors = Vec::new(); let mut old_versions = Vec::new(); let mut needs_diff_calculation = false; + // Maps main_buffer_id -> diff for inverted diffs (needed for recalculation when main buffer is edited) + let mut inverted_diff_main_buffers: HashMap> = HashMap::default(); for _ in 0..operations { match rng.random_range(0..100) { 0..=14 if !buffers.is_empty() => { @@ -2852,121 +2973,236 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { } 45..=55 if !reference.excerpts.is_empty() => { multibuffer.update(cx, |multibuffer, cx| { - let snapshot = multibuffer.snapshot(cx); - let excerpt_ix = rng.random_range(0..reference.excerpts.len()); - let excerpt = &reference.excerpts[excerpt_ix]; - let start = excerpt.range.start; - let end = excerpt.range.end; - let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap() - ..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap(); - - log::info!( - "expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})", - range.to_offset(&snapshot), - excerpt.id, - excerpt.buffer.read(cx).remote_id(), - ); - reference.expand_diff_hunks(excerpt.id, start..end, cx); - multibuffer.expand_diff_hunks(vec![range], cx); - }); + let snapshot = multibuffer.snapshot(cx); + let excerpt_ix = rng.random_range(0..reference.excerpts.len()); + let excerpt = &reference.excerpts[excerpt_ix]; + + // Skip inverted excerpts - hunks can't be collapsed + let buffer_id = excerpt.buffer.read(cx).remote_id(); + if reference.inverted_diffs.contains_key(&buffer_id) { + return; + } + + let start = excerpt.range.start; + let end = excerpt.range.end; + let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap() + ..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap(); + + log::info!( + "expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})", + range.to_offset(&snapshot), + excerpt.id, + buffer_id, + ); + reference.expand_diff_hunks(excerpt.id, start..end, cx); + multibuffer.expand_diff_hunks(vec![range], cx); + }); } 56..=85 if needs_diff_calculation => { multibuffer.update(cx, |multibuffer, cx| { for buffer in multibuffer.all_buffers() { let snapshot = buffer.read(cx).snapshot(); - multibuffer.diff_for(snapshot.remote_id()).unwrap().update( - cx, - |diff, cx| { + let buffer_id = snapshot.remote_id(); + + // Recalculate non-inverted diff if exists + if let Some(diff) = multibuffer.diff_for(buffer_id) { + diff.update(cx, |diff, cx| { + log::info!("recalculating diff for buffer {:?}", buffer_id,); + diff.recalculate_diff_sync(&snapshot.text, cx); + }); + } + + // Recalculate inverted diff if this is a main buffer for one + if let Some(inverted_diff) = inverted_diff_main_buffers.get(&buffer_id) { + inverted_diff.update(cx, |diff, cx| { log::info!( - "recalculating diff for buffer {:?}", - snapshot.remote_id(), + "recalculating inverted diff for main buffer {:?}", + buffer_id, ); diff.recalculate_diff_sync(&snapshot.text, cx); - }, - ); + }); + // Update the stored snapshot in the reference model + reference.update_inverted_diff_snapshot( + inverted_diff, + snapshot.text.clone(), + cx, + ); + } } reference.diffs_updated(cx); needs_diff_calculation = false; }); } _ => { - let buffer_handle = if buffers.is_empty() || rng.random_bool(0.4) { - let mut base_text = util::RandomCharIter::new(&mut rng) + // Decide if we're creating a new buffer or reusing an existing one + let create_new_buffer = buffers.is_empty() || rng.random_bool(0.4); + // Decide if this should be an inverted diff excerpt (only for new buffers, 30% chance) + let create_inverted = create_new_buffer && rng.random_bool(0.3); + + if create_inverted { + // Create a new main buffer + let mut main_buffer_text = util::RandomCharIter::new(&mut rng) .take(256) .collect::(); + let main_buffer = cx.new(|cx| Buffer::local(main_buffer_text.clone(), cx)); + text::LineEnding::normalize(&mut main_buffer_text); + let main_buffer_id = main_buffer.read_with(cx, |buffer, _| buffer.remote_id()); + base_texts.insert(main_buffer_id, main_buffer_text); + buffers.push(main_buffer.clone()); - let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx)); - text::LineEnding::normalize(&mut base_text); - base_texts.insert( - buffer.read_with(cx, |buffer, _| buffer.remote_id()), - base_text, - ); - buffers.push(buffer); - buffers.last().unwrap() - } else { - buffers.choose(&mut rng).unwrap() - }; + // Generate different base text for the inverted diff + let inverted_base_text: String = + util::RandomCharIter::new(&mut rng).take(256).collect(); - let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len()); - let prev_excerpt_id = reference - .excerpts - .get(prev_excerpt_ix) - .map_or(ExcerptId::max(), |e| e.id); - let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len()); + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text( + &inverted_base_text, + &main_buffer.read(cx).text_snapshot(), + cx, + ) + }); - let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| { - let end_row = rng.random_range(0..=buffer.max_point().row); - let start_row = rng.random_range(0..=end_row); - let end_ix = buffer.point_to_offset(Point::new(end_row, 0)); - let start_ix = buffer.point_to_offset(Point::new(start_row, 0)); - let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); + let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer()); + + // Track for recalculation when main buffer is edited + inverted_diff_main_buffers.insert(main_buffer_id, diff.clone()); + + let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len()); + let prev_excerpt_id = reference + .excerpts + .get(prev_excerpt_ix) + .map_or(ExcerptId::max(), |e| e.id); + let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len()); + + // Create excerpt from base_text_buffer + let (range, anchor_range) = base_text_buffer.read_with(cx, |buffer, _| { + let end_row = rng.random_range(0..=buffer.max_point().row); + let start_row = rng.random_range(0..=end_row); + let end_ix = buffer.point_to_offset(Point::new(end_row, 0)); + let start_ix = buffer.point_to_offset(Point::new(start_row, 0)); + let anchor_range = + buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); + + log::info!( + "Inserting inverted excerpt at {} of {} for base_text_buffer {}: {:?}[{:?}] = {:?}", + excerpt_ix, + reference.excerpts.len(), + buffer.remote_id(), + buffer.text(), + start_ix..end_ix, + &buffer.text()[start_ix..end_ix] + ); - log::info!( - "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", - excerpt_ix, - reference.excerpts.len(), - buffer.remote_id(), - buffer.text(), - start_ix..end_ix, - &buffer.text()[start_ix..end_ix] + (start_ix..end_ix, anchor_range) + }); + + let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .insert_excerpts_after( + prev_excerpt_id, + base_text_buffer.clone(), + [ExcerptRange::new(range.clone())], + cx, + ) + .pop() + .unwrap() + }); + + reference.insert_excerpt_after( + prev_excerpt_id, + excerpt_id, + (base_text_buffer.clone(), anchor_range), ); - (start_ix..end_ix, anchor_range) - }); + let main_buffer_snapshot = + main_buffer.read_with(cx, |buf, _| buf.text_snapshot()); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.add_inverted_diff(diff.clone(), main_buffer.clone(), cx); + }); + cx.update(|cx| { + reference.add_inverted_diff(diff, main_buffer_snapshot, cx); + }); + } else { + // Non-inverted: existing logic + let buffer_handle = if create_new_buffer { + let mut base_text = util::RandomCharIter::new(&mut rng) + .take(256) + .collect::(); - let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .insert_excerpts_after( - prev_excerpt_id, - buffer_handle.clone(), - [ExcerptRange::new(range.clone())], - cx, - ) - .pop() - .unwrap() - }); + let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx)); + text::LineEnding::normalize(&mut base_text); + base_texts.insert( + buffer.read_with(cx, |buffer, _| buffer.remote_id()), + base_text, + ); + buffers.push(buffer); + buffers.last().unwrap() + } else { + buffers.choose(&mut rng).unwrap() + }; - reference.insert_excerpt_after( - prev_excerpt_id, - excerpt_id, - (buffer_handle.clone(), anchor_range), - ); + let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len()); + let prev_excerpt_id = reference + .excerpts + .get(prev_excerpt_ix) + .map_or(ExcerptId::max(), |e| e.id); + let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len()); + + let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| { + let end_row = rng.random_range(0..=buffer.max_point().row); + let start_row = rng.random_range(0..=end_row); + let end_ix = buffer.point_to_offset(Point::new(end_row, 0)); + let start_ix = buffer.point_to_offset(Point::new(start_row, 0)); + let anchor_range = + buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); + + log::info!( + "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", + excerpt_ix, + reference.excerpts.len(), + buffer.remote_id(), + buffer.text(), + start_ix..end_ix, + &buffer.text()[start_ix..end_ix] + ); - multibuffer.update(cx, |multibuffer, cx| { - let id = buffer_handle.read(cx).remote_id(); - if multibuffer.diff_for(id).is_none() { - let base_text = base_texts.get(&id).unwrap(); - let diff = cx.new(|cx| { - BufferDiff::new_with_base_text( - base_text, - &buffer_handle.read(cx).text_snapshot(), + (start_ix..end_ix, anchor_range) + }); + + let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .insert_excerpts_after( + prev_excerpt_id, + buffer_handle.clone(), + [ExcerptRange::new(range.clone())], cx, ) - }); - reference.add_diff(diff.clone(), cx); - multibuffer.add_diff(diff, cx) - } - }); + .pop() + .unwrap() + }); + + reference.insert_excerpt_after( + prev_excerpt_id, + excerpt_id, + (buffer_handle.clone(), anchor_range), + ); + + multibuffer.update(cx, |multibuffer, cx| { + let id = buffer_handle.read(cx).remote_id(); + if multibuffer.diff_for(id).is_none() { + let base_text = base_texts.get(&id).unwrap(); + let diff = cx.new(|cx| { + BufferDiff::new_with_base_text( + base_text, + &buffer_handle.read(cx).text_snapshot(), + cx, + ) + }); + reference.add_diff(diff.clone(), cx); + multibuffer.add_diff(diff, cx) + } + }); + } } } @@ -3052,7 +3288,12 @@ fn check_multibuffer( expected_row_infos .into_iter() .filter_map(|info| { - if info.diff_status.is_some_and(|status| status.is_deleted()) { + // For inverted diffs, deleted rows are visible and should be counted. + // Only filter out deleted rows that are NOT from inverted diffs. + let is_inverted_diff = info + .buffer_id + .is_some_and(|id| reference.inverted_diffs.contains_key(&id)); + if info.diff_status.is_some_and(|status| status.is_deleted()) && !is_inverted_diff { None } else { info.buffer_row