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