display_map.rs

  1mod block_map;
  2mod fold_map;
  3mod patch;
  4mod tab_map;
  5mod wrap_map;
  6
  7use fold_map::{FoldMap, ToFoldPoint as _};
  8use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
  9use language::{Anchor, Buffer, Point, ToOffset, ToPoint};
 10use std::ops::Range;
 11use sum_tree::Bias;
 12use tab_map::TabMap;
 13use wrap_map::WrapMap;
 14pub use wrap_map::{BufferRows, HighlightedChunks};
 15
 16pub trait ToDisplayPoint {
 17    fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint;
 18}
 19
 20pub struct DisplayMap {
 21    buffer: ModelHandle<Buffer>,
 22    fold_map: FoldMap,
 23    tab_map: TabMap,
 24    wrap_map: ModelHandle<WrapMap>,
 25}
 26
 27impl Entity for DisplayMap {
 28    type Event = ();
 29}
 30
 31impl DisplayMap {
 32    pub fn new(
 33        buffer: ModelHandle<Buffer>,
 34        tab_size: usize,
 35        font_id: FontId,
 36        font_size: f32,
 37        wrap_width: Option<f32>,
 38        cx: &mut ModelContext<Self>,
 39    ) -> Self {
 40        let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
 41        let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
 42        let wrap_map =
 43            cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
 44        cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
 45        DisplayMap {
 46            buffer,
 47            fold_map,
 48            tab_map,
 49            wrap_map,
 50        }
 51    }
 52
 53    pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplayMapSnapshot {
 54        let (folds_snapshot, edits) = self.fold_map.read(cx);
 55        let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
 56        let (wraps_snapshot, _) = self
 57            .wrap_map
 58            .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
 59        DisplayMapSnapshot {
 60            buffer_snapshot: self.buffer.read(cx).snapshot(),
 61            folds_snapshot,
 62            tabs_snapshot,
 63            wraps_snapshot,
 64        }
 65    }
 66
 67    pub fn fold<T: ToOffset>(
 68        &mut self,
 69        ranges: impl IntoIterator<Item = Range<T>>,
 70        cx: &mut ModelContext<Self>,
 71    ) {
 72        let (mut fold_map, snapshot, edits) = self.fold_map.write(cx);
 73        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
 74        self.wrap_map
 75            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
 76        let (snapshot, edits) = fold_map.fold(ranges, cx);
 77        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
 78        self.wrap_map
 79            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
 80    }
 81
 82    pub fn unfold<T: ToOffset>(
 83        &mut self,
 84        ranges: impl IntoIterator<Item = Range<T>>,
 85        cx: &mut ModelContext<Self>,
 86    ) {
 87        let (mut fold_map, snapshot, edits) = self.fold_map.write(cx);
 88        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
 89        self.wrap_map
 90            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
 91        let (snapshot, edits) = fold_map.unfold(ranges, cx);
 92        let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
 93        self.wrap_map
 94            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
 95    }
 96
 97    pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
 98        self.wrap_map
 99            .update(cx, |map, cx| map.set_font(font_id, font_size, cx));
100    }
101
102    pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
103        self.wrap_map
104            .update(cx, |map, cx| map.set_wrap_width(width, cx))
105    }
106
107    #[cfg(test)]
108    pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
109        self.wrap_map.read(cx).is_rewrapping()
110    }
111}
112
113pub struct DisplayMapSnapshot {
114    buffer_snapshot: language::Snapshot,
115    folds_snapshot: fold_map::Snapshot,
116    tabs_snapshot: tab_map::Snapshot,
117    wraps_snapshot: wrap_map::Snapshot,
118}
119
120impl DisplayMapSnapshot {
121    #[cfg(test)]
122    pub fn fold_count(&self) -> usize {
123        self.folds_snapshot.fold_count()
124    }
125
126    pub fn is_empty(&self) -> bool {
127        self.buffer_snapshot.len() == 0
128    }
129
130    pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
131        self.wraps_snapshot.buffer_rows(start_row)
132    }
133
134    pub fn buffer_row_count(&self) -> u32 {
135        self.buffer_snapshot.max_point().row + 1
136    }
137
138    pub fn prev_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
139        loop {
140            *display_point.column_mut() = 0;
141            let mut point = display_point.to_buffer_point(self, Bias::Left);
142            point.column = 0;
143            let next_display_point = point.to_display_point(self, Bias::Left);
144            if next_display_point == display_point {
145                return (display_point, point);
146            }
147            display_point = next_display_point;
148        }
149    }
150
151    pub fn next_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
152        loop {
153            *display_point.column_mut() = self.line_len(display_point.row());
154            let mut point = display_point.to_buffer_point(self, Bias::Right);
155            point.column = self.buffer_snapshot.line_len(point.row);
156            let next_display_point = point.to_display_point(self, Bias::Right);
157            if next_display_point == display_point {
158                return (display_point, point);
159            }
160            display_point = next_display_point;
161        }
162    }
163
164    pub fn max_point(&self) -> DisplayPoint {
165        DisplayPoint(self.wraps_snapshot.max_point())
166    }
167
168    pub fn chunks_at(&self, display_row: u32) -> wrap_map::Chunks {
169        self.wraps_snapshot.chunks_at(display_row)
170    }
171
172    pub fn highlighted_chunks_for_rows(
173        &mut self,
174        display_rows: Range<u32>,
175    ) -> wrap_map::HighlightedChunks {
176        self.wraps_snapshot
177            .highlighted_chunks_for_rows(display_rows)
178    }
179
180    pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
181        let mut column = 0;
182        let mut chars = self.chunks_at(point.row()).flat_map(str::chars);
183        while column < point.column() {
184            if let Some(c) = chars.next() {
185                column += c.len_utf8() as u32;
186            } else {
187                break;
188            }
189        }
190        chars
191    }
192
193    pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
194        let mut count = 0;
195        let mut column = 0;
196        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
197            if column >= target {
198                break;
199            }
200            count += 1;
201            column += c.len_utf8() as u32;
202        }
203        count
204    }
205
206    pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
207        let mut count = 0;
208        let mut column = 0;
209        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
210            if c == '\n' || count >= char_count {
211                break;
212            }
213            count += 1;
214            column += c.len_utf8() as u32;
215        }
216        column
217    }
218
219    pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
220        DisplayPoint(self.wraps_snapshot.clip_point(point.0, bias))
221    }
222
223    pub fn folds_in_range<'a, T>(
224        &'a self,
225        range: Range<T>,
226    ) -> impl Iterator<Item = &'a Range<Anchor>>
227    where
228        T: ToOffset,
229    {
230        self.folds_snapshot.folds_in_range(range)
231    }
232
233    pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
234        self.folds_snapshot.intersects_fold(offset)
235    }
236
237    pub fn is_line_folded(&self, display_row: u32) -> bool {
238        let wrap_point = DisplayPoint::new(display_row, 0).0;
239        let row = self.wraps_snapshot.to_tab_point(wrap_point).row();
240        self.folds_snapshot.is_line_folded(row)
241    }
242
243    pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
244        self.wraps_snapshot.soft_wrap_indent(display_row)
245    }
246
247    pub fn text(&self) -> String {
248        self.chunks_at(0).collect()
249    }
250
251    pub fn line(&self, display_row: u32) -> String {
252        let mut result = String::new();
253        for chunk in self.chunks_at(display_row) {
254            if let Some(ix) = chunk.find('\n') {
255                result.push_str(&chunk[0..ix]);
256                break;
257            } else {
258                result.push_str(chunk);
259            }
260        }
261        result
262    }
263
264    pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
265        let mut indent = 0;
266        let mut is_blank = true;
267        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
268            if c == ' ' {
269                indent += 1;
270            } else {
271                is_blank = c == '\n';
272                break;
273            }
274        }
275        (indent, is_blank)
276    }
277
278    pub fn line_len(&self, row: u32) -> u32 {
279        self.wraps_snapshot.line_len(row)
280    }
281
282    pub fn longest_row(&self) -> u32 {
283        self.wraps_snapshot.longest_row()
284    }
285
286    pub fn anchor_before(&self, point: DisplayPoint, bias: Bias) -> Anchor {
287        self.buffer_snapshot
288            .anchor_before(point.to_buffer_point(self, bias))
289    }
290
291    pub fn anchor_after(&self, point: DisplayPoint, bias: Bias) -> Anchor {
292        self.buffer_snapshot
293            .anchor_after(point.to_buffer_point(self, bias))
294    }
295}
296
297#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
298pub struct DisplayPoint(wrap_map::WrapPoint);
299
300impl DisplayPoint {
301    pub fn new(row: u32, column: u32) -> Self {
302        Self(wrap_map::WrapPoint::new(row, column))
303    }
304
305    pub fn zero() -> Self {
306        Self::new(0, 0)
307    }
308
309    #[cfg(test)]
310    pub fn is_zero(&self) -> bool {
311        self.0.is_zero()
312    }
313
314    pub fn row(self) -> u32 {
315        self.0.row()
316    }
317
318    pub fn column(self) -> u32 {
319        self.0.column()
320    }
321
322    pub fn row_mut(&mut self) -> &mut u32 {
323        self.0.row_mut()
324    }
325
326    pub fn column_mut(&mut self) -> &mut u32 {
327        self.0.column_mut()
328    }
329
330    pub fn to_buffer_point(self, map: &DisplayMapSnapshot, bias: Bias) -> Point {
331        let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0);
332        let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
333        unexpanded_point.to_buffer_point(&map.folds_snapshot)
334    }
335
336    pub fn to_buffer_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize {
337        let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0);
338        let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
339        unexpanded_point.to_buffer_offset(&map.folds_snapshot)
340    }
341}
342
343impl ToDisplayPoint for Point {
344    fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint {
345        let fold_point = self.to_fold_point(&map.folds_snapshot, bias);
346        let tab_point = map.tabs_snapshot.to_tab_point(fold_point);
347        let wrap_point = map.wraps_snapshot.to_wrap_point(tab_point);
348        DisplayPoint(wrap_point)
349    }
350}
351
352impl ToDisplayPoint for Anchor {
353    fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint {
354        self.to_point(&map.buffer_snapshot)
355            .to_display_point(map, bias)
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use crate::{movement, test::*};
363    use gpui::{color::Color, MutableAppContext};
364    use language::{Language, LanguageConfig, RandomCharIter, SelectionGoal};
365    use rand::{prelude::StdRng, Rng};
366    use std::{env, sync::Arc};
367    use theme::SyntaxTheme;
368    use Bias::*;
369
370    #[gpui::test(iterations = 100)]
371    async fn test_random(mut cx: gpui::TestAppContext, mut rng: StdRng) {
372        cx.foreground().set_block_on_ticks(0..=50);
373        cx.foreground().forbid_parking();
374        let operations = env::var("OPERATIONS")
375            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
376            .unwrap_or(10);
377
378        let font_cache = cx.font_cache().clone();
379        let tab_size = rng.gen_range(1..=4);
380        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
381        let font_id = font_cache
382            .select_font(family_id, &Default::default())
383            .unwrap();
384        let font_size = 14.0;
385        let max_wrap_width = 300.0;
386        let mut wrap_width = if rng.gen_bool(0.1) {
387            None
388        } else {
389            Some(rng.gen_range(0.0..=max_wrap_width))
390        };
391
392        log::info!("tab size: {}", tab_size);
393        log::info!("wrap width: {:?}", wrap_width);
394
395        let buffer = cx.add_model(|cx| {
396            let len = rng.gen_range(0..10);
397            let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
398            Buffer::new(0, text, cx)
399        });
400
401        let map = cx.add_model(|cx| {
402            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
403        });
404        let (_observer, notifications) = Observer::new(&map, &mut cx);
405        let mut fold_count = 0;
406
407        for _i in 0..operations {
408            match rng.gen_range(0..100) {
409                0..=19 => {
410                    wrap_width = if rng.gen_bool(0.2) {
411                        None
412                    } else {
413                        Some(rng.gen_range(0.0..=max_wrap_width))
414                    };
415                    log::info!("setting wrap width to {:?}", wrap_width);
416                    map.update(&mut cx, |map, cx| map.set_wrap_width(wrap_width, cx));
417                }
418                20..=80 => {
419                    let mut ranges = Vec::new();
420                    for _ in 0..rng.gen_range(1..=3) {
421                        buffer.read_with(&cx, |buffer, _| {
422                            let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
423                            let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
424                            ranges.push(start..end);
425                        });
426                    }
427
428                    if rng.gen() && fold_count > 0 {
429                        log::info!("unfolding ranges: {:?}", ranges);
430                        map.update(&mut cx, |map, cx| {
431                            map.unfold(ranges, cx);
432                        });
433                    } else {
434                        log::info!("folding ranges: {:?}", ranges);
435                        map.update(&mut cx, |map, cx| {
436                            map.fold(ranges, cx);
437                        });
438                    }
439                }
440                _ => {
441                    buffer.update(&mut cx, |buffer, _| buffer.randomly_edit(&mut rng, 5));
442                }
443            }
444
445            if map.read_with(&cx, |map, cx| map.is_rewrapping(cx)) {
446                notifications.recv().await.unwrap();
447            }
448
449            let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
450            fold_count = snapshot.fold_count();
451            log::info!("buffer text: {:?}", buffer.read_with(&cx, |b, _| b.text()));
452            log::info!("display text: {:?}", snapshot.text());
453
454            // Line boundaries
455            for _ in 0..5 {
456                let row = rng.gen_range(0..=snapshot.max_point().row());
457                let column = rng.gen_range(0..=snapshot.line_len(row));
458                let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
459
460                let (prev_display_bound, prev_buffer_bound) = snapshot.prev_row_boundary(point);
461                let (next_display_bound, next_buffer_bound) = snapshot.next_row_boundary(point);
462
463                assert!(prev_display_bound <= point);
464                assert!(next_display_bound >= point);
465                assert_eq!(prev_buffer_bound.column, 0);
466                assert_eq!(prev_display_bound.column(), 0);
467                if next_display_bound < snapshot.max_point() {
468                    assert_eq!(
469                        buffer
470                            .read_with(&cx, |buffer, _| buffer.chars_at(next_buffer_bound).next()),
471                        Some('\n')
472                    )
473                }
474
475                assert_eq!(
476                    prev_display_bound,
477                    prev_buffer_bound.to_display_point(&snapshot, Left),
478                    "row boundary before {:?}. reported buffer row boundary: {:?}",
479                    point,
480                    prev_buffer_bound
481                );
482                assert_eq!(
483                    next_display_bound,
484                    next_buffer_bound.to_display_point(&snapshot, Right),
485                    "display row boundary after {:?}. reported buffer row boundary: {:?}",
486                    point,
487                    next_buffer_bound
488                );
489                assert_eq!(
490                    prev_buffer_bound,
491                    prev_display_bound.to_buffer_point(&snapshot, Left),
492                    "row boundary before {:?}. reported display row boundary: {:?}",
493                    point,
494                    prev_display_bound
495                );
496                assert_eq!(
497                    next_buffer_bound,
498                    next_display_bound.to_buffer_point(&snapshot, Right),
499                    "row boundary after {:?}. reported display row boundary: {:?}",
500                    point,
501                    next_display_bound
502                );
503            }
504
505            // Movement
506            for _ in 0..5 {
507                let row = rng.gen_range(0..=snapshot.max_point().row());
508                let column = rng.gen_range(0..=snapshot.line_len(row));
509                let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
510
511                log::info!("Moving from point {:?}", point);
512
513                let moved_right = movement::right(&snapshot, point).unwrap();
514                log::info!("Right {:?}", moved_right);
515                if point < snapshot.max_point() {
516                    assert!(moved_right > point);
517                    if point.column() == snapshot.line_len(point.row())
518                        || snapshot.soft_wrap_indent(point.row()).is_some()
519                            && point.column() == snapshot.line_len(point.row()) - 1
520                    {
521                        assert!(moved_right.row() > point.row());
522                    }
523                } else {
524                    assert_eq!(moved_right, point);
525                }
526
527                let moved_left = movement::left(&snapshot, point).unwrap();
528                log::info!("Left {:?}", moved_left);
529                if !point.is_zero() {
530                    assert!(moved_left < point);
531                    if point.column() == 0 {
532                        assert!(moved_left.row() < point.row());
533                    }
534                } else {
535                    assert!(moved_left.is_zero());
536                }
537            }
538        }
539    }
540
541    #[gpui::test]
542    fn test_soft_wraps(cx: &mut MutableAppContext) {
543        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
544        cx.foreground().forbid_parking();
545
546        let font_cache = cx.font_cache();
547
548        let tab_size = 4;
549        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
550        let font_id = font_cache
551            .select_font(family_id, &Default::default())
552            .unwrap();
553        let font_size = 12.0;
554        let wrap_width = Some(64.);
555
556        let text = "one two three four five\nsix seven eight";
557        let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
558        let map = cx.add_model(|cx| {
559            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
560        });
561
562        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
563        assert_eq!(
564            snapshot.chunks_at(0).collect::<String>(),
565            "one two \nthree four \nfive\nsix seven \neight"
566        );
567        assert_eq!(
568            snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
569            DisplayPoint::new(0, 7)
570        );
571        assert_eq!(
572            snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
573            DisplayPoint::new(1, 0)
574        );
575        assert_eq!(
576            movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
577            DisplayPoint::new(1, 0)
578        );
579        assert_eq!(
580            movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(),
581            DisplayPoint::new(0, 7)
582        );
583        assert_eq!(
584            movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None).unwrap(),
585            (DisplayPoint::new(0, 7), SelectionGoal::Column(10))
586        );
587        assert_eq!(
588            movement::down(
589                &snapshot,
590                DisplayPoint::new(0, 7),
591                SelectionGoal::Column(10)
592            )
593            .unwrap(),
594            (DisplayPoint::new(1, 10), SelectionGoal::Column(10))
595        );
596        assert_eq!(
597            movement::down(
598                &snapshot,
599                DisplayPoint::new(1, 10),
600                SelectionGoal::Column(10)
601            )
602            .unwrap(),
603            (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
604        );
605
606        buffer.update(cx, |buffer, cx| {
607            let ix = buffer.text().find("seven").unwrap();
608            buffer.edit(vec![ix..ix], "and ", cx);
609        });
610
611        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
612        assert_eq!(
613            snapshot.chunks_at(1).collect::<String>(),
614            "three four \nfive\nsix and \nseven eight"
615        );
616
617        // Re-wrap on font size changes
618        map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
619
620        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
621        assert_eq!(
622            snapshot.chunks_at(1).collect::<String>(),
623            "three \nfour five\nsix and \nseven \neight"
624        )
625    }
626
627    #[gpui::test]
628    fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
629        let text = sample_text(6, 6);
630        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
631        let tab_size = 4;
632        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
633        let font_id = cx
634            .font_cache()
635            .select_font(family_id, &Default::default())
636            .unwrap();
637        let font_size = 14.0;
638        let map = cx.add_model(|cx| {
639            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
640        });
641        buffer.update(cx, |buffer, cx| {
642            buffer.edit(
643                vec![
644                    Point::new(1, 0)..Point::new(1, 0),
645                    Point::new(1, 1)..Point::new(1, 1),
646                    Point::new(2, 1)..Point::new(2, 1),
647                ],
648                "\t",
649                cx,
650            )
651        });
652
653        assert_eq!(
654            map.update(cx, |map, cx| map.snapshot(cx))
655                .chunks_at(1)
656                .collect::<String>()
657                .lines()
658                .next(),
659            Some("    b   bbbbb")
660        );
661        assert_eq!(
662            map.update(cx, |map, cx| map.snapshot(cx))
663                .chunks_at(2)
664                .collect::<String>()
665                .lines()
666                .next(),
667            Some("c   ccccc")
668        );
669    }
670
671    #[gpui::test]
672    async fn test_highlighted_chunks_at(mut cx: gpui::TestAppContext) {
673        use unindent::Unindent as _;
674
675        let text = r#"
676            fn outer() {}
677
678            mod module {
679                fn inner() {}
680            }"#
681        .unindent();
682
683        let theme = SyntaxTheme::new(vec![
684            ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
685            ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
686        ]);
687        let lang = Arc::new(
688            Language::new(
689                LanguageConfig {
690                    name: "Test".to_string(),
691                    path_suffixes: vec![".test".to_string()],
692                    ..Default::default()
693                },
694                tree_sitter_rust::language(),
695            )
696            .with_highlights_query(
697                r#"
698                (mod_item name: (identifier) body: _ @mod.body)
699                (function_item name: (identifier) @fn.name)
700                "#,
701            )
702            .unwrap(),
703        );
704        lang.set_theme(&theme);
705
706        let buffer =
707            cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx));
708        buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
709
710        let tab_size = 2;
711        let font_cache = cx.font_cache();
712        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
713        let font_id = font_cache
714            .select_font(family_id, &Default::default())
715            .unwrap();
716        let font_size = 14.0;
717
718        let map =
719            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
720        assert_eq!(
721            cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
722            vec![
723                ("fn ".to_string(), None),
724                ("outer".to_string(), Some("fn.name")),
725                ("() {}\n\nmod module ".to_string(), None),
726                ("{\n    fn ".to_string(), Some("mod.body")),
727                ("inner".to_string(), Some("fn.name")),
728                ("() {}\n}".to_string(), Some("mod.body")),
729            ]
730        );
731        assert_eq!(
732            cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)),
733            vec![
734                ("    fn ".to_string(), Some("mod.body")),
735                ("inner".to_string(), Some("fn.name")),
736                ("() {}\n}".to_string(), Some("mod.body")),
737            ]
738        );
739
740        map.update(&mut cx, |map, cx| {
741            map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
742        });
743        assert_eq!(
744            cx.update(|cx| highlighted_chunks(0..2, &map, &theme, cx)),
745            vec![
746                ("fn ".to_string(), None),
747                ("out".to_string(), Some("fn.name")),
748                ("".to_string(), None),
749                ("  fn ".to_string(), Some("mod.body")),
750                ("inner".to_string(), Some("fn.name")),
751                ("() {}\n}".to_string(), Some("mod.body")),
752            ]
753        );
754    }
755
756    #[gpui::test]
757    async fn test_highlighted_chunks_with_soft_wrapping(mut cx: gpui::TestAppContext) {
758        use unindent::Unindent as _;
759
760        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
761
762        let text = r#"
763            fn outer() {}
764
765            mod module {
766                fn inner() {}
767            }"#
768        .unindent();
769
770        let theme = SyntaxTheme::new(vec![
771            ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
772            ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
773        ]);
774        let lang = Arc::new(
775            Language::new(
776                LanguageConfig {
777                    name: "Test".to_string(),
778                    path_suffixes: vec![".test".to_string()],
779                    ..Default::default()
780                },
781                tree_sitter_rust::language(),
782            )
783            .with_highlights_query(
784                r#"
785                (mod_item name: (identifier) body: _ @mod.body)
786                (function_item name: (identifier) @fn.name)
787                "#,
788            )
789            .unwrap(),
790        );
791        lang.set_theme(&theme);
792
793        let buffer =
794            cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx));
795        buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
796
797        let font_cache = cx.font_cache();
798
799        let tab_size = 4;
800        let family_id = font_cache.load_family(&["Courier"]).unwrap();
801        let font_id = font_cache
802            .select_font(family_id, &Default::default())
803            .unwrap();
804        let font_size = 16.0;
805
806        let map = cx
807            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx));
808        assert_eq!(
809            cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
810            [
811                ("fn \n".to_string(), None),
812                ("oute\nr".to_string(), Some("fn.name")),
813                ("() \n{}\n\n".to_string(), None),
814            ]
815        );
816        assert_eq!(
817            cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)),
818            [("{}\n\n".to_string(), None)]
819        );
820
821        map.update(&mut cx, |map, cx| {
822            map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
823        });
824        assert_eq!(
825            cx.update(|cx| highlighted_chunks(1..4, &map, &theme, cx)),
826            [
827                ("out".to_string(), Some("fn.name")),
828                ("\n".to_string(), None),
829                ("  \nfn ".to_string(), Some("mod.body")),
830                ("i\n".to_string(), Some("fn.name"))
831            ]
832        );
833    }
834
835    #[gpui::test]
836    fn test_clip_point(cx: &mut gpui::MutableAppContext) {
837        use Bias::{Left, Right};
838
839        let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
840        let display_text = "\n'a', 'α',   '✋',    '❎', '🍐'\n";
841        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
842
843        let tab_size = 4;
844        let font_cache = cx.font_cache();
845        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
846        let font_id = font_cache
847            .select_font(family_id, &Default::default())
848            .unwrap();
849        let font_size = 14.0;
850        let map = cx.add_model(|cx| {
851            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
852        });
853        let map = map.update(cx, |map, cx| map.snapshot(cx));
854
855        assert_eq!(map.text(), display_text);
856        for (input_column, bias, output_column) in vec![
857            ("'a', '".len(), Left, "'a', '".len()),
858            ("'a', '".len() + 1, Left, "'a', '".len()),
859            ("'a', '".len() + 1, Right, "'a', 'α".len()),
860            ("'a', 'α', ".len(), Left, "'a', 'α',".len()),
861            ("'a', 'α', ".len(), Right, "'a', 'α',   ".len()),
862            ("'a', 'α',   '".len() + 1, Left, "'a', 'α',   '".len()),
863            ("'a', 'α',   '".len() + 1, Right, "'a', 'α',   '✋".len()),
864            ("'a', 'α',   '✋',".len(), Right, "'a', 'α',   '✋',".len()),
865            ("'a', 'α',   '✋', ".len(), Left, "'a', 'α',   '✋',".len()),
866            (
867                "'a', 'α',   '✋', ".len(),
868                Right,
869                "'a', 'α',   '✋',    ".len(),
870            ),
871        ] {
872            assert_eq!(
873                map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
874                DisplayPoint::new(1, output_column as u32),
875                "clip_point(({}, {}))",
876                1,
877                input_column,
878            );
879        }
880    }
881
882    #[gpui::test]
883    fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
884        let text = "\t\tα\nβ\t\n🏀β\t\tγ";
885        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
886        let tab_size = 4;
887        let font_cache = cx.font_cache();
888        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
889        let font_id = font_cache
890            .select_font(family_id, &Default::default())
891            .unwrap();
892        let font_size = 14.0;
893
894        let map = cx.add_model(|cx| {
895            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
896        });
897        let map = map.update(cx, |map, cx| map.snapshot(cx));
898        assert_eq!(map.text(), "✅       α\nβ   \n🏀β      γ");
899        assert_eq!(
900            map.chunks_at(0).collect::<String>(),
901            "✅       α\nβ   \n🏀β      γ"
902        );
903        assert_eq!(map.chunks_at(1).collect::<String>(), "β   \n🏀β      γ");
904        assert_eq!(map.chunks_at(2).collect::<String>(), "🏀β      γ");
905
906        let point = Point::new(0, "\t\t".len() as u32);
907        let display_point = DisplayPoint::new(0, "".len() as u32);
908        assert_eq!(point.to_display_point(&map, Left), display_point);
909        assert_eq!(display_point.to_buffer_point(&map, Left), point,);
910
911        let point = Point::new(1, "β\t".len() as u32);
912        let display_point = DisplayPoint::new(1, "β   ".len() as u32);
913        assert_eq!(point.to_display_point(&map, Left), display_point);
914        assert_eq!(display_point.to_buffer_point(&map, Left), point,);
915
916        let point = Point::new(2, "🏀β\t\t".len() as u32);
917        let display_point = DisplayPoint::new(2, "🏀β      ".len() as u32);
918        assert_eq!(point.to_display_point(&map, Left), display_point);
919        assert_eq!(display_point.to_buffer_point(&map, Left), point,);
920
921        // Display points inside of expanded tabs
922        assert_eq!(
923            DisplayPoint::new(0, "".len() as u32).to_buffer_point(&map, Right),
924            Point::new(0, "\t\t".len() as u32),
925        );
926        assert_eq!(
927            DisplayPoint::new(0, "".len() as u32).to_buffer_point(&map, Left),
928            Point::new(0, "\t".len() as u32),
929        );
930        assert_eq!(
931            DisplayPoint::new(0, "".len() as u32).to_buffer_point(&map, Right),
932            Point::new(0, "\t".len() as u32),
933        );
934        assert_eq!(
935            DisplayPoint::new(0, "".len() as u32).to_buffer_point(&map, Left),
936            Point::new(0, "".len() as u32),
937        );
938
939        // Clipping display points inside of multi-byte characters
940        assert_eq!(
941            map.clip_point(DisplayPoint::new(0, "".len() as u32 - 1), Left),
942            DisplayPoint::new(0, 0)
943        );
944        assert_eq!(
945            map.clip_point(DisplayPoint::new(0, "".len() as u32 - 1), Bias::Right),
946            DisplayPoint::new(0, "".len() as u32)
947        );
948    }
949
950    #[gpui::test]
951    fn test_max_point(cx: &mut gpui::MutableAppContext) {
952        let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
953        let tab_size = 4;
954        let font_cache = cx.font_cache();
955        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
956        let font_id = font_cache
957            .select_font(family_id, &Default::default())
958            .unwrap();
959        let font_size = 14.0;
960        let map = cx.add_model(|cx| {
961            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
962        });
963        assert_eq!(
964            map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
965            DisplayPoint::new(1, 11)
966        )
967    }
968
969    fn highlighted_chunks<'a>(
970        rows: Range<u32>,
971        map: &ModelHandle<DisplayMap>,
972        theme: &'a SyntaxTheme,
973        cx: &mut MutableAppContext,
974    ) -> Vec<(String, Option<&'a str>)> {
975        let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
976        let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
977        for chunk in snapshot.highlighted_chunks_for_rows(rows) {
978            let style_name = chunk.highlight_id.name(theme);
979            if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
980                if style_name == *last_style_name {
981                    last_chunk.push_str(chunk.text);
982                } else {
983                    chunks.push((chunk.text.to_string(), style_name));
984                }
985            } else {
986                chunks.push((chunk.text.to_string(), style_name));
987            }
988        }
989        chunks
990    }
991}