1use super::wrap_map::{self, Edit as WrapEdit, Snapshot as WrapSnapshot, WrapPoint};
2use buffer::{rope, Anchor, Bias, Edit, Point, Rope, ToOffset, ToPoint as _};
3use gpui::{fonts::HighlightStyle, AppContext, ModelHandle};
4use language::{Buffer, HighlightedChunk};
5use parking_lot::Mutex;
6use std::{
7 cmp,
8 collections::HashSet,
9 iter,
10 ops::Range,
11 slice,
12 sync::{
13 atomic::{AtomicUsize, Ordering::SeqCst},
14 Arc,
15 },
16};
17use sum_tree::SumTree;
18
19pub struct BlockMap {
20 buffer: ModelHandle<Buffer>,
21 next_block_id: AtomicUsize,
22 wrap_snapshot: Mutex<WrapSnapshot>,
23 blocks: Vec<Arc<Block>>,
24 transforms: Mutex<SumTree<Transform>>,
25}
26
27pub struct BlockMapWriter<'a>(&'a mut BlockMap);
28
29pub struct BlockSnapshot {
30 wrap_snapshot: WrapSnapshot,
31 transforms: SumTree<Transform>,
32}
33
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub struct BlockId(usize);
36
37#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
38pub struct BlockPoint(pub super::Point);
39
40#[derive(Debug)]
41struct Block {
42 id: BlockId,
43 position: Anchor,
44 text: Rope,
45 runs: Vec<(usize, HighlightStyle)>,
46 disposition: BlockDisposition,
47}
48
49#[derive(Clone)]
50pub struct BlockProperties<P, T>
51where
52 P: Clone,
53 T: Clone,
54{
55 position: P,
56 text: T,
57 runs: Vec<(usize, HighlightStyle)>,
58 disposition: BlockDisposition,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
62enum BlockDisposition {
63 Above,
64 Below,
65}
66
67#[derive(Clone, Debug)]
68struct Transform {
69 summary: TransformSummary,
70 block: Option<Arc<Block>>,
71}
72
73#[derive(Copy, Clone, Debug, Default)]
74struct TransformSummary {
75 input_rows: u32,
76 output_rows: u32,
77}
78
79pub struct HighlightedChunks<'a> {
80 transforms: sum_tree::Cursor<'a, Transform, (OutputRow, InputRow)>,
81 input_chunks: wrap_map::HighlightedChunks<'a>,
82 input_chunk: HighlightedChunk<'a>,
83 block_chunks: Option<BlockChunks<'a>>,
84 output_row: u32,
85 max_output_row: u32,
86}
87
88struct BlockChunks<'a> {
89 chunks: rope::Chunks<'a>,
90 runs: iter::Peekable<slice::Iter<'a, (usize, HighlightStyle)>>,
91 chunk: Option<&'a str>,
92 run_start: usize,
93 offset: usize,
94}
95
96#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
97struct InputRow(u32);
98
99#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
100struct OutputRow(u32);
101
102impl BlockMap {
103 pub fn new(buffer: ModelHandle<Buffer>, wrap_snapshot: WrapSnapshot) -> Self {
104 Self {
105 buffer,
106 next_block_id: AtomicUsize::new(0),
107 blocks: Vec::new(),
108 transforms: Mutex::new(SumTree::from_item(
109 Transform::isomorphic(wrap_snapshot.max_point().row() + 1),
110 &(),
111 )),
112 wrap_snapshot: Mutex::new(wrap_snapshot),
113 }
114 }
115
116 pub fn read(
117 &self,
118 wrap_snapshot: WrapSnapshot,
119 edits: Vec<WrapEdit>,
120 cx: &AppContext,
121 ) -> BlockSnapshot {
122 self.apply_edits(&wrap_snapshot, edits, cx);
123 *self.wrap_snapshot.lock() = wrap_snapshot.clone();
124 BlockSnapshot {
125 wrap_snapshot,
126 transforms: self.transforms.lock().clone(),
127 }
128 }
129
130 pub fn write(
131 &mut self,
132 wrap_snapshot: WrapSnapshot,
133 edits: Vec<WrapEdit>,
134 cx: &AppContext,
135 ) -> BlockMapWriter {
136 self.apply_edits(&wrap_snapshot, edits, cx);
137 *self.wrap_snapshot.lock() = wrap_snapshot;
138 BlockMapWriter(self)
139 }
140
141 fn apply_edits(&self, wrap_snapshot: &WrapSnapshot, edits: Vec<WrapEdit>, cx: &AppContext) {
142 let buffer = self.buffer.read(cx);
143 let mut transforms = self.transforms.lock();
144 let mut new_transforms = SumTree::new();
145 let mut cursor = transforms.cursor::<InputRow>();
146 let mut edits = edits.into_iter().peekable();
147 let mut last_block_ix = 0;
148 let mut blocks_in_edit = Vec::new();
149
150 while let Some(mut edit) = edits.next() {
151 new_transforms.push_tree(
152 cursor.slice(&InputRow(edit.old.start), Bias::Left, &()),
153 &(),
154 );
155
156 let transform_start = cursor.start().0;
157 edit.new.start -= edit.old.start - transform_start;
158 edit.old.start = transform_start;
159
160 loop {
161 if edit.old.end > cursor.start().0 {
162 cursor.seek(&InputRow(edit.old.end), Bias::Left, &());
163 cursor.next(&());
164 let transform_end = cursor.start().0;
165 edit.new.end += transform_end - edit.old.end;
166 edit.old.end = transform_end;
167 }
168
169 if let Some(next_edit) = edits.peek() {
170 if edit.old.end >= next_edit.old.start {
171 let delta = next_edit.new.len() as i32 - next_edit.old.len() as i32;
172 edit.old.end = cmp::max(next_edit.old.end, edit.old.end);
173 edit.new.end = (edit.new.end as i32 + delta) as u32;
174 edits.next();
175 } else {
176 break;
177 }
178 } else {
179 break;
180 }
181 }
182
183 let start_anchor = buffer.anchor_before(Point::new(edit.new.start, 0));
184 let end_anchor = buffer.anchor_after(Point::new(edit.new.end, 0));
185 let start_block_ix = match self.blocks[last_block_ix..]
186 .binary_search_by(|probe| probe.position.cmp(&start_anchor, buffer).unwrap())
187 {
188 Ok(ix) | Err(ix) => last_block_ix + ix,
189 };
190 let end_block_ix = match self.blocks[start_block_ix..]
191 .binary_search_by(|probe| probe.position.cmp(&end_anchor, buffer).unwrap())
192 {
193 Ok(ix) | Err(ix) => start_block_ix + ix,
194 };
195 last_block_ix = end_block_ix;
196
197 blocks_in_edit.clear();
198 blocks_in_edit.extend(
199 self.blocks[start_block_ix..end_block_ix]
200 .iter()
201 .map(|block| (block.position.to_point(buffer).row, block)),
202 );
203 blocks_in_edit.sort_unstable_by_key(|(row, block)| (*row, block.disposition));
204
205 for (row, block) in blocks_in_edit.iter().copied() {
206 let insertion_row = if block.disposition.is_above() {
207 row
208 } else {
209 row + 1
210 };
211
212 let new_transforms_end = new_transforms.summary().input_rows;
213 if new_transforms_end < insertion_row {
214 new_transforms.push(
215 Transform::isomorphic(insertion_row - new_transforms_end),
216 &(),
217 );
218 }
219
220 new_transforms.push(Transform::block(block.clone()), &());
221 }
222
223 let new_transforms_end = new_transforms.summary().input_rows;
224 if new_transforms_end < edit.new.end {
225 new_transforms.push(
226 Transform::isomorphic(edit.new.end - new_transforms_end),
227 &(),
228 );
229 }
230 }
231 new_transforms.push_tree(cursor.suffix(&()), &());
232 drop(cursor);
233 *transforms = new_transforms;
234 }
235}
236
237impl std::ops::Deref for BlockPoint {
238 type Target = Point;
239
240 fn deref(&self) -> &Self::Target {
241 &self.0
242 }
243}
244
245impl std::ops::DerefMut for BlockPoint {
246 fn deref_mut(&mut self) -> &mut Self::Target {
247 &mut self.0
248 }
249}
250
251impl<'a> BlockMapWriter<'a> {
252 pub fn insert<P, T>(
253 &mut self,
254 blocks: impl IntoIterator<Item = BlockProperties<P, T>>,
255 cx: &AppContext,
256 ) -> Vec<BlockId>
257 where
258 P: ToOffset + Clone,
259 T: Into<Rope> + Clone,
260 {
261 let buffer = self.0.buffer.read(cx);
262 let mut ids = Vec::new();
263 let mut edits = Vec::<Edit<u32>>::new();
264
265 for block in blocks {
266 let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst));
267 ids.push(id);
268
269 let position = buffer.anchor_before(block.position);
270 let row = position.to_point(buffer).row;
271
272 let block_ix = match self
273 .0
274 .blocks
275 .binary_search_by(|probe| probe.position.cmp(&position, buffer).unwrap())
276 {
277 Ok(ix) | Err(ix) => ix,
278 };
279 let mut text = block.text.into();
280 text.push("\n");
281 self.0.blocks.insert(
282 block_ix,
283 Arc::new(Block {
284 id,
285 position,
286 text,
287 runs: block.runs,
288 disposition: block.disposition,
289 }),
290 );
291
292 if let Err(edit_ix) = edits.binary_search_by_key(&row, |edit| edit.old.start) {
293 edits.insert(
294 edit_ix,
295 Edit {
296 old: row..(row + 1),
297 new: row..(row + 1),
298 },
299 );
300 }
301 }
302
303 self.0.apply_edits(&*self.0.wrap_snapshot.lock(), edits, cx);
304 ids
305 }
306
307 pub fn remove(&mut self, _: HashSet<BlockId>, _: &AppContext) {
308 todo!()
309 }
310}
311
312impl BlockSnapshot {
313 #[cfg(test)]
314 fn text(&mut self) -> String {
315 self.highlighted_chunks_for_rows(0..(self.max_point().0.row + 1))
316 .map(|chunk| chunk.text)
317 .collect()
318 }
319
320 pub fn highlighted_chunks_for_rows(&mut self, rows: Range<u32>) -> HighlightedChunks {
321 let mut cursor = self.transforms.cursor::<(OutputRow, InputRow)>();
322 cursor.seek(&OutputRow(rows.start), Bias::Right, &());
323 let (input_start, output_start) = cursor.start();
324 let row_overshoot = rows.start - output_start.0;
325 let input_start_row = input_start.0 + row_overshoot;
326 let input_end_row = self
327 .to_wrap_point(BlockPoint(Point::new(rows.end, 0)))
328 .row();
329 let input_chunks = self
330 .wrap_snapshot
331 .highlighted_chunks_for_rows(input_start_row..input_end_row);
332 HighlightedChunks {
333 input_chunks,
334 input_chunk: Default::default(),
335 block_chunks: None,
336 transforms: cursor,
337 output_row: rows.start,
338 max_output_row: rows.end,
339 }
340 }
341
342 pub fn max_point(&self) -> BlockPoint {
343 self.to_block_point(self.wrap_snapshot.max_point())
344 }
345
346 pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint {
347 todo!()
348 }
349
350 pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint {
351 let mut cursor = self.transforms.cursor::<(InputRow, OutputRow)>();
352 cursor.seek(&InputRow(wrap_point.row()), Bias::Right, &());
353 while let Some(item) = cursor.item() {
354 if item.is_isomorphic() {
355 break;
356 }
357 cursor.next(&());
358 }
359 let (input_start, output_start) = cursor.start();
360 let row_overshoot = wrap_point.row() - input_start.0;
361 BlockPoint(Point::new(
362 output_start.0 + row_overshoot,
363 wrap_point.column(),
364 ))
365 }
366
367 pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint {
368 let mut cursor = self.transforms.cursor::<(OutputRow, InputRow)>();
369 cursor.seek(&OutputRow(block_point.0.row), Bias::Right, &());
370 let (output_start, input_start) = cursor.start();
371 let row_overshoot = block_point.0.row - output_start.0;
372 WrapPoint::new(input_start.0 + row_overshoot, block_point.0.column)
373 }
374}
375
376impl Transform {
377 fn isomorphic(rows: u32) -> Self {
378 Self {
379 summary: TransformSummary {
380 input_rows: rows,
381 output_rows: rows,
382 },
383 block: None,
384 }
385 }
386
387 fn block(block: Arc<Block>) -> Self {
388 Self {
389 summary: TransformSummary {
390 input_rows: 0,
391 output_rows: block.text.summary().lines.row,
392 },
393 block: Some(block),
394 }
395 }
396
397 fn is_isomorphic(&self) -> bool {
398 self.block.is_none()
399 }
400}
401
402impl<'a> Iterator for HighlightedChunks<'a> {
403 type Item = HighlightedChunk<'a>;
404
405 fn next(&mut self) -> Option<Self::Item> {
406 if self.output_row >= self.max_output_row {
407 return None;
408 }
409
410 if let Some(block_chunks) = self.block_chunks.as_mut() {
411 if let Some(block_chunk) = block_chunks.next() {
412 self.output_row += block_chunk.text.matches('\n').count() as u32;
413 return Some(block_chunk);
414 } else {
415 self.block_chunks.take();
416 }
417 }
418
419 let transform = self.transforms.item()?;
420 if let Some(block) = transform.block.as_ref() {
421 let block_start = self.transforms.start().0 .0;
422 let block_end = self.transforms.end(&()).0 .0;
423 let start_row_in_block = self.output_row - block_start;
424 let end_row_in_block = cmp::min(self.max_output_row, block_end) - block_start;
425 self.transforms.next(&());
426 let mut block_chunks = BlockChunks::new(block, start_row_in_block..end_row_in_block);
427 if let Some(block_chunk) = block_chunks.next() {
428 self.output_row += block_chunk.text.matches('\n').count() as u32;
429 return Some(block_chunk);
430 }
431 }
432
433 if self.input_chunk.text.is_empty() {
434 self.input_chunk = self.input_chunks.next()?;
435 }
436
437 let transform_end = self.transforms.end(&()).0 .0;
438 let (prefix_rows, prefix_bytes) =
439 offset_for_row(self.input_chunk.text, transform_end - self.output_row);
440 self.output_row += prefix_rows;
441 let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
442 self.input_chunk.text = suffix;
443 if self.output_row == transform_end {
444 self.transforms.next(&());
445 }
446
447 Some(HighlightedChunk {
448 text: prefix,
449 ..self.input_chunk
450 })
451 }
452}
453
454impl<'a> BlockChunks<'a> {
455 fn new(block: &'a Block, row_range: Range<u32>) -> Self {
456 let point_range = Point::new(row_range.start, 0)..Point::new(row_range.end, 0);
457 let offset_range = block.text.point_to_offset(point_range.start)
458 ..block.text.point_to_offset(point_range.end);
459
460 let mut runs = block.runs.iter().peekable();
461 let mut run_start = 0;
462 while let Some((run_len, _)) = runs.peek() {
463 let run_end = run_start + run_len;
464 if run_end <= offset_range.start {
465 run_start = run_end;
466 runs.next();
467 } else {
468 break;
469 }
470 }
471
472 Self {
473 chunk: None,
474 run_start,
475 chunks: block.text.chunks_in_range(offset_range.clone()),
476 runs,
477 offset: offset_range.start,
478 }
479 }
480}
481
482impl<'a> Iterator for BlockChunks<'a> {
483 type Item = HighlightedChunk<'a>;
484
485 fn next(&mut self) -> Option<Self::Item> {
486 if self.chunk.is_none() {
487 self.chunk = self.chunks.next();
488 }
489
490 let chunk = self.chunk?;
491 let mut chunk_len = chunk.len();
492 let mut highlight_style = None;
493 if let Some((run_len, style)) = self.runs.peek() {
494 highlight_style = Some(style.clone());
495 let run_end_in_chunk = self.run_start + run_len - self.offset;
496 if run_end_in_chunk <= chunk_len {
497 chunk_len = run_end_in_chunk;
498 self.run_start += run_len;
499 self.runs.next();
500 }
501 }
502
503 self.offset += chunk_len;
504 let (chunk, suffix) = chunk.split_at(chunk_len);
505 self.chunk = if suffix.is_empty() {
506 None
507 } else {
508 Some(suffix)
509 };
510
511 Some(HighlightedChunk {
512 text: chunk,
513 highlight_id: Default::default(),
514 diagnostic: None,
515 })
516 }
517}
518
519impl sum_tree::Item for Transform {
520 type Summary = TransformSummary;
521
522 fn summary(&self) -> Self::Summary {
523 self.summary
524 }
525}
526
527impl sum_tree::Summary for TransformSummary {
528 type Context = ();
529
530 fn add_summary(&mut self, summary: &Self, _: &()) {
531 self.input_rows += summary.input_rows;
532 self.output_rows += summary.output_rows;
533 }
534}
535
536impl<'a> sum_tree::Dimension<'a, TransformSummary> for InputRow {
537 fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
538 self.0 += summary.input_rows;
539 }
540}
541
542impl<'a> sum_tree::Dimension<'a, TransformSummary> for OutputRow {
543 fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
544 self.0 += summary.output_rows;
545 }
546}
547
548impl BlockDisposition {
549 fn is_above(&self) -> bool {
550 matches!(self, BlockDisposition::Above)
551 }
552}
553
554// Count the number of bytes prior to a target row.
555// If the string doesn't contain the target row, return the total number of rows it does contain.
556// Otherwise return the target row itself.
557fn offset_for_row(s: &str, target_row: u32) -> (u32, usize) {
558 let mut row = 0;
559 let mut offset = 0;
560 for (ix, line) in s.split('\n').enumerate() {
561 if ix > 0 {
562 row += 1;
563 offset += 1;
564 }
565 if row as u32 >= target_row {
566 break;
567 }
568 offset += line.len();
569 }
570 (row, offset)
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
577 use buffer::RandomCharIter;
578 use language::Buffer;
579 use rand::prelude::*;
580 use std::env;
581
582 #[gpui::test]
583 fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
584 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
585 let font_id = cx
586 .font_cache()
587 .select_font(family_id, &Default::default())
588 .unwrap();
589
590 let text = "aaa\nbbb\nccc\nddd\n";
591
592 let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
593 let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx);
594 let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
595 let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx);
596 let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot.clone());
597
598 let mut writer = block_map.write(wraps_snapshot.clone(), vec![], cx);
599 writer.insert(
600 vec![
601 BlockProperties {
602 position: Point::new(1, 0),
603 text: "BLOCK 1",
604 disposition: BlockDisposition::Above,
605 runs: vec![],
606 },
607 BlockProperties {
608 position: Point::new(1, 2),
609 text: "BLOCK 2",
610 disposition: BlockDisposition::Above,
611 runs: vec![],
612 },
613 BlockProperties {
614 position: Point::new(3, 2),
615 text: "BLOCK 3",
616 disposition: BlockDisposition::Below,
617 runs: vec![],
618 },
619 ],
620 cx,
621 );
622
623 let mut snapshot = block_map.read(wraps_snapshot, vec![], cx);
624 assert_eq!(
625 snapshot.text(),
626 "aaa\nBLOCK 1\nBLOCK 2\nbbb\nccc\nddd\nBLOCK 3\n"
627 );
628 assert_eq!(
629 snapshot.to_block_point(WrapPoint::new(1, 0)),
630 BlockPoint(Point::new(3, 0))
631 );
632
633 // Insert a line break, separating two block decorations into separate
634 // lines.
635 buffer.update(cx, |buffer, cx| {
636 buffer.edit([Point::new(1, 1)..Point::new(1, 1)], "!!!\n", cx)
637 });
638
639 let (folds_snapshot, fold_edits) = fold_map.read(cx);
640 let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
641 let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
642 wrap_map.sync(tabs_snapshot, tab_edits, cx)
643 });
644 let mut snapshot = block_map.read(wraps_snapshot, wrap_edits, cx);
645 assert_eq!(
646 snapshot.text(),
647 "aaa\nBLOCK 1\nb!!!\nBLOCK 2\nbb\nccc\nddd\nBLOCK 3\n"
648 );
649 }
650
651 #[gpui::test(iterations = 100)]
652 fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
653 let operations = env::var("OPERATIONS")
654 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
655 .unwrap_or(10);
656
657 let wrap_width = Some(rng.gen_range(0.0..=1000.0));
658 let tab_size = 1;
659 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
660 let font_id = cx
661 .font_cache()
662 .select_font(family_id, &Default::default())
663 .unwrap();
664 let font_size = 14.0;
665
666 log::info!("Wrap width: {:?}", wrap_width);
667
668 let buffer = cx.add_model(|cx| {
669 let len = rng.gen_range(0..10);
670 let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
671 Buffer::new(0, text, cx)
672 });
673 let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx);
674 let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
675 let (wrap_map, wraps_snapshot) =
676 WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
677 let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot);
678 let mut expected_blocks = Vec::new();
679
680 for _ in 0..operations {
681 match rng.gen_range(0..=100) {
682 0..=19 => {
683 let wrap_width = if rng.gen_bool(0.2) {
684 None
685 } else {
686 Some(rng.gen_range(0.0..=1000.0))
687 };
688 log::info!("Setting wrap width to {:?}", wrap_width);
689 wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
690 }
691 20..=39 => {
692 let block_count = rng.gen_range(1..=4);
693 let block_properties = (0..block_count)
694 .map(|_| {
695 let buffer = buffer.read(cx);
696 let position = buffer.anchor_before(rng.gen_range(0..=buffer.len()));
697
698 let len = rng.gen_range(0..10);
699 let text = Rope::from(
700 RandomCharIter::new(&mut rng)
701 .take(len)
702 .collect::<String>()
703 .as_str(),
704 );
705 BlockProperties {
706 position,
707 text,
708 runs: Vec::<(usize, HighlightStyle)>::new(),
709 disposition: if rng.gen() {
710 BlockDisposition::Above
711 } else {
712 BlockDisposition::Below
713 },
714 }
715 })
716 .collect::<Vec<_>>();
717
718 let (folds_snapshot, fold_edits) = fold_map.read(cx);
719 let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
720 let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
721 wrap_map.sync(tabs_snapshot, tab_edits, cx)
722 });
723 let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx);
724 let block_ids = block_map.insert(block_properties.clone(), cx);
725 expected_blocks.extend(block_ids.into_iter().zip(block_properties));
726 }
727 40..=59 => {
728 let block_count = rng.gen_range(1..=4.min(expected_blocks.len()));
729 let block_ids_to_remove = (0..block_count)
730 .map(|_| {
731 expected_blocks
732 .remove(rng.gen_range(0..expected_blocks.len()))
733 .0
734 })
735 .collect();
736
737 let (folds_snapshot, fold_edits) = fold_map.read(cx);
738 let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
739 let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
740 wrap_map.sync(tabs_snapshot, tab_edits, cx)
741 });
742 let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx);
743 block_map.remove(block_ids_to_remove, cx);
744 }
745 _ => {
746 buffer.update(cx, |buffer, _| buffer.randomly_edit(&mut rng, 5));
747 }
748 }
749
750 let (folds_snapshot, fold_edits) = fold_map.read(cx);
751 let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
752 let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
753 wrap_map.sync(tabs_snapshot, tab_edits, cx)
754 });
755 let mut blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits, cx);
756 assert_eq!(
757 blocks_snapshot.transforms.summary().input_rows,
758 wraps_snapshot.max_point().row() + 1
759 );
760
761 let buffer = buffer.read(cx);
762 let mut sorted_blocks = expected_blocks
763 .iter()
764 .cloned()
765 .map(|(_, block)| BlockProperties {
766 position: block.position.to_point(buffer),
767 text: block.text,
768 runs: block.runs,
769 disposition: block.disposition,
770 })
771 .collect::<Vec<_>>();
772 sorted_blocks.sort_unstable_by_key(|block| (block.position.row, block.disposition));
773 let mut sorted_blocks = sorted_blocks.into_iter().peekable();
774
775 let mut expected_text = String::new();
776 let input_text = wraps_snapshot.text();
777 for (row, input_line) in input_text.split('\n').enumerate() {
778 let row = row as u32;
779 if row > 0 {
780 expected_text.push('\n');
781 }
782
783 while let Some(block) = sorted_blocks.peek() {
784 if block.position.row == row && block.disposition == BlockDisposition::Above {
785 expected_text.extend(block.text.chunks());
786 expected_text.push('\n');
787 sorted_blocks.next();
788 } else {
789 break;
790 }
791 }
792
793 expected_text.push_str(input_line);
794
795 while let Some(block) = sorted_blocks.peek() {
796 if block.position.row == row && block.disposition == BlockDisposition::Below {
797 expected_text.push('\n');
798 expected_text.extend(block.text.chunks());
799 sorted_blocks.next();
800 } else {
801 break;
802 }
803 }
804 }
805
806 assert_eq!(blocks_snapshot.text(), expected_text);
807 }
808 }
809}