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