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 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| Buffer::local("abc\ndefg\nhijkl\nmn", cx));
 873            let multibuffer = cx.new_model(|cx| {
 874                let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
 875                multibuffer.push_excerpts(
 876                    buffer.clone(),
 877                    [
 878                        ExcerptRange {
 879                            context: Point::new(0, 0)..Point::new(1, 4),
 880                            primary: None,
 881                        },
 882                        ExcerptRange {
 883                            context: Point::new(2, 0)..Point::new(3, 2),
 884                            primary: None,
 885                        },
 886                    ],
 887                    cx,
 888                );
 889                multibuffer
 890            });
 891            let display_map =
 892                cx.new_model(|cx| DisplayMap::new(multibuffer, font, px(14.0), None, 2, 2, cx));
 893            let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
 894
 895            assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
 896
 897            let col_2_x =
 898                snapshot.x_for_display_point(DisplayPoint::new(2, 2), &text_layout_details);
 899
 900            // Can't move up into the first excerpt's header
 901            assert_eq!(
 902                up(
 903                    &snapshot,
 904                    DisplayPoint::new(2, 2),
 905                    SelectionGoal::HorizontalPosition(col_2_x.0),
 906                    false,
 907                    &text_layout_details
 908                ),
 909                (
 910                    DisplayPoint::new(2, 0),
 911                    SelectionGoal::HorizontalPosition(0.0)
 912                ),
 913            );
 914            assert_eq!(
 915                up(
 916                    &snapshot,
 917                    DisplayPoint::new(2, 0),
 918                    SelectionGoal::None,
 919                    false,
 920                    &text_layout_details
 921                ),
 922                (
 923                    DisplayPoint::new(2, 0),
 924                    SelectionGoal::HorizontalPosition(0.0)
 925                ),
 926            );
 927
 928            let col_4_x =
 929                snapshot.x_for_display_point(DisplayPoint::new(3, 4), &text_layout_details);
 930
 931            // Move up and down within first excerpt
 932            assert_eq!(
 933                up(
 934                    &snapshot,
 935                    DisplayPoint::new(3, 4),
 936                    SelectionGoal::HorizontalPosition(col_4_x.0),
 937                    false,
 938                    &text_layout_details
 939                ),
 940                (
 941                    DisplayPoint::new(2, 3),
 942                    SelectionGoal::HorizontalPosition(col_4_x.0)
 943                ),
 944            );
 945            assert_eq!(
 946                down(
 947                    &snapshot,
 948                    DisplayPoint::new(2, 3),
 949                    SelectionGoal::HorizontalPosition(col_4_x.0),
 950                    false,
 951                    &text_layout_details
 952                ),
 953                (
 954                    DisplayPoint::new(3, 4),
 955                    SelectionGoal::HorizontalPosition(col_4_x.0)
 956                ),
 957            );
 958
 959            let col_5_x =
 960                snapshot.x_for_display_point(DisplayPoint::new(6, 5), &text_layout_details);
 961
 962            // Move up and down across second excerpt's header
 963            assert_eq!(
 964                up(
 965                    &snapshot,
 966                    DisplayPoint::new(6, 5),
 967                    SelectionGoal::HorizontalPosition(col_5_x.0),
 968                    false,
 969                    &text_layout_details
 970                ),
 971                (
 972                    DisplayPoint::new(3, 4),
 973                    SelectionGoal::HorizontalPosition(col_5_x.0)
 974                ),
 975            );
 976            assert_eq!(
 977                down(
 978                    &snapshot,
 979                    DisplayPoint::new(3, 4),
 980                    SelectionGoal::HorizontalPosition(col_5_x.0),
 981                    false,
 982                    &text_layout_details
 983                ),
 984                (
 985                    DisplayPoint::new(6, 5),
 986                    SelectionGoal::HorizontalPosition(col_5_x.0)
 987                ),
 988            );
 989
 990            let max_point_x =
 991                snapshot.x_for_display_point(DisplayPoint::new(7, 2), &text_layout_details);
 992
 993            // Can't move down off the end
 994            assert_eq!(
 995                down(
 996                    &snapshot,
 997                    DisplayPoint::new(7, 0),
 998                    SelectionGoal::HorizontalPosition(0.0),
 999                    false,
1000                    &text_layout_details
1001                ),
1002                (
1003                    DisplayPoint::new(7, 2),
1004                    SelectionGoal::HorizontalPosition(max_point_x.0)
1005                ),
1006            );
1007            assert_eq!(
1008                down(
1009                    &snapshot,
1010                    DisplayPoint::new(7, 2),
1011                    SelectionGoal::HorizontalPosition(max_point_x.0),
1012                    false,
1013                    &text_layout_details
1014                ),
1015                (
1016                    DisplayPoint::new(7, 2),
1017                    SelectionGoal::HorizontalPosition(max_point_x.0)
1018                ),
1019            );
1020        });
1021    }
1022
1023    fn init_test(cx: &mut gpui::AppContext) {
1024        let settings_store = SettingsStore::test(cx);
1025        cx.set_global(settings_store);
1026        theme::init(theme::LoadThemes::JustBase, cx);
1027        language::init(cx);
1028        crate::init(cx);
1029        Project::init_settings(cx);
1030    }
1031}