1mod block_map;
2mod fold_map;
3mod tab_map;
4mod wrap_map;
5
6use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
7use block_map::{BlockMap, BlockPoint};
8use collections::{HashMap, HashSet};
9use fold_map::FoldMap;
10use gpui::{
11 fonts::{FontId, HighlightStyle},
12 Entity, ModelContext, ModelHandle,
13};
14use language::{Point, Subscription as BufferSubscription};
15use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc};
16use sum_tree::{Bias, TreeMap};
17use tab_map::TabMap;
18use wrap_map::WrapMap;
19
20pub use block_map::{
21 BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext,
22 BlockDisposition, BlockId, BlockProperties, RenderBlock, TransformBlock,
23};
24
25pub trait ToDisplayPoint {
26 fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
27}
28
29type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
30
31pub struct DisplayMap {
32 buffer: ModelHandle<MultiBuffer>,
33 buffer_subscription: BufferSubscription,
34 fold_map: FoldMap,
35 tab_map: TabMap,
36 wrap_map: ModelHandle<WrapMap>,
37 block_map: BlockMap,
38 text_highlights: TextHighlights,
39 pub clip_at_line_ends: bool,
40}
41
42impl Entity for DisplayMap {
43 type Event = ();
44}
45
46impl DisplayMap {
47 pub fn new(
48 buffer: ModelHandle<MultiBuffer>,
49 tab_size: usize,
50 font_id: FontId,
51 font_size: f32,
52 wrap_width: Option<f32>,
53 buffer_header_height: u8,
54 excerpt_header_height: u8,
55 cx: &mut ModelContext<Self>,
56 ) -> Self {
57 let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
58 let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
59 let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
60 let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
61 let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
62 cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
63 DisplayMap {
64 buffer,
65 buffer_subscription,
66 fold_map,
67 tab_map,
68 wrap_map,
69 block_map,
70 text_highlights: Default::default(),
71 clip_at_line_ends: false,
72 }
73 }
74
75 pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
76 let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
77 let edits = self.buffer_subscription.consume().into_inner();
78 let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
79 let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
80 let (wraps_snapshot, edits) = self
81 .wrap_map
82 .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
83 let blocks_snapshot = self.block_map.read(wraps_snapshot.clone(), edits);
84
85 DisplaySnapshot {
86 buffer_snapshot: self.buffer.read(cx).snapshot(cx),
87 folds_snapshot,
88 tabs_snapshot,
89 wraps_snapshot,
90 blocks_snapshot,
91 text_highlights: self.text_highlights.clone(),
92 clip_at_line_ends: self.clip_at_line_ends,
93 }
94 }
95
96 pub fn fold<T: ToOffset>(
97 &mut self,
98 ranges: impl IntoIterator<Item = Range<T>>,
99 cx: &mut ModelContext<Self>,
100 ) {
101 let snapshot = self.buffer.read(cx).snapshot(cx);
102 let edits = self.buffer_subscription.consume().into_inner();
103 let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
104 let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
105 let (snapshot, edits) = self
106 .wrap_map
107 .update(cx, |map, cx| map.sync(snapshot, edits, cx));
108 self.block_map.read(snapshot, edits);
109 let (snapshot, edits) = fold_map.fold(ranges);
110 let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
111 let (snapshot, edits) = self
112 .wrap_map
113 .update(cx, |map, cx| map.sync(snapshot, edits, cx));
114 self.block_map.read(snapshot, edits);
115 }
116
117 pub fn unfold<T: ToOffset>(
118 &mut self,
119 ranges: impl IntoIterator<Item = Range<T>>,
120 cx: &mut ModelContext<Self>,
121 ) {
122 let snapshot = self.buffer.read(cx).snapshot(cx);
123 let edits = self.buffer_subscription.consume().into_inner();
124 let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
125 let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
126 let (snapshot, edits) = self
127 .wrap_map
128 .update(cx, |map, cx| map.sync(snapshot, edits, cx));
129 self.block_map.read(snapshot, edits);
130 let (snapshot, edits) = fold_map.unfold(ranges);
131 let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
132 let (snapshot, edits) = self
133 .wrap_map
134 .update(cx, |map, cx| map.sync(snapshot, edits, cx));
135 self.block_map.read(snapshot, edits);
136 }
137
138 pub fn insert_blocks(
139 &mut self,
140 blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
141 cx: &mut ModelContext<Self>,
142 ) -> Vec<BlockId> {
143 let snapshot = self.buffer.read(cx).snapshot(cx);
144 let edits = self.buffer_subscription.consume().into_inner();
145 let (snapshot, edits) = self.fold_map.read(snapshot, edits);
146 let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
147 let (snapshot, edits) = self
148 .wrap_map
149 .update(cx, |map, cx| map.sync(snapshot, edits, cx));
150 let mut block_map = self.block_map.write(snapshot, edits);
151 block_map.insert(blocks)
152 }
153
154 pub fn replace_blocks(&mut self, styles: HashMap<BlockId, RenderBlock>) {
155 self.block_map.replace(styles);
156 }
157
158 pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
159 let snapshot = self.buffer.read(cx).snapshot(cx);
160 let edits = self.buffer_subscription.consume().into_inner();
161 let (snapshot, edits) = self.fold_map.read(snapshot, edits);
162 let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
163 let (snapshot, edits) = self
164 .wrap_map
165 .update(cx, |map, cx| map.sync(snapshot, edits, cx));
166 let mut block_map = self.block_map.write(snapshot, edits);
167 block_map.remove(ids);
168 }
169
170 pub fn highlight_text(
171 &mut self,
172 type_id: TypeId,
173 ranges: Vec<Range<Anchor>>,
174 style: HighlightStyle,
175 ) {
176 self.text_highlights
177 .insert(Some(type_id), Arc::new((style, ranges)));
178 }
179
180 pub fn clear_text_highlights(
181 &mut self,
182 type_id: TypeId,
183 ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
184 self.text_highlights.remove(&Some(type_id))
185 }
186
187 pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
188 self.wrap_map
189 .update(cx, |map, cx| map.set_font(font_id, font_size, cx));
190 }
191
192 pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
193 self.wrap_map
194 .update(cx, |map, cx| map.set_wrap_width(width, cx))
195 }
196
197 #[cfg(test)]
198 pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
199 self.wrap_map.read(cx).is_rewrapping()
200 }
201}
202
203pub struct DisplaySnapshot {
204 pub buffer_snapshot: MultiBufferSnapshot,
205 folds_snapshot: fold_map::FoldSnapshot,
206 tabs_snapshot: tab_map::TabSnapshot,
207 wraps_snapshot: wrap_map::WrapSnapshot,
208 blocks_snapshot: block_map::BlockSnapshot,
209 text_highlights: TextHighlights,
210 clip_at_line_ends: bool,
211}
212
213impl DisplaySnapshot {
214 #[cfg(test)]
215 pub fn fold_count(&self) -> usize {
216 self.folds_snapshot.fold_count()
217 }
218
219 pub fn is_empty(&self) -> bool {
220 self.buffer_snapshot.len() == 0
221 }
222
223 pub fn buffer_rows<'a>(&'a self, start_row: u32) -> DisplayBufferRows<'a> {
224 self.blocks_snapshot.buffer_rows(start_row)
225 }
226
227 pub fn max_buffer_row(&self) -> u32 {
228 self.buffer_snapshot.max_buffer_row()
229 }
230
231 pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
232 loop {
233 let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Left);
234 *fold_point.column_mut() = 0;
235 point = fold_point.to_buffer_point(&self.folds_snapshot);
236
237 let mut display_point = self.point_to_display_point(point, Bias::Left);
238 *display_point.column_mut() = 0;
239 let next_point = self.display_point_to_point(display_point, Bias::Left);
240 if next_point == point {
241 return (point, display_point);
242 }
243 point = next_point;
244 }
245 }
246
247 pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
248 loop {
249 let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Right);
250 *fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row());
251 point = fold_point.to_buffer_point(&self.folds_snapshot);
252
253 let mut display_point = self.point_to_display_point(point, Bias::Right);
254 *display_point.column_mut() = self.line_len(display_point.row());
255 let next_point = self.display_point_to_point(display_point, Bias::Right);
256 if next_point == point {
257 return (point, display_point);
258 }
259 point = next_point;
260 }
261 }
262
263 fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
264 let fold_point = self.folds_snapshot.to_fold_point(point, bias);
265 let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
266 let wrap_point = self.wraps_snapshot.from_tab_point(tab_point);
267 let block_point = self.blocks_snapshot.to_block_point(wrap_point);
268 DisplayPoint(block_point)
269 }
270
271 fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
272 let block_point = point.0;
273 let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
274 let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
275 let fold_point = self.tabs_snapshot.to_fold_point(tab_point, bias).0;
276 fold_point.to_buffer_point(&self.folds_snapshot)
277 }
278
279 pub fn max_point(&self) -> DisplayPoint {
280 DisplayPoint(self.blocks_snapshot.max_point())
281 }
282
283 pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
284 self.blocks_snapshot
285 .chunks(display_row..self.max_point().row() + 1, false, None)
286 .map(|h| h.text)
287 }
288
289 pub fn chunks<'a>(
290 &'a self,
291 display_rows: Range<u32>,
292 language_aware: bool,
293 ) -> DisplayChunks<'a> {
294 self.blocks_snapshot
295 .chunks(display_rows, language_aware, Some(&self.text_highlights))
296 }
297
298 pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
299 let mut column = 0;
300 let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
301 while column < point.column() {
302 if let Some(c) = chars.next() {
303 column += c.len_utf8() as u32;
304 } else {
305 break;
306 }
307 }
308 chars
309 }
310
311 pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
312 let mut count = 0;
313 let mut column = 0;
314 for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
315 if column >= target {
316 break;
317 }
318 count += 1;
319 column += c.len_utf8() as u32;
320 }
321 count
322 }
323
324 pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
325 let mut count = 0;
326 let mut column = 0;
327 for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
328 if c == '\n' || count >= char_count {
329 break;
330 }
331 count += 1;
332 column += c.len_utf8() as u32;
333 }
334 column
335 }
336
337 pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
338 let mut clipped = self.blocks_snapshot.clip_point(point.0, bias);
339 if self.clip_at_line_ends && clipped.column == self.line_len(clipped.row) {
340 clipped.column = clipped.column.saturating_sub(1);
341 clipped = self.blocks_snapshot.clip_point(clipped, Bias::Left);
342 }
343 DisplayPoint(clipped)
344 }
345
346 pub fn folds_in_range<'a, T>(
347 &'a self,
348 range: Range<T>,
349 ) -> impl Iterator<Item = &'a Range<Anchor>>
350 where
351 T: ToOffset,
352 {
353 self.folds_snapshot.folds_in_range(range)
354 }
355
356 pub fn blocks_in_range<'a>(
357 &'a self,
358 rows: Range<u32>,
359 ) -> impl Iterator<Item = (u32, &'a TransformBlock)> {
360 self.blocks_snapshot.blocks_in_range(rows)
361 }
362
363 pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
364 self.folds_snapshot.intersects_fold(offset)
365 }
366
367 pub fn is_line_folded(&self, display_row: u32) -> bool {
368 let block_point = BlockPoint(Point::new(display_row, 0));
369 let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
370 let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
371 self.folds_snapshot.is_line_folded(tab_point.row())
372 }
373
374 pub fn is_block_line(&self, display_row: u32) -> bool {
375 self.blocks_snapshot.is_block_line(display_row)
376 }
377
378 pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
379 let wrap_row = self
380 .blocks_snapshot
381 .to_wrap_point(BlockPoint::new(display_row, 0))
382 .row();
383 self.wraps_snapshot.soft_wrap_indent(wrap_row)
384 }
385
386 pub fn text(&self) -> String {
387 self.text_chunks(0).collect()
388 }
389
390 pub fn line(&self, display_row: u32) -> String {
391 let mut result = String::new();
392 for chunk in self.text_chunks(display_row) {
393 if let Some(ix) = chunk.find('\n') {
394 result.push_str(&chunk[0..ix]);
395 break;
396 } else {
397 result.push_str(chunk);
398 }
399 }
400 result
401 }
402
403 pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
404 let mut indent = 0;
405 let mut is_blank = true;
406 for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
407 if c == ' ' {
408 indent += 1;
409 } else {
410 is_blank = c == '\n';
411 break;
412 }
413 }
414 (indent, is_blank)
415 }
416
417 pub fn line_len(&self, row: u32) -> u32 {
418 self.blocks_snapshot.line_len(row)
419 }
420
421 pub fn longest_row(&self) -> u32 {
422 self.blocks_snapshot.longest_row()
423 }
424}
425
426#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
427pub struct DisplayPoint(BlockPoint);
428
429impl Debug for DisplayPoint {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 f.write_fmt(format_args!(
432 "DisplayPoint({}, {})",
433 self.row(),
434 self.column()
435 ))
436 }
437}
438
439impl DisplayPoint {
440 pub fn new(row: u32, column: u32) -> Self {
441 Self(BlockPoint(Point::new(row, column)))
442 }
443
444 pub fn zero() -> Self {
445 Self::new(0, 0)
446 }
447
448 pub fn is_zero(&self) -> bool {
449 self.0.is_zero()
450 }
451
452 pub fn row(self) -> u32 {
453 self.0.row
454 }
455
456 pub fn column(self) -> u32 {
457 self.0.column
458 }
459
460 pub fn row_mut(&mut self) -> &mut u32 {
461 &mut self.0.row
462 }
463
464 pub fn column_mut(&mut self) -> &mut u32 {
465 &mut self.0.column
466 }
467
468 pub fn to_point(self, map: &DisplaySnapshot) -> Point {
469 map.display_point_to_point(self, Bias::Left)
470 }
471
472 pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
473 let unblocked_point = map.blocks_snapshot.to_wrap_point(self.0);
474 let unwrapped_point = map.wraps_snapshot.to_tab_point(unblocked_point);
475 let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
476 unexpanded_point.to_buffer_offset(&map.folds_snapshot)
477 }
478}
479
480impl ToDisplayPoint for usize {
481 fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
482 map.point_to_display_point(self.to_point(&map.buffer_snapshot), Bias::Left)
483 }
484}
485
486impl ToDisplayPoint for Point {
487 fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
488 map.point_to_display_point(*self, Bias::Left)
489 }
490}
491
492impl ToDisplayPoint for Anchor {
493 fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
494 self.to_point(&map.buffer_snapshot).to_display_point(map)
495 }
496}
497
498#[cfg(test)]
499pub mod tests {
500 use super::*;
501 use crate::{
502 movement,
503 test::{marked_display_snapshot, marked_text_ranges},
504 };
505 use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
506 use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
507 use rand::{prelude::*, Rng};
508 use smol::stream::StreamExt;
509 use std::{env, sync::Arc};
510 use theme::SyntaxTheme;
511 use util::test::sample_text;
512 use Bias::*;
513
514 #[gpui::test(iterations = 100)]
515 async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
516 cx.foreground().set_block_on_ticks(0..=50);
517 cx.foreground().forbid_parking();
518 let operations = env::var("OPERATIONS")
519 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
520 .unwrap_or(10);
521
522 let font_cache = cx.font_cache().clone();
523 let tab_size = rng.gen_range(1..=4);
524 let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
525 let excerpt_header_height = rng.gen_range(1..=5);
526 let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
527 let font_id = font_cache
528 .select_font(family_id, &Default::default())
529 .unwrap();
530 let font_size = 14.0;
531 let max_wrap_width = 300.0;
532 let mut wrap_width = if rng.gen_bool(0.1) {
533 None
534 } else {
535 Some(rng.gen_range(0.0..=max_wrap_width))
536 };
537
538 log::info!("tab size: {}", tab_size);
539 log::info!("wrap width: {:?}", wrap_width);
540
541 let buffer = cx.update(|cx| {
542 if rng.gen() {
543 let len = rng.gen_range(0..10);
544 let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
545 MultiBuffer::build_simple(&text, cx)
546 } else {
547 MultiBuffer::build_random(&mut rng, cx)
548 }
549 });
550
551 let map = cx.add_model(|cx| {
552 DisplayMap::new(
553 buffer.clone(),
554 tab_size,
555 font_id,
556 font_size,
557 wrap_width,
558 buffer_start_excerpt_header_height,
559 excerpt_header_height,
560 cx,
561 )
562 });
563 let mut notifications = observe(&map, cx);
564 let mut fold_count = 0;
565 let mut blocks = Vec::new();
566
567 let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
568 log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
569 log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
570 log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
571 log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
572 log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
573 log::info!("display text: {:?}", snapshot.text());
574
575 for _i in 0..operations {
576 match rng.gen_range(0..100) {
577 0..=19 => {
578 wrap_width = if rng.gen_bool(0.2) {
579 None
580 } else {
581 Some(rng.gen_range(0.0..=max_wrap_width))
582 };
583 log::info!("setting wrap width to {:?}", wrap_width);
584 map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
585 }
586 20..=44 => {
587 map.update(cx, |map, cx| {
588 if rng.gen() || blocks.is_empty() {
589 let buffer = map.snapshot(cx).buffer_snapshot;
590 let block_properties = (0..rng.gen_range(1..=1))
591 .map(|_| {
592 let position =
593 buffer.anchor_after(buffer.clip_offset(
594 rng.gen_range(0..=buffer.len()),
595 Bias::Left,
596 ));
597
598 let disposition = if rng.gen() {
599 BlockDisposition::Above
600 } else {
601 BlockDisposition::Below
602 };
603 let height = rng.gen_range(1..5);
604 log::info!(
605 "inserting block {:?} {:?} with height {}",
606 disposition,
607 position.to_point(&buffer),
608 height
609 );
610 BlockProperties {
611 position,
612 height,
613 disposition,
614 render: Arc::new(|_| Empty::new().boxed()),
615 }
616 })
617 .collect::<Vec<_>>();
618 blocks.extend(map.insert_blocks(block_properties, cx));
619 } else {
620 blocks.shuffle(&mut rng);
621 let remove_count = rng.gen_range(1..=4.min(blocks.len()));
622 let block_ids_to_remove = (0..remove_count)
623 .map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
624 .collect();
625 log::info!("removing block ids {:?}", block_ids_to_remove);
626 map.remove_blocks(block_ids_to_remove, cx);
627 }
628 });
629 }
630 45..=79 => {
631 let mut ranges = Vec::new();
632 for _ in 0..rng.gen_range(1..=3) {
633 buffer.read_with(cx, |buffer, cx| {
634 let buffer = buffer.read(cx);
635 let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
636 let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
637 ranges.push(start..end);
638 });
639 }
640
641 if rng.gen() && fold_count > 0 {
642 log::info!("unfolding ranges: {:?}", ranges);
643 map.update(cx, |map, cx| {
644 map.unfold(ranges, cx);
645 });
646 } else {
647 log::info!("folding ranges: {:?}", ranges);
648 map.update(cx, |map, cx| {
649 map.fold(ranges, cx);
650 });
651 }
652 }
653 _ => {
654 buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 5, cx));
655 }
656 }
657
658 if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) {
659 notifications.next().await.unwrap();
660 }
661
662 let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
663 fold_count = snapshot.fold_count();
664 log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
665 log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
666 log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
667 log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
668 log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
669 log::info!("display text: {:?}", snapshot.text());
670
671 // Line boundaries
672 let buffer = &snapshot.buffer_snapshot;
673 for _ in 0..5 {
674 let row = rng.gen_range(0..=buffer.max_point().row);
675 let column = rng.gen_range(0..=buffer.line_len(row));
676 let point = buffer.clip_point(Point::new(row, column), Left);
677
678 let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
679 let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point);
680
681 assert!(prev_buffer_bound <= point);
682 assert!(next_buffer_bound >= point);
683 assert_eq!(prev_buffer_bound.column, 0);
684 assert_eq!(prev_display_bound.column(), 0);
685 if next_buffer_bound < buffer.max_point() {
686 assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n'));
687 }
688
689 assert_eq!(
690 prev_display_bound,
691 prev_buffer_bound.to_display_point(&snapshot),
692 "row boundary before {:?}. reported buffer row boundary: {:?}",
693 point,
694 prev_buffer_bound
695 );
696 assert_eq!(
697 next_display_bound,
698 next_buffer_bound.to_display_point(&snapshot),
699 "display row boundary after {:?}. reported buffer row boundary: {:?}",
700 point,
701 next_buffer_bound
702 );
703 assert_eq!(
704 prev_buffer_bound,
705 prev_display_bound.to_point(&snapshot),
706 "row boundary before {:?}. reported display row boundary: {:?}",
707 point,
708 prev_display_bound
709 );
710 assert_eq!(
711 next_buffer_bound,
712 next_display_bound.to_point(&snapshot),
713 "row boundary after {:?}. reported display row boundary: {:?}",
714 point,
715 next_display_bound
716 );
717 }
718
719 // Movement
720 let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left);
721 let max_point = snapshot.clip_point(snapshot.max_point(), Right);
722 for _ in 0..5 {
723 let row = rng.gen_range(0..=snapshot.max_point().row());
724 let column = rng.gen_range(0..=snapshot.line_len(row));
725 let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
726
727 log::info!("Moving from point {:?}", point);
728
729 let moved_right = movement::right(&snapshot, point);
730 log::info!("Right {:?}", moved_right);
731 if point < max_point {
732 assert!(moved_right > point);
733 if point.column() == snapshot.line_len(point.row())
734 || snapshot.soft_wrap_indent(point.row()).is_some()
735 && point.column() == snapshot.line_len(point.row()) - 1
736 {
737 assert!(moved_right.row() > point.row());
738 }
739 } else {
740 assert_eq!(moved_right, point);
741 }
742
743 let moved_left = movement::left(&snapshot, point);
744 log::info!("Left {:?}", moved_left);
745 if point > min_point {
746 assert!(moved_left < point);
747 if point.column() == 0 {
748 assert!(moved_left.row() < point.row());
749 }
750 } else {
751 assert_eq!(moved_left, point);
752 }
753 }
754 }
755 }
756
757 #[gpui::test(retries = 5)]
758 fn test_soft_wraps(cx: &mut MutableAppContext) {
759 cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
760 cx.foreground().forbid_parking();
761
762 let font_cache = cx.font_cache();
763
764 let tab_size = 4;
765 let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
766 let font_id = font_cache
767 .select_font(family_id, &Default::default())
768 .unwrap();
769 let font_size = 12.0;
770 let wrap_width = Some(64.);
771
772 let text = "one two three four five\nsix seven eight";
773 let buffer = MultiBuffer::build_simple(text, cx);
774 let map = cx.add_model(|cx| {
775 DisplayMap::new(
776 buffer.clone(),
777 tab_size,
778 font_id,
779 font_size,
780 wrap_width,
781 1,
782 1,
783 cx,
784 )
785 });
786
787 let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
788 assert_eq!(
789 snapshot.text_chunks(0).collect::<String>(),
790 "one two \nthree four \nfive\nsix seven \neight"
791 );
792 assert_eq!(
793 snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
794 DisplayPoint::new(0, 7)
795 );
796 assert_eq!(
797 snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
798 DisplayPoint::new(1, 0)
799 );
800 assert_eq!(
801 movement::right(&snapshot, DisplayPoint::new(0, 7)),
802 DisplayPoint::new(1, 0)
803 );
804 assert_eq!(
805 movement::left(&snapshot, DisplayPoint::new(1, 0)),
806 DisplayPoint::new(0, 7)
807 );
808 assert_eq!(
809 movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None),
810 (DisplayPoint::new(0, 7), SelectionGoal::Column(10))
811 );
812 assert_eq!(
813 movement::down(
814 &snapshot,
815 DisplayPoint::new(0, 7),
816 SelectionGoal::Column(10)
817 ),
818 (DisplayPoint::new(1, 10), SelectionGoal::Column(10))
819 );
820 assert_eq!(
821 movement::down(
822 &snapshot,
823 DisplayPoint::new(1, 10),
824 SelectionGoal::Column(10)
825 ),
826 (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
827 );
828
829 let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
830 buffer.update(cx, |buffer, cx| {
831 buffer.edit(vec![ix..ix], "and ", cx);
832 });
833
834 let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
835 assert_eq!(
836 snapshot.text_chunks(1).collect::<String>(),
837 "three four \nfive\nsix and \nseven eight"
838 );
839
840 // Re-wrap on font size changes
841 map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
842
843 let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
844 assert_eq!(
845 snapshot.text_chunks(1).collect::<String>(),
846 "three \nfour five\nsix and \nseven \neight"
847 )
848 }
849
850 #[gpui::test]
851 fn test_text_chunks(cx: &mut gpui::MutableAppContext) {
852 let text = sample_text(6, 6, 'a');
853 let buffer = MultiBuffer::build_simple(&text, cx);
854 let tab_size = 4;
855 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
856 let font_id = cx
857 .font_cache()
858 .select_font(family_id, &Default::default())
859 .unwrap();
860 let font_size = 14.0;
861 let map = cx.add_model(|cx| {
862 DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
863 });
864 buffer.update(cx, |buffer, cx| {
865 buffer.edit(
866 vec![
867 Point::new(1, 0)..Point::new(1, 0),
868 Point::new(1, 1)..Point::new(1, 1),
869 Point::new(2, 1)..Point::new(2, 1),
870 ],
871 "\t",
872 cx,
873 )
874 });
875
876 assert_eq!(
877 map.update(cx, |map, cx| map.snapshot(cx))
878 .text_chunks(1)
879 .collect::<String>()
880 .lines()
881 .next(),
882 Some(" b bbbbb")
883 );
884 assert_eq!(
885 map.update(cx, |map, cx| map.snapshot(cx))
886 .text_chunks(2)
887 .collect::<String>()
888 .lines()
889 .next(),
890 Some("c ccccc")
891 );
892 }
893
894 #[gpui::test]
895 async fn test_chunks(cx: &mut gpui::TestAppContext) {
896 use unindent::Unindent as _;
897
898 let text = r#"
899 fn outer() {}
900
901 mod module {
902 fn inner() {}
903 }"#
904 .unindent();
905
906 let theme = SyntaxTheme::new(vec![
907 ("mod.body".to_string(), Color::red().into()),
908 ("fn.name".to_string(), Color::blue().into()),
909 ]);
910 let language = Arc::new(
911 Language::new(
912 LanguageConfig {
913 name: "Test".into(),
914 path_suffixes: vec![".test".to_string()],
915 ..Default::default()
916 },
917 Some(tree_sitter_rust::language()),
918 )
919 .with_highlights_query(
920 r#"
921 (mod_item name: (identifier) body: _ @mod.body)
922 (function_item name: (identifier) @fn.name)
923 "#,
924 )
925 .unwrap(),
926 );
927 language.set_theme(&theme);
928
929 let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
930 buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
931 let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
932
933 let tab_size = 2;
934 let font_cache = cx.font_cache();
935 let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
936 let font_id = font_cache
937 .select_font(family_id, &Default::default())
938 .unwrap();
939 let font_size = 14.0;
940
941 let map = cx
942 .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
943 assert_eq!(
944 cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
945 vec![
946 ("fn ".to_string(), None),
947 ("outer".to_string(), Some(Color::blue())),
948 ("() {}\n\nmod module ".to_string(), None),
949 ("{\n fn ".to_string(), Some(Color::red())),
950 ("inner".to_string(), Some(Color::blue())),
951 ("() {}\n}".to_string(), Some(Color::red())),
952 ]
953 );
954 assert_eq!(
955 cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
956 vec![
957 (" fn ".to_string(), Some(Color::red())),
958 ("inner".to_string(), Some(Color::blue())),
959 ("() {}\n}".to_string(), Some(Color::red())),
960 ]
961 );
962
963 map.update(cx, |map, cx| {
964 map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
965 });
966 assert_eq!(
967 cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
968 vec![
969 ("fn ".to_string(), None),
970 ("out".to_string(), Some(Color::blue())),
971 ("…".to_string(), None),
972 (" fn ".to_string(), Some(Color::red())),
973 ("inner".to_string(), Some(Color::blue())),
974 ("() {}\n}".to_string(), Some(Color::red())),
975 ]
976 );
977 }
978
979 #[gpui::test]
980 async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
981 use unindent::Unindent as _;
982
983 cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
984
985 let text = r#"
986 fn outer() {}
987
988 mod module {
989 fn inner() {}
990 }"#
991 .unindent();
992
993 let theme = SyntaxTheme::new(vec![
994 ("mod.body".to_string(), Color::red().into()),
995 ("fn.name".to_string(), Color::blue().into()),
996 ]);
997 let language = Arc::new(
998 Language::new(
999 LanguageConfig {
1000 name: "Test".into(),
1001 path_suffixes: vec![".test".to_string()],
1002 ..Default::default()
1003 },
1004 Some(tree_sitter_rust::language()),
1005 )
1006 .with_highlights_query(
1007 r#"
1008 (mod_item name: (identifier) body: _ @mod.body)
1009 (function_item name: (identifier) @fn.name)
1010 "#,
1011 )
1012 .unwrap(),
1013 );
1014 language.set_theme(&theme);
1015
1016 let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
1017 buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
1018 let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
1019
1020 let font_cache = cx.font_cache();
1021
1022 let tab_size = 4;
1023 let family_id = font_cache.load_family(&["Courier"]).unwrap();
1024 let font_id = font_cache
1025 .select_font(family_id, &Default::default())
1026 .unwrap();
1027 let font_size = 16.0;
1028
1029 let map = cx.add_model(|cx| {
1030 DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
1031 });
1032 assert_eq!(
1033 cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
1034 [
1035 ("fn \n".to_string(), None),
1036 ("oute\nr".to_string(), Some(Color::blue())),
1037 ("() \n{}\n\n".to_string(), None),
1038 ]
1039 );
1040 assert_eq!(
1041 cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
1042 [("{}\n\n".to_string(), None)]
1043 );
1044
1045 map.update(cx, |map, cx| {
1046 map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
1047 });
1048 assert_eq!(
1049 cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
1050 [
1051 ("out".to_string(), Some(Color::blue())),
1052 ("…\n".to_string(), None),
1053 (" \nfn ".to_string(), Some(Color::red())),
1054 ("i\n".to_string(), Some(Color::blue()))
1055 ]
1056 );
1057 }
1058
1059 #[gpui::test]
1060 async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
1061 cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
1062
1063 let theme = SyntaxTheme::new(vec![
1064 ("operator".to_string(), Color::red().into()),
1065 ("string".to_string(), Color::green().into()),
1066 ]);
1067 let language = Arc::new(
1068 Language::new(
1069 LanguageConfig {
1070 name: "Test".into(),
1071 path_suffixes: vec![".test".to_string()],
1072 ..Default::default()
1073 },
1074 Some(tree_sitter_rust::language()),
1075 )
1076 .with_highlights_query(
1077 r#"
1078 ":" @operator
1079 (string_literal) @string
1080 "#,
1081 )
1082 .unwrap(),
1083 );
1084 language.set_theme(&theme);
1085
1086 let (text, highlighted_ranges) = marked_text_ranges(
1087 r#"const{} <a>: B = "c [d]""#,
1088 vec![('{', '}'), ('<', '>'), ('[', ']')],
1089 );
1090
1091 let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
1092 buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
1093
1094 let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
1095 let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
1096
1097 let font_cache = cx.font_cache();
1098 let tab_size = 4;
1099 let family_id = font_cache.load_family(&["Courier"]).unwrap();
1100 let font_id = font_cache
1101 .select_font(family_id, &Default::default())
1102 .unwrap();
1103 let font_size = 16.0;
1104 let map = cx
1105 .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
1106
1107 enum MyType {}
1108
1109 let style = HighlightStyle {
1110 color: Some(Color::blue()),
1111 ..Default::default()
1112 };
1113
1114 map.update(cx, |map, _cx| {
1115 map.highlight_text(
1116 TypeId::of::<MyType>(),
1117 highlighted_ranges
1118 .into_iter()
1119 .map(|range| {
1120 buffer_snapshot.anchor_before(range.start)
1121 ..buffer_snapshot.anchor_before(range.end)
1122 })
1123 .collect(),
1124 style,
1125 );
1126 });
1127
1128 assert_eq!(
1129 cx.update(|cx| chunks(0..10, &map, &theme, cx)),
1130 [
1131 ("const ".to_string(), None, None),
1132 ("a".to_string(), None, Some(Color::blue())),
1133 (":".to_string(), Some(Color::red()), None),
1134 (" B = ".to_string(), None, None),
1135 ("\"c ".to_string(), Some(Color::green()), None),
1136 ("d".to_string(), Some(Color::green()), Some(Color::blue())),
1137 ("\"".to_string(), Some(Color::green()), None),
1138 ]
1139 );
1140 }
1141
1142 #[gpui::test]
1143 fn test_clip_point(cx: &mut gpui::MutableAppContext) {
1144 fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
1145 let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
1146
1147 match bias {
1148 Bias::Left => {
1149 if shift_right {
1150 *markers[1].column_mut() += 1;
1151 }
1152
1153 assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
1154 }
1155 Bias::Right => {
1156 if shift_right {
1157 *markers[0].column_mut() += 1;
1158 }
1159
1160 assert_eq!(
1161 unmarked_snapshot.clip_point(dbg!(markers[0]), bias),
1162 markers[1]
1163 )
1164 }
1165 };
1166 }
1167
1168 use Bias::{Left, Right};
1169 assert("||α", false, Left, cx);
1170 assert("||α", true, Left, cx);
1171 assert("||α", false, Right, cx);
1172 assert("|α|", true, Right, cx);
1173 assert("||✋", false, Left, cx);
1174 assert("||✋", true, Left, cx);
1175 assert("||✋", false, Right, cx);
1176 assert("|✋|", true, Right, cx);
1177 assert("||🍐", false, Left, cx);
1178 assert("||🍐", true, Left, cx);
1179 assert("||🍐", false, Right, cx);
1180 assert("|🍐|", true, Right, cx);
1181 assert("||\t", false, Left, cx);
1182 assert("||\t", true, Left, cx);
1183 assert("||\t", false, Right, cx);
1184 assert("|\t|", true, Right, cx);
1185 assert(" ||\t", false, Left, cx);
1186 assert(" ||\t", true, Left, cx);
1187 assert(" ||\t", false, Right, cx);
1188 assert(" |\t|", true, Right, cx);
1189 assert(" ||\t", false, Left, cx);
1190 assert(" ||\t", false, Right, cx);
1191 }
1192
1193 #[gpui::test]
1194 fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
1195 fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
1196 let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
1197 unmarked_snapshot.clip_at_line_ends = true;
1198 assert_eq!(
1199 unmarked_snapshot.clip_point(markers[1], Bias::Left),
1200 markers[0]
1201 );
1202 }
1203
1204 assert("||", cx);
1205 assert("|a|", cx);
1206 assert("a|b|", cx);
1207 assert("a|α|", cx);
1208 }
1209
1210 #[gpui::test]
1211 fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
1212 let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
1213 let buffer = MultiBuffer::build_simple(text, cx);
1214 let tab_size = 4;
1215 let font_cache = cx.font_cache();
1216 let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
1217 let font_id = font_cache
1218 .select_font(family_id, &Default::default())
1219 .unwrap();
1220 let font_size = 14.0;
1221
1222 let map = cx.add_model(|cx| {
1223 DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
1224 });
1225 let map = map.update(cx, |map, cx| map.snapshot(cx));
1226 assert_eq!(map.text(), "✅ α\nβ \n🏀β γ");
1227 assert_eq!(
1228 map.text_chunks(0).collect::<String>(),
1229 "✅ α\nβ \n🏀β γ"
1230 );
1231 assert_eq!(map.text_chunks(1).collect::<String>(), "β \n🏀β γ");
1232 assert_eq!(map.text_chunks(2).collect::<String>(), "🏀β γ");
1233
1234 let point = Point::new(0, "✅\t\t".len() as u32);
1235 let display_point = DisplayPoint::new(0, "✅ ".len() as u32);
1236 assert_eq!(point.to_display_point(&map), display_point);
1237 assert_eq!(display_point.to_point(&map), point);
1238
1239 let point = Point::new(1, "β\t".len() as u32);
1240 let display_point = DisplayPoint::new(1, "β ".len() as u32);
1241 assert_eq!(point.to_display_point(&map), display_point);
1242 assert_eq!(display_point.to_point(&map), point,);
1243
1244 let point = Point::new(2, "🏀β\t\t".len() as u32);
1245 let display_point = DisplayPoint::new(2, "🏀β ".len() as u32);
1246 assert_eq!(point.to_display_point(&map), display_point);
1247 assert_eq!(display_point.to_point(&map), point,);
1248
1249 // Display points inside of expanded tabs
1250 assert_eq!(
1251 DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map),
1252 Point::new(0, "✅\t".len() as u32),
1253 );
1254 assert_eq!(
1255 DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map),
1256 Point::new(0, "✅".len() as u32),
1257 );
1258
1259 // Clipping display points inside of multi-byte characters
1260 assert_eq!(
1261 map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left),
1262 DisplayPoint::new(0, 0)
1263 );
1264 assert_eq!(
1265 map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right),
1266 DisplayPoint::new(0, "✅".len() as u32)
1267 );
1268 }
1269
1270 #[gpui::test]
1271 fn test_max_point(cx: &mut gpui::MutableAppContext) {
1272 let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
1273 let tab_size = 4;
1274 let font_cache = cx.font_cache();
1275 let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
1276 let font_id = font_cache
1277 .select_font(family_id, &Default::default())
1278 .unwrap();
1279 let font_size = 14.0;
1280 let map = cx.add_model(|cx| {
1281 DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
1282 });
1283 assert_eq!(
1284 map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
1285 DisplayPoint::new(1, 11)
1286 )
1287 }
1288
1289 fn syntax_chunks<'a>(
1290 rows: Range<u32>,
1291 map: &ModelHandle<DisplayMap>,
1292 theme: &'a SyntaxTheme,
1293 cx: &mut MutableAppContext,
1294 ) -> Vec<(String, Option<Color>)> {
1295 chunks(rows, map, theme, cx)
1296 .into_iter()
1297 .map(|(text, color, _)| (text, color))
1298 .collect()
1299 }
1300
1301 fn chunks<'a>(
1302 rows: Range<u32>,
1303 map: &ModelHandle<DisplayMap>,
1304 theme: &'a SyntaxTheme,
1305 cx: &mut MutableAppContext,
1306 ) -> Vec<(String, Option<Color>, Option<Color>)> {
1307 let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
1308 let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
1309 for chunk in snapshot.chunks(rows, true) {
1310 let syntax_color = chunk
1311 .syntax_highlight_id
1312 .and_then(|id| id.style(theme)?.color);
1313 let highlight_color = chunk.highlight_style.and_then(|style| style.color);
1314 if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
1315 if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
1316 last_chunk.push_str(chunk.text);
1317 continue;
1318 }
1319 }
1320 chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
1321 }
1322 chunks
1323 }
1324}