1use super::{
2 Highlights,
3 fold_map::{self, Chunk, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
4};
5use language::Point;
6use multi_buffer::MultiBufferSnapshot;
7use std::{cmp, mem, num::NonZeroU32, ops::Range};
8use sum_tree::Bias;
9
10const MAX_EXPANSION_COLUMN: u32 = 256;
11
12/// Keeps track of hard tabs in a text buffer.
13///
14/// See the [`display_map` module documentation](crate::display_map) for more information.
15pub struct TabMap(TabSnapshot);
16
17impl TabMap {
18 pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
19 let snapshot = TabSnapshot {
20 fold_snapshot,
21 tab_size,
22 max_expansion_column: MAX_EXPANSION_COLUMN,
23 version: 0,
24 };
25 (Self(snapshot.clone()), snapshot)
26 }
27
28 #[cfg(test)]
29 pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
30 self.0.max_expansion_column = column;
31 self.0.clone()
32 }
33
34 pub fn sync(
35 &mut self,
36 fold_snapshot: FoldSnapshot,
37 mut fold_edits: Vec<FoldEdit>,
38 tab_size: NonZeroU32,
39 ) -> (TabSnapshot, Vec<TabEdit>) {
40 let old_snapshot = &mut self.0;
41 let mut new_snapshot = TabSnapshot {
42 fold_snapshot,
43 tab_size,
44 max_expansion_column: old_snapshot.max_expansion_column,
45 version: old_snapshot.version,
46 };
47
48 if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
49 new_snapshot.version += 1;
50 }
51
52 let mut tab_edits = Vec::with_capacity(fold_edits.len());
53
54 if old_snapshot.tab_size == new_snapshot.tab_size {
55 // Expand each edit to include the next tab on the same line as the edit,
56 // and any subsequent tabs on that line that moved across the tab expansion
57 // boundary.
58 for fold_edit in &mut fold_edits {
59 let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
60 let old_end_row_successor_offset = cmp::min(
61 FoldPoint::new(old_end.row() + 1, 0),
62 old_snapshot.fold_snapshot.max_point(),
63 )
64 .to_offset(&old_snapshot.fold_snapshot);
65 let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
66
67 let mut offset_from_edit = 0;
68 let mut first_tab_offset = None;
69 let mut last_tab_with_changed_expansion_offset = None;
70 'outer: for chunk in old_snapshot.fold_snapshot.chunks(
71 fold_edit.old.end..old_end_row_successor_offset,
72 false,
73 Highlights::default(),
74 ) {
75 for (ix, _) in chunk.text.match_indices('\t') {
76 let offset_from_edit = offset_from_edit + (ix as u32);
77 if first_tab_offset.is_none() {
78 first_tab_offset = Some(offset_from_edit);
79 }
80
81 let old_column = old_end.column() + offset_from_edit;
82 let new_column = new_end.column() + offset_from_edit;
83 let was_expanded = old_column < old_snapshot.max_expansion_column;
84 let is_expanded = new_column < new_snapshot.max_expansion_column;
85 if was_expanded != is_expanded {
86 last_tab_with_changed_expansion_offset = Some(offset_from_edit);
87 } else if !was_expanded && !is_expanded {
88 break 'outer;
89 }
90 }
91
92 offset_from_edit += chunk.text.len() as u32;
93 if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
94 && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
95 {
96 break;
97 }
98 }
99
100 if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
101 fold_edit.old.end.0 += offset as usize + 1;
102 fold_edit.new.end.0 += offset as usize + 1;
103 }
104 }
105
106 let _old_alloc_ptr = fold_edits.as_ptr();
107 // Combine any edits that overlap due to the expansion.
108 let mut fold_edits = fold_edits.into_iter();
109 let fold_edits = if let Some(mut first_edit) = fold_edits.next() {
110 // This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
111 #[allow(clippy::filter_map_identity)]
112 let mut v: Vec<_> = fold_edits
113 .scan(&mut first_edit, |state, edit| {
114 if state.old.end >= edit.old.start {
115 state.old.end = edit.old.end;
116 state.new.end = edit.new.end;
117 Some(None) // Skip this edit, it's merged
118 } else {
119 let new_state = edit;
120 let result = Some(Some(state.clone())); // Yield the previous edit
121 **state = new_state;
122 result
123 }
124 })
125 .filter_map(|x| x)
126 .collect();
127 v.push(first_edit);
128 debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Fold edits were reallocated");
129 v
130 } else {
131 vec![]
132 };
133
134 for fold_edit in fold_edits {
135 let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
136 let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
137 let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
138 let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
139 tab_edits.push(TabEdit {
140 old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
141 new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
142 });
143 }
144 } else {
145 new_snapshot.version += 1;
146 tab_edits.push(TabEdit {
147 old: TabPoint::zero()..old_snapshot.max_point(),
148 new: TabPoint::zero()..new_snapshot.max_point(),
149 });
150 }
151
152 *old_snapshot = new_snapshot;
153 (old_snapshot.clone(), tab_edits)
154 }
155}
156
157#[derive(Clone)]
158pub struct TabSnapshot {
159 pub fold_snapshot: FoldSnapshot,
160 pub tab_size: NonZeroU32,
161 pub max_expansion_column: u32,
162 pub version: usize,
163}
164
165impl TabSnapshot {
166 pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
167 &self.fold_snapshot.inlay_snapshot.buffer
168 }
169
170 pub fn line_len(&self, row: u32) -> u32 {
171 let max_point = self.max_point();
172 if row < max_point.row() {
173 self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
174 .0
175 .column
176 } else {
177 max_point.column()
178 }
179 }
180
181 pub fn text_summary(&self) -> TextSummary {
182 self.text_summary_for_range(TabPoint::zero()..self.max_point())
183 }
184
185 pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
186 let input_start = self.to_fold_point(range.start, Bias::Left).0;
187 let input_end = self.to_fold_point(range.end, Bias::Right).0;
188 let input_summary = self
189 .fold_snapshot
190 .text_summary_for_range(input_start..input_end);
191
192 let mut first_line_chars = 0;
193 let line_end = if range.start.row() == range.end.row() {
194 range.end
195 } else {
196 self.max_point()
197 };
198 for c in self
199 .chunks(range.start..line_end, false, Highlights::default())
200 .flat_map(|chunk| chunk.text.chars())
201 {
202 if c == '\n' {
203 break;
204 }
205 first_line_chars += 1;
206 }
207
208 let mut last_line_chars = 0;
209 if range.start.row() == range.end.row() {
210 last_line_chars = first_line_chars;
211 } else {
212 for _ in self
213 .chunks(
214 TabPoint::new(range.end.row(), 0)..range.end,
215 false,
216 Highlights::default(),
217 )
218 .flat_map(|chunk| chunk.text.chars())
219 {
220 last_line_chars += 1;
221 }
222 }
223
224 TextSummary {
225 lines: range.end.0 - range.start.0,
226 first_line_chars,
227 last_line_chars,
228 longest_row: input_summary.longest_row,
229 longest_row_chars: input_summary.longest_row_chars,
230 }
231 }
232
233 pub fn chunks<'a>(
234 &'a self,
235 range: Range<TabPoint>,
236 language_aware: bool,
237 highlights: Highlights<'a>,
238 ) -> TabChunks<'a> {
239 let (input_start, expanded_char_column, to_next_stop) =
240 self.to_fold_point(range.start, Bias::Left);
241 let input_column = input_start.column();
242 let input_start = input_start.to_offset(&self.fold_snapshot);
243 let input_end = self
244 .to_fold_point(range.end, Bias::Right)
245 .0
246 .to_offset(&self.fold_snapshot);
247 let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
248 range.end.column() - range.start.column()
249 } else {
250 to_next_stop
251 };
252
253 TabChunks {
254 snapshot: self,
255 fold_chunks: self.fold_snapshot.chunks(
256 input_start..input_end,
257 language_aware,
258 highlights,
259 ),
260 input_column,
261 column: expanded_char_column,
262 max_expansion_column: self.max_expansion_column,
263 output_position: range.start.0,
264 max_output_position: range.end.0,
265 tab_size: self.tab_size,
266 chunk: Chunk {
267 text: &SPACES[0..(to_next_stop as usize)],
268 is_tab: true,
269 ..Default::default()
270 },
271 inside_leading_tab: to_next_stop > 0,
272 }
273 }
274
275 pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> {
276 self.fold_snapshot.row_infos(row)
277 }
278
279 #[cfg(test)]
280 pub fn text(&self) -> String {
281 self.chunks(
282 TabPoint::zero()..self.max_point(),
283 false,
284 Highlights::default(),
285 )
286 .map(|chunk| chunk.text)
287 .collect()
288 }
289
290 pub fn max_point(&self) -> TabPoint {
291 self.to_tab_point(self.fold_snapshot.max_point())
292 }
293
294 pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
295 self.to_tab_point(
296 self.fold_snapshot
297 .clip_point(self.to_fold_point(point, bias).0, bias),
298 )
299 }
300
301 pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
302 let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
303 let expanded = self.expand_tabs(chars, input.column());
304 TabPoint::new(input.row(), expanded)
305 }
306
307 pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
308 let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
309 let expanded = output.column();
310 let (collapsed, expanded_char_column, to_next_stop) =
311 self.collapse_tabs(chars, expanded, bias);
312 (
313 FoldPoint::new(output.row(), collapsed),
314 expanded_char_column,
315 to_next_stop,
316 )
317 }
318
319 pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
320 let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
321 let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
322 self.to_tab_point(fold_point)
323 }
324
325 pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
326 let fold_point = self.to_fold_point(point, bias).0;
327 let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
328 self.fold_snapshot
329 .inlay_snapshot
330 .to_buffer_point(inlay_point)
331 }
332
333 fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
334 let tab_size = self.tab_size.get();
335
336 let mut expanded_chars = 0;
337 let mut expanded_bytes = 0;
338 let mut collapsed_bytes = 0;
339 let end_column = column.min(self.max_expansion_column);
340 for c in chars {
341 if collapsed_bytes >= end_column {
342 break;
343 }
344 if c == '\t' {
345 let tab_len = tab_size - expanded_chars % tab_size;
346 expanded_bytes += tab_len;
347 expanded_chars += tab_len;
348 } else {
349 expanded_bytes += c.len_utf8() as u32;
350 expanded_chars += 1;
351 }
352 collapsed_bytes += c.len_utf8() as u32;
353 }
354 expanded_bytes + column.saturating_sub(collapsed_bytes)
355 }
356
357 fn collapse_tabs(
358 &self,
359 chars: impl Iterator<Item = char>,
360 column: u32,
361 bias: Bias,
362 ) -> (u32, u32, u32) {
363 let tab_size = self.tab_size.get();
364
365 let mut expanded_bytes = 0;
366 let mut expanded_chars = 0;
367 let mut collapsed_bytes = 0;
368 for c in chars {
369 if expanded_bytes >= column {
370 break;
371 }
372 if collapsed_bytes >= self.max_expansion_column {
373 break;
374 }
375
376 if c == '\t' {
377 let tab_len = tab_size - (expanded_chars % tab_size);
378 expanded_chars += tab_len;
379 expanded_bytes += tab_len;
380 if expanded_bytes > column {
381 expanded_chars -= expanded_bytes - column;
382 return match bias {
383 Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
384 Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
385 };
386 }
387 } else {
388 expanded_chars += 1;
389 expanded_bytes += c.len_utf8() as u32;
390 }
391
392 if expanded_bytes > column && matches!(bias, Bias::Left) {
393 expanded_chars -= 1;
394 break;
395 }
396
397 collapsed_bytes += c.len_utf8() as u32;
398 }
399 (
400 collapsed_bytes + column.saturating_sub(expanded_bytes),
401 expanded_chars,
402 0,
403 )
404 }
405}
406
407#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
408pub struct TabPoint(pub Point);
409
410impl TabPoint {
411 pub fn new(row: u32, column: u32) -> Self {
412 Self(Point::new(row, column))
413 }
414
415 pub fn zero() -> Self {
416 Self::new(0, 0)
417 }
418
419 pub fn row(self) -> u32 {
420 self.0.row
421 }
422
423 pub fn column(self) -> u32 {
424 self.0.column
425 }
426}
427
428impl From<Point> for TabPoint {
429 fn from(point: Point) -> Self {
430 Self(point)
431 }
432}
433
434pub type TabEdit = text::Edit<TabPoint>;
435
436#[derive(Clone, Debug, Default, Eq, PartialEq)]
437pub struct TextSummary {
438 pub lines: Point,
439 pub first_line_chars: u32,
440 pub last_line_chars: u32,
441 pub longest_row: u32,
442 pub longest_row_chars: u32,
443}
444
445impl<'a> From<&'a str> for TextSummary {
446 fn from(text: &'a str) -> Self {
447 let sum = text::TextSummary::from(text);
448
449 TextSummary {
450 lines: sum.lines,
451 first_line_chars: sum.first_line_chars,
452 last_line_chars: sum.last_line_chars,
453 longest_row: sum.longest_row,
454 longest_row_chars: sum.longest_row_chars,
455 }
456 }
457}
458
459impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
460 fn add_assign(&mut self, other: &'a Self) {
461 let joined_chars = self.last_line_chars + other.first_line_chars;
462 if joined_chars > self.longest_row_chars {
463 self.longest_row = self.lines.row;
464 self.longest_row_chars = joined_chars;
465 }
466 if other.longest_row_chars > self.longest_row_chars {
467 self.longest_row = self.lines.row + other.longest_row;
468 self.longest_row_chars = other.longest_row_chars;
469 }
470
471 if self.lines.row == 0 {
472 self.first_line_chars += other.first_line_chars;
473 }
474
475 if other.lines.row == 0 {
476 self.last_line_chars += other.first_line_chars;
477 } else {
478 self.last_line_chars = other.last_line_chars;
479 }
480
481 self.lines += &other.lines;
482 }
483}
484
485// Handles a tab width <= 16
486const SPACES: &str = " ";
487
488pub struct TabChunks<'a> {
489 snapshot: &'a TabSnapshot,
490 fold_chunks: FoldChunks<'a>,
491 chunk: Chunk<'a>,
492 column: u32,
493 max_expansion_column: u32,
494 output_position: Point,
495 input_column: u32,
496 max_output_position: Point,
497 tab_size: NonZeroU32,
498 inside_leading_tab: bool,
499}
500
501impl TabChunks<'_> {
502 pub(crate) fn seek(&mut self, range: Range<TabPoint>) {
503 let (input_start, expanded_char_column, to_next_stop) =
504 self.snapshot.to_fold_point(range.start, Bias::Left);
505 let input_column = input_start.column();
506 let input_start = input_start.to_offset(&self.snapshot.fold_snapshot);
507 let input_end = self
508 .snapshot
509 .to_fold_point(range.end, Bias::Right)
510 .0
511 .to_offset(&self.snapshot.fold_snapshot);
512 let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
513 range.end.column() - range.start.column()
514 } else {
515 to_next_stop
516 };
517
518 self.fold_chunks.seek(input_start..input_end);
519 self.input_column = input_column;
520 self.column = expanded_char_column;
521 self.output_position = range.start.0;
522 self.max_output_position = range.end.0;
523 self.chunk = Chunk {
524 text: &SPACES[0..(to_next_stop as usize)],
525 is_tab: true,
526 ..Default::default()
527 };
528 self.inside_leading_tab = to_next_stop > 0;
529 }
530}
531
532impl<'a> Iterator for TabChunks<'a> {
533 type Item = Chunk<'a>;
534
535 fn next(&mut self) -> Option<Self::Item> {
536 if self.chunk.text.is_empty() {
537 if let Some(chunk) = self.fold_chunks.next() {
538 self.chunk = chunk;
539 if self.inside_leading_tab {
540 self.chunk.text = &self.chunk.text[1..];
541 self.inside_leading_tab = false;
542 self.input_column += 1;
543 }
544 } else {
545 return None;
546 }
547 }
548
549 for (ix, c) in self.chunk.text.char_indices() {
550 match c {
551 '\t' => {
552 if ix > 0 {
553 let (prefix, suffix) = self.chunk.text.split_at(ix);
554 self.chunk.text = suffix;
555 return Some(Chunk {
556 text: prefix,
557 ..self.chunk.clone()
558 });
559 } else {
560 self.chunk.text = &self.chunk.text[1..];
561 let tab_size = if self.input_column < self.max_expansion_column {
562 self.tab_size.get()
563 } else {
564 1
565 };
566 let mut len = tab_size - self.column % tab_size;
567 let next_output_position = cmp::min(
568 self.output_position + Point::new(0, len),
569 self.max_output_position,
570 );
571 len = next_output_position.column - self.output_position.column;
572 self.column += len;
573 self.input_column += 1;
574 self.output_position = next_output_position;
575 return Some(Chunk {
576 text: &SPACES[..len as usize],
577 is_tab: true,
578 ..self.chunk.clone()
579 });
580 }
581 }
582 '\n' => {
583 self.column = 0;
584 self.input_column = 0;
585 self.output_position += Point::new(1, 0);
586 }
587 _ => {
588 self.column += 1;
589 if !self.inside_leading_tab {
590 self.input_column += c.len_utf8() as u32;
591 }
592 self.output_position.column += c.len_utf8() as u32;
593 }
594 }
595 }
596
597 Some(mem::take(&mut self.chunk))
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::{
605 MultiBuffer,
606 display_map::{fold_map::FoldMap, inlay_map::InlayMap},
607 };
608 use rand::{Rng, prelude::StdRng};
609
610 #[gpui::test]
611 fn test_expand_tabs(cx: &mut gpui::App) {
612 let buffer = MultiBuffer::build_simple("", cx);
613 let buffer_snapshot = buffer.read(cx).snapshot(cx);
614 let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
615 let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
616 let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
617
618 assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
619 assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
620 assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
621 }
622
623 #[gpui::test]
624 fn test_long_lines(cx: &mut gpui::App) {
625 let max_expansion_column = 12;
626 let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
627 let output = "A BC DEF G HI J K L M";
628
629 let buffer = MultiBuffer::build_simple(input, cx);
630 let buffer_snapshot = buffer.read(cx).snapshot(cx);
631 let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
632 let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
633 let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
634
635 tab_snapshot.max_expansion_column = max_expansion_column;
636 assert_eq!(tab_snapshot.text(), output);
637
638 for (ix, c) in input.char_indices() {
639 assert_eq!(
640 tab_snapshot
641 .chunks(
642 TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
643 false,
644 Highlights::default(),
645 )
646 .map(|c| c.text)
647 .collect::<String>(),
648 &output[ix..],
649 "text from index {ix}"
650 );
651
652 if c != '\t' {
653 let input_point = Point::new(0, ix as u32);
654 let output_point = Point::new(0, output.find(c).unwrap() as u32);
655 assert_eq!(
656 tab_snapshot.to_tab_point(FoldPoint(input_point)),
657 TabPoint(output_point),
658 "to_tab_point({input_point:?})"
659 );
660 assert_eq!(
661 tab_snapshot
662 .to_fold_point(TabPoint(output_point), Bias::Left)
663 .0,
664 FoldPoint(input_point),
665 "to_fold_point({output_point:?})"
666 );
667 }
668 }
669 }
670
671 #[gpui::test]
672 fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::App) {
673 let max_expansion_column = 8;
674 let input = "abcdefg⋯hij";
675
676 let buffer = MultiBuffer::build_simple(input, cx);
677 let buffer_snapshot = buffer.read(cx).snapshot(cx);
678 let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
679 let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
680 let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
681
682 tab_snapshot.max_expansion_column = max_expansion_column;
683 assert_eq!(tab_snapshot.text(), input);
684 }
685
686 #[gpui::test]
687 fn test_marking_tabs(cx: &mut gpui::App) {
688 let input = "\t \thello";
689
690 let buffer = MultiBuffer::build_simple(input, cx);
691 let buffer_snapshot = buffer.read(cx).snapshot(cx);
692 let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
693 let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
694 let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
695
696 assert_eq!(
697 chunks(&tab_snapshot, TabPoint::zero()),
698 vec![
699 (" ".to_string(), true),
700 (" ".to_string(), false),
701 (" ".to_string(), true),
702 ("hello".to_string(), false),
703 ]
704 );
705 assert_eq!(
706 chunks(&tab_snapshot, TabPoint::new(0, 2)),
707 vec![
708 (" ".to_string(), true),
709 (" ".to_string(), false),
710 (" ".to_string(), true),
711 ("hello".to_string(), false),
712 ]
713 );
714
715 fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
716 let mut chunks = Vec::new();
717 let mut was_tab = false;
718 let mut text = String::new();
719 for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
720 {
721 if chunk.is_tab != was_tab {
722 if !text.is_empty() {
723 chunks.push((mem::take(&mut text), was_tab));
724 }
725 was_tab = chunk.is_tab;
726 }
727 text.push_str(chunk.text);
728 }
729
730 if !text.is_empty() {
731 chunks.push((text, was_tab));
732 }
733 chunks
734 }
735 }
736
737 #[gpui::test(iterations = 100)]
738 fn test_random_tabs(cx: &mut gpui::App, mut rng: StdRng) {
739 let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
740 let len = rng.random_range(0..30);
741 let buffer = if rng.random() {
742 let text = util::RandomCharIter::new(&mut rng)
743 .take(len)
744 .collect::<String>();
745 MultiBuffer::build_simple(&text, cx)
746 } else {
747 MultiBuffer::build_random(&mut rng, cx)
748 };
749 let buffer_snapshot = buffer.read(cx).snapshot(cx);
750 log::info!("Buffer text: {:?}", buffer_snapshot.text());
751
752 let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
753 log::info!("InlayMap text: {:?}", inlay_snapshot.text());
754 let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
755 fold_map.randomly_mutate(&mut rng);
756 let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
757 log::info!("FoldMap text: {:?}", fold_snapshot.text());
758 let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
759 log::info!("InlayMap text: {:?}", inlay_snapshot.text());
760
761 let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
762 let tabs_snapshot = tab_map.set_max_expansion_column(32);
763
764 let text = text::Rope::from(tabs_snapshot.text().as_str());
765 log::info!(
766 "TabMap text (tab size: {}): {:?}",
767 tab_size,
768 tabs_snapshot.text(),
769 );
770
771 for _ in 0..5 {
772 let end_row = rng.random_range(0..=text.max_point().row);
773 let end_column = rng.random_range(0..=text.line_len(end_row));
774 let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
775 let start_row = rng.random_range(0..=text.max_point().row);
776 let start_column = rng.random_range(0..=text.line_len(start_row));
777 let mut start =
778 TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
779 if start > end {
780 mem::swap(&mut start, &mut end);
781 }
782
783 let expected_text = text
784 .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
785 .collect::<String>();
786 let expected_summary = TextSummary::from(expected_text.as_str());
787 assert_eq!(
788 tabs_snapshot
789 .chunks(start..end, false, Highlights::default())
790 .map(|c| c.text)
791 .collect::<String>(),
792 expected_text,
793 "chunks({:?}..{:?})",
794 start,
795 end
796 );
797
798 let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
799 if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
800 actual_summary.longest_row = expected_summary.longest_row;
801 actual_summary.longest_row_chars = expected_summary.longest_row_chars;
802 }
803 assert_eq!(actual_summary, expected_summary);
804 }
805
806 for row in 0..=text.max_point().row {
807 assert_eq!(
808 tabs_snapshot.line_len(row),
809 text.line_len(row),
810 "line_len({row})"
811 );
812 }
813 }
814}