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