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