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