char_map.rs

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