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(Debug, PartialEq)]
  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            .map(|offset| {
 692                [
 693                    Inlay {
 694                        id: InlayId::Suggestion(post_inc(&mut id)),
 695                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
 696                        text: format!("test").into(),
 697                    },
 698                    Inlay {
 699                        id: InlayId::Suggestion(post_inc(&mut id)),
 700                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
 701                        text: format!("test").into(),
 702                    },
 703                    Inlay {
 704                        id: InlayId::Hint(post_inc(&mut id)),
 705                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
 706                        text: format!("test").into(),
 707                    },
 708                    Inlay {
 709                        id: InlayId::Hint(post_inc(&mut id)),
 710                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
 711                        text: format!("test").into(),
 712                    },
 713                ]
 714            })
 715            .flatten()
 716            .collect();
 717        let snapshot = display_map.update(cx, |map, cx| {
 718            map.splice_inlays(Vec::new(), inlays, cx);
 719            map.snapshot(cx)
 720        });
 721
 722        assert_eq!(
 723            find_preceding_boundary_display_point(
 724                &snapshot,
 725                buffer_snapshot.len().to_display_point(&snapshot),
 726                FindRange::MultiLine,
 727                |left, _| left == 'e',
 728            ),
 729            snapshot
 730                .buffer_snapshot
 731                .offset_to_point(5)
 732                .to_display_point(&snapshot),
 733            "Should not stop at inlays when looking for boundaries"
 734        );
 735    }
 736
 737    #[gpui::test]
 738    fn test_next_word_end(cx: &mut gpui::AppContext) {
 739        init_test(cx);
 740
 741        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
 742            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 743            assert_eq!(
 744                next_word_end(&snapshot, display_points[0]),
 745                display_points[1]
 746            );
 747        }
 748
 749        assert("\nˇ   loremˇ", cx);
 750        assert("    ˇloremˇ", cx);
 751        assert("    lorˇemˇ", cx);
 752        assert("    loremˇ    ˇ\nipsum\n", cx);
 753        assert("\nˇ\nˇ\n\n", cx);
 754        assert("loremˇ    ipsumˇ   ", cx);
 755        assert("loremˇ-ˇipsum", cx);
 756        assert("loremˇ#$@-ˇipsum", cx);
 757        assert("loremˇ_ipsumˇ", cx);
 758        assert(" ˇbcΔˇ", cx);
 759        assert(" abˇ——ˇcd", cx);
 760    }
 761
 762    #[gpui::test]
 763    fn test_next_subword_end(cx: &mut gpui::AppContext) {
 764        init_test(cx);
 765
 766        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
 767            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 768            assert_eq!(
 769                next_subword_end(&snapshot, display_points[0]),
 770                display_points[1]
 771            );
 772        }
 773
 774        // Subword boundaries are respected
 775        assert("loˇremˇ_ipsum", cx);
 776        assert("ˇloremˇ_ipsum", cx);
 777        assert("loremˇ_ipsumˇ", cx);
 778        assert("loremˇ_ipsumˇ_dolor", cx);
 779        assert("loˇremˇIpsum", cx);
 780        assert("loremˇIpsumˇDolor", cx);
 781
 782        // Word boundaries are still respected
 783        assert("\nˇ   loremˇ", cx);
 784        assert("    ˇloremˇ", cx);
 785        assert("    lorˇemˇ", cx);
 786        assert("    loremˇ    ˇ\nipsum\n", cx);
 787        assert("\nˇ\nˇ\n\n", cx);
 788        assert("loremˇ    ipsumˇ   ", cx);
 789        assert("loremˇ-ˇipsum", cx);
 790        assert("loremˇ#$@-ˇipsum", cx);
 791        assert("loremˇ_ipsumˇ", cx);
 792        assert(" ˇbcˇΔ", cx);
 793        assert(" abˇ——ˇcd", cx);
 794    }
 795
 796    #[gpui::test]
 797    fn test_find_boundary(cx: &mut gpui::AppContext) {
 798        init_test(cx);
 799
 800        fn assert(
 801            marked_text: &str,
 802            cx: &mut gpui::AppContext,
 803            is_boundary: impl FnMut(char, char) -> bool,
 804        ) {
 805            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 806            assert_eq!(
 807                find_boundary(
 808                    &snapshot,
 809                    display_points[0],
 810                    FindRange::MultiLine,
 811                    is_boundary,
 812                ),
 813                display_points[1]
 814            );
 815        }
 816
 817        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
 818            left == 'j' && right == 'k'
 819        });
 820        assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
 821            left == '\n' && right == 'i'
 822        });
 823        let mut line_count = 0;
 824        assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
 825            if left == '\n' {
 826                line_count += 1;
 827                line_count == 2
 828            } else {
 829                false
 830            }
 831        });
 832    }
 833
 834    #[gpui::test]
 835    fn test_surrounding_word(cx: &mut gpui::AppContext) {
 836        init_test(cx);
 837
 838        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
 839            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 840            assert_eq!(
 841                surrounding_word(&snapshot, display_points[1]),
 842                display_points[0]..display_points[2],
 843                "{}",
 844                marked_text.to_string()
 845            );
 846        }
 847
 848        assert("ˇˇloremˇ  ipsum", cx);
 849        assert("ˇloˇremˇ  ipsum", cx);
 850        assert("ˇloremˇˇ  ipsum", cx);
 851        assert("loremˇ ˇ  ˇipsum", cx);
 852        assert("lorem\nˇˇˇ\nipsum", cx);
 853        assert("lorem\nˇˇipsumˇ", cx);
 854        assert("loremˇ,ˇˇ ipsum", cx);
 855        assert("ˇloremˇˇ, ipsum", cx);
 856    }
 857
 858    #[gpui::test]
 859    async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
 860        cx.update(|cx| {
 861            init_test(cx);
 862        });
 863
 864        let mut cx = EditorTestContext::new(cx).await;
 865        let editor = cx.editor.clone();
 866        let window = cx.window.clone();
 867        _ = cx.update_window(window, |_, cx| {
 868            let text_layout_details =
 869                editor.update(cx, |editor, cx| editor.text_layout_details(cx));
 870
 871            let font = font("Helvetica");
 872
 873            let buffer = cx.new_model(|cx| {
 874                Buffer::new(
 875                    0,
 876                    BufferId::new(cx.entity_id().as_u64()).unwrap(),
 877                    "abc\ndefg\nhijkl\nmn",
 878                )
 879            });
 880            let multibuffer = cx.new_model(|cx| {
 881                let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
 882                multibuffer.push_excerpts(
 883                    buffer.clone(),
 884                    [
 885                        ExcerptRange {
 886                            context: Point::new(0, 0)..Point::new(1, 4),
 887                            primary: None,
 888                        },
 889                        ExcerptRange {
 890                            context: Point::new(2, 0)..Point::new(3, 2),
 891                            primary: None,
 892                        },
 893                    ],
 894                    cx,
 895                );
 896                multibuffer
 897            });
 898            let display_map =
 899                cx.new_model(|cx| DisplayMap::new(multibuffer, font, px(14.0), None, 2, 2, cx));
 900            let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
 901
 902            assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
 903
 904            let col_2_x =
 905                snapshot.x_for_display_point(DisplayPoint::new(2, 2), &text_layout_details);
 906
 907            // Can't move up into the first excerpt's header
 908            assert_eq!(
 909                up(
 910                    &snapshot,
 911                    DisplayPoint::new(2, 2),
 912                    SelectionGoal::HorizontalPosition(col_2_x.0),
 913                    false,
 914                    &text_layout_details
 915                ),
 916                (
 917                    DisplayPoint::new(2, 0),
 918                    SelectionGoal::HorizontalPosition(0.0)
 919                ),
 920            );
 921            assert_eq!(
 922                up(
 923                    &snapshot,
 924                    DisplayPoint::new(2, 0),
 925                    SelectionGoal::None,
 926                    false,
 927                    &text_layout_details
 928                ),
 929                (
 930                    DisplayPoint::new(2, 0),
 931                    SelectionGoal::HorizontalPosition(0.0)
 932                ),
 933            );
 934
 935            let col_4_x =
 936                snapshot.x_for_display_point(DisplayPoint::new(3, 4), &text_layout_details);
 937
 938            // Move up and down within first excerpt
 939            assert_eq!(
 940                up(
 941                    &snapshot,
 942                    DisplayPoint::new(3, 4),
 943                    SelectionGoal::HorizontalPosition(col_4_x.0),
 944                    false,
 945                    &text_layout_details
 946                ),
 947                (
 948                    DisplayPoint::new(2, 3),
 949                    SelectionGoal::HorizontalPosition(col_4_x.0)
 950                ),
 951            );
 952            assert_eq!(
 953                down(
 954                    &snapshot,
 955                    DisplayPoint::new(2, 3),
 956                    SelectionGoal::HorizontalPosition(col_4_x.0),
 957                    false,
 958                    &text_layout_details
 959                ),
 960                (
 961                    DisplayPoint::new(3, 4),
 962                    SelectionGoal::HorizontalPosition(col_4_x.0)
 963                ),
 964            );
 965
 966            let col_5_x =
 967                snapshot.x_for_display_point(DisplayPoint::new(6, 5), &text_layout_details);
 968
 969            // Move up and down across second excerpt's header
 970            assert_eq!(
 971                up(
 972                    &snapshot,
 973                    DisplayPoint::new(6, 5),
 974                    SelectionGoal::HorizontalPosition(col_5_x.0),
 975                    false,
 976                    &text_layout_details
 977                ),
 978                (
 979                    DisplayPoint::new(3, 4),
 980                    SelectionGoal::HorizontalPosition(col_5_x.0)
 981                ),
 982            );
 983            assert_eq!(
 984                down(
 985                    &snapshot,
 986                    DisplayPoint::new(3, 4),
 987                    SelectionGoal::HorizontalPosition(col_5_x.0),
 988                    false,
 989                    &text_layout_details
 990                ),
 991                (
 992                    DisplayPoint::new(6, 5),
 993                    SelectionGoal::HorizontalPosition(col_5_x.0)
 994                ),
 995            );
 996
 997            let max_point_x =
 998                snapshot.x_for_display_point(DisplayPoint::new(7, 2), &text_layout_details);
 999
1000            // Can't move down off the end
1001            assert_eq!(
1002                down(
1003                    &snapshot,
1004                    DisplayPoint::new(7, 0),
1005                    SelectionGoal::HorizontalPosition(0.0),
1006                    false,
1007                    &text_layout_details
1008                ),
1009                (
1010                    DisplayPoint::new(7, 2),
1011                    SelectionGoal::HorizontalPosition(max_point_x.0)
1012                ),
1013            );
1014            assert_eq!(
1015                down(
1016                    &snapshot,
1017                    DisplayPoint::new(7, 2),
1018                    SelectionGoal::HorizontalPosition(max_point_x.0),
1019                    false,
1020                    &text_layout_details
1021                ),
1022                (
1023                    DisplayPoint::new(7, 2),
1024                    SelectionGoal::HorizontalPosition(max_point_x.0)
1025                ),
1026            );
1027        });
1028    }
1029
1030    fn init_test(cx: &mut gpui::AppContext) {
1031        let settings_store = SettingsStore::test(cx);
1032        cx.set_global(settings_store);
1033        theme::init(theme::LoadThemes::JustBase, cx);
1034        language::init(cx);
1035        crate::init(cx);
1036        Project::init_settings(cx);
1037    }
1038}