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