Skip to repository content1754 lines · 65.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:34:01.226Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
tab_map.rs
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