movement.rs

  1use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
  2use crate::{char_kind, CharKind, ToOffset, ToPoint};
  3use language::Point;
  4use std::ops::Range;
  5
  6#[derive(Debug, PartialEq)]
  7pub enum FindRange {
  8    SingleLine,
  9    MultiLine,
 10}
 11
 12pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 13    if point.column() > 0 {
 14        *point.column_mut() -= 1;
 15    } else if point.row() > 0 {
 16        *point.row_mut() -= 1;
 17        *point.column_mut() = map.line_len(point.row());
 18    }
 19    map.clip_point(point, Bias::Left)
 20}
 21
 22pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 23    if point.column() > 0 {
 24        *point.column_mut() -= 1;
 25    }
 26    map.clip_point(point, Bias::Left)
 27}
 28
 29pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 30    let max_column = map.line_len(point.row());
 31    if point.column() < max_column {
 32        *point.column_mut() += 1;
 33    } else if point.row() < map.max_point().row() {
 34        *point.row_mut() += 1;
 35        *point.column_mut() = 0;
 36    }
 37    map.clip_point(point, Bias::Right)
 38}
 39
 40pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 41    *point.column_mut() += 1;
 42    map.clip_point(point, Bias::Right)
 43}
 44
 45pub fn up(
 46    map: &DisplaySnapshot,
 47    start: DisplayPoint,
 48    goal: SelectionGoal,
 49    preserve_column_at_start: bool,
 50) -> (DisplayPoint, SelectionGoal) {
 51    up_by_rows(map, start, 1, goal, preserve_column_at_start)
 52}
 53
 54pub fn down(
 55    map: &DisplaySnapshot,
 56    start: DisplayPoint,
 57    goal: SelectionGoal,
 58    preserve_column_at_end: bool,
 59) -> (DisplayPoint, SelectionGoal) {
 60    down_by_rows(map, start, 1, goal, preserve_column_at_end)
 61}
 62
 63pub fn up_by_rows(
 64    map: &DisplaySnapshot,
 65    start: DisplayPoint,
 66    row_count: u32,
 67    goal: SelectionGoal,
 68    preserve_column_at_start: bool,
 69) -> (DisplayPoint, SelectionGoal) {
 70    let mut goal_column = match goal {
 71        SelectionGoal::Column(column) => column,
 72        SelectionGoal::ColumnRange { end, .. } => end,
 73        _ => map.column_to_chars(start.row(), start.column()),
 74    };
 75
 76    let prev_row = start.row().saturating_sub(row_count);
 77    let mut point = map.clip_point(
 78        DisplayPoint::new(prev_row, map.line_len(prev_row)),
 79        Bias::Left,
 80    );
 81    if point.row() < start.row() {
 82        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
 83    } else if preserve_column_at_start {
 84        return (start, goal);
 85    } else {
 86        point = DisplayPoint::new(0, 0);
 87        goal_column = 0;
 88    }
 89
 90    let mut clipped_point = map.clip_point(point, Bias::Left);
 91    if clipped_point.row() < point.row() {
 92        clipped_point = map.clip_point(point, Bias::Right);
 93    }
 94    (clipped_point, SelectionGoal::Column(goal_column))
 95}
 96
 97pub fn down_by_rows(
 98    map: &DisplaySnapshot,
 99    start: DisplayPoint,
100    row_count: u32,
101    goal: SelectionGoal,
102    preserve_column_at_end: bool,
103) -> (DisplayPoint, SelectionGoal) {
104    let mut goal_column = match goal {
105        SelectionGoal::Column(column) => column,
106        SelectionGoal::ColumnRange { end, .. } => end,
107        _ => map.column_to_chars(start.row(), start.column()),
108    };
109
110    let new_row = start.row() + row_count;
111    let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
112    if point.row() > start.row() {
113        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
114    } else if preserve_column_at_end {
115        return (start, goal);
116    } else {
117        point = map.max_point();
118        goal_column = map.column_to_chars(point.row(), point.column())
119    }
120
121    let mut clipped_point = map.clip_point(point, Bias::Right);
122    if clipped_point.row() > point.row() {
123        clipped_point = map.clip_point(point, Bias::Left);
124    }
125    (clipped_point, SelectionGoal::Column(goal_column))
126}
127
128pub fn line_beginning(
129    map: &DisplaySnapshot,
130    display_point: DisplayPoint,
131    stop_at_soft_boundaries: bool,
132) -> DisplayPoint {
133    let point = display_point.to_point(map);
134    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
135    let line_start = map.prev_line_boundary(point).1;
136
137    if stop_at_soft_boundaries && display_point != soft_line_start {
138        soft_line_start
139    } else {
140        line_start
141    }
142}
143
144pub fn indented_line_beginning(
145    map: &DisplaySnapshot,
146    display_point: DisplayPoint,
147    stop_at_soft_boundaries: bool,
148) -> DisplayPoint {
149    let point = display_point.to_point(map);
150    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
151    let indent_start = Point::new(
152        point.row,
153        map.buffer_snapshot.indent_size_for_line(point.row).len,
154    )
155    .to_display_point(map);
156    let line_start = map.prev_line_boundary(point).1;
157
158    if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
159    {
160        soft_line_start
161    } else if stop_at_soft_boundaries && display_point != indent_start {
162        indent_start
163    } else {
164        line_start
165    }
166}
167
168pub fn line_end(
169    map: &DisplaySnapshot,
170    display_point: DisplayPoint,
171    stop_at_soft_boundaries: bool,
172) -> DisplayPoint {
173    let soft_line_end = map.clip_point(
174        DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
175        Bias::Left,
176    );
177    if stop_at_soft_boundaries && display_point != soft_line_end {
178        soft_line_end
179    } else {
180        map.next_line_boundary(display_point.to_point(map)).1
181    }
182}
183
184pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
185    let raw_point = point.to_point(map);
186    let scope = map.buffer_snapshot.language_scope_at(raw_point);
187
188    find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
189        (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
190            || left == '\n'
191    })
192}
193
194pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
195    let raw_point = point.to_point(map);
196    let scope = map.buffer_snapshot.language_scope_at(raw_point);
197
198    find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
199        let is_word_start =
200            char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
201        let is_subword_start =
202            left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
203        is_word_start || is_subword_start || left == '\n'
204    })
205}
206
207pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
208    let raw_point = point.to_point(map);
209    let scope = map.buffer_snapshot.language_scope_at(raw_point);
210
211    find_boundary(map, point, FindRange::MultiLine, |left, right| {
212        (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
213            || right == '\n'
214    })
215}
216
217pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
218    let raw_point = point.to_point(map);
219    let scope = map.buffer_snapshot.language_scope_at(raw_point);
220
221    find_boundary(map, point, FindRange::MultiLine, |left, right| {
222        let is_word_end =
223            (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
224        let is_subword_end =
225            left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
226        is_word_end || is_subword_end || right == '\n'
227    })
228}
229
230pub fn start_of_paragraph(
231    map: &DisplaySnapshot,
232    display_point: DisplayPoint,
233    mut count: usize,
234) -> DisplayPoint {
235    let point = display_point.to_point(map);
236    if point.row == 0 {
237        return DisplayPoint::zero();
238    }
239
240    let mut found_non_blank_line = false;
241    for row in (0..point.row + 1).rev() {
242        let blank = map.buffer_snapshot.is_line_blank(row);
243        if found_non_blank_line && blank {
244            if count <= 1 {
245                return Point::new(row, 0).to_display_point(map);
246            }
247            count -= 1;
248            found_non_blank_line = false;
249        }
250
251        found_non_blank_line |= !blank;
252    }
253
254    DisplayPoint::zero()
255}
256
257pub fn end_of_paragraph(
258    map: &DisplaySnapshot,
259    display_point: DisplayPoint,
260    mut count: usize,
261) -> DisplayPoint {
262    let point = display_point.to_point(map);
263    if point.row == map.max_buffer_row() {
264        return map.max_point();
265    }
266
267    let mut found_non_blank_line = false;
268    for row in point.row..map.max_buffer_row() + 1 {
269        let blank = map.buffer_snapshot.is_line_blank(row);
270        if found_non_blank_line && blank {
271            if count <= 1 {
272                return Point::new(row, 0).to_display_point(map);
273            }
274            count -= 1;
275            found_non_blank_line = false;
276        }
277
278        found_non_blank_line |= !blank;
279    }
280
281    map.max_point()
282}
283
284/// Scans for a boundary preceding the given start point `from` until a boundary is found,
285/// indicated by the given predicate returning true.
286/// The predicate is called with the character to the left and right of the candidate boundary location.
287/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned.
288pub fn find_preceding_boundary(
289    map: &DisplaySnapshot,
290    from: DisplayPoint,
291    find_range: FindRange,
292    mut is_boundary: impl FnMut(char, char) -> bool,
293) -> DisplayPoint {
294    let mut prev_ch = None;
295    let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
296
297    for ch in map.buffer_snapshot.reversed_chars_at(offset) {
298        if find_range == FindRange::SingleLine && ch == '\n' {
299            break;
300        }
301        if let Some(prev_ch) = prev_ch {
302            if is_boundary(ch, prev_ch) {
303                break;
304            }
305        }
306
307        offset -= ch.len_utf8();
308        prev_ch = Some(ch);
309    }
310
311    map.clip_point(offset.to_display_point(map), Bias::Left)
312}
313
314/// Scans for a boundary following the given start point until a boundary is found, indicated by the
315/// given predicate returning true. The predicate is called with the character to the left and right
316/// of the candidate boundary location, and will be called with `\n` characters indicating the start
317/// or end of a line.
318pub fn find_boundary(
319    map: &DisplaySnapshot,
320    from: DisplayPoint,
321    find_range: FindRange,
322    mut is_boundary: impl FnMut(char, char) -> bool,
323) -> DisplayPoint {
324    let mut offset = from.to_offset(&map, Bias::Right);
325    let mut prev_ch = None;
326
327    for ch in map.buffer_snapshot.chars_at(offset) {
328        if find_range == FindRange::SingleLine && ch == '\n' {
329            break;
330        }
331        if let Some(prev_ch) = prev_ch {
332            if is_boundary(prev_ch, ch) {
333                break;
334            }
335        }
336
337        offset += ch.len_utf8();
338        prev_ch = Some(ch);
339    }
340    map.clip_point(offset.to_display_point(map), Bias::Right)
341}
342
343pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
344    let raw_point = point.to_point(map);
345    let scope = map.buffer_snapshot.language_scope_at(raw_point);
346    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
347    let text = &map.buffer_snapshot;
348    let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
349    let prev_char_kind = text
350        .reversed_chars_at(ix)
351        .next()
352        .map(|c| char_kind(&scope, c));
353    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
354}
355
356pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
357    let position = map
358        .clip_point(position, Bias::Left)
359        .to_offset(map, Bias::Left);
360    let (range, _) = map.buffer_snapshot.surrounding_word(position);
361    let start = range
362        .start
363        .to_point(&map.buffer_snapshot)
364        .to_display_point(map);
365    let end = range
366        .end
367        .to_point(&map.buffer_snapshot)
368        .to_display_point(map);
369    start..end
370}
371
372pub fn split_display_range_by_lines(
373    map: &DisplaySnapshot,
374    range: Range<DisplayPoint>,
375) -> Vec<Range<DisplayPoint>> {
376    let mut result = Vec::new();
377
378    let mut start = range.start;
379    // Loop over all the covered rows until the one containing the range end
380    for row in range.start.row()..range.end.row() {
381        let row_end_column = map.line_len(row);
382        let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
383        if start != end {
384            result.push(start..end);
385        }
386        start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
387    }
388
389    // Add the final range from the start of the last end to the original range end.
390    result.push(start..range.end);
391
392    result
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::{
399        display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
400        InlayId, MultiBuffer,
401    };
402    use settings::SettingsStore;
403    use util::post_inc;
404
405    #[gpui::test]
406    fn test_previous_word_start(cx: &mut gpui::AppContext) {
407        init_test(cx);
408
409        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
410            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
411            assert_eq!(
412                previous_word_start(&snapshot, display_points[1]),
413                display_points[0]
414            );
415        }
416
417        assert("\nˇ   ˇlorem", cx);
418        assert("ˇ\nˇ   lorem", cx);
419        assert("    ˇloremˇ", cx);
420        assert("ˇ    ˇlorem", cx);
421        assert("    ˇlorˇem", cx);
422        assert("\nlorem\nˇ   ˇipsum", cx);
423        assert("\n\nˇ\nˇ", cx);
424        assert("    ˇlorem  ˇipsum", cx);
425        assert("loremˇ-ˇipsum", cx);
426        assert("loremˇ-#$@ˇipsum", cx);
427        assert("ˇlorem_ˇipsum", cx);
428        assert(" ˇdefγˇ", cx);
429        assert(" ˇbcΔˇ", cx);
430        assert(" abˇ——ˇcd", cx);
431    }
432
433    #[gpui::test]
434    fn test_previous_subword_start(cx: &mut gpui::AppContext) {
435        init_test(cx);
436
437        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
438            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
439            assert_eq!(
440                previous_subword_start(&snapshot, display_points[1]),
441                display_points[0]
442            );
443        }
444
445        // Subword boundaries are respected
446        assert("lorem_ˇipˇsum", cx);
447        assert("lorem_ˇipsumˇ", cx);
448        assert("ˇlorem_ˇipsum", cx);
449        assert("lorem_ˇipsum_ˇdolor", cx);
450        assert("loremˇIpˇsum", cx);
451        assert("loremˇIpsumˇ", cx);
452
453        // Word boundaries are still respected
454        assert("\nˇ   ˇlorem", cx);
455        assert("    ˇloremˇ", cx);
456        assert("    ˇlorˇem", cx);
457        assert("\nlorem\nˇ   ˇipsum", cx);
458        assert("\n\nˇ\nˇ", cx);
459        assert("    ˇlorem  ˇipsum", cx);
460        assert("loremˇ-ˇipsum", cx);
461        assert("loremˇ-#$@ˇipsum", cx);
462        assert(" ˇdefγˇ", cx);
463        assert(" bcˇΔˇ", cx);
464        assert(" ˇbcδˇ", cx);
465        assert(" abˇ——ˇcd", cx);
466    }
467
468    #[gpui::test]
469    fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
470        init_test(cx);
471
472        fn assert(
473            marked_text: &str,
474            cx: &mut gpui::AppContext,
475            is_boundary: impl FnMut(char, char) -> bool,
476        ) {
477            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
478            assert_eq!(
479                find_preceding_boundary(
480                    &snapshot,
481                    display_points[1],
482                    FindRange::MultiLine,
483                    is_boundary
484                ),
485                display_points[0]
486            );
487        }
488
489        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
490            left == 'c' && right == 'd'
491        });
492        assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
493            left == '\n' && right == 'g'
494        });
495        let mut line_count = 0;
496        assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
497            if left == '\n' {
498                line_count += 1;
499                line_count == 2
500            } else {
501                false
502            }
503        });
504    }
505
506    #[gpui::test]
507    fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
508        init_test(cx);
509
510        let input_text = "abcdefghijklmnopqrstuvwxys";
511        let family_id = cx
512            .font_cache()
513            .load_family(&["Helvetica"], &Default::default())
514            .unwrap();
515        let font_id = cx
516            .font_cache()
517            .select_font(family_id, &Default::default())
518            .unwrap();
519        let font_size = 14.0;
520        let buffer = MultiBuffer::build_simple(input_text, cx);
521        let buffer_snapshot = buffer.read(cx).snapshot(cx);
522        let display_map =
523            cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
524
525        // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
526        let mut id = 0;
527        let inlays = (0..buffer_snapshot.len())
528            .map(|offset| {
529                [
530                    Inlay {
531                        id: InlayId::Suggestion(post_inc(&mut id)),
532                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
533                        text: format!("test").into(),
534                    },
535                    Inlay {
536                        id: InlayId::Suggestion(post_inc(&mut id)),
537                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
538                        text: format!("test").into(),
539                    },
540                    Inlay {
541                        id: InlayId::Hint(post_inc(&mut id)),
542                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
543                        text: format!("test").into(),
544                    },
545                    Inlay {
546                        id: InlayId::Hint(post_inc(&mut id)),
547                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
548                        text: format!("test").into(),
549                    },
550                ]
551            })
552            .flatten()
553            .collect();
554        let snapshot = display_map.update(cx, |map, cx| {
555            map.splice_inlays(Vec::new(), inlays, cx);
556            map.snapshot(cx)
557        });
558
559        assert_eq!(
560            find_preceding_boundary(
561                &snapshot,
562                buffer_snapshot.len().to_display_point(&snapshot),
563                FindRange::MultiLine,
564                |left, _| left == 'e',
565            ),
566            snapshot
567                .buffer_snapshot
568                .offset_to_point(5)
569                .to_display_point(&snapshot),
570            "Should not stop at inlays when looking for boundaries"
571        );
572    }
573
574    #[gpui::test]
575    fn test_next_word_end(cx: &mut gpui::AppContext) {
576        init_test(cx);
577
578        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
579            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
580            assert_eq!(
581                next_word_end(&snapshot, display_points[0]),
582                display_points[1]
583            );
584        }
585
586        assert("\nˇ   loremˇ", cx);
587        assert("    ˇloremˇ", cx);
588        assert("    lorˇemˇ", cx);
589        assert("    loremˇ    ˇ\nipsum\n", cx);
590        assert("\nˇ\nˇ\n\n", cx);
591        assert("loremˇ    ipsumˇ   ", cx);
592        assert("loremˇ-ˇipsum", cx);
593        assert("loremˇ#$@-ˇipsum", cx);
594        assert("loremˇ_ipsumˇ", cx);
595        assert(" ˇbcΔˇ", cx);
596        assert(" abˇ——ˇcd", cx);
597    }
598
599    #[gpui::test]
600    fn test_next_subword_end(cx: &mut gpui::AppContext) {
601        init_test(cx);
602
603        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
604            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
605            assert_eq!(
606                next_subword_end(&snapshot, display_points[0]),
607                display_points[1]
608            );
609        }
610
611        // Subword boundaries are respected
612        assert("loˇremˇ_ipsum", cx);
613        assert("ˇloremˇ_ipsum", cx);
614        assert("loremˇ_ipsumˇ", cx);
615        assert("loremˇ_ipsumˇ_dolor", cx);
616        assert("loˇremˇIpsum", cx);
617        assert("loremˇIpsumˇDolor", cx);
618
619        // Word boundaries are still respected
620        assert("\nˇ   loremˇ", cx);
621        assert("    ˇloremˇ", cx);
622        assert("    lorˇemˇ", cx);
623        assert("    loremˇ    ˇ\nipsum\n", cx);
624        assert("\nˇ\nˇ\n\n", cx);
625        assert("loremˇ    ipsumˇ   ", cx);
626        assert("loremˇ-ˇipsum", cx);
627        assert("loremˇ#$@-ˇipsum", cx);
628        assert("loremˇ_ipsumˇ", cx);
629        assert(" ˇbcˇΔ", cx);
630        assert(" abˇ——ˇcd", cx);
631    }
632
633    #[gpui::test]
634    fn test_find_boundary(cx: &mut gpui::AppContext) {
635        init_test(cx);
636
637        fn assert(
638            marked_text: &str,
639            cx: &mut gpui::AppContext,
640            is_boundary: impl FnMut(char, char) -> bool,
641        ) {
642            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
643            assert_eq!(
644                find_boundary(
645                    &snapshot,
646                    display_points[0],
647                    FindRange::MultiLine,
648                    is_boundary
649                ),
650                display_points[1]
651            );
652        }
653
654        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
655            left == 'j' && right == 'k'
656        });
657        assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
658            left == '\n' && right == 'i'
659        });
660        let mut line_count = 0;
661        assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
662            if left == '\n' {
663                line_count += 1;
664                line_count == 2
665            } else {
666                false
667            }
668        });
669    }
670
671    #[gpui::test]
672    fn test_surrounding_word(cx: &mut gpui::AppContext) {
673        init_test(cx);
674
675        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
676            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
677            assert_eq!(
678                surrounding_word(&snapshot, display_points[1]),
679                display_points[0]..display_points[2]
680            );
681        }
682
683        assert("ˇˇloremˇ  ipsum", cx);
684        assert("ˇloˇremˇ  ipsum", cx);
685        assert("ˇloremˇˇ  ipsum", cx);
686        assert("loremˇ ˇ  ˇipsum", cx);
687        assert("lorem\nˇˇˇ\nipsum", cx);
688        assert("lorem\nˇˇipsumˇ", cx);
689        assert("lorem,ˇˇ ˇipsum", cx);
690        assert("ˇloremˇˇ, ipsum", cx);
691    }
692
693    #[gpui::test]
694    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
695        init_test(cx);
696
697        let family_id = cx
698            .font_cache()
699            .load_family(&["Helvetica"], &Default::default())
700            .unwrap();
701        let font_id = cx
702            .font_cache()
703            .select_font(family_id, &Default::default())
704            .unwrap();
705
706        let buffer =
707            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
708        let multibuffer = cx.add_model(|cx| {
709            let mut multibuffer = MultiBuffer::new(0);
710            multibuffer.push_excerpts(
711                buffer.clone(),
712                [
713                    ExcerptRange {
714                        context: Point::new(0, 0)..Point::new(1, 4),
715                        primary: None,
716                    },
717                    ExcerptRange {
718                        context: Point::new(2, 0)..Point::new(3, 2),
719                        primary: None,
720                    },
721                ],
722                cx,
723            );
724            multibuffer
725        });
726        let display_map =
727            cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
728        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
729
730        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
731
732        // Can't move up into the first excerpt's header
733        assert_eq!(
734            up(
735                &snapshot,
736                DisplayPoint::new(2, 2),
737                SelectionGoal::Column(2),
738                false
739            ),
740            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
741        );
742        assert_eq!(
743            up(
744                &snapshot,
745                DisplayPoint::new(2, 0),
746                SelectionGoal::None,
747                false
748            ),
749            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
750        );
751
752        // Move up and down within first excerpt
753        assert_eq!(
754            up(
755                &snapshot,
756                DisplayPoint::new(3, 4),
757                SelectionGoal::Column(4),
758                false
759            ),
760            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
761        );
762        assert_eq!(
763            down(
764                &snapshot,
765                DisplayPoint::new(2, 3),
766                SelectionGoal::Column(4),
767                false
768            ),
769            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
770        );
771
772        // Move up and down across second excerpt's header
773        assert_eq!(
774            up(
775                &snapshot,
776                DisplayPoint::new(6, 5),
777                SelectionGoal::Column(5),
778                false
779            ),
780            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
781        );
782        assert_eq!(
783            down(
784                &snapshot,
785                DisplayPoint::new(3, 4),
786                SelectionGoal::Column(5),
787                false
788            ),
789            (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
790        );
791
792        // Can't move down off the end
793        assert_eq!(
794            down(
795                &snapshot,
796                DisplayPoint::new(7, 0),
797                SelectionGoal::Column(0),
798                false
799            ),
800            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
801        );
802        assert_eq!(
803            down(
804                &snapshot,
805                DisplayPoint::new(7, 2),
806                SelectionGoal::Column(2),
807                false
808            ),
809            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
810        );
811    }
812
813    fn init_test(cx: &mut gpui::AppContext) {
814        cx.set_global(SettingsStore::test(cx));
815        theme::init((), cx);
816        language::init(cx);
817        crate::init(cx);
818    }
819}