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