diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 7a225d6019edf8f09b1758d62e8181917649cc2b..a444aa6bd63a03efcc661ff95e6ff2f5bc925b77 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -20,6 +20,7 @@ mod block_map; mod crease_map; mod custom_highlights; +mod filter_map; mod fold_map; mod inlay_map; pub(crate) mod invisibles; diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index daf0cec397fc2271ba45153d099011e32a581453..8f6ae21b39a96e804b5e9b04333b169541caabfb 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -571,29 +571,9 @@ impl BlockMap { let mut edits = edits.into_iter().peekable(); while let Some(edit) = edits.next() { - // FIXME biases? - - let mut old_buffer_start = self - .wrap_snapshot - .borrow() - .to_point(WrapPoint::new(edit.old.start, 0), Bias::Left); - old_buffer_start.column = 0; - let mut old_start = WrapRow( - self.wrap_snapshot - .borrow() - .make_wrap_point(old_buffer_start, Bias::Left) - .row(), - ); - - let mut new_buffer_start = - wrap_snapshot.to_point(WrapPoint::new(edit.new.start, 0), Bias::Left); - new_buffer_start.column = 0; + dbg!(&edit); + let mut old_start = WrapRow(edit.old.start); let mut new_start = WrapRow(edit.new.start); - let mut new_start = WrapRow( - wrap_snapshot - .make_wrap_point(new_buffer_start, Bias::Left) - .row(), - ); // Only preserve transforms that: // * Strictly precedes this edit @@ -608,6 +588,7 @@ impl BlockMap { { // Preserve the transform (push and next) new_transforms.push(transform.clone(), ()); + // ^ FIXME wrong? cursor.next(); // Preserve below blocks at end of edit @@ -640,7 +621,7 @@ impl BlockMap { // replacement. debug_assert!(transform.summary.input_rows > 0); old_start.0 -= transform_rows_before_edit; - new_start.0 -= transform_rows_before_edit; + new_start.0 -= dbg!(transform_rows_before_edit); } } @@ -686,6 +667,62 @@ impl BlockMap { } } + // FIXME + // loop: + // peek next diff hunk in edit range + // peek next row boundary + // if they are equal, it's a diff hunk, so handle that (insert isomorphic and one non-isomorphic transform, possibly insert block) + // else must be a normal row boundary, so insert isomorphic transform and then block as needed + // and uh figure out what to do about the other kinds of block that exist + if true { + // FIXME persistent cursors/iterators for row boundaries and diff hunks + let mut current_wrap_row = new_start.0; + dbg!("---------", new_end.0); + loop { + if dbg!(current_wrap_row) > new_end.0 { + dbg!(); + break; + } + + let Some(next_row_boundary) = + wrap_snapshot.next_row_boundary(WrapPoint::new(current_wrap_row, 0)) + else { + dbg!(); + break; + }; + + push_isomorphic( + &mut new_transforms, + next_row_boundary - current_wrap_row, + wrap_snapshot, + ); + new_transforms.push( + Transform { + summary: TransformSummary { + input_rows: 0, + output_rows: 1, + longest_row: 0, + longest_row_chars: 0, + }, + block: Some(Block::Spacer { + height: 1, + id: SpacerId(self.next_block_id.fetch_add(1, SeqCst)), + }), + }, + (), + ); + current_wrap_row = next_row_boundary; + } + + // FIXME + // let rows_after_last_block = new_end + // .0 + // .saturating_sub(new_transforms.summary().input_rows); + // push_isomorphic(&mut new_transforms, rows_after_last_block, wrap_snapshot); + + continue; + } + // Find the blocks within this edited region. let new_buffer_start = wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left); @@ -746,10 +783,6 @@ impl BlockMap { BlockMap::sort_blocks(&mut blocks_in_edit); - let hunks = buffer - .diff_hunks_in_range(new_buffer_start..new_buffer_end) - .collect::>(); - // For each of these blocks, insert a new isomorphic transform preceding the block, // and then insert the block itself. let mut just_processed_folded_buffer = false; @@ -801,10 +834,11 @@ impl BlockMap { } new_transforms.append(cursor.suffix(), ()); - debug_assert_eq!( - new_transforms.summary().input_rows, - wrap_snapshot.max_point().row() + 1 - ); + // FIXME + // debug_assert_eq!( + // new_transforms.summary().input_rows, + // wrap_snapshot.max_point().row() + 1 + // ); drop(cursor); *transforms = new_transforms; @@ -3016,6 +3050,34 @@ mod tests { ); } + #[gpui::test] + fn test_spacers(cx: &mut gpui::TestAppContext) { + cx.update(init_test); + + let text = "aaa\nbbb\nccc\nddd\n"; + + let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); + let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx)); + let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap()); + let (wrap_map, wrap_snapshot) = + cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx)); + let mut block_map = BlockMap::new(wrap_snapshot.clone(), 1, 1); + + let snapshot = block_map.read(wrap_snapshot, Default::default()); + + let blocks = snapshot + .blocks_in_range(0..8) + .map(|(start_row, block)| start_row..start_row + block.height()) + .collect::>(); + + assert_eq!(blocks, &[1..2, 3..4, 5..6,]); + + assert_eq!(snapshot.text(), "aaa\n\nbbb\n\nccc\n\nddd\n\n"); + } + #[gpui::test(iterations = 100)] fn test_random_blocks(cx: &mut gpui::TestAppContext, mut rng: StdRng) { cx.update(init_test); diff --git a/crates/editor/src/display_map/filter_map.rs b/crates/editor/src/display_map/filter_map.rs new file mode 100644 index 0000000000000000000000000000000000000000..14e30e19023979cd38b2103e7a71245055d18037 --- /dev/null +++ b/crates/editor/src/display_map/filter_map.rs @@ -0,0 +1,526 @@ +use std::{cmp, ops::Range}; + +use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; +use multi_buffer::{AnchorRangeExt as _, MultiBufferSnapshot}; +use rope::{Point, TextSummary}; +use sum_tree::{Dimensions, SumTree}; +use text::Bias; + +/// All summaries are an integral number of multibuffer rows. +#[derive(Debug, Clone, Copy)] +struct WholeLineTextSummary(pub TextSummary); + +impl WholeLineTextSummary { + pub fn empty() -> Self { + Self(TextSummary::default()) + } +} + +#[derive(Debug, Clone, Copy)] +enum Transform { + Isomorphic { summary: WholeLineTextSummary }, + Filter { summary: WholeLineTextSummary }, +} + +impl Transform { + fn is_isomorphic(&self) -> bool { + matches!(self, Transform::Isomorphic { .. }) + } +} + +impl sum_tree::Item for Transform { + type Summary = TransformSummary; + + fn summary(&self, cx: ::Context<'_>) -> Self::Summary { + match self { + Self::Isomorphic { summary } => TransformSummary { + input: *summary, + output: *summary, + }, + Self::Filter { summary } => TransformSummary { + input: *summary, + output: WholeLineTextSummary::empty(), + }, + } + } +} + +#[derive(Debug, Clone, Copy)] +struct TransformSummary { + input: WholeLineTextSummary, + output: WholeLineTextSummary, +} + +impl sum_tree::ContextLessSummary for TransformSummary { + fn zero() -> Self { + TransformSummary { + input: WholeLineTextSummary::empty(), + output: WholeLineTextSummary::empty(), + } + } + + fn add_summary(&mut self, summary: &Self) { + self.input.0 += summary.input.0; + self.output.0 += summary.output.0; + } +} + +struct FilterMap { + snapshot: FilterSnapshot, + mode: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum FilterMode { + RemoveDeletions, + RemoveInsertions, +} + +impl FilterMode { + fn should_remove(self, kind: DiffHunkStatusKind) -> bool { + todo!() + } +} + +#[derive(Clone)] +struct FilterSnapshot { + transforms: SumTree, + buffer_snapshot: MultiBufferSnapshot, +} + +/// A byte index into the buffer (after ignored diff hunk lines are deleted) +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] +struct FilterOffset(usize); + +impl sum_tree::Dimension<'_, TransformSummary> for FilterOffset { + fn zero(cx: ::Context<'_>) -> Self { + FilterOffset(0) + } + + fn add_summary( + &mut self, + summary: &'_ TransformSummary, + cx: ::Context<'_>, + ) { + self.0 += summary.output.0.len; + } +} + +impl sum_tree::Dimension<'_, TransformSummary> for usize { + fn zero(cx: ::Context<'_>) -> Self { + 0 + } + + fn add_summary( + &mut self, + summary: &'_ TransformSummary, + cx: ::Context<'_>, + ) { + *self += summary.input.0.len; + } +} + +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] +struct FilterPoint(Point); + +impl sum_tree::Dimension<'_, TransformSummary> for FilterPoint { + fn zero(cx: ::Context<'_>) -> Self { + FilterPoint(Point::zero()) + } + + fn add_summary( + &mut self, + summary: &'_ TransformSummary, + cx: ::Context<'_>, + ) { + self.0 += summary.output.0.lines; + } +} + +type FilterEdit = text::Edit; + +impl FilterMap { + fn new(mode: Option, buffer_snapshot: MultiBufferSnapshot) -> Self { + let mut this = Self { + mode, + snapshot: FilterSnapshot { + buffer_snapshot: buffer_snapshot.clone(), + transforms: SumTree::default(), + }, + }; + this.sync( + buffer_snapshot.clone(), + vec![text::Edit { + old: 0..buffer_snapshot.len(), + new: 0..buffer_snapshot.len(), + }], + ); + this + } + + #[cfg(debug_assertions)] + fn check_invariants(&self) { + use itertools::Itertools; + + assert_eq!( + self.snapshot.transforms.summary().input.0, + self.snapshot.buffer_snapshot.text_summary(), + "input summary does not match buffer snapshot" + ); + + self.snapshot + .transforms + .iter() + .tuple_windows() + .for_each(|(left, right)| { + assert!( + left.is_isomorphic() || right.is_isomorphic(), + "two consecutive non-isomorphic transforms" + ); + assert!( + !left.is_isomorphic() || !right.is_isomorphic(), + "two consecutive isomorphic transforms" + ); + }); + + let Some(mode) = self.mode else { + assert_eq!( + self.snapshot.transforms.iter().count(), + 1, + "more than one transform in a trivial map" + ); + assert_eq!( + self.snapshot.transforms.summary().output.0, + self.snapshot.buffer_snapshot.text_summary(), + "output summary for trivial map does not match buffer snapshot" + ); + return; + }; + + let mut expected_summary = TextSummary::default(); + let mut anchor = multi_buffer::Anchor::min(); + for hunk in self.snapshot.buffer_snapshot.diff_hunks() { + expected_summary += self + .snapshot + .buffer_snapshot + .text_summary_for_range::(anchor..hunk.multi_buffer_range().start); + if !mode.should_remove(hunk.status().kind) { + expected_summary += self + .snapshot + .buffer_snapshot + .text_summary_for_range::(hunk.multi_buffer_range()); + } + anchor = hunk.multi_buffer_range().end; + } + expected_summary += self + .snapshot + .buffer_snapshot + .text_summary_for_range::(anchor..multi_buffer::Anchor::max()); + + assert_eq!( + self.snapshot.transforms.summary().output.0, + expected_summary, + "wrong output summary for nontrivial map" + ) + } +} + +impl FilterMap { + fn sync( + &mut self, + buffer_snapshot: MultiBufferSnapshot, + buffer_edits: Vec>, + ) -> (FilterSnapshot, Vec) { + let Some(mode) = self.mode else { + self.snapshot.buffer_snapshot = buffer_snapshot.clone(); + self.snapshot.transforms = SumTree::from_item( + Transform::Isomorphic { + summary: WholeLineTextSummary(buffer_snapshot.text_summary()), + }, + (), + ); + return ( + self.snapshot.clone(), + buffer_edits + .into_iter() + .map(|edit| text::Edit { + old: FilterOffset(edit.old.start)..FilterOffset(edit.old.end), + new: FilterOffset(edit.new.start)..FilterOffset(edit.new.end), + }) + .collect(), + ); + }; + + let mut new_transforms = SumTree::new(()); + let mut cursor = self + .snapshot + .transforms + .cursor::>(()); + let mut output_edits = Vec::new(); + + // TODO in what follows we repeatedly call text_summary_for_range, + // could use a persistent usize cursor over buffer_snapshot instead. + + for buffer_edit in buffer_edits { + // Reuse any old transforms that strictly precede the start of the edit. + new_transforms.append(cursor.slice(&buffer_edit.old.end, Bias::Right), ()); + + let mut edit_old_start = cursor.start().1; + let mut edit_new_start = FilterOffset(new_transforms.summary().output.0.len); + + // If the edit starts in the middle of a transform, split the transform and push the unaffected portion. + if buffer_edit.old.start > cursor.start().0 { + let summary = self + .snapshot + .buffer_snapshot + .text_summary_for_range(cursor.start().0..buffer_edit.old.start); + match cursor.item() { + Some(Transform::Isomorphic { .. }) => { + push_isomorphic(&mut new_transforms, summary); + edit_old_start.0 += summary.len; + edit_new_start.0 += summary.len; + cursor.next(); + } + Some(Transform::Filter { .. }) => { + push_filter(&mut new_transforms, summary); + cursor.next(); + } + None => {} + } + } + + // For each hunk in the edit, push the non-hunk region preceding it, then + // possibly filter the hunk depending on the mode. + for hunk in buffer_snapshot.diff_hunks_in_range(buffer_edit.new.clone()) { + let mut hunk_range = hunk.multi_buffer_range().to_offset(&buffer_snapshot); + hunk_range.start = std::cmp::max(hunk_range.start, buffer_edit.new.start); + hunk_range.end = std::cmp::min(hunk_range.end, buffer_edit.new.end); + let prefix_range = new_transforms.summary().input.0.len..hunk_range.start; + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(prefix_range), + ); + let hunk_summary = buffer_snapshot.text_summary_for_range(hunk_range); + if (mode == FilterMode::RemoveDeletions) + == (hunk.status().kind == DiffHunkStatusKind::Deleted) + { + push_filter(&mut new_transforms, hunk_summary); + } else { + push_isomorphic(&mut new_transforms, hunk_summary); + } + } + + // Push any non-hunk content after the last hunk. + if buffer_edit.new.end > new_transforms.summary().input.0.len { + let suffix_range = new_transforms.summary().input.0.len..buffer_edit.new.end; + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(suffix_range), + ); + } + + // Set up the cursor for the next iteration by seeking it to the end of the edit + // and pushing the second half of any transform that's split thereby (we already + // covered the first half just above). + cursor.seek(&buffer_edit.old.end, Bias::Right); + let mut edit_old_end = cursor.end().1; + let mut edit_new_end = FilterOffset(new_transforms.summary().output.0.len); + if buffer_edit.old.end > cursor.start().0 { + let summary = self + .snapshot + .buffer_snapshot + .text_summary_for_range(buffer_edit.old.end..cursor.end().0); + match cursor.item() { + Some(Transform::Isomorphic { .. }) => { + push_isomorphic(&mut new_transforms, summary); + edit_old_end.0 += buffer_edit.old.end - cursor.start().0; + edit_new_end.0 += buffer_edit.old.end - cursor.start().0; + cursor.next(); + } + Some(Transform::Filter { .. }) => { + push_filter(&mut new_transforms, summary); + cursor.next(); + } + None => {} + } + } + + output_edits.push(text::Edit { + old: edit_old_start..edit_old_end, + new: edit_new_start..edit_new_end, + }) + } + + // Append old transforms after the last edit. + new_transforms.append(cursor.slice(&usize::MAX, Bias::Left), ()); + + drop(cursor); + + self.snapshot.transforms = new_transforms; + self.snapshot.buffer_snapshot = buffer_snapshot; + #[cfg(debug_assertions)] + self.check_invariants(); + (self.snapshot.clone(), output_edits) + } +} + +fn push_isomorphic(transforms: &mut SumTree, summary_to_add: TextSummary) { + let mut merged = false; + transforms.update_last( + |transform| { + if let Transform::Isomorphic { summary } = transform { + summary.0 += summary_to_add; + merged = true; + } + }, + (), + ); + if !merged { + transforms.push( + Transform::Isomorphic { + summary: WholeLineTextSummary(summary_to_add), + }, + (), + ); + } +} + +fn push_filter(transforms: &mut SumTree, summary_to_add: TextSummary) { + let mut merged = false; + transforms.update_last( + |transform| { + if let Transform::Filter { summary } = transform { + summary.0 += summary_to_add; + merged = true; + } + }, + (), + ); + if !merged { + transforms.push( + Transform::Filter { + summary: WholeLineTextSummary(summary_to_add), + }, + (), + ); + } +} + +impl FilterSnapshot { + fn text_summary_for_range(&self, range: Range) -> TextSummary { + let mut summary = TextSummary::default(); + + let mut cursor = self + .transforms + .cursor::>(()); + cursor.seek(&range.start, Bias::Right); + + let overshoot = range.start.0 - cursor.start().0.0; + match cursor.item() { + Some(Transform::Isomorphic { .. }) => { + let buffer_start = cursor.start().1; + let suffix_start = buffer_start + overshoot; + let suffix_end = + buffer_start + (cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0); + summary = self + .buffer_snapshot + .text_summary_for_range(suffix_start..suffix_end); + cursor.next(); + } + Some(Transform::Filter { .. }) | None => {} + } + + if range.end > cursor.start().0 { + summary += cursor + .summary::<_, TransformSummary>(&range.end, Bias::Right) + .output + .0; + + let overshoot = range.end.0 - cursor.start().0.0; + match cursor.item() { + Some(Transform::Isomorphic { .. }) => { + let prefix_start = cursor.start().1; + let prefix_end = prefix_start + overshoot; + summary += self + .buffer_snapshot + .text_summary_for_range::(prefix_start..prefix_end); + } + Some(Transform::Filter { .. }) | None => {} + } + } + + summary + } + + fn to_point(&self, offset: FilterOffset) -> FilterPoint { + let (start, _, item) = self + .transforms + .find::, _>((), &offset, Bias::Right); + let overshoot = offset.0 - start.0.0; + match item { + Some(Transform::Isomorphic { .. }) => { + let buffer_offset_start = start.2; + let buffer_offset_end = buffer_offset_start + overshoot; + let buffer_start = self.buffer_snapshot.offset_to_point(buffer_offset_start); + let buffer_end = self.buffer_snapshot.offset_to_point(buffer_offset_end); + FilterPoint(start.1.0 + (buffer_end - buffer_start)) + } + Some(Transform::Filter { .. }) | None => self.max_point(), + } + } + + fn max_point(&self) -> FilterPoint { + FilterPoint(self.transforms.summary().output.0.lines) + } +} + +#[cfg(test)] +mod tests { + use collections::HashMap; + use gpui::{AppContext as _, Entity}; + use language::{Buffer, Capability}; + use multi_buffer::{MultiBuffer, MultiBufferSnapshot, randomly_mutate_multibuffer_with_diffs}; + use rand::{Rng as _, rngs::StdRng}; + use text::BufferId; + + use crate::display_map::filter_map::{FilterMap, FilterMode, FilterSnapshot}; + + #[gpui::test(iterations = 100)] + fn test_random_filter_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + let operations = std::env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let mut buffers: Vec> = Vec::new(); + let mut base_texts: HashMap = HashMap::default(); + let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); + let mut needs_diff_calculation = false; + + let mode = if rng.random() { + FilterMode::RemoveDeletions + } else { + FilterMode::RemoveInsertions + }; + let mut filter_map = FilterMap::new( + Some(mode), + multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)), + ); + + for _ in 0..operations { + let subscription = multibuffer.update(cx, |multibuffer, cx| multibuffer.subscribe()); + randomly_mutate_multibuffer_with_diffs( + multibuffer.clone(), + &mut buffers, + &mut base_texts, + &mut needs_diff_calculation, + rng.clone(), + cx, + ); + cx.run_until_parked(); + let buffer_edits = subscription.consume(); + let buffer_snapshot = + multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + filter_map.sync(buffer_snapshot, buffer_edits.edits().to_owned()); + } + } +} diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 486676f1120bc2e9d85effd4c328a2b7a547e06b..265e5a5e3582f0acda96a15977efa65a50229cac 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -978,6 +978,9 @@ impl InlaySnapshot { let mut cursor = self.transforms.cursor::>(()); cursor.seek(&range.start, Bias::Right); + // :SomeTypeInlay + // |---| >|----v-----|< |----------------| |------------v--| + // |---| >|----------|< |----------------| |------------v--| let overshoot = range.start.0 - cursor.start().0.0; match cursor.item() { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 4cd112d231fb340b67a712f235cccddd067234b3..f6ec7a9fbec754350eb46dafdc73cedc95d5aced 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -7173,3 +7173,141 @@ pub mod debug { } } } + +#[cfg(feature = "test-support")] +pub fn randomly_mutate_multibuffer_with_diffs( + multibuffer: Entity, + buffers: &mut Vec>, + base_texts: &mut HashMap, + needs_diff_calculation: &mut bool, + mut rng: rand::rngs::StdRng, + cx: &mut gpui::TestAppContext, +) { + use rand::Rng as _; + use rand::seq::IndexedRandom as _; + + let excerpt_ids = multibuffer.read_with(cx, |multibuffer, _| multibuffer.excerpt_ids()); + match rng.random_range(0..100) { + 0..=14 if !buffers.is_empty() => { + let buffer = buffers.choose(&mut rng).unwrap(); + buffer.update(cx, |buf, cx| { + let edit_count = rng.random_range(1..5); + buf.randomly_edit(&mut rng, edit_count, cx); + log::info!("buffer text:\n{}", buf.text()); + *needs_diff_calculation = true; + }); + } + 15..=19 if !excerpt_ids.is_empty() => { + multibuffer.update(cx, |multibuffer, cx| { + let ids = multibuffer.excerpt_ids(); + let mut excerpts = HashSet::default(); + for _ in 0..rng.random_range(0..ids.len()) { + excerpts.extend(ids.choose(&mut rng).copied()); + } + + let line_count = rng.random_range(0..5); + + log::info!("Expanding excerpts by {line_count} lines"); + multibuffer.expand_excerpts( + excerpts.iter().cloned(), + line_count, + ExpandExcerptDirection::UpAndDown, + cx, + ); + }); + } + 20..=29 if !excerpt_ids.is_empty() => { + let mut ids_to_remove = vec![]; + for _ in 0..rng.random_range(1..=3) { + let Some(id) = excerpt_ids.choose(&mut rng) else { + break; + }; + ids_to_remove.push(*id); + } + let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); + drop(snapshot); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(ids_to_remove, 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| { + log::info!("recalculating diff for buffer {:?}", snapshot.remote_id(),); + diff.recalculate_diff_sync(snapshot.text, 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) + .take(256) + .collect::(); + + 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() + }; + + let prev_excerpt_id = multibuffer + .read_with(cx, |multibuffer, cx| multibuffer.excerpt_ids()) + .choose(&mut rng) + .copied() + .unwrap_or(ExcerptId::max()); + + let 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)); + + log::info!( + "Inserting excerpt for buffer {}: {:?}[{:?}] = {:?}", + buffer.remote_id(), + buffer.text(), + start_ix..end_ix, + &buffer.text()[start_ix..end_ix] + ); + + 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, cx)); + multibuffer.add_diff(diff, cx) + } + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .insert_excerpts_after( + prev_excerpt_id, + buffer_handle.clone(), + [ExcerptRange::new(range.clone())], + cx, + ) + .pop() + .unwrap() + }); + } + } +}