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