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