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