tab_map.rs

  1use super::{
  2    fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
  3    TextHighlights,
  4};
  5use crate::MultiBufferSnapshot;
  6use gpui::fonts::HighlightStyle;
  7use language::{Chunk, Point};
  8use std::{cmp, mem, num::NonZeroU32, ops::Range};
  9use sum_tree::Bias;
 10
 11const MAX_EXPANSION_COLUMN: u32 = 256;
 12
 13pub struct TabMap(TabSnapshot);
 14
 15impl TabMap {
 16    pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
 17        let snapshot = TabSnapshot {
 18            fold_snapshot,
 19            tab_size,
 20            max_expansion_column: MAX_EXPANSION_COLUMN,
 21            version: 0,
 22        };
 23        (Self(snapshot.clone()), snapshot)
 24    }
 25
 26    #[cfg(test)]
 27    pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
 28        self.0.max_expansion_column = column;
 29        self.0.clone()
 30    }
 31
 32    pub fn sync(
 33        &mut self,
 34        fold_snapshot: FoldSnapshot,
 35        mut fold_edits: Vec<FoldEdit>,
 36        tab_size: NonZeroU32,
 37    ) -> (TabSnapshot, Vec<TabEdit>) {
 38        let old_snapshot = &mut self.0;
 39        let mut new_snapshot = TabSnapshot {
 40            fold_snapshot,
 41            tab_size,
 42            max_expansion_column: old_snapshot.max_expansion_column,
 43            version: old_snapshot.version,
 44        };
 45
 46        if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
 47            new_snapshot.version += 1;
 48        }
 49
 50        let mut tab_edits = Vec::with_capacity(fold_edits.len());
 51
 52        if old_snapshot.tab_size == new_snapshot.tab_size {
 53            // Expand each edit to include the next tab on the same line as the edit,
 54            // and any subsequent tabs on that line that moved across the tab expansion
 55            // boundary.
 56            for fold_edit in &mut fold_edits {
 57                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
 58                let old_end_row_successor_offset = cmp::min(
 59                    FoldPoint::new(old_end.row() + 1, 0),
 60                    old_snapshot.fold_snapshot.max_point(),
 61                )
 62                .to_offset(&old_snapshot.fold_snapshot);
 63                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
 64
 65                let mut offset_from_edit = 0;
 66                let mut first_tab_offset = None;
 67                let mut last_tab_with_changed_expansion_offset = None;
 68                'outer: for chunk in old_snapshot.fold_snapshot.chunks(
 69                    fold_edit.old.end..old_end_row_successor_offset,
 70                    false,
 71                    None,
 72                    None,
 73                ) {
 74                    for (ix, _) in chunk.text.match_indices('\t') {
 75                        let offset_from_edit = offset_from_edit + (ix as u32);
 76                        if first_tab_offset.is_none() {
 77                            first_tab_offset = Some(offset_from_edit);
 78                        }
 79
 80                        let old_column = old_end.column() + offset_from_edit;
 81                        let new_column = new_end.column() + offset_from_edit;
 82                        let was_expanded = old_column < old_snapshot.max_expansion_column;
 83                        let is_expanded = new_column < new_snapshot.max_expansion_column;
 84                        if was_expanded != is_expanded {
 85                            last_tab_with_changed_expansion_offset = Some(offset_from_edit);
 86                        } else if !was_expanded && !is_expanded {
 87                            break 'outer;
 88                        }
 89                    }
 90
 91                    offset_from_edit += chunk.text.len() as u32;
 92                    if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
 93                        && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
 94                    {
 95                        break;
 96                    }
 97                }
 98
 99                if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
100                    fold_edit.old.end.0 += offset as usize + 1;
101                    fold_edit.new.end.0 += offset as usize + 1;
102                }
103            }
104
105            // Combine any edits that overlap due to the expansion.
106            let mut ix = 1;
107            while ix < fold_edits.len() {
108                let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
109                let prev_edit = prev_edits.last_mut().unwrap();
110                let edit = &next_edits[0];
111                if prev_edit.old.end >= edit.old.start {
112                    prev_edit.old.end = edit.old.end;
113                    prev_edit.new.end = edit.new.end;
114                    fold_edits.remove(ix);
115                } else {
116                    ix += 1;
117                }
118            }
119
120            for fold_edit in fold_edits {
121                let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
122                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
123                let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
124                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
125                tab_edits.push(TabEdit {
126                    old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
127                    new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
128                });
129            }
130        } else {
131            new_snapshot.version += 1;
132            tab_edits.push(TabEdit {
133                old: TabPoint::zero()..old_snapshot.max_point(),
134                new: TabPoint::zero()..new_snapshot.max_point(),
135            });
136        }
137
138        *old_snapshot = new_snapshot;
139        (old_snapshot.clone(), tab_edits)
140    }
141}
142
143#[derive(Clone)]
144pub struct TabSnapshot {
145    pub fold_snapshot: FoldSnapshot,
146    pub tab_size: NonZeroU32,
147    pub max_expansion_column: u32,
148    pub version: usize,
149}
150
151impl TabSnapshot {
152    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
153        &self.fold_snapshot.inlay_snapshot.buffer
154    }
155
156    pub fn line_len(&self, row: u32) -> u32 {
157        let max_point = self.max_point();
158        if row < max_point.row() {
159            self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
160                .0
161                .column
162        } else {
163            max_point.column()
164        }
165    }
166
167    pub fn text_summary(&self) -> TextSummary {
168        self.text_summary_for_range(TabPoint::zero()..self.max_point())
169    }
170
171    pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
172        let input_start = self.to_fold_point(range.start, Bias::Left).0;
173        let input_end = self.to_fold_point(range.end, Bias::Right).0;
174        let input_summary = self
175            .fold_snapshot
176            .text_summary_for_range(input_start..input_end);
177
178        let mut first_line_chars = 0;
179        let line_end = if range.start.row() == range.end.row() {
180            range.end
181        } else {
182            self.max_point()
183        };
184        for c in self
185            .chunks(range.start..line_end, false, None, None)
186            .flat_map(|chunk| chunk.text.chars())
187        {
188            if c == '\n' {
189                break;
190            }
191            first_line_chars += 1;
192        }
193
194        let mut last_line_chars = 0;
195        if range.start.row() == range.end.row() {
196            last_line_chars = first_line_chars;
197        } else {
198            for _ in self
199                .chunks(
200                    TabPoint::new(range.end.row(), 0)..range.end,
201                    false,
202                    None,
203                    None,
204                )
205                .flat_map(|chunk| chunk.text.chars())
206            {
207                last_line_chars += 1;
208            }
209        }
210
211        TextSummary {
212            lines: range.end.0 - range.start.0,
213            first_line_chars,
214            last_line_chars,
215            longest_row: input_summary.longest_row,
216            longest_row_chars: input_summary.longest_row_chars,
217        }
218    }
219
220    pub fn chunks<'a>(
221        &'a self,
222        range: Range<TabPoint>,
223        language_aware: bool,
224        text_highlights: Option<&'a TextHighlights>,
225        inlay_highlights: Option<HighlightStyle>,
226    ) -> TabChunks<'a> {
227        let (input_start, expanded_char_column, to_next_stop) =
228            self.to_fold_point(range.start, Bias::Left);
229        let input_column = input_start.column();
230        let input_start = input_start.to_offset(&self.fold_snapshot);
231        let input_end = self
232            .to_fold_point(range.end, Bias::Right)
233            .0
234            .to_offset(&self.fold_snapshot);
235        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
236            range.end.column() - range.start.column()
237        } else {
238            to_next_stop
239        };
240
241        TabChunks {
242            fold_chunks: self.fold_snapshot.chunks(
243                input_start..input_end,
244                language_aware,
245                text_highlights,
246                inlay_highlights,
247            ),
248            input_column,
249            column: expanded_char_column,
250            max_expansion_column: self.max_expansion_column,
251            output_position: range.start.0,
252            max_output_position: range.end.0,
253            tab_size: self.tab_size,
254            chunk: Chunk {
255                text: &SPACES[0..(to_next_stop as usize)],
256                is_tab: true,
257                ..Default::default()
258            },
259            inside_leading_tab: to_next_stop > 0,
260        }
261    }
262
263    pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
264        self.fold_snapshot.buffer_rows(row)
265    }
266
267    #[cfg(test)]
268    pub fn text(&self) -> String {
269        self.chunks(TabPoint::zero()..self.max_point(), false, None, None)
270            .map(|chunk| chunk.text)
271            .collect()
272    }
273
274    pub fn max_point(&self) -> TabPoint {
275        self.to_tab_point(self.fold_snapshot.max_point())
276    }
277
278    pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
279        self.to_tab_point(
280            self.fold_snapshot
281                .clip_point(self.to_fold_point(point, bias).0, bias),
282        )
283    }
284
285    pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
286        let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
287        let expanded = self.expand_tabs(chars, input.column());
288        TabPoint::new(input.row(), expanded)
289    }
290
291    pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
292        let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
293        let expanded = output.column();
294        let (collapsed, expanded_char_column, to_next_stop) =
295            self.collapse_tabs(chars, expanded, bias);
296        (
297            FoldPoint::new(output.row(), collapsed as u32),
298            expanded_char_column,
299            to_next_stop,
300        )
301    }
302
303    pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
304        let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
305        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
306        self.to_tab_point(fold_point)
307    }
308
309    pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
310        let fold_point = self.to_fold_point(point, bias).0;
311        let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
312        self.fold_snapshot
313            .inlay_snapshot
314            .to_buffer_point(inlay_point)
315    }
316
317    fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
318        let tab_size = self.tab_size.get();
319
320        let mut expanded_chars = 0;
321        let mut expanded_bytes = 0;
322        let mut collapsed_bytes = 0;
323        let end_column = column.min(self.max_expansion_column);
324        for c in chars {
325            if collapsed_bytes >= end_column {
326                break;
327            }
328            if c == '\t' {
329                let tab_len = tab_size - expanded_chars % tab_size;
330                expanded_bytes += tab_len;
331                expanded_chars += tab_len;
332            } else {
333                expanded_bytes += c.len_utf8() as u32;
334                expanded_chars += 1;
335            }
336            collapsed_bytes += c.len_utf8() as u32;
337        }
338        expanded_bytes + column.saturating_sub(collapsed_bytes)
339    }
340
341    fn collapse_tabs(
342        &self,
343        chars: impl Iterator<Item = char>,
344        column: u32,
345        bias: Bias,
346    ) -> (u32, u32, u32) {
347        let tab_size = self.tab_size.get();
348
349        let mut expanded_bytes = 0;
350        let mut expanded_chars = 0;
351        let mut collapsed_bytes = 0;
352        for c in chars {
353            if expanded_bytes >= column {
354                break;
355            }
356            if collapsed_bytes >= self.max_expansion_column {
357                break;
358            }
359
360            if c == '\t' {
361                let tab_len = tab_size - (expanded_chars % tab_size);
362                expanded_chars += tab_len;
363                expanded_bytes += tab_len;
364                if expanded_bytes > column {
365                    expanded_chars -= expanded_bytes - column;
366                    return match bias {
367                        Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
368                        Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
369                    };
370                }
371            } else {
372                expanded_chars += 1;
373                expanded_bytes += c.len_utf8() as u32;
374            }
375
376            if expanded_bytes > column && matches!(bias, Bias::Left) {
377                expanded_chars -= 1;
378                break;
379            }
380
381            collapsed_bytes += c.len_utf8() as u32;
382        }
383        (
384            collapsed_bytes + column.saturating_sub(expanded_bytes),
385            expanded_chars,
386            0,
387        )
388    }
389}
390
391#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
392pub struct TabPoint(pub Point);
393
394impl TabPoint {
395    pub fn new(row: u32, column: u32) -> Self {
396        Self(Point::new(row, column))
397    }
398
399    pub fn zero() -> Self {
400        Self::new(0, 0)
401    }
402
403    pub fn row(self) -> u32 {
404        self.0.row
405    }
406
407    pub fn column(self) -> u32 {
408        self.0.column
409    }
410}
411
412impl From<Point> for TabPoint {
413    fn from(point: Point) -> Self {
414        Self(point)
415    }
416}
417
418pub type TabEdit = text::Edit<TabPoint>;
419
420#[derive(Clone, Debug, Default, Eq, PartialEq)]
421pub struct TextSummary {
422    pub lines: Point,
423    pub first_line_chars: u32,
424    pub last_line_chars: u32,
425    pub longest_row: u32,
426    pub longest_row_chars: u32,
427}
428
429impl<'a> From<&'a str> for TextSummary {
430    fn from(text: &'a str) -> Self {
431        let sum = text::TextSummary::from(text);
432
433        TextSummary {
434            lines: sum.lines,
435            first_line_chars: sum.first_line_chars,
436            last_line_chars: sum.last_line_chars,
437            longest_row: sum.longest_row,
438            longest_row_chars: sum.longest_row_chars,
439        }
440    }
441}
442
443impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
444    fn add_assign(&mut self, other: &'a Self) {
445        let joined_chars = self.last_line_chars + other.first_line_chars;
446        if joined_chars > self.longest_row_chars {
447            self.longest_row = self.lines.row;
448            self.longest_row_chars = joined_chars;
449        }
450        if other.longest_row_chars > self.longest_row_chars {
451            self.longest_row = self.lines.row + other.longest_row;
452            self.longest_row_chars = other.longest_row_chars;
453        }
454
455        if self.lines.row == 0 {
456            self.first_line_chars += other.first_line_chars;
457        }
458
459        if other.lines.row == 0 {
460            self.last_line_chars += other.first_line_chars;
461        } else {
462            self.last_line_chars = other.last_line_chars;
463        }
464
465        self.lines += &other.lines;
466    }
467}
468
469// Handles a tab width <= 16
470const SPACES: &str = "                ";
471
472pub struct TabChunks<'a> {
473    fold_chunks: FoldChunks<'a>,
474    chunk: Chunk<'a>,
475    column: u32,
476    max_expansion_column: u32,
477    output_position: Point,
478    input_column: u32,
479    max_output_position: Point,
480    tab_size: NonZeroU32,
481    inside_leading_tab: bool,
482}
483
484impl<'a> Iterator for TabChunks<'a> {
485    type Item = Chunk<'a>;
486
487    fn next(&mut self) -> Option<Self::Item> {
488        if self.chunk.text.is_empty() {
489            if let Some(chunk) = self.fold_chunks.next() {
490                self.chunk = chunk;
491                if self.inside_leading_tab {
492                    self.chunk.text = &self.chunk.text[1..];
493                    self.inside_leading_tab = false;
494                    self.input_column += 1;
495                }
496            } else {
497                return None;
498            }
499        }
500
501        for (ix, c) in self.chunk.text.char_indices() {
502            match c {
503                '\t' => {
504                    if ix > 0 {
505                        let (prefix, suffix) = self.chunk.text.split_at(ix);
506                        self.chunk.text = suffix;
507                        return Some(Chunk {
508                            text: prefix,
509                            ..self.chunk
510                        });
511                    } else {
512                        self.chunk.text = &self.chunk.text[1..];
513                        let tab_size = if self.input_column < self.max_expansion_column {
514                            self.tab_size.get() as u32
515                        } else {
516                            1
517                        };
518                        let mut len = tab_size - self.column % tab_size;
519                        let next_output_position = cmp::min(
520                            self.output_position + Point::new(0, len),
521                            self.max_output_position,
522                        );
523                        len = next_output_position.column - self.output_position.column;
524                        self.column += len;
525                        self.input_column += 1;
526                        self.output_position = next_output_position;
527                        return Some(Chunk {
528                            text: &SPACES[..len as usize],
529                            is_tab: true,
530                            ..self.chunk
531                        });
532                    }
533                }
534                '\n' => {
535                    self.column = 0;
536                    self.input_column = 0;
537                    self.output_position += Point::new(1, 0);
538                }
539                _ => {
540                    self.column += 1;
541                    if !self.inside_leading_tab {
542                        self.input_column += c.len_utf8() as u32;
543                    }
544                    self.output_position.column += c.len_utf8() as u32;
545                }
546            }
547        }
548
549        Some(mem::take(&mut self.chunk))
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use crate::{
557        display_map::{fold_map::FoldMap, inlay_map::InlayMap},
558        MultiBuffer,
559    };
560    use rand::{prelude::StdRng, Rng};
561
562    #[gpui::test]
563    fn test_expand_tabs(cx: &mut gpui::AppContext) {
564        let buffer = MultiBuffer::build_simple("", cx);
565        let buffer_snapshot = buffer.read(cx).snapshot(cx);
566        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
567        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
568        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
569
570        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
571        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
572        assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
573    }
574
575    #[gpui::test]
576    fn test_long_lines(cx: &mut gpui::AppContext) {
577        let max_expansion_column = 12;
578        let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
579        let output = "A   BC  DEF G   HI J K L M";
580
581        let buffer = MultiBuffer::build_simple(input, cx);
582        let buffer_snapshot = buffer.read(cx).snapshot(cx);
583        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
584        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
585        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
586
587        tab_snapshot.max_expansion_column = max_expansion_column;
588        assert_eq!(tab_snapshot.text(), output);
589
590        for (ix, c) in input.char_indices() {
591            assert_eq!(
592                tab_snapshot
593                    .chunks(
594                        TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
595                        false,
596                        None,
597                        None,
598                    )
599                    .map(|c| c.text)
600                    .collect::<String>(),
601                &output[ix..],
602                "text from index {ix}"
603            );
604
605            if c != '\t' {
606                let input_point = Point::new(0, ix as u32);
607                let output_point = Point::new(0, output.find(c).unwrap() as u32);
608                assert_eq!(
609                    tab_snapshot.to_tab_point(FoldPoint(input_point)),
610                    TabPoint(output_point),
611                    "to_tab_point({input_point:?})"
612                );
613                assert_eq!(
614                    tab_snapshot
615                        .to_fold_point(TabPoint(output_point), Bias::Left)
616                        .0,
617                    FoldPoint(input_point),
618                    "to_fold_point({output_point:?})"
619                );
620            }
621        }
622    }
623
624    #[gpui::test]
625    fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::AppContext) {
626        let max_expansion_column = 8;
627        let input = "abcdefg⋯hij";
628
629        let buffer = MultiBuffer::build_simple(input, cx);
630        let buffer_snapshot = buffer.read(cx).snapshot(cx);
631        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
632        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
633        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
634
635        tab_snapshot.max_expansion_column = max_expansion_column;
636        assert_eq!(tab_snapshot.text(), input);
637    }
638
639    #[gpui::test]
640    fn test_marking_tabs(cx: &mut gpui::AppContext) {
641        let input = "\t \thello";
642
643        let buffer = MultiBuffer::build_simple(&input, cx);
644        let buffer_snapshot = buffer.read(cx).snapshot(cx);
645        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
646        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
647        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
648
649        assert_eq!(
650            chunks(&tab_snapshot, TabPoint::zero()),
651            vec![
652                ("    ".to_string(), true),
653                (" ".to_string(), false),
654                ("   ".to_string(), true),
655                ("hello".to_string(), false),
656            ]
657        );
658        assert_eq!(
659            chunks(&tab_snapshot, TabPoint::new(0, 2)),
660            vec![
661                ("  ".to_string(), true),
662                (" ".to_string(), false),
663                ("   ".to_string(), true),
664                ("hello".to_string(), false),
665            ]
666        );
667
668        fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
669            let mut chunks = Vec::new();
670            let mut was_tab = false;
671            let mut text = String::new();
672            for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
673                if chunk.is_tab != was_tab {
674                    if !text.is_empty() {
675                        chunks.push((mem::take(&mut text), was_tab));
676                    }
677                    was_tab = chunk.is_tab;
678                }
679                text.push_str(chunk.text);
680            }
681
682            if !text.is_empty() {
683                chunks.push((text, was_tab));
684            }
685            chunks
686        }
687    }
688
689    #[gpui::test(iterations = 100)]
690    fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
691        let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
692        let len = rng.gen_range(0..30);
693        let buffer = if rng.gen() {
694            let text = util::RandomCharIter::new(&mut rng)
695                .take(len)
696                .collect::<String>();
697            MultiBuffer::build_simple(&text, cx)
698        } else {
699            MultiBuffer::build_random(&mut rng, cx)
700        };
701        let buffer_snapshot = buffer.read(cx).snapshot(cx);
702        log::info!("Buffer text: {:?}", buffer_snapshot.text());
703
704        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
705        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
706        let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
707        fold_map.randomly_mutate(&mut rng);
708        let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
709        log::info!("FoldMap text: {:?}", fold_snapshot.text());
710        let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
711        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
712
713        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
714        let tabs_snapshot = tab_map.set_max_expansion_column(32);
715
716        let text = text::Rope::from(tabs_snapshot.text().as_str());
717        log::info!(
718            "TabMap text (tab size: {}): {:?}",
719            tab_size,
720            tabs_snapshot.text(),
721        );
722
723        for _ in 0..5 {
724            let end_row = rng.gen_range(0..=text.max_point().row);
725            let end_column = rng.gen_range(0..=text.line_len(end_row));
726            let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
727            let start_row = rng.gen_range(0..=text.max_point().row);
728            let start_column = rng.gen_range(0..=text.line_len(start_row));
729            let mut start =
730                TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
731            if start > end {
732                mem::swap(&mut start, &mut end);
733            }
734
735            let expected_text = text
736                .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
737                .collect::<String>();
738            let expected_summary = TextSummary::from(expected_text.as_str());
739            assert_eq!(
740                tabs_snapshot
741                    .chunks(start..end, false, None, None)
742                    .map(|c| c.text)
743                    .collect::<String>(),
744                expected_text,
745                "chunks({:?}..{:?})",
746                start,
747                end
748            );
749
750            let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
751            if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
752                actual_summary.longest_row = expected_summary.longest_row;
753                actual_summary.longest_row_chars = expected_summary.longest_row_chars;
754            }
755            assert_eq!(actual_summary, expected_summary);
756        }
757
758        for row in 0..=text.max_point().row {
759            assert_eq!(
760                tabs_snapshot.line_len(row),
761                text.line_len(row),
762                "line_len({row})"
763            );
764        }
765    }
766}