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;
   9use serde::Deserialize;
  10
  11use std::{ops::Range, sync::Arc};
  12
  13/// Defines search strategy for items in `movement` module.
  14/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
  15/// `FindRange::MultiLine` keeps going until the end of a string.
  16#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
  17pub enum FindRange {
  18    SingleLine,
  19    MultiLine,
  20}
  21
  22/// TextLayoutDetails encompasses everything we need to move vertically
  23/// taking into account variable width characters.
  24pub struct TextLayoutDetails {
  25    pub(crate) text_system: Arc<WindowTextSystem>,
  26    pub(crate) editor_style: EditorStyle,
  27    pub(crate) rem_size: Pixels,
  28    pub scroll_anchor: ScrollAnchor,
  29    pub visible_rows: Option<f32>,
  30    pub vertical_scroll_margin: f32,
  31}
  32
  33/// Returns a column to the left of the current point, wrapping
  34/// to the previous line if that point is at the start of line.
  35pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  36    if point.column() > 0 {
  37        *point.column_mut() -= 1;
  38    } else if point.row() > 0 {
  39        *point.row_mut() -= 1;
  40        *point.column_mut() = map.line_len(point.row());
  41    }
  42    map.clip_point(point, Bias::Left)
  43}
  44
  45/// Returns a column to the left of the current point, doing nothing if
  46/// that point is already at the start of line.
  47pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  48    if point.column() > 0 {
  49        *point.column_mut() -= 1;
  50    }
  51    map.clip_point(point, Bias::Left)
  52}
  53
  54/// Returns a column to the right of the current point, doing nothing
  55// if that point is at the end of the line.
  56pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  57    if point.column() < map.line_len(point.row()) {
  58        *point.column_mut() += 1;
  59    } else if point.row() < map.max_point().row() {
  60        *point.row_mut() += 1;
  61        *point.column_mut() = 0;
  62    }
  63    map.clip_point(point, Bias::Right)
  64}
  65
  66/// Returns a column to the right of the current point, not performing any wrapping
  67/// if that point is already at the end of line.
  68pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  69    *point.column_mut() += 1;
  70    map.clip_point(point, Bias::Right)
  71}
  72
  73/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
  74pub fn up(
  75    map: &DisplaySnapshot,
  76    start: DisplayPoint,
  77    goal: SelectionGoal,
  78    preserve_column_at_start: bool,
  79    text_layout_details: &TextLayoutDetails,
  80) -> (DisplayPoint, SelectionGoal) {
  81    up_by_rows(
  82        map,
  83        start,
  84        1,
  85        goal,
  86        preserve_column_at_start,
  87        text_layout_details,
  88    )
  89}
  90
  91/// Returns a display point for the next displayed line (which might be a soft-wrapped line).
  92pub fn down(
  93    map: &DisplaySnapshot,
  94    start: DisplayPoint,
  95    goal: SelectionGoal,
  96    preserve_column_at_end: bool,
  97    text_layout_details: &TextLayoutDetails,
  98) -> (DisplayPoint, SelectionGoal) {
  99    down_by_rows(
 100        map,
 101        start,
 102        1,
 103        goal,
 104        preserve_column_at_end,
 105        text_layout_details,
 106    )
 107}
 108
 109pub(crate) fn up_by_rows(
 110    map: &DisplaySnapshot,
 111    start: DisplayPoint,
 112    row_count: u32,
 113    goal: SelectionGoal,
 114    preserve_column_at_start: bool,
 115    text_layout_details: &TextLayoutDetails,
 116) -> (DisplayPoint, SelectionGoal) {
 117    let mut goal_x = match goal {
 118        SelectionGoal::HorizontalPosition(x) => x.into(),
 119        SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
 120        SelectionGoal::HorizontalRange { end, .. } => end.into(),
 121        _ => map.x_for_display_point(start, text_layout_details),
 122    };
 123
 124    let prev_row = start.row().saturating_sub(row_count);
 125    let mut point = map.clip_point(
 126        DisplayPoint::new(prev_row, map.line_len(prev_row)),
 127        Bias::Left,
 128    );
 129    if point.row() < start.row() {
 130        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
 131    } else if preserve_column_at_start {
 132        return (start, goal);
 133    } else {
 134        point = DisplayPoint::new(0, 0);
 135        goal_x = px(0.);
 136    }
 137
 138    let mut clipped_point = map.clip_point(point, Bias::Left);
 139    if clipped_point.row() < point.row() {
 140        clipped_point = map.clip_point(point, Bias::Right);
 141    }
 142    (
 143        clipped_point,
 144        SelectionGoal::HorizontalPosition(goal_x.into()),
 145    )
 146}
 147
 148pub(crate) fn down_by_rows(
 149    map: &DisplaySnapshot,
 150    start: DisplayPoint,
 151    row_count: u32,
 152    goal: SelectionGoal,
 153    preserve_column_at_end: bool,
 154    text_layout_details: &TextLayoutDetails,
 155) -> (DisplayPoint, SelectionGoal) {
 156    let mut goal_x = match goal {
 157        SelectionGoal::HorizontalPosition(x) => x.into(),
 158        SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
 159        SelectionGoal::HorizontalRange { end, .. } => end.into(),
 160        _ => map.x_for_display_point(start, text_layout_details),
 161    };
 162
 163    let new_row = start.row() + row_count;
 164    let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
 165    if point.row() > start.row() {
 166        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
 167    } else if preserve_column_at_end {
 168        return (start, goal);
 169    } else {
 170        point = map.max_point();
 171        goal_x = map.x_for_display_point(point, text_layout_details)
 172    }
 173
 174    let mut clipped_point = map.clip_point(point, Bias::Right);
 175    if clipped_point.row() > point.row() {
 176        clipped_point = map.clip_point(point, Bias::Left);
 177    }
 178    (
 179        clipped_point,
 180        SelectionGoal::HorizontalPosition(goal_x.into()),
 181    )
 182}
 183
 184/// Returns a position of the start of line.
 185/// If `stop_at_soft_boundaries` is true, the returned position is that of the
 186/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
 187/// Otherwise it's always going to be the start of a logical line.
 188pub fn line_beginning(
 189    map: &DisplaySnapshot,
 190    display_point: DisplayPoint,
 191    stop_at_soft_boundaries: bool,
 192) -> DisplayPoint {
 193    let point = display_point.to_point(map);
 194    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
 195    let line_start = map.prev_line_boundary(point).1;
 196
 197    if stop_at_soft_boundaries && display_point != soft_line_start {
 198        soft_line_start
 199    } else {
 200        line_start
 201    }
 202}
 203
 204/// Returns the last indented position on a given line.
 205/// If `stop_at_soft_boundaries` is true, the returned [`DisplayPoint`] is that of a
 206/// displayed line (e.g. if there's soft wrap it's gonna be returned),
 207/// otherwise it's always going to be a start of a logical line.
 208pub fn indented_line_beginning(
 209    map: &DisplaySnapshot,
 210    display_point: DisplayPoint,
 211    stop_at_soft_boundaries: bool,
 212) -> DisplayPoint {
 213    let point = display_point.to_point(map);
 214    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
 215    let indent_start = Point::new(
 216        point.row,
 217        map.buffer_snapshot.indent_size_for_line(point.row).len,
 218    )
 219    .to_display_point(map);
 220    let line_start = map.prev_line_boundary(point).1;
 221
 222    if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
 223    {
 224        soft_line_start
 225    } else if stop_at_soft_boundaries && display_point != indent_start {
 226        indent_start
 227    } else {
 228        line_start
 229    }
 230}
 231
 232/// Returns a position of the end of line.
 233
 234/// If `stop_at_soft_boundaries` is true, the returned position is that of the
 235/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
 236/// Otherwise it's always going to be the end of a logical line.
 237pub fn line_end(
 238    map: &DisplaySnapshot,
 239    display_point: DisplayPoint,
 240    stop_at_soft_boundaries: bool,
 241) -> DisplayPoint {
 242    let soft_line_end = map.clip_point(
 243        DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
 244        Bias::Left,
 245    );
 246    if stop_at_soft_boundaries && display_point != soft_line_end {
 247        soft_line_end
 248    } else {
 249        map.next_line_boundary(display_point.to_point(map)).1
 250    }
 251}
 252
 253/// Returns a position of the previous word boundary, where a word character is defined as either
 254/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
 255pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
 256    let raw_point = point.to_point(map);
 257    let scope = map.buffer_snapshot.language_scope_at(raw_point);
 258
 259    find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
 260        (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
 261            || left == '\n'
 262    })
 263}
 264
 265/// Returns a position of the previous subword boundary, where a subword is defined as a run of
 266/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
 267/// lowerspace characters and uppercase characters.
 268pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
 269    let raw_point = point.to_point(map);
 270    let scope = map.buffer_snapshot.language_scope_at(raw_point);
 271
 272    find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
 273        let is_word_start =
 274            char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
 275        let is_subword_start =
 276            left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
 277        is_word_start || is_subword_start || left == '\n'
 278    })
 279}
 280
 281/// Returns a position of the next word boundary, where a word character is defined as either
 282/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
 283pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
 284    let raw_point = point.to_point(map);
 285    let scope = map.buffer_snapshot.language_scope_at(raw_point);
 286
 287    find_boundary(map, point, FindRange::MultiLine, |left, right| {
 288        (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
 289            || right == '\n'
 290    })
 291}
 292
 293/// Returns a position of the next subword boundary, where a subword is defined as a run of
 294/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
 295/// lowerspace characters and uppercase characters.
 296pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
 297    let raw_point = point.to_point(map);
 298    let scope = map.buffer_snapshot.language_scope_at(raw_point);
 299
 300    find_boundary(map, point, FindRange::MultiLine, |left, right| {
 301        let is_word_end =
 302            (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
 303        let is_subword_end =
 304            left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
 305        is_word_end || is_subword_end || right == '\n'
 306    })
 307}
 308
 309/// Returns a position of the start of the current paragraph, where a paragraph
 310/// is defined as a run of non-blank lines.
 311pub fn start_of_paragraph(
 312    map: &DisplaySnapshot,
 313    display_point: DisplayPoint,
 314    mut count: usize,
 315) -> DisplayPoint {
 316    let point = display_point.to_point(map);
 317    if point.row == 0 {
 318        return DisplayPoint::zero();
 319    }
 320
 321    let mut found_non_blank_line = false;
 322    for row in (0..point.row + 1).rev() {
 323        let blank = map.buffer_snapshot.is_line_blank(row);
 324        if found_non_blank_line && blank {
 325            if count <= 1 {
 326                return Point::new(row, 0).to_display_point(map);
 327            }
 328            count -= 1;
 329            found_non_blank_line = false;
 330        }
 331
 332        found_non_blank_line |= !blank;
 333    }
 334
 335    DisplayPoint::zero()
 336}
 337
 338/// Returns a position of the end of the current paragraph, where a paragraph
 339/// is defined as a run of non-blank lines.
 340pub fn end_of_paragraph(
 341    map: &DisplaySnapshot,
 342    display_point: DisplayPoint,
 343    mut count: usize,
 344) -> DisplayPoint {
 345    let point = display_point.to_point(map);
 346    if point.row == map.max_buffer_row() {
 347        return map.max_point();
 348    }
 349
 350    let mut found_non_blank_line = false;
 351    for row in point.row..map.max_buffer_row() + 1 {
 352        let blank = map.buffer_snapshot.is_line_blank(row);
 353        if found_non_blank_line && blank {
 354            if count <= 1 {
 355                return Point::new(row, 0).to_display_point(map);
 356            }
 357            count -= 1;
 358            found_non_blank_line = false;
 359        }
 360
 361        found_non_blank_line |= !blank;
 362    }
 363
 364    map.max_point()
 365}
 366
 367/// Scans for a boundary preceding the given start point `from` until a boundary is found,
 368/// indicated by the given predicate returning true.
 369/// The predicate is called with the character to the left and right of the candidate boundary location.
 370/// 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.
 371pub fn find_preceding_boundary_point(
 372    buffer_snapshot: &MultiBufferSnapshot,
 373    from: Point,
 374    find_range: FindRange,
 375    mut is_boundary: impl FnMut(char, char) -> bool,
 376) -> Point {
 377    let mut prev_ch = None;
 378    let mut offset = from.to_offset(&buffer_snapshot);
 379
 380    for ch in buffer_snapshot.reversed_chars_at(offset) {
 381        if find_range == FindRange::SingleLine && ch == '\n' {
 382            break;
 383        }
 384        if let Some(prev_ch) = prev_ch {
 385            if is_boundary(ch, prev_ch) {
 386                break;
 387            }
 388        }
 389
 390        offset -= ch.len_utf8();
 391        prev_ch = Some(ch);
 392    }
 393
 394    offset.to_point(&buffer_snapshot)
 395}
 396
 397/// Scans for a boundary preceding the given start point `from` until a boundary is found,
 398/// indicated by the given predicate returning true.
 399/// The predicate is called with the character to the left and right of the candidate boundary location.
 400/// 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.
 401pub fn find_preceding_boundary_display_point(
 402    map: &DisplaySnapshot,
 403    from: DisplayPoint,
 404    find_range: FindRange,
 405    is_boundary: impl FnMut(char, char) -> bool,
 406) -> DisplayPoint {
 407    let result = find_preceding_boundary_point(
 408        &map.buffer_snapshot,
 409        from.to_point(map),
 410        find_range,
 411        is_boundary,
 412    );
 413    map.clip_point(result.to_display_point(map), Bias::Left)
 414}
 415
 416/// Scans for a boundary following the given start point until a boundary is found, indicated by the
 417/// given predicate returning true. The predicate is called with the character to the left and right
 418/// of the candidate boundary location, and will be called with `\n` characters indicating the start
 419/// or end of a line. The function supports optionally returning the point just before the boundary
 420/// is found via return_point_before_boundary.
 421pub fn find_boundary_point(
 422    map: &DisplaySnapshot,
 423    from: DisplayPoint,
 424    find_range: FindRange,
 425    mut is_boundary: impl FnMut(char, char) -> bool,
 426    return_point_before_boundary: bool,
 427) -> DisplayPoint {
 428    let mut offset = from.to_offset(&map, Bias::Right);
 429    let mut prev_offset = offset;
 430    let mut prev_ch = None;
 431
 432    for ch in map.buffer_snapshot.chars_at(offset) {
 433        if find_range == FindRange::SingleLine && ch == '\n' {
 434            break;
 435        }
 436        if let Some(prev_ch) = prev_ch {
 437            if is_boundary(prev_ch, ch) {
 438                if return_point_before_boundary {
 439                    return map.clip_point(prev_offset.to_display_point(map), Bias::Right);
 440                } else {
 441                    break;
 442                }
 443            }
 444        }
 445        prev_offset = offset;
 446        offset += ch.len_utf8();
 447        prev_ch = Some(ch);
 448    }
 449    map.clip_point(offset.to_display_point(map), Bias::Right)
 450}
 451
 452pub fn find_boundary(
 453    map: &DisplaySnapshot,
 454    from: DisplayPoint,
 455    find_range: FindRange,
 456    is_boundary: impl FnMut(char, char) -> bool,
 457) -> DisplayPoint {
 458    return find_boundary_point(map, from, find_range, is_boundary, false);
 459}
 460
 461pub fn find_boundary_exclusive(
 462    map: &DisplaySnapshot,
 463    from: DisplayPoint,
 464    find_range: FindRange,
 465    is_boundary: impl FnMut(char, char) -> bool,
 466) -> DisplayPoint {
 467    return find_boundary_point(map, from, find_range, is_boundary, true);
 468}
 469
 470/// Returns an iterator over the characters following a given offset in the [`DisplaySnapshot`].
 471/// The returned value also contains a range of the start/end of a returned character in
 472/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
 473pub fn chars_after(
 474    map: &DisplaySnapshot,
 475    mut offset: usize,
 476) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
 477    map.buffer_snapshot.chars_at(offset).map(move |ch| {
 478        let before = offset;
 479        offset = offset + ch.len_utf8();
 480        (ch, before..offset)
 481    })
 482}
 483
 484/// Returns a reverse iterator over the characters following a given offset in the [`DisplaySnapshot`].
 485/// The returned value also contains a range of the start/end of a returned character in
 486/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
 487pub fn chars_before(
 488    map: &DisplaySnapshot,
 489    mut offset: usize,
 490) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
 491    map.buffer_snapshot
 492        .reversed_chars_at(offset)
 493        .map(move |ch| {
 494            let after = offset;
 495            offset = offset - ch.len_utf8();
 496            (ch, offset..after)
 497        })
 498}
 499
 500pub(crate) fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
 501    let raw_point = point.to_point(map);
 502    let scope = map.buffer_snapshot.language_scope_at(raw_point);
 503    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
 504    let text = &map.buffer_snapshot;
 505    let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
 506    let prev_char_kind = text
 507        .reversed_chars_at(ix)
 508        .next()
 509        .map(|c| char_kind(&scope, c));
 510    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
 511}
 512
 513pub(crate) fn surrounding_word(
 514    map: &DisplaySnapshot,
 515    position: DisplayPoint,
 516) -> Range<DisplayPoint> {
 517    let position = map
 518        .clip_point(position, Bias::Left)
 519        .to_offset(map, Bias::Left);
 520    let (range, _) = map.buffer_snapshot.surrounding_word(position);
 521    let start = range
 522        .start
 523        .to_point(&map.buffer_snapshot)
 524        .to_display_point(map);
 525    let end = range
 526        .end
 527        .to_point(&map.buffer_snapshot)
 528        .to_display_point(map);
 529    start..end
 530}
 531
 532/// Returns a list of lines (represented as a [`DisplayPoint`] range) contained
 533/// within a passed range.
 534///
 535/// The line ranges are **always* going to be in bounds of a requested range, which means that
 536/// the first and the last lines might not necessarily represent the
 537/// full range of a logical line (as their `.start`/`.end` values are clipped to those of a passed in range).
 538pub fn split_display_range_by_lines(
 539    map: &DisplaySnapshot,
 540    range: Range<DisplayPoint>,
 541) -> Vec<Range<DisplayPoint>> {
 542    let mut result = Vec::new();
 543
 544    let mut start = range.start;
 545    // Loop over all the covered rows until the one containing the range end
 546    for row in range.start.row()..range.end.row() {
 547        let row_end_column = map.line_len(row);
 548        let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
 549        if start != end {
 550            result.push(start..end);
 551        }
 552        start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
 553    }
 554
 555    // Add the final range from the start of the last end to the original range end.
 556    result.push(start..range.end);
 557
 558    result
 559}
 560
 561#[cfg(test)]
 562mod tests {
 563    use super::*;
 564    use crate::{
 565        display_map::Inlay,
 566        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
 567        Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
 568    };
 569    use gpui::{font, Context as _};
 570    use language::Capability;
 571    use project::Project;
 572    use settings::SettingsStore;
 573    use text::BufferId;
 574    use util::post_inc;
 575
 576    #[gpui::test]
 577    fn test_previous_word_start(cx: &mut gpui::AppContext) {
 578        init_test(cx);
 579
 580        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
 581            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 582            assert_eq!(
 583                previous_word_start(&snapshot, display_points[1]),
 584                display_points[0]
 585            );
 586        }
 587
 588        assert("\nˇ   ˇlorem", cx);
 589        assert("ˇ\nˇ   lorem", cx);
 590        assert("    ˇloremˇ", cx);
 591        assert("ˇ    ˇlorem", cx);
 592        assert("    ˇlorˇem", cx);
 593        assert("\nlorem\nˇ   ˇipsum", cx);
 594        assert("\n\nˇ\nˇ", cx);
 595        assert("    ˇlorem  ˇipsum", cx);
 596        assert("loremˇ-ˇipsum", cx);
 597        assert("loremˇ-#$@ˇipsum", cx);
 598        assert("ˇlorem_ˇipsum", cx);
 599        assert(" ˇdefγˇ", cx);
 600        assert(" ˇbcΔˇ", cx);
 601        assert(" abˇ——ˇcd", cx);
 602    }
 603
 604    #[gpui::test]
 605    fn test_previous_subword_start(cx: &mut gpui::AppContext) {
 606        init_test(cx);
 607
 608        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
 609            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 610            assert_eq!(
 611                previous_subword_start(&snapshot, display_points[1]),
 612                display_points[0]
 613            );
 614        }
 615
 616        // Subword boundaries are respected
 617        assert("lorem_ˇipˇsum", cx);
 618        assert("lorem_ˇipsumˇ", cx);
 619        assert("ˇlorem_ˇipsum", cx);
 620        assert("lorem_ˇipsum_ˇdolor", cx);
 621        assert("loremˇIpˇsum", cx);
 622        assert("loremˇIpsumˇ", cx);
 623
 624        // Word boundaries are still respected
 625        assert("\nˇ   ˇlorem", cx);
 626        assert("    ˇloremˇ", cx);
 627        assert("    ˇlorˇem", cx);
 628        assert("\nlorem\nˇ   ˇipsum", cx);
 629        assert("\n\nˇ\nˇ", cx);
 630        assert("    ˇlorem  ˇipsum", cx);
 631        assert("loremˇ-ˇipsum", cx);
 632        assert("loremˇ-#$@ˇipsum", cx);
 633        assert(" ˇdefγˇ", cx);
 634        assert(" bcˇΔˇ", cx);
 635        assert(" ˇbcδˇ", cx);
 636        assert(" abˇ——ˇcd", cx);
 637    }
 638
 639    #[gpui::test]
 640    fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
 641        init_test(cx);
 642
 643        fn assert(
 644            marked_text: &str,
 645            cx: &mut gpui::AppContext,
 646            is_boundary: impl FnMut(char, char) -> bool,
 647        ) {
 648            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 649            assert_eq!(
 650                find_preceding_boundary_display_point(
 651                    &snapshot,
 652                    display_points[1],
 653                    FindRange::MultiLine,
 654                    is_boundary
 655                ),
 656                display_points[0]
 657            );
 658        }
 659
 660        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
 661            left == 'c' && right == 'd'
 662        });
 663        assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
 664            left == '\n' && right == 'g'
 665        });
 666        let mut line_count = 0;
 667        assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
 668            if left == '\n' {
 669                line_count += 1;
 670                line_count == 2
 671            } else {
 672                false
 673            }
 674        });
 675    }
 676
 677    #[gpui::test]
 678    fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
 679        init_test(cx);
 680
 681        let input_text = "abcdefghijklmnopqrstuvwxys";
 682        let font = font("Helvetica");
 683        let font_size = px(14.0);
 684        let buffer = MultiBuffer::build_simple(input_text, cx);
 685        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 686        let display_map =
 687            cx.new_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
 688
 689        // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
 690        let mut id = 0;
 691        let inlays = (0..buffer_snapshot.len())
 692            .flat_map(|offset| {
 693                [
 694                    Inlay {
 695                        id: InlayId::Suggestion(post_inc(&mut id)),
 696                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
 697                        text: "test".into(),
 698                    },
 699                    Inlay {
 700                        id: InlayId::Suggestion(post_inc(&mut id)),
 701                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
 702                        text: "test".into(),
 703                    },
 704                    Inlay {
 705                        id: InlayId::Hint(post_inc(&mut id)),
 706                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
 707                        text: "test".into(),
 708                    },
 709                    Inlay {
 710                        id: InlayId::Hint(post_inc(&mut id)),
 711                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
 712                        text: "test".into(),
 713                    },
 714                ]
 715            })
 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
 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;
 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}