movement.rs

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