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