1mod fold_map;
2
3use crate::settings::StyleId;
4
5use super::{buffer, Anchor, Bias, Buffer, Edit, Point, ToOffset, ToPoint};
6pub use fold_map::BufferRows;
7use fold_map::{FoldMap, FoldMapSnapshot};
8use gpui::{AppContext, ModelHandle};
9use std::{mem, ops::Range};
10
11pub struct DisplayMap {
12 buffer: ModelHandle<Buffer>,
13 fold_map: FoldMap,
14 tab_size: usize,
15}
16
17impl DisplayMap {
18 pub fn new(buffer: ModelHandle<Buffer>, tab_size: usize, ctx: &AppContext) -> Self {
19 DisplayMap {
20 buffer: buffer.clone(),
21 fold_map: FoldMap::new(buffer, ctx),
22 tab_size,
23 }
24 }
25
26 pub fn snapshot(&self, ctx: &AppContext) -> DisplayMapSnapshot {
27 DisplayMapSnapshot {
28 folds_snapshot: self.fold_map.snapshot(ctx),
29 tab_size: self.tab_size,
30 }
31 }
32
33 pub fn folds_in_range<'a, T>(
34 &'a self,
35 range: Range<T>,
36 app: &'a AppContext,
37 ) -> impl Iterator<Item = &'a Range<Anchor>>
38 where
39 T: ToOffset,
40 {
41 self.fold_map.folds_in_range(range, app)
42 }
43
44 pub fn fold<T: ToOffset>(
45 &mut self,
46 ranges: impl IntoIterator<Item = Range<T>>,
47 ctx: &AppContext,
48 ) {
49 self.fold_map.fold(ranges, ctx)
50 }
51
52 pub fn unfold<T: ToOffset>(
53 &mut self,
54 ranges: impl IntoIterator<Item = Range<T>>,
55 ctx: &AppContext,
56 ) {
57 self.fold_map.unfold(ranges, ctx)
58 }
59
60 pub fn intersects_fold<T: ToOffset>(&self, offset: T, ctx: &AppContext) -> bool {
61 self.fold_map.intersects_fold(offset, ctx)
62 }
63
64 pub fn is_line_folded(&self, display_row: u32, ctx: &AppContext) -> bool {
65 self.fold_map.is_line_folded(display_row, ctx)
66 }
67
68 pub fn text(&self, ctx: &AppContext) -> String {
69 self.snapshot(ctx).chunks_at(DisplayPoint::zero()).collect()
70 }
71
72 pub fn line(&self, display_row: u32, ctx: &AppContext) -> String {
73 let mut result = String::new();
74 for chunk in self
75 .snapshot(ctx)
76 .chunks_at(DisplayPoint::new(display_row, 0))
77 {
78 if let Some(ix) = chunk.find('\n') {
79 result.push_str(&chunk[0..ix]);
80 break;
81 } else {
82 result.push_str(chunk);
83 }
84 }
85 result
86 }
87
88 pub fn line_indent(&self, display_row: u32, ctx: &AppContext) -> (u32, bool) {
89 let mut indent = 0;
90 let mut is_blank = true;
91 for c in self
92 .snapshot(ctx)
93 .chars_at(DisplayPoint::new(display_row, 0))
94 {
95 if c == ' ' {
96 indent += 1;
97 } else {
98 is_blank = c == '\n';
99 break;
100 }
101 }
102 (indent, is_blank)
103 }
104
105 pub fn line_len(&self, row: u32, ctx: &AppContext) -> u32 {
106 DisplayPoint::new(row, self.fold_map.line_len(row, ctx))
107 .expand_tabs(self, ctx)
108 .column()
109 }
110
111 pub fn max_point(&self, ctx: &AppContext) -> DisplayPoint {
112 self.snapshot(ctx).max_point().expand_tabs(self, ctx)
113 }
114
115 pub fn longest_row(&self, ctx: &AppContext) -> u32 {
116 self.fold_map.longest_row(ctx)
117 }
118
119 pub fn anchor_before(&self, point: DisplayPoint, bias: Bias, app: &AppContext) -> Anchor {
120 self.buffer
121 .read(app)
122 .anchor_before(point.to_buffer_point(self, bias, app))
123 }
124
125 pub fn anchor_after(&self, point: DisplayPoint, bias: Bias, app: &AppContext) -> Anchor {
126 self.buffer
127 .read(app)
128 .anchor_after(point.to_buffer_point(self, bias, app))
129 }
130}
131
132pub struct DisplayMapSnapshot {
133 folds_snapshot: FoldMapSnapshot,
134 tab_size: usize,
135}
136
137impl DisplayMapSnapshot {
138 pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
139 self.folds_snapshot.buffer_rows(start_row)
140 }
141
142 pub fn max_point(&self) -> DisplayPoint {
143 self.expand_tabs(self.folds_snapshot.max_point())
144 }
145
146 pub fn chunks_at(&self, point: DisplayPoint) -> Chunks {
147 let (point, expanded_char_column, to_next_stop) = self.collapse_tabs(point, Bias::Left);
148 let fold_chunks = self
149 .folds_snapshot
150 .chunks_at(self.folds_snapshot.to_display_offset(point));
151 Chunks {
152 fold_chunks,
153 column: expanded_char_column,
154 tab_size: self.tab_size,
155 chunk: &SPACES[0..to_next_stop],
156 skip_leading_tab: to_next_stop > 0,
157 }
158 }
159
160 pub fn highlighted_chunks_for_rows(&mut self, rows: Range<u32>) -> HighlightedChunks {
161 let start = DisplayPoint::new(rows.start, 0);
162 let start = self.folds_snapshot.to_display_offset(start);
163 let end = DisplayPoint::new(rows.end, 0).min(self.max_point());
164 let end = self.folds_snapshot.to_display_offset(end);
165 HighlightedChunks {
166 fold_chunks: self.folds_snapshot.highlighted_chunks(start..end),
167 column: 0,
168 tab_size: self.tab_size,
169 chunk: "",
170 style_id: Default::default(),
171 }
172 }
173
174 pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
175 self.chunks_at(point).flat_map(str::chars)
176 }
177
178 pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
179 let mut count = 0;
180 let mut column = 0;
181 for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
182 if column >= target {
183 break;
184 }
185 count += 1;
186 column += c.len_utf8() as u32;
187 }
188 count
189 }
190
191 pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
192 let mut count = 0;
193 let mut column = 0;
194 for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
195 if c == '\n' || count >= char_count {
196 break;
197 }
198 count += 1;
199 column += c.len_utf8() as u32;
200 }
201 column
202 }
203
204 pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
205 self.expand_tabs(
206 self.folds_snapshot
207 .clip_point(self.collapse_tabs(point, bias).0, bias),
208 )
209 }
210
211 fn expand_tabs(&self, mut point: DisplayPoint) -> DisplayPoint {
212 let chars = self
213 .folds_snapshot
214 .chars_at(DisplayPoint(Point::new(point.row(), 0)));
215 let expanded = expand_tabs(chars, point.column() as usize, self.tab_size);
216 *point.column_mut() = expanded as u32;
217 point
218 }
219
220 fn collapse_tabs(&self, mut point: DisplayPoint, bias: Bias) -> (DisplayPoint, usize, usize) {
221 let chars = self
222 .folds_snapshot
223 .chars_at(DisplayPoint(Point::new(point.row(), 0)));
224 let expanded = point.column() as usize;
225 let (collapsed, expanded_char_column, to_next_stop) =
226 collapse_tabs(chars, expanded, bias, self.tab_size);
227 *point.column_mut() = collapsed as u32;
228 (point, expanded_char_column, to_next_stop)
229 }
230}
231
232#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
233pub struct DisplayPoint(Point);
234
235impl DisplayPoint {
236 pub fn new(row: u32, column: u32) -> Self {
237 Self(Point::new(row, column))
238 }
239
240 pub fn zero() -> Self {
241 Self::new(0, 0)
242 }
243
244 pub fn row(self) -> u32 {
245 self.0.row
246 }
247
248 pub fn column(self) -> u32 {
249 self.0.column
250 }
251
252 pub fn row_mut(&mut self) -> &mut u32 {
253 &mut self.0.row
254 }
255
256 pub fn column_mut(&mut self) -> &mut u32 {
257 &mut self.0.column
258 }
259
260 pub fn to_buffer_point(self, map: &DisplayMap, bias: Bias, ctx: &AppContext) -> Point {
261 map.fold_map
262 .to_buffer_point(self.collapse_tabs(map, bias, ctx), ctx)
263 }
264
265 pub fn to_buffer_offset(self, map: &DisplayMap, bias: Bias, ctx: &AppContext) -> usize {
266 map.fold_map
267 .to_buffer_offset(self.collapse_tabs(&map, bias, ctx), ctx)
268 }
269
270 fn expand_tabs(self, map: &DisplayMap, ctx: &AppContext) -> Self {
271 map.snapshot(ctx).expand_tabs(self)
272 }
273
274 fn collapse_tabs(self, map: &DisplayMap, bias: Bias, ctx: &AppContext) -> Self {
275 map.snapshot(ctx).collapse_tabs(self, bias).0
276 }
277}
278
279impl Point {
280 pub fn to_display_point(self, map: &DisplayMap, ctx: &AppContext) -> DisplayPoint {
281 let mut display_point = map.fold_map.to_display_point(self, ctx);
282 let snapshot = map.fold_map.snapshot(ctx);
283 let chars = snapshot.chars_at(DisplayPoint::new(display_point.row(), 0));
284 *display_point.column_mut() =
285 expand_tabs(chars, display_point.column() as usize, map.tab_size) as u32;
286 display_point
287 }
288}
289
290impl Anchor {
291 pub fn to_display_point(&self, map: &DisplayMap, app: &AppContext) -> DisplayPoint {
292 self.to_point(map.buffer.read(app))
293 .to_display_point(map, app)
294 }
295}
296
297// Handles a tab width <= 16
298const SPACES: &'static str = " ";
299
300pub struct Chunks<'a> {
301 fold_chunks: fold_map::Chunks<'a>,
302 chunk: &'a str,
303 column: usize,
304 tab_size: usize,
305 skip_leading_tab: bool,
306}
307
308impl<'a> Iterator for Chunks<'a> {
309 type Item = &'a str;
310
311 fn next(&mut self) -> Option<Self::Item> {
312 if self.chunk.is_empty() {
313 if let Some(chunk) = self.fold_chunks.next() {
314 self.chunk = chunk;
315 if self.skip_leading_tab {
316 self.chunk = &self.chunk[1..];
317 self.skip_leading_tab = false;
318 }
319 } else {
320 return None;
321 }
322 }
323
324 for (ix, c) in self.chunk.char_indices() {
325 match c {
326 '\t' => {
327 if ix > 0 {
328 let (prefix, suffix) = self.chunk.split_at(ix);
329 self.chunk = suffix;
330 return Some(prefix);
331 } else {
332 self.chunk = &self.chunk[1..];
333 let len = self.tab_size - self.column % self.tab_size;
334 self.column += len;
335 return Some(&SPACES[0..len]);
336 }
337 }
338 '\n' => self.column = 0,
339 _ => self.column += 1,
340 }
341 }
342
343 let result = Some(self.chunk);
344 self.chunk = "";
345 result
346 }
347}
348
349pub struct HighlightedChunks<'a> {
350 fold_chunks: fold_map::HighlightedChunks<'a>,
351 chunk: &'a str,
352 style_id: StyleId,
353 column: usize,
354 tab_size: usize,
355}
356
357impl<'a> Iterator for HighlightedChunks<'a> {
358 type Item = (&'a str, StyleId);
359
360 fn next(&mut self) -> Option<Self::Item> {
361 if self.chunk.is_empty() {
362 if let Some((chunk, style_id)) = self.fold_chunks.next() {
363 self.chunk = chunk;
364 self.style_id = style_id;
365 } else {
366 return None;
367 }
368 }
369
370 for (ix, c) in self.chunk.char_indices() {
371 match c {
372 '\t' => {
373 if ix > 0 {
374 let (prefix, suffix) = self.chunk.split_at(ix);
375 self.chunk = suffix;
376 return Some((prefix, self.style_id));
377 } else {
378 self.chunk = &self.chunk[1..];
379 let len = self.tab_size - self.column % self.tab_size;
380 self.column += len;
381 return Some((&SPACES[0..len], self.style_id));
382 }
383 }
384 '\n' => self.column = 0,
385 _ => self.column += 1,
386 }
387 }
388
389 Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
390 }
391}
392
393pub fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
394 let mut expanded_chars = 0;
395 let mut expanded_bytes = 0;
396 let mut collapsed_bytes = 0;
397 for c in chars {
398 if collapsed_bytes == column {
399 break;
400 }
401 if c == '\t' {
402 let tab_len = tab_size - expanded_chars % tab_size;
403 expanded_bytes += tab_len;
404 expanded_chars += tab_len;
405 } else {
406 expanded_bytes += c.len_utf8();
407 expanded_chars += 1;
408 }
409 collapsed_bytes += c.len_utf8();
410 }
411 expanded_bytes
412}
413
414pub fn collapse_tabs(
415 mut chars: impl Iterator<Item = char>,
416 column: usize,
417 bias: Bias,
418 tab_size: usize,
419) -> (usize, usize, usize) {
420 let mut expanded_bytes = 0;
421 let mut expanded_chars = 0;
422 let mut collapsed_bytes = 0;
423 while let Some(c) = chars.next() {
424 if expanded_bytes >= column {
425 break;
426 }
427
428 if c == '\t' {
429 let tab_len = tab_size - (expanded_chars % tab_size);
430 expanded_chars += tab_len;
431 expanded_bytes += tab_len;
432 if expanded_bytes > column {
433 expanded_chars -= expanded_bytes - column;
434 return match bias {
435 Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
436 Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
437 };
438 }
439 } else {
440 expanded_chars += 1;
441 expanded_bytes += c.len_utf8();
442 }
443
444 if expanded_bytes > column && matches!(bias, Bias::Left) {
445 expanded_chars -= 1;
446 break;
447 }
448
449 collapsed_bytes += c.len_utf8();
450 }
451 (collapsed_bytes, expanded_chars, 0)
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::{
458 language::{Language, LanguageConfig},
459 settings::Theme,
460 test::*,
461 };
462 use buffer::History;
463 use std::sync::Arc;
464
465 #[gpui::test]
466 fn test_chunks_at(app: &mut gpui::MutableAppContext) {
467 let text = sample_text(6, 6);
468 let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
469 let map = DisplayMap::new(buffer.clone(), 4, app.as_ref());
470 buffer
471 .update(app, |buffer, ctx| {
472 buffer.edit(
473 vec![
474 Point::new(1, 0)..Point::new(1, 0),
475 Point::new(1, 1)..Point::new(1, 1),
476 Point::new(2, 1)..Point::new(2, 1),
477 ],
478 "\t",
479 Some(ctx),
480 )
481 })
482 .unwrap();
483
484 assert_eq!(
485 &map.snapshot(app.as_ref())
486 .chunks_at(DisplayPoint::new(1, 0))
487 .collect::<String>()[0..10],
488 " b bb"
489 );
490 assert_eq!(
491 &map.snapshot(app.as_ref())
492 .chunks_at(DisplayPoint::new(1, 2))
493 .collect::<String>()[0..10],
494 " b bbbb"
495 );
496 assert_eq!(
497 &map.snapshot(app.as_ref())
498 .chunks_at(DisplayPoint::new(1, 6))
499 .collect::<String>()[0..13],
500 " bbbbb\nc c"
501 );
502 }
503
504 #[gpui::test]
505 async fn test_highlighted_chunks_at(mut app: gpui::TestAppContext) {
506 use unindent::Unindent as _;
507
508 let grammar = tree_sitter_rust::language();
509 let text = r#"
510 fn outer() {}
511
512 mod module {
513 fn inner() {}
514 }"#
515 .unindent();
516 let highlight_query = tree_sitter::Query::new(
517 grammar,
518 r#"
519 (mod_item name: (identifier) body: _ @mod.body)
520 (function_item name: (identifier) @fn.name)"#,
521 )
522 .unwrap();
523 let theme = Theme::parse(
524 r#"
525 [syntax]
526 "mod.body" = 0xff0000
527 "fn.name" = 0x00ff00"#,
528 )
529 .unwrap();
530 let lang = Arc::new(Language {
531 config: LanguageConfig {
532 name: "Test".to_string(),
533 path_suffixes: vec![".test".to_string()],
534 ..Default::default()
535 },
536 grammar: grammar.clone(),
537 highlight_query,
538 theme_mapping: Default::default(),
539 });
540 lang.set_theme(&theme);
541
542 let buffer = app.add_model(|ctx| {
543 Buffer::from_history(0, History::new(text.into()), None, Some(lang), ctx)
544 });
545 buffer.condition(&app, |buf, _| !buf.is_parsing()).await;
546
547 let mut map = app.read(|ctx| DisplayMap::new(buffer, 2, ctx));
548 assert_eq!(
549 app.read(|ctx| highlighted_chunks(0..5, &map, &theme, ctx)),
550 vec![
551 ("fn ".to_string(), None),
552 ("outer".to_string(), Some("fn.name")),
553 ("() {}\n\nmod module ".to_string(), None),
554 ("{\n fn ".to_string(), Some("mod.body")),
555 ("inner".to_string(), Some("fn.name")),
556 ("() {}\n}".to_string(), Some("mod.body")),
557 ]
558 );
559 assert_eq!(
560 app.read(|ctx| highlighted_chunks(3..5, &map, &theme, ctx)),
561 vec![
562 (" fn ".to_string(), Some("mod.body")),
563 ("inner".to_string(), Some("fn.name")),
564 ("() {}\n}".to_string(), Some("mod.body")),
565 ]
566 );
567
568 app.read(|ctx| map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], ctx));
569 assert_eq!(
570 app.read(|ctx| highlighted_chunks(0..2, &map, &theme, ctx)),
571 vec![
572 ("fn ".to_string(), None),
573 ("out".to_string(), Some("fn.name")),
574 ("…".to_string(), None),
575 (" fn ".to_string(), Some("mod.body")),
576 ("inner".to_string(), Some("fn.name")),
577 ("() {}\n}".to_string(), Some("mod.body")),
578 ]
579 );
580
581 fn highlighted_chunks<'a>(
582 rows: Range<u32>,
583 map: &DisplayMap,
584 theme: &'a Theme,
585 ctx: &AppContext,
586 ) -> Vec<(String, Option<&'a str>)> {
587 let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
588 for (chunk, style_id) in map.snapshot(ctx).highlighted_chunks_for_rows(rows) {
589 let style_name = theme.syntax_style_name(style_id);
590 if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
591 if style_name == *last_style_name {
592 last_chunk.push_str(chunk);
593 } else {
594 chunks.push((chunk.to_string(), style_name));
595 }
596 } else {
597 chunks.push((chunk.to_string(), style_name));
598 }
599 }
600 chunks
601 }
602 }
603
604 #[gpui::test]
605 fn test_clip_point(app: &mut gpui::MutableAppContext) {
606 use Bias::{Left, Right};
607
608 let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
609 let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n";
610 let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
611 let ctx = app.as_ref();
612 let map = DisplayMap::new(buffer.clone(), 4, ctx);
613 assert_eq!(map.text(ctx), display_text);
614
615 let map = map.snapshot(ctx);
616 for (input_column, bias, output_column) in vec![
617 ("'a', '".len(), Left, "'a', '".len()),
618 ("'a', '".len() + 1, Left, "'a', '".len()),
619 ("'a', '".len() + 1, Right, "'a', 'α".len()),
620 ("'a', 'α', ".len(), Left, "'a', 'α',".len()),
621 ("'a', 'α', ".len(), Right, "'a', 'α', ".len()),
622 ("'a', 'α', '".len() + 1, Left, "'a', 'α', '".len()),
623 ("'a', 'α', '".len() + 1, Right, "'a', 'α', '✋".len()),
624 ("'a', 'α', '✋',".len(), Right, "'a', 'α', '✋',".len()),
625 ("'a', 'α', '✋', ".len(), Left, "'a', 'α', '✋',".len()),
626 (
627 "'a', 'α', '✋', ".len(),
628 Right,
629 "'a', 'α', '✋', ".len(),
630 ),
631 ] {
632 assert_eq!(
633 map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
634 DisplayPoint::new(1, output_column as u32),
635 "clip_point(({}, {}))",
636 1,
637 input_column,
638 );
639 }
640 }
641
642 #[test]
643 fn test_expand_tabs() {
644 assert_eq!(expand_tabs("\t".chars(), 0, 4), 0);
645 assert_eq!(expand_tabs("\t".chars(), 1, 4), 4);
646 assert_eq!(expand_tabs("\ta".chars(), 2, 4), 5);
647 }
648
649 #[gpui::test]
650 fn test_tabs_with_multibyte_chars(app: &mut gpui::MutableAppContext) {
651 let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
652 let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
653 let ctx = app.as_ref();
654 let map = DisplayMap::new(buffer.clone(), 4, ctx);
655 assert_eq!(map.text(ctx), "✅ α\nβ \n🏀β γ");
656
657 let point = Point::new(0, "✅\t\t".len() as u32);
658 let display_point = DisplayPoint::new(0, "✅ ".len() as u32);
659 assert_eq!(point.to_display_point(&map, ctx), display_point);
660 assert_eq!(display_point.to_buffer_point(&map, Bias::Left, ctx), point,);
661
662 let point = Point::new(1, "β\t".len() as u32);
663 let display_point = DisplayPoint::new(1, "β ".len() as u32);
664 assert_eq!(point.to_display_point(&map, ctx), display_point);
665 assert_eq!(display_point.to_buffer_point(&map, Bias::Left, ctx), point,);
666
667 let point = Point::new(2, "🏀β\t\t".len() as u32);
668 let display_point = DisplayPoint::new(2, "🏀β ".len() as u32);
669 assert_eq!(point.to_display_point(&map, ctx), display_point);
670 assert_eq!(display_point.to_buffer_point(&map, Bias::Left, ctx), point,);
671
672 // Display points inside of expanded tabs
673 assert_eq!(
674 DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Bias::Right, ctx),
675 Point::new(0, "✅\t\t".len() as u32),
676 );
677 assert_eq!(
678 DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Bias::Left, ctx),
679 Point::new(0, "✅\t".len() as u32),
680 );
681 assert_eq!(
682 map.snapshot(ctx)
683 .chunks_at(DisplayPoint::new(0, "✅ ".len() as u32))
684 .collect::<String>(),
685 " α\nβ \n🏀β γ"
686 );
687 assert_eq!(
688 DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Bias::Right, ctx),
689 Point::new(0, "✅\t".len() as u32),
690 );
691 assert_eq!(
692 DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Bias::Left, ctx),
693 Point::new(0, "✅".len() as u32),
694 );
695 assert_eq!(
696 map.snapshot(ctx)
697 .chunks_at(DisplayPoint::new(0, "✅ ".len() as u32))
698 .collect::<String>(),
699 " α\nβ \n🏀β γ"
700 );
701
702 // Clipping display points inside of multi-byte characters
703 assert_eq!(
704 map.snapshot(ctx)
705 .clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Left),
706 DisplayPoint::new(0, 0)
707 );
708 assert_eq!(
709 map.snapshot(ctx)
710 .clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right),
711 DisplayPoint::new(0, "✅".len() as u32)
712 );
713 }
714
715 #[gpui::test]
716 fn test_max_point(app: &mut gpui::MutableAppContext) {
717 let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx));
718 let map = DisplayMap::new(buffer.clone(), 4, app.as_ref());
719 assert_eq!(map.max_point(app.as_ref()), DisplayPoint::new(1, 11))
720 }
721}