tab_map.rs

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