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