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