movement.rs

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