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