movement.rs

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