tab_map.rs

   1use super::{
   2    Highlights,
   3    fold_map::{self, Chunk, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
   4};
   5
   6use language::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 in a text buffer.
  14///
  15/// See the [`display_map` module documentation](crate::display_map) for more information.
  16pub struct TabMap(TabSnapshot);
  17
  18impl TabMap {
  19    pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
  20        let snapshot = TabSnapshot {
  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) -> TabSnapshot {
  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    ) -> (TabSnapshot, Vec<TabEdit>) {
  41        let old_snapshot = &mut self.0;
  42        let mut new_snapshot = TabSnapshot {
  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                    // todo(performance use tabs bitmask)
  77                    for (ix, _) in chunk.text.match_indices('\t') {
  78                        let offset_from_edit = offset_from_edit + (ix as u32);
  79                        if first_tab_offset.is_none() {
  80                            first_tab_offset = Some(offset_from_edit);
  81                        }
  82
  83                        let old_column = old_end.column() + offset_from_edit;
  84                        let new_column = new_end.column() + offset_from_edit;
  85                        let was_expanded = old_column < old_snapshot.max_expansion_column;
  86                        let is_expanded = new_column < new_snapshot.max_expansion_column;
  87                        if was_expanded != is_expanded {
  88                            last_tab_with_changed_expansion_offset = Some(offset_from_edit);
  89                        } else if !was_expanded && !is_expanded {
  90                            break 'outer;
  91                        }
  92                    }
  93
  94                    offset_from_edit += chunk.text.len() as u32;
  95                    if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
  96                        && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
  97                    {
  98                        break;
  99                    }
 100                }
 101
 102                if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
 103                    fold_edit.old.end.0 += offset as usize + 1;
 104                    fold_edit.new.end.0 += offset as usize + 1;
 105                }
 106            }
 107
 108            let _old_alloc_ptr = fold_edits.as_ptr();
 109            // Combine any edits that overlap due to the expansion.
 110            let mut fold_edits = fold_edits.into_iter();
 111            let fold_edits = if let Some(mut first_edit) = fold_edits.next() {
 112                // This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
 113                #[allow(clippy::filter_map_identity)]
 114                let mut v: Vec<_> = fold_edits
 115                    .scan(&mut first_edit, |state, edit| {
 116                        if state.old.end >= edit.old.start {
 117                            state.old.end = edit.old.end;
 118                            state.new.end = edit.new.end;
 119                            Some(None) // Skip this edit, it's merged
 120                        } else {
 121                            let new_state = edit;
 122                            let result = Some(Some(state.clone())); // Yield the previous edit
 123                            **state = new_state;
 124                            result
 125                        }
 126                    })
 127                    .filter_map(|x| x)
 128                    .collect();
 129                v.push(first_edit);
 130                debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Fold edits were reallocated");
 131                v
 132            } else {
 133                vec![]
 134            };
 135
 136            for fold_edit in fold_edits {
 137                let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
 138                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
 139                let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
 140                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
 141                tab_edits.push(TabEdit {
 142                    old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
 143                    new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
 144                });
 145            }
 146        } else {
 147            new_snapshot.version += 1;
 148            tab_edits.push(TabEdit {
 149                old: TabPoint::zero()..old_snapshot.max_point(),
 150                new: TabPoint::zero()..new_snapshot.max_point(),
 151            });
 152        }
 153
 154        *old_snapshot = new_snapshot;
 155        (old_snapshot.clone(), tab_edits)
 156    }
 157}
 158
 159#[derive(Clone)]
 160pub struct TabSnapshot {
 161    pub fold_snapshot: FoldSnapshot,
 162    pub tab_size: NonZeroU32,
 163    pub max_expansion_column: u32,
 164    pub version: usize,
 165}
 166
 167impl TabSnapshot {
 168    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
 169        &self.fold_snapshot.inlay_snapshot.buffer
 170    }
 171
 172    pub fn line_len(&self, row: u32) -> u32 {
 173        let max_point = self.max_point();
 174        if row < max_point.row() {
 175            self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
 176                .0
 177                .column
 178        } else {
 179            max_point.column()
 180        }
 181    }
 182
 183    pub fn text_summary(&self) -> TextSummary {
 184        self.text_summary_for_range(TabPoint::zero()..self.max_point())
 185    }
 186
 187    pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
 188        let input_start = self.to_fold_point(range.start, Bias::Left).0;
 189        let input_end = self.to_fold_point(range.end, Bias::Right).0;
 190        let input_summary = self
 191            .fold_snapshot
 192            .text_summary_for_range(input_start..input_end);
 193
 194        let mut first_line_chars = 0;
 195        let line_end = if range.start.row() == range.end.row() {
 196            range.end
 197        } else {
 198            self.max_point()
 199        };
 200        for c in self
 201            .chunks(range.start..line_end, false, Highlights::default())
 202            .flat_map(|chunk| chunk.text.chars())
 203        {
 204            if c == '\n' {
 205                break;
 206            }
 207            first_line_chars += 1;
 208        }
 209
 210        let mut last_line_chars = 0;
 211        if range.start.row() == range.end.row() {
 212            last_line_chars = first_line_chars;
 213        } else {
 214            for _ in self
 215                .chunks(
 216                    TabPoint::new(range.end.row(), 0)..range.end,
 217                    false,
 218                    Highlights::default(),
 219                )
 220                .flat_map(|chunk| chunk.text.chars())
 221            {
 222                last_line_chars += 1;
 223            }
 224        }
 225
 226        TextSummary {
 227            lines: range.end.0 - range.start.0,
 228            first_line_chars,
 229            last_line_chars,
 230            longest_row: input_summary.longest_row,
 231            longest_row_chars: input_summary.longest_row_chars,
 232        }
 233    }
 234
 235    pub(crate) fn chunks<'a>(
 236        &'a self,
 237        range: Range<TabPoint>,
 238        language_aware: bool,
 239        highlights: Highlights<'a>,
 240    ) -> TabChunks<'a> {
 241        let (input_start, expanded_char_column, to_next_stop) =
 242            self.to_fold_point(range.start, Bias::Left);
 243        let input_column = input_start.column();
 244        let input_start = input_start.to_offset(&self.fold_snapshot);
 245        let input_end = self
 246            .to_fold_point(range.end, Bias::Right)
 247            .0
 248            .to_offset(&self.fold_snapshot);
 249        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
 250            range.end.column() - range.start.column()
 251        } else {
 252            to_next_stop
 253        };
 254
 255        TabChunks {
 256            snapshot: self,
 257            fold_chunks: self.fold_snapshot.chunks(
 258                input_start..input_end,
 259                language_aware,
 260                highlights,
 261            ),
 262            input_column,
 263            column: expanded_char_column,
 264            max_expansion_column: self.max_expansion_column,
 265            output_position: range.start.0,
 266            max_output_position: range.end.0,
 267            tab_size: self.tab_size,
 268            chunk: Chunk {
 269                text: &SPACES[0..(to_next_stop as usize)],
 270                is_tab: true,
 271                ..Default::default()
 272            },
 273            inside_leading_tab: to_next_stop > 0,
 274        }
 275    }
 276
 277    pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> {
 278        self.fold_snapshot.row_infos(row)
 279    }
 280
 281    #[cfg(test)]
 282    pub fn text(&self) -> String {
 283        self.chunks(
 284            TabPoint::zero()..self.max_point(),
 285            false,
 286            Highlights::default(),
 287        )
 288        .map(|chunk| chunk.text)
 289        .collect()
 290    }
 291
 292    pub fn max_point(&self) -> TabPoint {
 293        self.to_tab_point(self.fold_snapshot.max_point())
 294    }
 295
 296    pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
 297        self.to_tab_point(
 298            self.fold_snapshot
 299                .clip_point(self.to_fold_point(point, bias).0, bias),
 300        )
 301    }
 302
 303    pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
 304        let chunks = self.fold_snapshot.chunks_at(FoldPoint::new(input.row(), 0));
 305        let tab_cursor = TabStopCursor::new(chunks);
 306        let expanded = self.expand_tabs(tab_cursor, input.column());
 307        TabPoint::new(input.row(), expanded)
 308    }
 309
 310    pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
 311        let chunks = self
 312            .fold_snapshot
 313            .chunks_at(FoldPoint::new(output.row(), 0));
 314
 315        let tab_cursor = TabStopCursor::new(chunks);
 316        let expanded = output.column();
 317        let (collapsed, expanded_char_column, to_next_stop) =
 318            self.collapse_tabs(tab_cursor, expanded, bias);
 319
 320        let result = (
 321            FoldPoint::new(output.row(), collapsed),
 322            expanded_char_column,
 323            to_next_stop,
 324        );
 325
 326        result
 327    }
 328
 329    pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
 330        let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
 331        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
 332        self.to_tab_point(fold_point)
 333    }
 334
 335    pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
 336        let fold_point = self.to_fold_point(point, bias).0;
 337        let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
 338        self.fold_snapshot
 339            .inlay_snapshot
 340            .to_buffer_point(inlay_point)
 341    }
 342
 343    fn expand_tabs<'a, I>(&self, mut cursor: TabStopCursor<'a, I>, column: u32) -> u32
 344    where
 345        I: Iterator<Item = Chunk<'a>>,
 346    {
 347        let tab_size = self.tab_size.get();
 348
 349        let end_column = column.min(self.max_expansion_column);
 350        let mut seek_target = end_column;
 351        let mut tab_count = 0;
 352        let mut expanded_tab_len = 0;
 353
 354        while let Some(tab_stop) = cursor.seek(seek_target) {
 355            let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count;
 356            let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size);
 357            tab_count += 1;
 358            expanded_tab_len += tab_len;
 359
 360            seek_target = end_column - cursor.byte_offset;
 361        }
 362
 363        let left_over_char_bytes = if !cursor.is_char_boundary() {
 364            cursor.bytes_until_next_char().unwrap_or(0) as u32
 365        } else {
 366            0
 367        };
 368
 369        let collapsed_bytes = cursor.byte_offset() + left_over_char_bytes;
 370        let expanded_bytes =
 371            cursor.byte_offset() + expanded_tab_len - tab_count + left_over_char_bytes;
 372
 373        expanded_bytes + column.saturating_sub(collapsed_bytes)
 374    }
 375
 376    fn collapse_tabs<'a, I>(
 377        &self,
 378        mut cursor: TabStopCursor<'a, I>,
 379        column: u32,
 380        bias: Bias,
 381    ) -> (u32, u32, u32)
 382    where
 383        I: Iterator<Item = Chunk<'a>>,
 384    {
 385        let tab_size = self.tab_size.get();
 386        let mut collapsed_column = column;
 387        let mut seek_target = column.min(self.max_expansion_column);
 388        let mut tab_count = 0;
 389        let mut expanded_tab_len = 0;
 390
 391        while let Some(tab_stop) = cursor.seek(seek_target) {
 392            // Calculate how much we want to expand this tab stop (into spaces)
 393            let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count;
 394            let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size);
 395            // Increment tab count
 396            tab_count += 1;
 397            // The count of how many spaces we've added to this line in place of tab bytes
 398            expanded_tab_len += tab_len;
 399
 400            // The count of bytes at this point in the iteration while considering tab_count and previous expansions
 401            let expanded_bytes = tab_stop.byte_offset + expanded_tab_len - tab_count;
 402
 403            // Did we expand past the search target?
 404            if expanded_bytes > column {
 405                let mut expanded_chars = tab_stop.char_offset + expanded_tab_len - tab_count;
 406                // We expanded past the search target, so need to account for the offshoot
 407                expanded_chars -= expanded_bytes - column;
 408                return match bias {
 409                    Bias::Left => (
 410                        cursor.byte_offset() - 1,
 411                        expanded_chars,
 412                        expanded_bytes - column,
 413                    ),
 414                    Bias::Right => (cursor.byte_offset(), expanded_chars, 0),
 415                };
 416            } else {
 417                // otherwise we only want to move the cursor collapse column forward
 418                collapsed_column = collapsed_column - tab_len + 1;
 419                seek_target = (collapsed_column - cursor.byte_offset)
 420                    .min(self.max_expansion_column - cursor.byte_offset);
 421            }
 422        }
 423
 424        let collapsed_bytes = cursor.byte_offset();
 425        let expanded_bytes = cursor.byte_offset() + expanded_tab_len - tab_count;
 426        let expanded_chars = cursor.char_offset() + expanded_tab_len - tab_count;
 427        (
 428            collapsed_bytes + column.saturating_sub(expanded_bytes),
 429            expanded_chars,
 430            0,
 431        )
 432    }
 433}
 434
 435#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 436pub struct TabPoint(pub Point);
 437
 438impl TabPoint {
 439    pub fn new(row: u32, column: u32) -> Self {
 440        Self(Point::new(row, column))
 441    }
 442
 443    pub fn zero() -> Self {
 444        Self::new(0, 0)
 445    }
 446
 447    pub fn row(self) -> u32 {
 448        self.0.row
 449    }
 450
 451    pub fn column(self) -> u32 {
 452        self.0.column
 453    }
 454}
 455
 456impl From<Point> for TabPoint {
 457    fn from(point: Point) -> Self {
 458        Self(point)
 459    }
 460}
 461
 462pub type TabEdit = text::Edit<TabPoint>;
 463
 464#[derive(Clone, Debug, Default, Eq, PartialEq)]
 465pub struct TextSummary {
 466    pub lines: Point,
 467    pub first_line_chars: u32,
 468    pub last_line_chars: u32,
 469    pub longest_row: u32,
 470    pub longest_row_chars: u32,
 471}
 472
 473impl<'a> From<&'a str> for TextSummary {
 474    fn from(text: &'a str) -> Self {
 475        let sum = text::TextSummary::from(text);
 476
 477        TextSummary {
 478            lines: sum.lines,
 479            first_line_chars: sum.first_line_chars,
 480            last_line_chars: sum.last_line_chars,
 481            longest_row: sum.longest_row,
 482            longest_row_chars: sum.longest_row_chars,
 483        }
 484    }
 485}
 486
 487impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
 488    fn add_assign(&mut self, other: &'a Self) {
 489        let joined_chars = self.last_line_chars + other.first_line_chars;
 490        if joined_chars > self.longest_row_chars {
 491            self.longest_row = self.lines.row;
 492            self.longest_row_chars = joined_chars;
 493        }
 494        if other.longest_row_chars > self.longest_row_chars {
 495            self.longest_row = self.lines.row + other.longest_row;
 496            self.longest_row_chars = other.longest_row_chars;
 497        }
 498
 499        if self.lines.row == 0 {
 500            self.first_line_chars += other.first_line_chars;
 501        }
 502
 503        if other.lines.row == 0 {
 504            self.last_line_chars += other.first_line_chars;
 505        } else {
 506            self.last_line_chars = other.last_line_chars;
 507        }
 508
 509        self.lines += &other.lines;
 510    }
 511}
 512
 513// Handles a tab width <= 16
 514const SPACES: &str = "                ";
 515
 516pub struct TabChunks<'a> {
 517    snapshot: &'a TabSnapshot,
 518    fold_chunks: FoldChunks<'a>,
 519    chunk: Chunk<'a>,
 520    column: u32,
 521    max_expansion_column: u32,
 522    output_position: Point,
 523    input_column: u32,
 524    max_output_position: Point,
 525    tab_size: NonZeroU32,
 526    inside_leading_tab: bool,
 527}
 528
 529impl TabChunks<'_> {
 530    pub(crate) fn seek(&mut self, range: Range<TabPoint>) {
 531        let (input_start, expanded_char_column, to_next_stop) =
 532            self.snapshot.to_fold_point(range.start, Bias::Left);
 533        let input_column = input_start.column();
 534        let input_start = input_start.to_offset(&self.snapshot.fold_snapshot);
 535        let input_end = self
 536            .snapshot
 537            .to_fold_point(range.end, Bias::Right)
 538            .0
 539            .to_offset(&self.snapshot.fold_snapshot);
 540        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
 541            range.end.column() - range.start.column()
 542        } else {
 543            to_next_stop
 544        };
 545
 546        self.fold_chunks.seek(input_start..input_end);
 547        self.input_column = input_column;
 548        self.column = expanded_char_column;
 549        self.output_position = range.start.0;
 550        self.max_output_position = range.end.0;
 551        self.chunk = Chunk {
 552            text: &SPACES[0..(to_next_stop as usize)],
 553            is_tab: true,
 554            chars: 1u128.unbounded_shl(to_next_stop) - 1,
 555            ..Default::default()
 556        };
 557        self.inside_leading_tab = to_next_stop > 0;
 558    }
 559}
 560
 561impl<'a> Iterator for TabChunks<'a> {
 562    type Item = Chunk<'a>;
 563
 564    fn next(&mut self) -> Option<Self::Item> {
 565        if self.chunk.text.is_empty() {
 566            if let Some(chunk) = self.fold_chunks.next() {
 567                self.chunk = chunk;
 568                if self.inside_leading_tab {
 569                    self.chunk.text = &self.chunk.text[1..];
 570                    self.inside_leading_tab = false;
 571                    self.input_column += 1;
 572                }
 573            } else {
 574                return None;
 575            }
 576        }
 577
 578        //todo(improve performance by using tab cursor)
 579        for (ix, c) in self.chunk.text.char_indices() {
 580            match c {
 581                '\t' => {
 582                    if ix > 0 {
 583                        let (prefix, suffix) = self.chunk.text.split_at(ix);
 584
 585                        let (chars, tabs) = if ix == 128 {
 586                            let output = (self.chunk.chars, self.chunk.tabs);
 587                            self.chunk.chars = 0;
 588                            self.chunk.tabs = 0;
 589                            output
 590                        } else {
 591                            let mask = (1 << ix) - 1;
 592                            let output = (self.chunk.chars & mask, self.chunk.tabs & mask);
 593                            self.chunk.chars = self.chunk.chars >> ix;
 594                            self.chunk.tabs = self.chunk.tabs >> ix;
 595                            output
 596                        };
 597
 598                        self.chunk.text = suffix;
 599                        return Some(Chunk {
 600                            text: prefix,
 601                            chars,
 602                            tabs,
 603                            ..self.chunk.clone()
 604                        });
 605                    } else {
 606                        self.chunk.text = &self.chunk.text[1..];
 607                        self.chunk.tabs >>= 1;
 608                        self.chunk.chars >>= 1;
 609                        let tab_size = if self.input_column < self.max_expansion_column {
 610                            self.tab_size.get()
 611                        } else {
 612                            1
 613                        };
 614                        let mut len = tab_size - self.column % tab_size;
 615                        let next_output_position = cmp::min(
 616                            self.output_position + Point::new(0, len),
 617                            self.max_output_position,
 618                        );
 619                        len = next_output_position.column - self.output_position.column;
 620                        self.column += len;
 621                        self.input_column += 1;
 622                        self.output_position = next_output_position;
 623                        return Some(Chunk {
 624                            text: &SPACES[..len as usize],
 625                            is_tab: true,
 626                            chars: 1u128.unbounded_shl(len) - 1,
 627                            tabs: 0,
 628                            ..self.chunk.clone()
 629                        });
 630                    }
 631                }
 632                '\n' => {
 633                    self.column = 0;
 634                    self.input_column = 0;
 635                    self.output_position += Point::new(1, 0);
 636                }
 637                _ => {
 638                    self.column += 1;
 639                    if !self.inside_leading_tab {
 640                        self.input_column += c.len_utf8() as u32;
 641                    }
 642                    self.output_position.column += c.len_utf8() as u32;
 643                }
 644            }
 645        }
 646
 647        Some(mem::take(&mut self.chunk))
 648    }
 649}
 650
 651#[cfg(test)]
 652mod tests {
 653    use super::*;
 654    use crate::{
 655        MultiBuffer,
 656        display_map::{
 657            fold_map::{FoldMap, FoldOffset},
 658            inlay_map::InlayMap,
 659        },
 660    };
 661    use rand::{Rng, prelude::StdRng};
 662    use util;
 663
 664    impl TabSnapshot {
 665        fn expected_collapse_tabs(
 666            &self,
 667            chars: impl Iterator<Item = char>,
 668            column: u32,
 669            bias: Bias,
 670        ) -> (u32, u32, u32) {
 671            let tab_size = self.tab_size.get();
 672
 673            let mut expanded_bytes = 0;
 674            let mut expanded_chars = 0;
 675            let mut collapsed_bytes = 0;
 676            for c in chars {
 677                if expanded_bytes >= column {
 678                    break;
 679                }
 680                if collapsed_bytes >= self.max_expansion_column {
 681                    break;
 682                }
 683
 684                if c == '\t' {
 685                    let tab_len = tab_size - (expanded_chars % tab_size);
 686                    expanded_chars += tab_len;
 687                    expanded_bytes += tab_len;
 688                    if expanded_bytes > column {
 689                        expanded_chars -= expanded_bytes - column;
 690                        return match bias {
 691                            Bias::Left => {
 692                                (collapsed_bytes, expanded_chars, expanded_bytes - column)
 693                            }
 694                            Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
 695                        };
 696                    }
 697                } else {
 698                    expanded_chars += 1;
 699                    expanded_bytes += c.len_utf8() as u32;
 700                }
 701
 702                if expanded_bytes > column && matches!(bias, Bias::Left) {
 703                    expanded_chars -= 1;
 704                    break;
 705                }
 706
 707                collapsed_bytes += c.len_utf8() as u32;
 708            }
 709
 710            (
 711                collapsed_bytes + column.saturating_sub(expanded_bytes),
 712                expanded_chars,
 713                0,
 714            )
 715        }
 716
 717        pub fn expected_to_tab_point(&self, input: FoldPoint) -> TabPoint {
 718            let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
 719            let expanded = self.expected_expand_tabs(chars, input.column());
 720            TabPoint::new(input.row(), expanded)
 721        }
 722
 723        fn expected_expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
 724            let tab_size = self.tab_size.get();
 725
 726            let mut expanded_chars = 0;
 727            let mut expanded_bytes = 0;
 728            let mut collapsed_bytes = 0;
 729            let end_column = column.min(self.max_expansion_column);
 730            for c in chars {
 731                if collapsed_bytes >= end_column {
 732                    break;
 733                }
 734                if c == '\t' {
 735                    let tab_len = tab_size - expanded_chars % tab_size;
 736                    expanded_bytes += tab_len;
 737                    expanded_chars += tab_len;
 738                } else {
 739                    expanded_bytes += c.len_utf8() as u32;
 740                    expanded_chars += 1;
 741                }
 742                collapsed_bytes += c.len_utf8() as u32;
 743            }
 744
 745            expanded_bytes + column.saturating_sub(collapsed_bytes)
 746        }
 747
 748        fn expected_to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
 749            let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
 750            let expanded = output.column();
 751            let (collapsed, expanded_char_column, to_next_stop) =
 752                self.expected_collapse_tabs(chars, expanded, bias);
 753            (
 754                FoldPoint::new(output.row(), collapsed),
 755                expanded_char_column,
 756                to_next_stop,
 757            )
 758        }
 759    }
 760
 761    #[gpui::test]
 762    fn test_expand_tabs(cx: &mut gpui::App) {
 763        let test_values = [
 764            ("κg🏀 f\nwo🏀❌by🍐❎β🍗c\tβ❎ \ncλ🎉", 17),
 765            (" \twςe", 4),
 766            ("", 1),
 767            ("i❎\t", 3),
 768        ];
 769        let buffer = MultiBuffer::build_simple("", cx);
 770        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 771        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
 772        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
 773        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 774
 775        for (text, column) in test_values {
 776            let mut tabs = 0u128;
 777            let mut chars = 0u128;
 778            for (idx, c) in text.char_indices() {
 779                if c == '\t' {
 780                    tabs |= 1 << idx;
 781                }
 782                chars |= 1 << idx;
 783            }
 784
 785            let chunks = [Chunk {
 786                text,
 787                tabs,
 788                chars,
 789                ..Default::default()
 790            }];
 791
 792            let cursor = TabStopCursor::new(chunks);
 793
 794            assert_eq!(
 795                tab_snapshot.expected_expand_tabs(text.chars(), column),
 796                tab_snapshot.expand_tabs(cursor, column)
 797            );
 798        }
 799    }
 800
 801    #[gpui::test]
 802    fn test_collapse_tabs(cx: &mut gpui::App) {
 803        let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
 804
 805        let buffer = MultiBuffer::build_simple(input, cx);
 806        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 807        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
 808        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
 809        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 810
 811        for (ix, _) in input.char_indices() {
 812            let range = TabPoint::new(0, ix as u32)..tab_snapshot.max_point();
 813
 814            assert_eq!(
 815                tab_snapshot.expected_to_fold_point(range.start, Bias::Left),
 816                tab_snapshot.to_fold_point(range.start, Bias::Left),
 817                "Failed with tab_point at column {ix}"
 818            );
 819            assert_eq!(
 820                tab_snapshot.expected_to_fold_point(range.start, Bias::Right),
 821                tab_snapshot.to_fold_point(range.start, Bias::Right),
 822                "Failed with tab_point at column {ix}"
 823            );
 824
 825            assert_eq!(
 826                tab_snapshot.expected_to_fold_point(range.end, Bias::Left),
 827                tab_snapshot.to_fold_point(range.end, Bias::Left),
 828                "Failed with tab_point at column {ix}"
 829            );
 830            assert_eq!(
 831                tab_snapshot.expected_to_fold_point(range.end, Bias::Right),
 832                tab_snapshot.to_fold_point(range.end, Bias::Right),
 833                "Failed with tab_point at column {ix}"
 834            );
 835        }
 836    }
 837
 838    #[gpui::test]
 839    fn test_to_fold_point_panic_reproduction(cx: &mut gpui::App) {
 840        // This test reproduces a specific panic where to_fold_point returns incorrect results
 841        let _text = "use macro_rules_attribute::apply;\nuse serde_json::Value;\nuse smol::{\n    io::AsyncReadExt,\n    process::{Command, Stdio},\n};\nuse smol_macros::main;\nuse std::io;\n\nfn test_random() {\n    // Generate a random value\n    let random_value = std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .unwrap()\n        .as_secs()\n        % 100;\n\n    // Create some complex nested data structures\n    let mut vector = Vec::new();\n    for i in 0..random_value {\n        vector.push(i);\n    }\n    ";
 842
 843        let text = "γ\tw⭐\n🍐🍗 \t";
 844        let buffer = MultiBuffer::build_simple(text, cx);
 845        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 846        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
 847        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
 848        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 849
 850        // This should panic with the expected vs actual mismatch
 851        let tab_point = TabPoint::new(0, 9);
 852        let result = tab_snapshot.to_fold_point(tab_point, Bias::Left);
 853        let expected = tab_snapshot.expected_to_fold_point(tab_point, Bias::Left);
 854
 855        assert_eq!(result, expected);
 856    }
 857
 858    #[gpui::test(iterations = 100)]
 859    fn test_collapse_tabs_random(cx: &mut gpui::App, mut rng: StdRng) {
 860        // Generate random input string with up to 200 characters including tabs
 861        // to stay within the MAX_EXPANSION_COLUMN limit of 256
 862        let len = rng.random_range(0..=2048);
 863        let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
 864        let mut input = String::with_capacity(len);
 865
 866        for _ in 0..len {
 867            if rng.random_bool(0.1) {
 868                // 10% chance of inserting a tab
 869                input.push('\t');
 870            } else {
 871                // 90% chance of inserting a random ASCII character (excluding tab, newline, carriage return)
 872                let ch = loop {
 873                    let ascii_code = rng.random_range(32..=126); // printable ASCII range
 874                    let ch = ascii_code as u8 as char;
 875                    if ch != '\t' {
 876                        break ch;
 877                    }
 878                };
 879                input.push(ch);
 880            }
 881        }
 882
 883        let buffer = MultiBuffer::build_simple(&input, cx);
 884        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 885        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
 886        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
 887        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 888        tab_snapshot.max_expansion_column = rng.random_range(0..323);
 889        tab_snapshot.tab_size = tab_size;
 890
 891        for (ix, _) in input.char_indices() {
 892            let range = TabPoint::new(0, ix as u32)..tab_snapshot.max_point();
 893
 894            assert_eq!(
 895                tab_snapshot.expected_to_fold_point(range.start, Bias::Left),
 896                tab_snapshot.to_fold_point(range.start, Bias::Left),
 897                "Failed with input: {}, with idx: {ix}",
 898                input
 899            );
 900            assert_eq!(
 901                tab_snapshot.expected_to_fold_point(range.start, Bias::Right),
 902                tab_snapshot.to_fold_point(range.start, Bias::Right),
 903                "Failed with input: {}, with idx: {ix}",
 904                input
 905            );
 906
 907            assert_eq!(
 908                tab_snapshot.expected_to_fold_point(range.end, Bias::Left),
 909                tab_snapshot.to_fold_point(range.end, Bias::Left),
 910                "Failed with input: {}, with idx: {ix}",
 911                input
 912            );
 913            assert_eq!(
 914                tab_snapshot.expected_to_fold_point(range.end, Bias::Right),
 915                tab_snapshot.to_fold_point(range.end, Bias::Right),
 916                "Failed with input: {}, with idx: {ix}",
 917                input
 918            );
 919        }
 920    }
 921
 922    #[gpui::test]
 923    fn test_long_lines(cx: &mut gpui::App) {
 924        let max_expansion_column = 12;
 925        let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
 926        let output = "A   BC  DEF G   HI J K L M";
 927
 928        let buffer = MultiBuffer::build_simple(input, cx);
 929        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 930        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
 931        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
 932        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 933
 934        tab_snapshot.max_expansion_column = max_expansion_column;
 935        assert_eq!(tab_snapshot.text(), output);
 936
 937        for (ix, c) in input.char_indices() {
 938            assert_eq!(
 939                tab_snapshot
 940                    .chunks(
 941                        TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
 942                        false,
 943                        Highlights::default(),
 944                    )
 945                    .map(|c| c.text)
 946                    .collect::<String>(),
 947                &output[ix..],
 948                "text from index {ix}"
 949            );
 950
 951            if c != '\t' {
 952                let input_point = Point::new(0, ix as u32);
 953                let output_point = Point::new(0, output.find(c).unwrap() as u32);
 954                assert_eq!(
 955                    tab_snapshot.to_tab_point(FoldPoint(input_point)),
 956                    TabPoint(output_point),
 957                    "to_tab_point({input_point:?})"
 958                );
 959                assert_eq!(
 960                    tab_snapshot
 961                        .to_fold_point(TabPoint(output_point), Bias::Left)
 962                        .0,
 963                    FoldPoint(input_point),
 964                    "to_fold_point({output_point:?})"
 965                );
 966            }
 967        }
 968    }
 969
 970    #[gpui::test]
 971    fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::App) {
 972        let max_expansion_column = 8;
 973        let input = "abcdefg⋯hij";
 974
 975        let buffer = MultiBuffer::build_simple(input, cx);
 976        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 977        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
 978        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
 979        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 980
 981        tab_snapshot.max_expansion_column = max_expansion_column;
 982        assert_eq!(tab_snapshot.text(), input);
 983    }
 984
 985    #[gpui::test]
 986    fn test_marking_tabs(cx: &mut gpui::App) {
 987        let input = "\t \thello";
 988
 989        let buffer = MultiBuffer::build_simple(input, cx);
 990        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 991        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
 992        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
 993        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 994
 995        assert_eq!(
 996            chunks(&tab_snapshot, TabPoint::zero()),
 997            vec![
 998                ("    ".to_string(), true),
 999                (" ".to_string(), false),
1000                ("   ".to_string(), true),
1001                ("hello".to_string(), false),
1002            ]
1003        );
1004        assert_eq!(
1005            chunks(&tab_snapshot, TabPoint::new(0, 2)),
1006            vec![
1007                ("  ".to_string(), true),
1008                (" ".to_string(), false),
1009                ("   ".to_string(), true),
1010                ("hello".to_string(), false),
1011            ]
1012        );
1013
1014        fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
1015            let mut chunks = Vec::new();
1016            let mut was_tab = false;
1017            let mut text = String::new();
1018            for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
1019            {
1020                if chunk.is_tab != was_tab {
1021                    if !text.is_empty() {
1022                        chunks.push((mem::take(&mut text), was_tab));
1023                    }
1024                    was_tab = chunk.is_tab;
1025                }
1026                text.push_str(chunk.text);
1027            }
1028
1029            if !text.is_empty() {
1030                chunks.push((text, was_tab));
1031            }
1032            chunks
1033        }
1034    }
1035
1036    #[gpui::test(iterations = 100)]
1037    fn test_random_tabs(cx: &mut gpui::App, mut rng: StdRng) {
1038        let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
1039        let len = rng.random_range(0..30);
1040        let buffer = if rng.random() {
1041            let text = util::RandomCharIter::new(&mut rng)
1042                .take(len)
1043                .collect::<String>();
1044            MultiBuffer::build_simple(&text, cx)
1045        } else {
1046            MultiBuffer::build_random(&mut rng, cx)
1047        };
1048        let buffer_snapshot = buffer.read(cx).snapshot(cx);
1049        log::info!("Buffer text: {:?}", buffer_snapshot.text());
1050
1051        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
1052        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
1053        let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
1054        fold_map.randomly_mutate(&mut rng);
1055        let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
1056        log::info!("FoldMap text: {:?}", fold_snapshot.text());
1057        let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
1058        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
1059
1060        let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
1061        let tabs_snapshot = tab_map.set_max_expansion_column(32);
1062
1063        let text = text::Rope::from(tabs_snapshot.text().as_str());
1064        log::info!(
1065            "TabMap text (tab size: {}): {:?}",
1066            tab_size,
1067            tabs_snapshot.text(),
1068        );
1069
1070        for _ in 0..5 {
1071            let end_row = rng.random_range(0..=text.max_point().row);
1072            let end_column = rng.random_range(0..=text.line_len(end_row));
1073            let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
1074            let start_row = rng.random_range(0..=text.max_point().row);
1075            let start_column = rng.random_range(0..=text.line_len(start_row));
1076            let mut start =
1077                TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
1078            if start > end {
1079                mem::swap(&mut start, &mut end);
1080            }
1081
1082            let expected_text = text
1083                .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
1084                .collect::<String>();
1085            let expected_summary = TextSummary::from(expected_text.as_str());
1086            assert_eq!(
1087                tabs_snapshot
1088                    .chunks(start..end, false, Highlights::default())
1089                    .map(|c| c.text)
1090                    .collect::<String>(),
1091                expected_text,
1092                "chunks({:?}..{:?})",
1093                start,
1094                end
1095            );
1096
1097            let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
1098            if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
1099                actual_summary.longest_row = expected_summary.longest_row;
1100                actual_summary.longest_row_chars = expected_summary.longest_row_chars;
1101            }
1102            assert_eq!(actual_summary, expected_summary);
1103        }
1104
1105        for row in 0..=text.max_point().row {
1106            assert_eq!(
1107                tabs_snapshot.line_len(row),
1108                text.line_len(row),
1109                "line_len({row})"
1110            );
1111        }
1112    }
1113
1114    #[gpui::test(iterations = 100)]
1115    fn test_to_tab_point_random(cx: &mut gpui::App, mut rng: StdRng) {
1116        let tab_size = NonZeroU32::new(rng.random_range(1..=16)).unwrap();
1117        let len = rng.random_range(0..=2000);
1118
1119        // Generate random text using RandomCharIter
1120        let text = util::RandomCharIter::new(&mut rng)
1121            .take(len)
1122            .collect::<String>();
1123
1124        // Create buffer and tab map
1125        let buffer = MultiBuffer::build_simple(&text, cx);
1126        let buffer_snapshot = buffer.read(cx).snapshot(cx);
1127        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
1128        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
1129        let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
1130
1131        let mut next_inlay_id = 0;
1132        let (inlay_snapshot, inlay_edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
1133        let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
1134        let max_fold_point = fold_snapshot.max_point();
1135        let (mut tab_snapshot, _) = tab_map.sync(fold_snapshot.clone(), fold_edits, tab_size);
1136
1137        // Test random fold points
1138        for _ in 0..50 {
1139            tab_snapshot.max_expansion_column = rng.random_range(0..=256);
1140            // Generate random fold point
1141            let row = rng.random_range(0..=max_fold_point.row());
1142            let max_column = if row < max_fold_point.row() {
1143                fold_snapshot.line_len(row)
1144            } else {
1145                max_fold_point.column()
1146            };
1147            let column = rng.random_range(0..=max_column + 10);
1148            let fold_point = FoldPoint::new(row, column);
1149
1150            let actual = tab_snapshot.to_tab_point(fold_point);
1151            let expected = tab_snapshot.expected_to_tab_point(fold_point);
1152
1153            assert_eq!(
1154                actual, expected,
1155                "to_tab_point mismatch for fold_point {:?} in text {:?}",
1156                fold_point, text
1157            );
1158        }
1159    }
1160
1161    #[gpui::test]
1162    fn test_tab_stop_cursor_utf8(cx: &mut gpui::App) {
1163        let text = "\tfoo\tbarbarbar\t\tbaz\n";
1164        let buffer = MultiBuffer::build_simple(text, cx);
1165        let buffer_snapshot = buffer.read(cx).snapshot(cx);
1166        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
1167        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
1168        let chunks = fold_snapshot.chunks(
1169            FoldOffset(0)..fold_snapshot.len(),
1170            false,
1171            Default::default(),
1172        );
1173        let mut cursor = TabStopCursor::new(chunks);
1174        assert!(cursor.seek(0).is_none());
1175        let mut tab_stops = Vec::new();
1176
1177        let mut all_tab_stops = Vec::new();
1178        let mut byte_offset = 0;
1179        for (offset, ch) in buffer.read(cx).snapshot(cx).text().char_indices() {
1180            byte_offset += ch.len_utf8() as u32;
1181
1182            if ch == '\t' {
1183                all_tab_stops.push(TabStop {
1184                    byte_offset,
1185                    char_offset: offset as u32 + 1,
1186                });
1187            }
1188        }
1189
1190        while let Some(tab_stop) = cursor.seek(u32::MAX) {
1191            tab_stops.push(tab_stop);
1192        }
1193        pretty_assertions::assert_eq!(tab_stops.as_slice(), all_tab_stops.as_slice(),);
1194
1195        assert_eq!(cursor.byte_offset(), byte_offset);
1196    }
1197
1198    #[gpui::test]
1199    fn test_tab_stop_with_end_range_utf8(cx: &mut gpui::App) {
1200        let input = "A\tBC\t"; // DEF\tG\tHI\tJ\tK\tL\tM
1201
1202        let buffer = MultiBuffer::build_simple(input, cx);
1203        let buffer_snapshot = buffer.read(cx).snapshot(cx);
1204        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
1205        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
1206
1207        let chunks = fold_snapshot.chunks_at(FoldPoint::new(0, 0));
1208        let mut cursor = TabStopCursor::new(chunks);
1209
1210        let mut actual_tab_stops = Vec::new();
1211
1212        let mut expected_tab_stops = Vec::new();
1213        let mut byte_offset = 0;
1214        for (offset, ch) in buffer.read(cx).snapshot(cx).text().char_indices() {
1215            byte_offset += ch.len_utf8() as u32;
1216
1217            if ch == '\t' {
1218                expected_tab_stops.push(TabStop {
1219                    byte_offset,
1220                    char_offset: offset as u32 + 1,
1221                });
1222            }
1223        }
1224
1225        while let Some(tab_stop) = cursor.seek(u32::MAX) {
1226            actual_tab_stops.push(tab_stop);
1227        }
1228        pretty_assertions::assert_eq!(actual_tab_stops.as_slice(), expected_tab_stops.as_slice(),);
1229
1230        assert_eq!(cursor.byte_offset(), byte_offset);
1231    }
1232
1233    #[gpui::test(iterations = 100)]
1234    fn test_tab_stop_cursor_random_utf8(cx: &mut gpui::App, mut rng: StdRng) {
1235        // Generate random input string with up to 512 characters including tabs
1236        let len = rng.random_range(0..=2048);
1237        let mut input = String::with_capacity(len);
1238
1239        let mut skip_tabs = rng.random_bool(0.10);
1240        for idx in 0..len {
1241            if idx % 128 == 0 {
1242                skip_tabs = rng.random_bool(0.10);
1243            }
1244
1245            if rng.random_bool(0.15) && !skip_tabs {
1246                input.push('\t');
1247            } else {
1248                let ch = loop {
1249                    let ascii_code = rng.random_range(32..=126); // printable ASCII range
1250                    let ch = ascii_code as u8 as char;
1251                    if ch != '\t' {
1252                        break ch;
1253                    }
1254                };
1255                input.push(ch);
1256            }
1257        }
1258
1259        // Build the buffer and create cursor
1260        let buffer = MultiBuffer::build_simple(&input, cx);
1261        let buffer_snapshot = buffer.read(cx).snapshot(cx);
1262        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
1263        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
1264
1265        // First, collect all expected tab positions
1266        let mut all_tab_stops = Vec::new();
1267        let mut byte_offset = 1;
1268        let mut char_offset = 1;
1269        for ch in buffer_snapshot.text().chars() {
1270            if ch == '\t' {
1271                all_tab_stops.push(TabStop {
1272                    byte_offset,
1273                    char_offset,
1274                });
1275            }
1276            byte_offset += ch.len_utf8() as u32;
1277            char_offset += 1;
1278        }
1279
1280        // Test with various distances
1281        let distances = vec![1, 5, 10, 50, 100, u32::MAX];
1282        // let distances = vec![150];
1283
1284        for distance in distances {
1285            let chunks = fold_snapshot.chunks_at(FoldPoint::new(0, 0));
1286            let mut cursor = TabStopCursor::new(chunks);
1287
1288            let mut found_tab_stops = Vec::new();
1289            let mut position = distance;
1290            while let Some(tab_stop) = cursor.seek(position) {
1291                found_tab_stops.push(tab_stop);
1292                position = distance - tab_stop.byte_offset;
1293            }
1294
1295            let expected_found_tab_stops: Vec<_> = all_tab_stops
1296                .iter()
1297                .take_while(|tab_stop| tab_stop.byte_offset <= distance)
1298                .cloned()
1299                .collect();
1300
1301            pretty_assertions::assert_eq!(
1302                found_tab_stops,
1303                expected_found_tab_stops,
1304                "TabStopCursor output mismatch for distance {}. Input: {:?}",
1305                distance,
1306                input
1307            );
1308
1309            let final_position = cursor.byte_offset();
1310            if !found_tab_stops.is_empty() {
1311                let last_tab_stop = found_tab_stops.last().unwrap();
1312                assert!(
1313                    final_position >= last_tab_stop.byte_offset,
1314                    "Cursor final position {} is before last tab stop {}. Input: {:?}",
1315                    final_position,
1316                    last_tab_stop.byte_offset,
1317                    input
1318                );
1319            }
1320        }
1321    }
1322
1323    #[gpui::test]
1324    fn test_tab_stop_cursor_utf16(cx: &mut gpui::App) {
1325        let text = "\r\t😁foo\tb😀arbar🤯bar\t\tbaz\n";
1326        let buffer = MultiBuffer::build_simple(text, cx);
1327        let buffer_snapshot = buffer.read(cx).snapshot(cx);
1328        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
1329        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
1330        let chunks = fold_snapshot.chunks(
1331            FoldOffset(0)..fold_snapshot.len(),
1332            false,
1333            Default::default(),
1334        );
1335        let mut cursor = TabStopCursor::new(chunks);
1336        assert!(cursor.seek(0).is_none());
1337
1338        let mut expected_tab_stops = Vec::new();
1339        let mut byte_offset = 0;
1340        for (i, ch) in fold_snapshot.chars_at(FoldPoint::new(0, 0)).enumerate() {
1341            byte_offset += ch.len_utf8() as u32;
1342
1343            if ch == '\t' {
1344                expected_tab_stops.push(TabStop {
1345                    byte_offset,
1346                    char_offset: i as u32 + 1,
1347                });
1348            }
1349        }
1350
1351        let mut actual_tab_stops = Vec::new();
1352        while let Some(tab_stop) = cursor.seek(u32::MAX) {
1353            actual_tab_stops.push(tab_stop);
1354        }
1355
1356        pretty_assertions::assert_eq!(actual_tab_stops.as_slice(), expected_tab_stops.as_slice(),);
1357
1358        assert_eq!(cursor.byte_offset(), byte_offset);
1359    }
1360
1361    #[gpui::test(iterations = 100)]
1362    fn test_tab_stop_cursor_random_utf16(cx: &mut gpui::App, mut rng: StdRng) {
1363        // Generate random input string with up to 512 characters including tabs
1364        let len = rng.random_range(0..=2048);
1365        let input = util::RandomCharIter::new(&mut rng)
1366            .take(len)
1367            .collect::<String>();
1368
1369        // Build the buffer and create cursor
1370        let buffer = MultiBuffer::build_simple(&input, cx);
1371        let buffer_snapshot = buffer.read(cx).snapshot(cx);
1372        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
1373        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
1374
1375        // First, collect all expected tab positions
1376        let mut all_tab_stops = Vec::new();
1377        let mut byte_offset = 0;
1378        for (i, ch) in buffer_snapshot.text().chars().enumerate() {
1379            byte_offset += ch.len_utf8() as u32;
1380            if ch == '\t' {
1381                all_tab_stops.push(TabStop {
1382                    byte_offset,
1383                    char_offset: i as u32 + 1,
1384                });
1385            }
1386        }
1387
1388        // Test with various distances
1389        // let distances = vec![1, 5, 10, 50, 100, u32::MAX];
1390        let distances = vec![150];
1391
1392        for distance in distances {
1393            let chunks = fold_snapshot.chunks_at(FoldPoint::new(0, 0));
1394            let mut cursor = TabStopCursor::new(chunks);
1395
1396            let mut found_tab_stops = Vec::new();
1397            let mut position = distance;
1398            while let Some(tab_stop) = cursor.seek(position) {
1399                found_tab_stops.push(tab_stop);
1400                position = distance - tab_stop.byte_offset;
1401            }
1402
1403            let expected_found_tab_stops: Vec<_> = all_tab_stops
1404                .iter()
1405                .take_while(|tab_stop| tab_stop.byte_offset <= distance)
1406                .cloned()
1407                .collect();
1408
1409            pretty_assertions::assert_eq!(
1410                found_tab_stops,
1411                expected_found_tab_stops,
1412                "TabStopCursor output mismatch for distance {}. Input: {:?}",
1413                distance,
1414                input
1415            );
1416
1417            let final_position = cursor.byte_offset();
1418            if !found_tab_stops.is_empty() {
1419                let last_tab_stop = found_tab_stops.last().unwrap();
1420                assert!(
1421                    final_position >= last_tab_stop.byte_offset,
1422                    "Cursor final position {} is before last tab stop {}. Input: {:?}",
1423                    final_position,
1424                    last_tab_stop.byte_offset,
1425                    input
1426                );
1427            }
1428        }
1429    }
1430}
1431
1432struct TabStopCursor<'a, I>
1433where
1434    I: Iterator<Item = Chunk<'a>>,
1435{
1436    chunks: I,
1437    byte_offset: u32,
1438    char_offset: u32,
1439    /// Chunk
1440    /// last tab position iterated through
1441    current_chunk: Option<(Chunk<'a>, u32)>,
1442}
1443
1444impl<'a, I> TabStopCursor<'a, I>
1445where
1446    I: Iterator<Item = Chunk<'a>>,
1447{
1448    fn new(chunks: impl IntoIterator<Item = Chunk<'a>, IntoIter = I>) -> Self {
1449        Self {
1450            chunks: chunks.into_iter(),
1451            byte_offset: 0,
1452            char_offset: 0,
1453            current_chunk: None,
1454        }
1455    }
1456
1457    fn bytes_until_next_char(&self) -> Option<usize> {
1458        self.current_chunk.as_ref().and_then(|(chunk, idx)| {
1459            let mut idx = *idx;
1460            let mut diff = 0;
1461            while idx > 0 && chunk.chars & (1 << idx) == 0 {
1462                idx -= 1;
1463                diff += 1;
1464            }
1465
1466            if chunk.chars & (1 << idx) != 0 {
1467                Some(
1468                    (chunk.text[idx as usize..].chars().next()?)
1469                        .len_utf8()
1470                        .saturating_sub(diff),
1471                )
1472            } else {
1473                None
1474            }
1475        })
1476    }
1477
1478    fn is_char_boundary(&self) -> bool {
1479        self.current_chunk
1480            .as_ref()
1481            .is_some_and(|(chunk, idx)| (chunk.chars & (1 << *idx.min(&127))) != 0)
1482    }
1483
1484    /// distance: length to move forward while searching for the next tab stop
1485    fn seek(&mut self, distance: u32) -> Option<TabStop> {
1486        if distance == 0 {
1487            return None;
1488        }
1489
1490        let mut distance_traversed = 0;
1491
1492        while let Some((mut chunk, chunk_position)) = self
1493            .current_chunk
1494            .take()
1495            .or_else(|| self.chunks.next().zip(Some(0)))
1496        {
1497            if chunk.tabs == 0 {
1498                let chunk_distance = chunk.text.len() as u32 - chunk_position;
1499                if chunk_distance + distance_traversed >= distance {
1500                    let overshoot = distance_traversed.abs_diff(distance);
1501
1502                    self.byte_offset += overshoot;
1503                    self.char_offset += get_char_offset(
1504                        chunk_position..(chunk_position + overshoot).saturating_sub(1).min(127),
1505                        chunk.chars,
1506                    );
1507
1508                    self.current_chunk = Some((chunk, chunk_position + overshoot));
1509
1510                    return None;
1511                }
1512
1513                self.byte_offset += chunk_distance;
1514                self.char_offset += get_char_offset(
1515                    chunk_position..(chunk_position + chunk_distance).saturating_sub(1).min(127),
1516                    chunk.chars,
1517                );
1518                distance_traversed += chunk_distance;
1519                continue;
1520            }
1521            let tab_position = chunk.tabs.trailing_zeros() + 1;
1522
1523            if distance_traversed + tab_position - chunk_position > distance {
1524                let cursor_position = distance_traversed.abs_diff(distance);
1525
1526                self.char_offset += get_char_offset(
1527                    chunk_position..(chunk_position + cursor_position - 1),
1528                    chunk.chars,
1529                );
1530                self.current_chunk = Some((chunk, cursor_position + chunk_position));
1531                self.byte_offset += cursor_position;
1532
1533                return None;
1534            }
1535
1536            self.byte_offset += tab_position - chunk_position;
1537            self.char_offset += get_char_offset(chunk_position..(tab_position - 1), chunk.chars);
1538
1539            let tabstop = TabStop {
1540                char_offset: self.char_offset,
1541                byte_offset: self.byte_offset,
1542            };
1543
1544            chunk.tabs = (chunk.tabs - 1) & chunk.tabs;
1545
1546            if tab_position as usize != chunk.text.len() {
1547                self.current_chunk = Some((chunk, tab_position));
1548            }
1549
1550            return Some(tabstop);
1551        }
1552
1553        None
1554    }
1555
1556    fn byte_offset(&self) -> u32 {
1557        self.byte_offset
1558    }
1559
1560    fn char_offset(&self) -> u32 {
1561        self.char_offset
1562    }
1563}
1564
1565#[inline(always)]
1566fn get_char_offset(range: Range<u32>, bit_map: u128) -> u32 {
1567    // This edge case can happen when we're at chunk position 128
1568
1569    if range.start == range.end {
1570        return if (1u128 << range.start) & bit_map == 0 {
1571            0
1572        } else {
1573            1
1574        };
1575    }
1576    let end_shift: u128 = 127u128 - range.end.min(127) as u128;
1577    let mut bit_mask = (u128::MAX >> range.start) << range.start;
1578    bit_mask = (bit_mask << end_shift) >> end_shift;
1579    let bit_map = bit_map & bit_mask;
1580
1581    bit_map.count_ones()
1582}
1583
1584#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1585struct TabStop {
1586    char_offset: u32,
1587    byte_offset: u32,
1588}