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