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