Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:34:01.226Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

tab_map.rs

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