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