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