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, 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.buffer_snapshot.max_row().0 {
 386        return map.max_point();
 387    }
 388
 389    let mut found_non_blank_line = false;
 390    for row in point.row..=map.buffer_snapshot.max_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_preceding_boundary_trail(
 492    map: &DisplaySnapshot,
 493    head: DisplayPoint,
 494    mut is_boundary: impl FnMut(char, char) -> bool,
 495) -> (Option<DisplayPoint>, DisplayPoint) {
 496    let mut offset = head.to_offset(map, Bias::Left);
 497    let mut trail_offset = None;
 498
 499    let mut prev_ch = map.buffer_snapshot.chars_at(offset).next();
 500    let mut forward = map.buffer_snapshot.reversed_chars_at(offset).peekable();
 501
 502    // Skip newlines
 503    while let Some(&ch) = forward.peek() {
 504        if ch == '\n' {
 505            prev_ch = forward.next();
 506            offset -= ch.len_utf8();
 507            trail_offset = Some(offset);
 508        } else {
 509            break;
 510        }
 511    }
 512
 513    // Find the boundary
 514    let start_offset = offset;
 515    for ch in forward {
 516        if let Some(prev_ch) = prev_ch {
 517            if is_boundary(prev_ch, ch) {
 518                if start_offset == offset {
 519                    trail_offset = Some(offset);
 520                } else {
 521                    break;
 522                }
 523            }
 524        }
 525        offset -= ch.len_utf8();
 526        prev_ch = Some(ch);
 527    }
 528
 529    let trail = trail_offset
 530        .map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Left));
 531
 532    (
 533        trail,
 534        map.clip_point(offset.to_display_point(map), Bias::Left),
 535    )
 536}
 537
 538/// Finds the location of a boundary
 539pub fn find_boundary_trail(
 540    map: &DisplaySnapshot,
 541    head: DisplayPoint,
 542    mut is_boundary: impl FnMut(char, char) -> bool,
 543) -> (Option<DisplayPoint>, DisplayPoint) {
 544    let mut offset = head.to_offset(map, Bias::Right);
 545    let mut trail_offset = None;
 546
 547    let mut prev_ch = map.buffer_snapshot.reversed_chars_at(offset).next();
 548    let mut forward = map.buffer_snapshot.chars_at(offset).peekable();
 549
 550    // Skip newlines
 551    while let Some(&ch) = forward.peek() {
 552        if ch == '\n' {
 553            prev_ch = forward.next();
 554            offset += ch.len_utf8();
 555            trail_offset = Some(offset);
 556        } else {
 557            break;
 558        }
 559    }
 560
 561    // Find the boundary
 562    let start_offset = offset;
 563    for ch in forward {
 564        if let Some(prev_ch) = prev_ch {
 565            if is_boundary(prev_ch, ch) {
 566                if start_offset == offset {
 567                    trail_offset = Some(offset);
 568                } else {
 569                    break;
 570                }
 571            }
 572        }
 573        offset += ch.len_utf8();
 574        prev_ch = Some(ch);
 575    }
 576
 577    let trail = trail_offset
 578        .map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Right));
 579
 580    (
 581        trail,
 582        map.clip_point(offset.to_display_point(map), Bias::Right),
 583    )
 584}
 585
 586pub fn find_boundary(
 587    map: &DisplaySnapshot,
 588    from: DisplayPoint,
 589    find_range: FindRange,
 590    is_boundary: impl FnMut(char, char) -> bool,
 591) -> DisplayPoint {
 592    find_boundary_point(map, from, find_range, is_boundary, false)
 593}
 594
 595pub fn find_boundary_exclusive(
 596    map: &DisplaySnapshot,
 597    from: DisplayPoint,
 598    find_range: FindRange,
 599    is_boundary: impl FnMut(char, char) -> bool,
 600) -> DisplayPoint {
 601    find_boundary_point(map, from, find_range, is_boundary, true)
 602}
 603
 604/// Returns an iterator over the characters following a given offset in the [`DisplaySnapshot`].
 605/// The returned value also contains a range of the start/end of a returned character in
 606/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
 607pub fn chars_after(
 608    map: &DisplaySnapshot,
 609    mut offset: usize,
 610) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
 611    map.buffer_snapshot.chars_at(offset).map(move |ch| {
 612        let before = offset;
 613        offset += ch.len_utf8();
 614        (ch, before..offset)
 615    })
 616}
 617
 618/// Returns a reverse iterator over the characters following a given offset in the [`DisplaySnapshot`].
 619/// The returned value also contains a range of the start/end of a returned character in
 620/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
 621pub fn chars_before(
 622    map: &DisplaySnapshot,
 623    mut offset: usize,
 624) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
 625    map.buffer_snapshot
 626        .reversed_chars_at(offset)
 627        .map(move |ch| {
 628            let after = offset;
 629            offset -= ch.len_utf8();
 630            (ch, offset..after)
 631        })
 632}
 633
 634pub(crate) fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
 635    let raw_point = point.to_point(map);
 636    let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
 637    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
 638    let text = &map.buffer_snapshot;
 639    let next_char_kind = text.chars_at(ix).next().map(|c| classifier.kind(c));
 640    let prev_char_kind = text
 641        .reversed_chars_at(ix)
 642        .next()
 643        .map(|c| classifier.kind(c));
 644    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
 645}
 646
 647pub(crate) fn surrounding_word(
 648    map: &DisplaySnapshot,
 649    position: DisplayPoint,
 650) -> Range<DisplayPoint> {
 651    let position = map
 652        .clip_point(position, Bias::Left)
 653        .to_offset(map, Bias::Left);
 654    let (range, _) = map.buffer_snapshot.surrounding_word(position, false);
 655    let start = range
 656        .start
 657        .to_point(&map.buffer_snapshot)
 658        .to_display_point(map);
 659    let end = range
 660        .end
 661        .to_point(&map.buffer_snapshot)
 662        .to_display_point(map);
 663    start..end
 664}
 665
 666/// Returns a list of lines (represented as a [`DisplayPoint`] range) contained
 667/// within a passed range.
 668///
 669/// The line ranges are **always* going to be in bounds of a requested range, which means that
 670/// the first and the last lines might not necessarily represent the
 671/// full range of a logical line (as their `.start`/`.end` values are clipped to those of a passed in range).
 672pub fn split_display_range_by_lines(
 673    map: &DisplaySnapshot,
 674    range: Range<DisplayPoint>,
 675) -> Vec<Range<DisplayPoint>> {
 676    let mut result = Vec::new();
 677
 678    let mut start = range.start;
 679    // Loop over all the covered rows until the one containing the range end
 680    for row in range.start.row().0..range.end.row().0 {
 681        let row_end_column = map.line_len(DisplayRow(row));
 682        let end = map.clip_point(
 683            DisplayPoint::new(DisplayRow(row), row_end_column),
 684            Bias::Left,
 685        );
 686        if start != end {
 687            result.push(start..end);
 688        }
 689        start = map.clip_point(DisplayPoint::new(DisplayRow(row + 1), 0), Bias::Left);
 690    }
 691
 692    // Add the final range from the start of the last end to the original range end.
 693    result.push(start..range.end);
 694
 695    result
 696}
 697
 698#[cfg(test)]
 699mod tests {
 700    use super::*;
 701    use crate::{
 702        display_map::Inlay,
 703        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
 704        Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
 705    };
 706    use gpui::{font, px, AppContext as _};
 707    use language::Capability;
 708    use project::Project;
 709    use settings::SettingsStore;
 710    use util::post_inc;
 711
 712    #[gpui::test]
 713    fn test_previous_word_start(cx: &mut gpui::App) {
 714        init_test(cx);
 715
 716        fn assert(marked_text: &str, cx: &mut gpui::App) {
 717            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 718            assert_eq!(
 719                previous_word_start(&snapshot, display_points[1]),
 720                display_points[0]
 721            );
 722        }
 723
 724        assert("\nˇ   ˇlorem", cx);
 725        assert("ˇ\nˇ   lorem", cx);
 726        assert("    ˇloremˇ", cx);
 727        assert("ˇ    ˇlorem", cx);
 728        assert("    ˇlorˇem", cx);
 729        assert("\nlorem\nˇ   ˇipsum", cx);
 730        assert("\n\nˇ\nˇ", cx);
 731        assert("    ˇlorem  ˇipsum", cx);
 732        assert("loremˇ-ˇipsum", cx);
 733        assert("loremˇ-#$@ˇipsum", cx);
 734        assert("ˇlorem_ˇipsum", cx);
 735        assert(" ˇdefγˇ", cx);
 736        assert(" ˇbcΔˇ", cx);
 737        assert(" abˇ——ˇcd", cx);
 738    }
 739
 740    #[gpui::test]
 741    fn test_previous_subword_start(cx: &mut gpui::App) {
 742        init_test(cx);
 743
 744        fn assert(marked_text: &str, cx: &mut gpui::App) {
 745            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 746            assert_eq!(
 747                previous_subword_start(&snapshot, display_points[1]),
 748                display_points[0]
 749            );
 750        }
 751
 752        // Subword boundaries are respected
 753        assert("lorem_ˇipˇsum", cx);
 754        assert("lorem_ˇipsumˇ", cx);
 755        assert("ˇlorem_ˇipsum", cx);
 756        assert("lorem_ˇipsum_ˇdolor", cx);
 757        assert("loremˇIpˇsum", cx);
 758        assert("loremˇIpsumˇ", cx);
 759
 760        // Word boundaries are still respected
 761        assert("\nˇ   ˇlorem", cx);
 762        assert("    ˇloremˇ", cx);
 763        assert("    ˇlorˇem", cx);
 764        assert("\nlorem\nˇ   ˇipsum", cx);
 765        assert("\n\nˇ\nˇ", cx);
 766        assert("    ˇlorem  ˇipsum", cx);
 767        assert("loremˇ-ˇipsum", cx);
 768        assert("loremˇ-#$@ˇipsum", cx);
 769        assert(" ˇdefγˇ", cx);
 770        assert(" bcˇΔˇ", cx);
 771        assert(" ˇbcδˇ", cx);
 772        assert(" abˇ——ˇcd", cx);
 773    }
 774
 775    #[gpui::test]
 776    fn test_find_preceding_boundary(cx: &mut gpui::App) {
 777        init_test(cx);
 778
 779        fn assert(
 780            marked_text: &str,
 781            cx: &mut gpui::App,
 782            is_boundary: impl FnMut(char, char) -> bool,
 783        ) {
 784            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 785            assert_eq!(
 786                find_preceding_boundary_display_point(
 787                    &snapshot,
 788                    display_points[1],
 789                    FindRange::MultiLine,
 790                    is_boundary
 791                ),
 792                display_points[0]
 793            );
 794        }
 795
 796        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
 797            left == 'c' && right == 'd'
 798        });
 799        assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
 800            left == '\n' && right == 'g'
 801        });
 802        let mut line_count = 0;
 803        assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
 804            if left == '\n' {
 805                line_count += 1;
 806                line_count == 2
 807            } else {
 808                false
 809            }
 810        });
 811    }
 812
 813    #[gpui::test]
 814    fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::App) {
 815        init_test(cx);
 816
 817        let input_text = "abcdefghijklmnopqrstuvwxys";
 818        let font = font("Helvetica");
 819        let font_size = px(14.0);
 820        let buffer = MultiBuffer::build_simple(input_text, cx);
 821        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 822
 823        let display_map = cx.new(|cx| {
 824            DisplayMap::new(
 825                buffer,
 826                font,
 827                font_size,
 828                None,
 829                true,
 830                1,
 831                1,
 832                1,
 833                FoldPlaceholder::test(),
 834                cx,
 835            )
 836        });
 837
 838        // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
 839        let mut id = 0;
 840        let inlays = (0..buffer_snapshot.len())
 841            .flat_map(|offset| {
 842                [
 843                    Inlay {
 844                        id: InlayId::InlineCompletion(post_inc(&mut id)),
 845                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
 846                        text: "test".into(),
 847                    },
 848                    Inlay {
 849                        id: InlayId::InlineCompletion(post_inc(&mut id)),
 850                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
 851                        text: "test".into(),
 852                    },
 853                    Inlay {
 854                        id: InlayId::Hint(post_inc(&mut id)),
 855                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
 856                        text: "test".into(),
 857                    },
 858                    Inlay {
 859                        id: InlayId::Hint(post_inc(&mut id)),
 860                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
 861                        text: "test".into(),
 862                    },
 863                ]
 864            })
 865            .collect();
 866        let snapshot = display_map.update(cx, |map, cx| {
 867            map.splice_inlays(&[], inlays, cx);
 868            map.snapshot(cx)
 869        });
 870
 871        assert_eq!(
 872            find_preceding_boundary_display_point(
 873                &snapshot,
 874                buffer_snapshot.len().to_display_point(&snapshot),
 875                FindRange::MultiLine,
 876                |left, _| left == 'e',
 877            ),
 878            snapshot
 879                .buffer_snapshot
 880                .offset_to_point(5)
 881                .to_display_point(&snapshot),
 882            "Should not stop at inlays when looking for boundaries"
 883        );
 884    }
 885
 886    #[gpui::test]
 887    fn test_next_word_end(cx: &mut gpui::App) {
 888        init_test(cx);
 889
 890        fn assert(marked_text: &str, cx: &mut gpui::App) {
 891            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 892            assert_eq!(
 893                next_word_end(&snapshot, display_points[0]),
 894                display_points[1]
 895            );
 896        }
 897
 898        assert("\nˇ   loremˇ", cx);
 899        assert("    ˇloremˇ", cx);
 900        assert("    lorˇemˇ", cx);
 901        assert("    loremˇ    ˇ\nipsum\n", cx);
 902        assert("\nˇ\nˇ\n\n", cx);
 903        assert("loremˇ    ipsumˇ   ", cx);
 904        assert("loremˇ-ˇipsum", cx);
 905        assert("loremˇ#$@-ˇipsum", cx);
 906        assert("loremˇ_ipsumˇ", cx);
 907        assert(" ˇbcΔˇ", cx);
 908        assert(" abˇ——ˇcd", cx);
 909    }
 910
 911    #[gpui::test]
 912    fn test_next_subword_end(cx: &mut gpui::App) {
 913        init_test(cx);
 914
 915        fn assert(marked_text: &str, cx: &mut gpui::App) {
 916            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 917            assert_eq!(
 918                next_subword_end(&snapshot, display_points[0]),
 919                display_points[1]
 920            );
 921        }
 922
 923        // Subword boundaries are respected
 924        assert("loˇremˇ_ipsum", cx);
 925        assert("ˇloremˇ_ipsum", cx);
 926        assert("loremˇ_ipsumˇ", cx);
 927        assert("loremˇ_ipsumˇ_dolor", cx);
 928        assert("loˇremˇIpsum", cx);
 929        assert("loremˇIpsumˇDolor", cx);
 930
 931        // Word boundaries are still respected
 932        assert("\nˇ   loremˇ", cx);
 933        assert("    ˇloremˇ", cx);
 934        assert("    lorˇemˇ", cx);
 935        assert("    loremˇ    ˇ\nipsum\n", cx);
 936        assert("\nˇ\nˇ\n\n", cx);
 937        assert("loremˇ    ipsumˇ   ", cx);
 938        assert("loremˇ-ˇipsum", cx);
 939        assert("loremˇ#$@-ˇipsum", cx);
 940        assert("loremˇ_ipsumˇ", cx);
 941        assert(" ˇbcˇΔ", cx);
 942        assert(" abˇ——ˇcd", cx);
 943    }
 944
 945    #[gpui::test]
 946    fn test_find_boundary(cx: &mut gpui::App) {
 947        init_test(cx);
 948
 949        fn assert(
 950            marked_text: &str,
 951            cx: &mut gpui::App,
 952            is_boundary: impl FnMut(char, char) -> bool,
 953        ) {
 954            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 955            assert_eq!(
 956                find_boundary(
 957                    &snapshot,
 958                    display_points[0],
 959                    FindRange::MultiLine,
 960                    is_boundary,
 961                ),
 962                display_points[1]
 963            );
 964        }
 965
 966        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
 967            left == 'j' && right == 'k'
 968        });
 969        assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
 970            left == '\n' && right == 'i'
 971        });
 972        let mut line_count = 0;
 973        assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
 974            if left == '\n' {
 975                line_count += 1;
 976                line_count == 2
 977            } else {
 978                false
 979            }
 980        });
 981    }
 982
 983    #[gpui::test]
 984    fn test_surrounding_word(cx: &mut gpui::App) {
 985        init_test(cx);
 986
 987        fn assert(marked_text: &str, cx: &mut gpui::App) {
 988            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 989            assert_eq!(
 990                surrounding_word(&snapshot, display_points[1]),
 991                display_points[0]..display_points[2],
 992                "{}",
 993                marked_text
 994            );
 995        }
 996
 997        assert("ˇˇloremˇ  ipsum", cx);
 998        assert("ˇloˇremˇ  ipsum", cx);
 999        assert("ˇloremˇˇ  ipsum", cx);
1000        assert("loremˇ ˇ  ˇipsum", cx);
1001        assert("lorem\nˇˇˇ\nipsum", cx);
1002        assert("lorem\nˇˇipsumˇ", cx);
1003        assert("loremˇ,ˇˇ ipsum", cx);
1004        assert("ˇloremˇˇ, ipsum", cx);
1005    }
1006
1007    #[gpui::test]
1008    async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
1009        cx.update(|cx| {
1010            init_test(cx);
1011        });
1012
1013        let mut cx = EditorTestContext::new(cx).await;
1014        let editor = cx.editor.clone();
1015        let window = cx.window;
1016        _ = cx.update_window(window, |_, window, cx| {
1017            let text_layout_details = editor.read(cx).text_layout_details(window);
1018
1019            let font = font("Helvetica");
1020
1021            let buffer = cx.new(|cx| Buffer::local("abc\ndefg\nhijkl\nmn", cx));
1022            let multibuffer = cx.new(|cx| {
1023                let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1024                multibuffer.push_excerpts(
1025                    buffer.clone(),
1026                    [
1027                        ExcerptRange {
1028                            context: Point::new(0, 0)..Point::new(1, 4),
1029                            primary: None,
1030                        },
1031                        ExcerptRange {
1032                            context: Point::new(2, 0)..Point::new(3, 2),
1033                            primary: None,
1034                        },
1035                    ],
1036                    cx,
1037                );
1038                multibuffer
1039            });
1040            let display_map = cx.new(|cx| {
1041                DisplayMap::new(
1042                    multibuffer,
1043                    font,
1044                    px(14.0),
1045                    None,
1046                    true,
1047                    0,
1048                    2,
1049                    0,
1050                    FoldPlaceholder::test(),
1051                    cx,
1052                )
1053            });
1054            let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
1055
1056            assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
1057
1058            let col_2_x = snapshot
1059                .x_for_display_point(DisplayPoint::new(DisplayRow(2), 2), &text_layout_details);
1060
1061            // Can't move up into the first excerpt's header
1062            assert_eq!(
1063                up(
1064                    &snapshot,
1065                    DisplayPoint::new(DisplayRow(2), 2),
1066                    SelectionGoal::HorizontalPosition(col_2_x.0),
1067                    false,
1068                    &text_layout_details
1069                ),
1070                (
1071                    DisplayPoint::new(DisplayRow(2), 0),
1072                    SelectionGoal::HorizontalPosition(col_2_x.0),
1073                ),
1074            );
1075            assert_eq!(
1076                up(
1077                    &snapshot,
1078                    DisplayPoint::new(DisplayRow(2), 0),
1079                    SelectionGoal::None,
1080                    false,
1081                    &text_layout_details
1082                ),
1083                (
1084                    DisplayPoint::new(DisplayRow(2), 0),
1085                    SelectionGoal::HorizontalPosition(0.0),
1086                ),
1087            );
1088
1089            let col_4_x = snapshot
1090                .x_for_display_point(DisplayPoint::new(DisplayRow(3), 4), &text_layout_details);
1091
1092            // Move up and down within first excerpt
1093            assert_eq!(
1094                up(
1095                    &snapshot,
1096                    DisplayPoint::new(DisplayRow(3), 4),
1097                    SelectionGoal::HorizontalPosition(col_4_x.0),
1098                    false,
1099                    &text_layout_details
1100                ),
1101                (
1102                    DisplayPoint::new(DisplayRow(2), 3),
1103                    SelectionGoal::HorizontalPosition(col_4_x.0)
1104                ),
1105            );
1106            assert_eq!(
1107                down(
1108                    &snapshot,
1109                    DisplayPoint::new(DisplayRow(2), 3),
1110                    SelectionGoal::HorizontalPosition(col_4_x.0),
1111                    false,
1112                    &text_layout_details
1113                ),
1114                (
1115                    DisplayPoint::new(DisplayRow(3), 4),
1116                    SelectionGoal::HorizontalPosition(col_4_x.0)
1117                ),
1118            );
1119
1120            let col_5_x = snapshot
1121                .x_for_display_point(DisplayPoint::new(DisplayRow(6), 5), &text_layout_details);
1122
1123            // Move up and down across second excerpt's header
1124            assert_eq!(
1125                up(
1126                    &snapshot,
1127                    DisplayPoint::new(DisplayRow(6), 5),
1128                    SelectionGoal::HorizontalPosition(col_5_x.0),
1129                    false,
1130                    &text_layout_details
1131                ),
1132                (
1133                    DisplayPoint::new(DisplayRow(3), 4),
1134                    SelectionGoal::HorizontalPosition(col_5_x.0)
1135                ),
1136            );
1137            assert_eq!(
1138                down(
1139                    &snapshot,
1140                    DisplayPoint::new(DisplayRow(3), 4),
1141                    SelectionGoal::HorizontalPosition(col_5_x.0),
1142                    false,
1143                    &text_layout_details
1144                ),
1145                (
1146                    DisplayPoint::new(DisplayRow(6), 5),
1147                    SelectionGoal::HorizontalPosition(col_5_x.0)
1148                ),
1149            );
1150
1151            let max_point_x = snapshot
1152                .x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details);
1153
1154            // Can't move down off the end, and attempting to do so leaves the selection goal unchanged
1155            assert_eq!(
1156                down(
1157                    &snapshot,
1158                    DisplayPoint::new(DisplayRow(7), 0),
1159                    SelectionGoal::HorizontalPosition(0.0),
1160                    false,
1161                    &text_layout_details
1162                ),
1163                (
1164                    DisplayPoint::new(DisplayRow(7), 2),
1165                    SelectionGoal::HorizontalPosition(0.0)
1166                ),
1167            );
1168            assert_eq!(
1169                down(
1170                    &snapshot,
1171                    DisplayPoint::new(DisplayRow(7), 2),
1172                    SelectionGoal::HorizontalPosition(max_point_x.0),
1173                    false,
1174                    &text_layout_details
1175                ),
1176                (
1177                    DisplayPoint::new(DisplayRow(7), 2),
1178                    SelectionGoal::HorizontalPosition(max_point_x.0)
1179                ),
1180            );
1181        });
1182    }
1183
1184    fn init_test(cx: &mut gpui::App) {
1185        let settings_store = SettingsStore::test(cx);
1186        cx.set_global(settings_store);
1187        workspace::init_settings(cx);
1188        theme::init(theme::LoadThemes::JustBase, cx);
1189        language::init(cx);
1190        crate::init(cx);
1191        Project::init_settings(cx);
1192    }
1193}