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