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