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