suggestion_map.rs

  1use super::{
  2    fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
  3    TextHighlights,
  4};
  5use crate::{MultiBufferSnapshot, ToPoint};
  6use gpui::fonts::HighlightStyle;
  7use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
  8use parking_lot::Mutex;
  9use std::{
 10    cmp,
 11    ops::{Add, AddAssign, Range, Sub},
 12};
 13use util::post_inc;
 14
 15pub type SuggestionEdit = Edit<SuggestionOffset>;
 16
 17#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 18pub struct SuggestionOffset(pub usize);
 19
 20impl Add for SuggestionOffset {
 21    type Output = Self;
 22
 23    fn add(self, rhs: Self) -> Self::Output {
 24        Self(self.0 + rhs.0)
 25    }
 26}
 27
 28impl Sub for SuggestionOffset {
 29    type Output = Self;
 30
 31    fn sub(self, rhs: Self) -> Self::Output {
 32        Self(self.0 - rhs.0)
 33    }
 34}
 35
 36impl AddAssign for SuggestionOffset {
 37    fn add_assign(&mut self, rhs: Self) {
 38        self.0 += rhs.0;
 39    }
 40}
 41
 42#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 43pub struct SuggestionPoint(pub Point);
 44
 45impl SuggestionPoint {
 46    pub fn new(row: u32, column: u32) -> Self {
 47        Self(Point::new(row, column))
 48    }
 49
 50    pub fn row(self) -> u32 {
 51        self.0.row
 52    }
 53
 54    pub fn column(self) -> u32 {
 55        self.0.column
 56    }
 57}
 58
 59#[derive(Clone, Debug)]
 60pub struct Suggestion<T> {
 61    pub position: T,
 62    pub text: Rope,
 63}
 64
 65pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
 66
 67impl SuggestionMap {
 68    pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
 69        let snapshot = SuggestionSnapshot {
 70            fold_snapshot,
 71            suggestion: None,
 72            version: 0,
 73        };
 74        (Self(Mutex::new(snapshot.clone())), snapshot)
 75    }
 76
 77    pub fn replace<T>(
 78        &self,
 79        new_suggestion: Option<Suggestion<T>>,
 80        fold_snapshot: FoldSnapshot,
 81        fold_edits: Vec<FoldEdit>,
 82    ) -> (SuggestionSnapshot, Vec<SuggestionEdit>)
 83    where
 84        T: ToPoint,
 85    {
 86        let new_suggestion = new_suggestion.map(|new_suggestion| {
 87            let buffer_point = new_suggestion
 88                .position
 89                .to_point(fold_snapshot.buffer_snapshot());
 90            let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
 91            let fold_offset = fold_point.to_offset(&fold_snapshot);
 92            Suggestion {
 93                position: fold_offset,
 94                text: new_suggestion.text,
 95            }
 96        });
 97
 98        let (_, edits) = self.sync(fold_snapshot, fold_edits);
 99        let mut snapshot = self.0.lock();
100
101        let mut patch = Patch::new(edits);
102        if let Some(suggestion) = snapshot.suggestion.take() {
103            patch = patch.compose([SuggestionEdit {
104                old: SuggestionOffset(suggestion.position.0)
105                    ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
106                new: SuggestionOffset(suggestion.position.0)
107                    ..SuggestionOffset(suggestion.position.0),
108            }]);
109        }
110
111        if let Some(suggestion) = new_suggestion.as_ref() {
112            patch = patch.compose([SuggestionEdit {
113                old: SuggestionOffset(suggestion.position.0)
114                    ..SuggestionOffset(suggestion.position.0),
115                new: SuggestionOffset(suggestion.position.0)
116                    ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
117            }]);
118        }
119
120        snapshot.suggestion = new_suggestion;
121        snapshot.version += 1;
122        (snapshot.clone(), patch.into_inner())
123    }
124
125    pub fn sync(
126        &self,
127        fold_snapshot: FoldSnapshot,
128        fold_edits: Vec<FoldEdit>,
129    ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
130        let mut snapshot = self.0.lock();
131
132        if snapshot.fold_snapshot.version != fold_snapshot.version {
133            snapshot.version += 1;
134        }
135
136        let mut suggestion_edits = Vec::new();
137
138        let mut suggestion_old_len = 0;
139        let mut suggestion_new_len = 0;
140        for fold_edit in fold_edits {
141            let start = fold_edit.new.start;
142            let end = FoldOffset(start.0 + fold_edit.old_len().0);
143            if let Some(suggestion) = snapshot.suggestion.as_mut() {
144                if end <= suggestion.position {
145                    suggestion.position.0 += fold_edit.new_len().0;
146                    suggestion.position.0 -= fold_edit.old_len().0;
147                } else if start > suggestion.position {
148                    suggestion_old_len = suggestion.text.len();
149                    suggestion_new_len = suggestion_old_len;
150                } else {
151                    suggestion_old_len = suggestion.text.len();
152                    snapshot.suggestion.take();
153                    suggestion_edits.push(SuggestionEdit {
154                        old: SuggestionOffset(fold_edit.old.start.0)
155                            ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
156                        new: SuggestionOffset(fold_edit.new.start.0)
157                            ..SuggestionOffset(fold_edit.new.end.0),
158                    });
159                    continue;
160                }
161            }
162
163            suggestion_edits.push(SuggestionEdit {
164                old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
165                    ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
166                new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
167                    ..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
168            });
169        }
170        snapshot.fold_snapshot = fold_snapshot;
171
172        (snapshot.clone(), suggestion_edits)
173    }
174}
175
176#[derive(Clone)]
177pub struct SuggestionSnapshot {
178    pub fold_snapshot: FoldSnapshot,
179    pub suggestion: Option<Suggestion<FoldOffset>>,
180    pub version: usize,
181}
182
183impl SuggestionSnapshot {
184    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
185        self.fold_snapshot.buffer_snapshot()
186    }
187
188    pub fn max_point(&self) -> SuggestionPoint {
189        if let Some(suggestion) = self.suggestion.as_ref() {
190            let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
191            let mut max_point = suggestion_point.0;
192            max_point += suggestion.text.max_point();
193            max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
194            SuggestionPoint(max_point)
195        } else {
196            SuggestionPoint(self.fold_snapshot.max_point().0)
197        }
198    }
199
200    pub fn len(&self) -> SuggestionOffset {
201        if let Some(suggestion) = self.suggestion.as_ref() {
202            let mut len = suggestion.position.0;
203            len += suggestion.text.len();
204            len += self.fold_snapshot.len().0 - suggestion.position.0;
205            SuggestionOffset(len)
206        } else {
207            SuggestionOffset(self.fold_snapshot.len().0)
208        }
209    }
210
211    pub fn line_len(&self, row: u32) -> u32 {
212        if let Some(suggestion) = &self.suggestion {
213            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
214            let suggestion_end = suggestion_start + suggestion.text.max_point();
215
216            if row < suggestion_start.row {
217                self.fold_snapshot.line_len(row)
218            } else if row > suggestion_end.row {
219                self.fold_snapshot
220                    .line_len(suggestion_start.row + (row - suggestion_end.row))
221            } else {
222                let mut result = suggestion.text.line_len(row - suggestion_start.row);
223                if row == suggestion_start.row {
224                    result += suggestion_start.column;
225                }
226                if row == suggestion_end.row {
227                    result +=
228                        self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
229                }
230                result
231            }
232        } else {
233            self.fold_snapshot.line_len(row)
234        }
235    }
236
237    pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
238        if let Some(suggestion) = self.suggestion.as_ref() {
239            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
240            let suggestion_end = suggestion_start + suggestion.text.max_point();
241            if point.0 <= suggestion_start {
242                SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
243            } else if point.0 > suggestion_end {
244                let fold_point = self.fold_snapshot.clip_point(
245                    FoldPoint(suggestion_start + (point.0 - suggestion_end)),
246                    bias,
247                );
248                let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
249                if bias == Bias::Left && suggestion_point == suggestion_end {
250                    SuggestionPoint(suggestion_start)
251                } else {
252                    SuggestionPoint(suggestion_point)
253                }
254            } else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
255                SuggestionPoint(suggestion_start)
256            } else {
257                let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
258                    > suggestion_start.column
259                {
260                    FoldPoint(suggestion_start + Point::new(0, 1))
261                } else {
262                    FoldPoint(suggestion_start + Point::new(1, 0))
263                };
264                let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
265                SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
266            }
267        } else {
268            SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
269        }
270    }
271
272    pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
273        if let Some(suggestion) = self.suggestion.as_ref() {
274            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
275            let suggestion_end = suggestion_start + suggestion.text.max_point();
276
277            if point.0 <= suggestion_start {
278                SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
279            } else if point.0 > suggestion_end {
280                let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
281                    .to_offset(&self.fold_snapshot);
282                SuggestionOffset(fold_offset.0 + suggestion.text.len())
283            } else {
284                let offset_in_suggestion =
285                    suggestion.text.point_to_offset(point.0 - suggestion_start);
286                SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
287            }
288        } else {
289            SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
290        }
291    }
292
293    pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
294        if let Some(suggestion) = self.suggestion.as_ref() {
295            let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
296            if offset.0 <= suggestion.position.0 {
297                SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
298            } else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
299                let fold_point = FoldOffset(offset.0 - suggestion.text.len())
300                    .to_point(&self.fold_snapshot)
301                    .0;
302
303                SuggestionPoint(
304                    suggestion_point_start
305                        + suggestion.text.max_point()
306                        + (fold_point - suggestion_point_start),
307                )
308            } else {
309                let point_in_suggestion = suggestion
310                    .text
311                    .offset_to_point(offset.0 - suggestion.position.0);
312                SuggestionPoint(suggestion_point_start + point_in_suggestion)
313            }
314        } else {
315            SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
316        }
317    }
318
319    pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
320        if let Some(suggestion) = self.suggestion.as_ref() {
321            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
322            let suggestion_end = suggestion_start + suggestion.text.max_point();
323
324            if point.0 <= suggestion_start {
325                FoldPoint(point.0)
326            } else if point.0 > suggestion_end {
327                FoldPoint(suggestion_start + (point.0 - suggestion_end))
328            } else {
329                FoldPoint(suggestion_start)
330            }
331        } else {
332            FoldPoint(point.0)
333        }
334    }
335
336    pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
337        if let Some(suggestion) = self.suggestion.as_ref() {
338            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
339
340            if point.0 <= suggestion_start {
341                SuggestionPoint(point.0)
342            } else {
343                let suggestion_end = suggestion_start + suggestion.text.max_point();
344                SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
345            }
346        } else {
347            SuggestionPoint(point.0)
348        }
349    }
350
351    pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
352        if let Some(suggestion) = self.suggestion.as_ref() {
353            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
354            let suggestion_end = suggestion_start + suggestion.text.max_point();
355            let mut summary = TextSummary::default();
356
357            let prefix_range =
358                cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
359            if prefix_range.start < prefix_range.end {
360                summary += self.fold_snapshot.text_summary_for_range(
361                    FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
362                );
363            }
364
365            let suggestion_range =
366                cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
367            if suggestion_range.start < suggestion_range.end {
368                let point_range = suggestion_range.start - suggestion_start
369                    ..suggestion_range.end - suggestion_start;
370                let offset_range = suggestion.text.point_to_offset(point_range.start)
371                    ..suggestion.text.point_to_offset(point_range.end);
372                summary += suggestion
373                    .text
374                    .cursor(offset_range.start)
375                    .summary::<TextSummary>(offset_range.end);
376            }
377
378            let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
379            if suffix_range.start < suffix_range.end {
380                let start = suggestion_start + (suffix_range.start - suggestion_end);
381                let end = suggestion_start + (suffix_range.end - suggestion_end);
382                summary += self
383                    .fold_snapshot
384                    .text_summary_for_range(FoldPoint(start)..FoldPoint(end));
385            }
386
387            summary
388        } else {
389            self.fold_snapshot
390                .text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
391        }
392    }
393
394    pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
395        let start = self.to_offset(start);
396        self.chunks(start..self.len(), false, None, None)
397            .flat_map(|chunk| chunk.text.chars())
398    }
399
400    pub fn chunks<'a>(
401        &'a self,
402        range: Range<SuggestionOffset>,
403        language_aware: bool,
404        text_highlights: Option<&'a TextHighlights>,
405        suggestion_highlight: Option<HighlightStyle>,
406    ) -> SuggestionChunks<'a> {
407        if let Some(suggestion) = self.suggestion.as_ref() {
408            let suggestion_range =
409                suggestion.position.0..suggestion.position.0 + suggestion.text.len();
410
411            let prefix_chunks = if range.start.0 < suggestion_range.start {
412                Some(self.fold_snapshot.chunks(
413                    FoldOffset(range.start.0)
414                        ..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
415                    language_aware,
416                    text_highlights,
417                ))
418            } else {
419                None
420            };
421
422            let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
423                ..cmp::min(range.end.0, suggestion_range.end);
424            let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
425            {
426                let start = clipped_suggestion_range.start - suggestion_range.start;
427                let end = clipped_suggestion_range.end - suggestion_range.start;
428                Some(suggestion.text.chunks_in_range(start..end))
429            } else {
430                None
431            };
432
433            let suffix_chunks = if range.end.0 > suggestion_range.end {
434                let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
435                let end = range.end.0 - suggestion_range.len();
436                Some(self.fold_snapshot.chunks(
437                    FoldOffset(start)..FoldOffset(end),
438                    language_aware,
439                    text_highlights,
440                ))
441            } else {
442                None
443            };
444
445            SuggestionChunks {
446                prefix_chunks,
447                suggestion_chunks,
448                suffix_chunks,
449                highlight_style: suggestion_highlight,
450            }
451        } else {
452            SuggestionChunks {
453                prefix_chunks: Some(self.fold_snapshot.chunks(
454                    FoldOffset(range.start.0)..FoldOffset(range.end.0),
455                    language_aware,
456                    text_highlights,
457                )),
458                suggestion_chunks: None,
459                suffix_chunks: None,
460                highlight_style: None,
461            }
462        }
463    }
464
465    pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
466        let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
467            let start = suggestion.position.to_point(&self.fold_snapshot).0;
468            let end = start + suggestion.text.max_point();
469            start.row..end.row
470        } else {
471            u32::MAX..u32::MAX
472        };
473
474        let fold_buffer_rows = if row <= suggestion_range.start {
475            self.fold_snapshot.buffer_rows(row)
476        } else if row > suggestion_range.end {
477            self.fold_snapshot
478                .buffer_rows(row - (suggestion_range.end - suggestion_range.start))
479        } else {
480            let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
481            rows.next();
482            rows
483        };
484
485        SuggestionBufferRows {
486            current_row: row,
487            suggestion_row_start: suggestion_range.start,
488            suggestion_row_end: suggestion_range.end,
489            fold_buffer_rows,
490        }
491    }
492
493    #[cfg(test)]
494    pub fn text(&self) -> String {
495        self.chunks(Default::default()..self.len(), false, None, None)
496            .map(|chunk| chunk.text)
497            .collect()
498    }
499}
500
501pub struct SuggestionChunks<'a> {
502    prefix_chunks: Option<FoldChunks<'a>>,
503    suggestion_chunks: Option<text::Chunks<'a>>,
504    suffix_chunks: Option<FoldChunks<'a>>,
505    highlight_style: Option<HighlightStyle>,
506}
507
508impl<'a> Iterator for SuggestionChunks<'a> {
509    type Item = Chunk<'a>;
510
511    fn next(&mut self) -> Option<Self::Item> {
512        if let Some(chunks) = self.prefix_chunks.as_mut() {
513            if let Some(chunk) = chunks.next() {
514                return Some(chunk);
515            } else {
516                self.prefix_chunks = None;
517            }
518        }
519
520        if let Some(chunks) = self.suggestion_chunks.as_mut() {
521            if let Some(chunk) = chunks.next() {
522                return Some(Chunk {
523                    text: chunk,
524                    syntax_highlight_id: None,
525                    highlight_style: self.highlight_style,
526                    diagnostic_severity: None,
527                    is_unnecessary: false,
528                });
529            } else {
530                self.suggestion_chunks = None;
531            }
532        }
533
534        if let Some(chunks) = self.suffix_chunks.as_mut() {
535            if let Some(chunk) = chunks.next() {
536                return Some(chunk);
537            } else {
538                self.suffix_chunks = None;
539            }
540        }
541
542        None
543    }
544}
545
546#[derive(Clone)]
547pub struct SuggestionBufferRows<'a> {
548    current_row: u32,
549    suggestion_row_start: u32,
550    suggestion_row_end: u32,
551    fold_buffer_rows: FoldBufferRows<'a>,
552}
553
554impl<'a> Iterator for SuggestionBufferRows<'a> {
555    type Item = Option<u32>;
556
557    fn next(&mut self) -> Option<Self::Item> {
558        let row = post_inc(&mut self.current_row);
559        if row <= self.suggestion_row_start || row > self.suggestion_row_end {
560            self.fold_buffer_rows.next()
561        } else {
562            Some(None)
563        }
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use crate::{display_map::fold_map::FoldMap, MultiBuffer};
571    use gpui::MutableAppContext;
572    use rand::{prelude::StdRng, Rng};
573    use settings::Settings;
574    use std::{
575        env,
576        ops::{Bound, RangeBounds},
577    };
578
579    #[gpui::test]
580    fn test_basic(cx: &mut MutableAppContext) {
581        let buffer = MultiBuffer::build_simple("abcdefghi", cx);
582        let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
583        let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
584        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
585        assert_eq!(suggestion_snapshot.text(), "abcdefghi");
586
587        let (suggestion_snapshot, _) = suggestion_map.replace(
588            Some(Suggestion {
589                position: 3,
590                text: "123\n456".into(),
591            }),
592            fold_snapshot,
593            Default::default(),
594        );
595        assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
596
597        buffer.update(cx, |buffer, cx| {
598            buffer.edit(
599                [(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
600                None,
601                cx,
602            )
603        });
604        let (fold_snapshot, fold_edits) = fold_map.read(
605            buffer.read(cx).snapshot(cx),
606            buffer_edits.consume().into_inner(),
607        );
608        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
609        assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
610
611        let (mut fold_map_writer, _, _) =
612            fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
613        let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
614        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
615        assert_eq!(suggestion_snapshot.text(), "⋯abcDEF123\n456dGHIefghiJKL");
616
617        let (mut fold_map_writer, _, _) =
618            fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
619        let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
620        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
621        assert_eq!(suggestion_snapshot.text(), "⋯abc⋯GHIefghiJKL");
622    }
623
624    #[gpui::test(iterations = 100)]
625    fn test_random_suggestions(cx: &mut MutableAppContext, mut rng: StdRng) {
626        cx.set_global(Settings::test(cx));
627        let operations = env::var("OPERATIONS")
628            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
629            .unwrap_or(10);
630
631        let len = rng.gen_range(0..30);
632        let buffer = if rng.gen() {
633            let text = util::RandomCharIter::new(&mut rng)
634                .take(len)
635                .collect::<String>();
636            MultiBuffer::build_simple(&text, cx)
637        } else {
638            MultiBuffer::build_random(&mut rng, cx)
639        };
640        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
641        log::info!("buffer text: {:?}", buffer_snapshot.text());
642
643        let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
644        let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
645
646        for _ in 0..operations {
647            let mut suggestion_edits = Patch::default();
648
649            let mut prev_suggestion_text = suggestion_snapshot.text();
650            let mut buffer_edits = Vec::new();
651            match rng.gen_range(0..=100) {
652                0..=29 => {
653                    let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
654                    suggestion_edits = suggestion_edits.compose(edits);
655                }
656                30..=59 => {
657                    for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
658                        fold_snapshot = new_fold_snapshot;
659                        let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
660                        suggestion_edits = suggestion_edits.compose(edits);
661                    }
662                }
663                _ => buffer.update(cx, |buffer, cx| {
664                    let subscription = buffer.subscribe();
665                    let edit_count = rng.gen_range(1..=5);
666                    buffer.randomly_mutate(&mut rng, edit_count, cx);
667                    buffer_snapshot = buffer.snapshot(cx);
668                    let edits = subscription.consume().into_inner();
669                    log::info!("editing {:?}", edits);
670                    buffer_edits.extend(edits);
671                }),
672            };
673
674            let (new_fold_snapshot, fold_edits) =
675                fold_map.read(buffer_snapshot.clone(), buffer_edits);
676            fold_snapshot = new_fold_snapshot;
677            let (new_suggestion_snapshot, edits) =
678                suggestion_map.sync(fold_snapshot.clone(), fold_edits);
679            suggestion_snapshot = new_suggestion_snapshot;
680            suggestion_edits = suggestion_edits.compose(edits);
681
682            log::info!("buffer text: {:?}", buffer_snapshot.text());
683            log::info!("folds text: {:?}", fold_snapshot.text());
684            log::info!("suggestions text: {:?}", suggestion_snapshot.text());
685
686            let mut expected_text = Rope::from(fold_snapshot.text().as_str());
687            let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
688            if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
689                expected_text.replace(
690                    suggestion.position.0..suggestion.position.0,
691                    &suggestion.text.to_string(),
692                );
693                let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
694                let suggestion_end = suggestion_start + suggestion.text.max_point();
695                expected_buffer_rows.splice(
696                    (suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
697                    (0..suggestion_end.row - suggestion_start.row).map(|_| None),
698                );
699            }
700            assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
701            for row_start in 0..expected_buffer_rows.len() {
702                assert_eq!(
703                    suggestion_snapshot
704                        .buffer_rows(row_start as u32)
705                        .collect::<Vec<_>>(),
706                    &expected_buffer_rows[row_start..],
707                    "incorrect buffer rows starting at {}",
708                    row_start
709                );
710            }
711
712            for _ in 0..5 {
713                let mut end = rng.gen_range(0..=suggestion_snapshot.len().0);
714                end = expected_text.clip_offset(end, Bias::Right);
715                let mut start = rng.gen_range(0..=end);
716                start = expected_text.clip_offset(start, Bias::Right);
717
718                let actual_text = suggestion_snapshot
719                    .chunks(
720                        SuggestionOffset(start)..SuggestionOffset(end),
721                        false,
722                        None,
723                        None,
724                    )
725                    .map(|chunk| chunk.text)
726                    .collect::<String>();
727                assert_eq!(
728                    actual_text,
729                    expected_text.slice(start..end).to_string(),
730                    "incorrect text in range {:?}",
731                    start..end
732                );
733
734                let start_point = SuggestionPoint(expected_text.offset_to_point(start));
735                let end_point = SuggestionPoint(expected_text.offset_to_point(end));
736                assert_eq!(
737                    suggestion_snapshot.text_summary_for_range(start_point..end_point),
738                    expected_text.slice(start..end).summary()
739                );
740            }
741
742            for edit in suggestion_edits.into_inner() {
743                prev_suggestion_text.replace_range(
744                    edit.new.start.0..edit.new.start.0 + edit.old_len().0,
745                    &suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
746                );
747            }
748            assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
749
750            assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
751            assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
752
753            let mut suggestion_point = SuggestionPoint::default();
754            let mut suggestion_offset = SuggestionOffset::default();
755            for ch in expected_text.chars() {
756                assert_eq!(
757                    suggestion_snapshot.to_offset(suggestion_point),
758                    suggestion_offset,
759                    "invalid to_offset({:?})",
760                    suggestion_point
761                );
762                assert_eq!(
763                    suggestion_snapshot.to_point(suggestion_offset),
764                    suggestion_point,
765                    "invalid to_point({:?})",
766                    suggestion_offset
767                );
768                assert_eq!(
769                    suggestion_snapshot
770                        .to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
771                    suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
772                );
773
774                let mut bytes = [0; 4];
775                for byte in ch.encode_utf8(&mut bytes).as_bytes() {
776                    suggestion_offset.0 += 1;
777                    if *byte == b'\n' {
778                        suggestion_point.0 += Point::new(1, 0);
779                    } else {
780                        suggestion_point.0 += Point::new(0, 1);
781                    }
782
783                    let clipped_left_point =
784                        suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
785                    let clipped_right_point =
786                        suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
787                    assert!(
788                        clipped_left_point <= clipped_right_point,
789                        "clipped left point {:?} is greater than clipped right point {:?}",
790                        clipped_left_point,
791                        clipped_right_point
792                    );
793                    assert_eq!(
794                        clipped_left_point.0,
795                        expected_text.clip_point(clipped_left_point.0, Bias::Left)
796                    );
797                    assert_eq!(
798                        clipped_right_point.0,
799                        expected_text.clip_point(clipped_right_point.0, Bias::Right)
800                    );
801                    assert!(clipped_left_point <= suggestion_snapshot.max_point());
802                    assert!(clipped_right_point <= suggestion_snapshot.max_point());
803
804                    if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
805                        let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
806                        let suggestion_end = suggestion_start + suggestion.text.max_point();
807                        let invalid_range = (
808                            Bound::Excluded(suggestion_start),
809                            Bound::Included(suggestion_end),
810                        );
811                        assert!(
812                            !invalid_range.contains(&clipped_left_point.0),
813                            "clipped left point {:?} is inside invalid suggestion range {:?}",
814                            clipped_left_point,
815                            invalid_range
816                        );
817                        assert!(
818                            !invalid_range.contains(&clipped_right_point.0),
819                            "clipped right point {:?} is inside invalid suggestion range {:?}",
820                            clipped_right_point,
821                            invalid_range
822                        );
823                    }
824                }
825            }
826        }
827    }
828
829    impl SuggestionMap {
830        pub fn randomly_mutate(
831            &self,
832            rng: &mut impl Rng,
833        ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
834            let fold_snapshot = self.0.lock().fold_snapshot.clone();
835            let new_suggestion = if rng.gen_bool(0.3) {
836                None
837            } else {
838                let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
839                let len = rng.gen_range(0..30);
840                Some(Suggestion {
841                    position: index,
842                    text: util::RandomCharIter::new(rng)
843                        .take(len)
844                        .filter(|ch| *ch != '\r')
845                        .collect::<String>()
846                        .as_str()
847                        .into(),
848                })
849            };
850
851            log::info!("replacing suggestion with {:?}", new_suggestion);
852            self.replace(new_suggestion, fold_snapshot, Default::default())
853        }
854    }
855}