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::{DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::ScrollAnchor};
   6use gpui::{Pixels, WindowTextSystem};
   7use language::{CharClassifier, Point};
   8use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
   9use serde::Deserialize;
  10use workspace::searchable::Direction;
  11
  12use std::{ops::Range, sync::Arc};
  13
  14/// Defines search strategy for items in `movement` module.
  15/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
  16/// `FindRange::MultiLine` keeps going until the end of a string.
  17#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
  18pub enum FindRange {
  19    SingleLine,
  20    MultiLine,
  21}
  22
  23/// TextLayoutDetails encompasses everything we need to move vertically
  24/// taking into account variable width characters.
  25pub struct TextLayoutDetails {
  26    pub(crate) text_system: Arc<WindowTextSystem>,
  27    pub(crate) editor_style: EditorStyle,
  28    pub(crate) rem_size: Pixels,
  29    pub scroll_anchor: ScrollAnchor,
  30    pub visible_rows: Option<f32>,
  31    pub vertical_scroll_margin: f32,
  32}
  33
  34/// Returns a column to the left of the current point, wrapping
  35/// to the previous line if that point is at the start of line.
  36pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  37    if point.column() > 0 {
  38        *point.column_mut() -= 1;
  39    } else if point.row().0 > 0 {
  40        *point.row_mut() -= 1;
  41        *point.column_mut() = map.line_len(point.row());
  42    }
  43    map.clip_point(point, Bias::Left)
  44}
  45
  46/// Returns a column to the left of the current point, doing nothing if
  47/// that point is already at the start of line.
  48pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  49    if point.column() > 0 {
  50        *point.column_mut() -= 1;
  51    } else if point.column() == 0 {
  52        // If the current sofr_wrap mode is used, the column corresponding to the display is 0,
  53        //  which does not necessarily mean that the actual beginning of a paragraph
  54        if map.display_point_to_fold_point(point, Bias::Left).column() > 0 {
  55            return left(map, point);
  56        }
  57    }
  58    map.clip_point(point, Bias::Left)
  59}
  60
  61/// Returns a column to the right of the current point, wrapping
  62/// to the next line if that point is at the end of line.
  63pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  64    if point.column() < map.line_len(point.row()) {
  65        *point.column_mut() += 1;
  66    } else if point.row() < map.max_point().row() {
  67        *point.row_mut() += 1;
  68        *point.column_mut() = 0;
  69    }
  70    map.clip_point(point, Bias::Right)
  71}
  72
  73/// Returns a column to the right of the current point, not performing any wrapping
  74/// if that point is already at the end of line.
  75pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  76    *point.column_mut() += 1;
  77    map.clip_point(point, Bias::Right)
  78}
  79
  80/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
  81pub fn up(
  82    map: &DisplaySnapshot,
  83    start: DisplayPoint,
  84    goal: SelectionGoal,
  85    preserve_column_at_start: bool,
  86    text_layout_details: &TextLayoutDetails,
  87) -> (DisplayPoint, SelectionGoal) {
  88    up_by_rows(
  89        map,
  90        start,
  91        1,
  92        goal,
  93        preserve_column_at_start,
  94        text_layout_details,
  95    )
  96}
  97
  98/// Returns a display point for the next displayed line (which might be a soft-wrapped line).
  99pub fn down(
 100    map: &DisplaySnapshot,
 101    start: DisplayPoint,
 102    goal: SelectionGoal,
 103    preserve_column_at_end: bool,
 104    text_layout_details: &TextLayoutDetails,
 105) -> (DisplayPoint, SelectionGoal) {
 106    down_by_rows(
 107        map,
 108        start,
 109        1,
 110        goal,
 111        preserve_column_at_end,
 112        text_layout_details,
 113    )
 114}
 115
 116pub(crate) fn up_by_rows(
 117    map: &DisplaySnapshot,
 118    start: DisplayPoint,
 119    row_count: u32,
 120    goal: SelectionGoal,
 121    preserve_column_at_start: bool,
 122    text_layout_details: &TextLayoutDetails,
 123) -> (DisplayPoint, SelectionGoal) {
 124    let goal_x = match goal {
 125        SelectionGoal::HorizontalPosition(x) => x.into(),
 126        SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
 127        SelectionGoal::HorizontalRange { end, .. } => end.into(),
 128        _ => map.x_for_display_point(start, text_layout_details),
 129    };
 130
 131    let prev_row = DisplayRow(start.row().0.saturating_sub(row_count));
 132    let mut point = map.clip_point(
 133        DisplayPoint::new(prev_row, map.line_len(prev_row)),
 134        Bias::Left,
 135    );
 136    if point.row() < start.row() {
 137        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
 138    } else if preserve_column_at_start {
 139        return (start, goal);
 140    } else {
 141        point = DisplayPoint::new(DisplayRow(0), 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 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    }
 178
 179    let mut clipped_point = map.clip_point(point, Bias::Right);
 180    if clipped_point.row() > point.row() {
 181        clipped_point = map.clip_point(point, Bias::Left);
 182    }
 183    (
 184        clipped_point,
 185        SelectionGoal::HorizontalPosition(goal_x.into()),
 186    )
 187}
 188
 189/// Returns a position of the start of line.
 190/// If `stop_at_soft_boundaries` is true, the returned position is that of the
 191/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
 192/// Otherwise it's always going to be the start of a logical line.
 193pub fn line_beginning(
 194    map: &DisplaySnapshot,
 195    display_point: DisplayPoint,
 196    stop_at_soft_boundaries: bool,
 197) -> DisplayPoint {
 198    let point = display_point.to_point(map);
 199    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
 200    let line_start = map.prev_line_boundary(point).1;
 201
 202    if stop_at_soft_boundaries && display_point != soft_line_start {
 203        soft_line_start
 204    } else {
 205        line_start
 206    }
 207}
 208
 209/// Returns the last indented position on a given line.
 210/// If `stop_at_soft_boundaries` is true, the returned [`DisplayPoint`] is that of a
 211/// displayed line (e.g. if there's soft wrap it's gonna be returned),
 212/// otherwise it's always going to be a start of a logical line.
 213pub fn indented_line_beginning(
 214    map: &DisplaySnapshot,
 215    display_point: DisplayPoint,
 216    stop_at_soft_boundaries: bool,
 217    stop_at_indent: 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_indent && (display_point > indent_start || display_point == line_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    let mut is_first_iteration = true;
 268    find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
 269        // Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello.
 270        if is_first_iteration
 271            && classifier.is_punctuation(right)
 272            && !classifier.is_punctuation(left)
 273            && left != '\n'
 274        {
 275            is_first_iteration = false;
 276            return false;
 277        }
 278        is_first_iteration = false;
 279
 280        (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
 281            || left == '\n'
 282    })
 283}
 284
 285/// Returns a position of the previous word boundary, where a word character is defined as either
 286/// uppercase letter, lowercase letter, '_' character, language-specific word character (like '-' in CSS) or newline.
 287pub fn previous_word_start_or_newline(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        (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
 293            || left == '\n'
 294            || right == '\n'
 295    })
 296}
 297
 298/// Text movements are too greedy, making deletions too greedy too.
 299/// Makes deletions more ergonomic by potentially reducing the deletion range based on its text contents:
 300/// * whitespace sequences with length >= 2 stop the deletion after removal (despite movement jumping over the word behind the whitespaces)
 301/// * brackets stop the deletion after removal (despite movement currently not accounting for these and jumping over)
 302pub fn adjust_greedy_deletion(
 303    map: &DisplaySnapshot,
 304    delete_from: DisplayPoint,
 305    delete_until: DisplayPoint,
 306    ignore_brackets: bool,
 307) -> DisplayPoint {
 308    if delete_from == delete_until {
 309        return delete_until;
 310    }
 311    let is_backward = delete_from > delete_until;
 312    let delete_range = if is_backward {
 313        map.display_point_to_point(delete_until, Bias::Left)
 314            .to_offset(&map.buffer_snapshot)
 315            ..map
 316                .display_point_to_point(delete_from, Bias::Right)
 317                .to_offset(&map.buffer_snapshot)
 318    } else {
 319        map.display_point_to_point(delete_from, Bias::Left)
 320            .to_offset(&map.buffer_snapshot)
 321            ..map
 322                .display_point_to_point(delete_until, Bias::Right)
 323                .to_offset(&map.buffer_snapshot)
 324    };
 325
 326    let trimmed_delete_range = if ignore_brackets {
 327        delete_range
 328    } else {
 329        let brackets_in_delete_range = map
 330            .buffer_snapshot
 331            .bracket_ranges(delete_range.clone())
 332            .into_iter()
 333            .flatten()
 334            .flat_map(|(left_bracket, right_bracket)| {
 335                [
 336                    left_bracket.start,
 337                    left_bracket.end,
 338                    right_bracket.start,
 339                    right_bracket.end,
 340                ]
 341            })
 342            .filter(|&bracket| delete_range.start < bracket && bracket < delete_range.end);
 343        let closest_bracket = if is_backward {
 344            brackets_in_delete_range.max()
 345        } else {
 346            brackets_in_delete_range.min()
 347        };
 348
 349        if is_backward {
 350            closest_bracket.unwrap_or(delete_range.start)..delete_range.end
 351        } else {
 352            delete_range.start..closest_bracket.unwrap_or(delete_range.end)
 353        }
 354    };
 355
 356    let mut whitespace_sequences = Vec::new();
 357    let mut current_offset = trimmed_delete_range.start;
 358    let mut whitespace_sequence_length = 0;
 359    let mut whitespace_sequence_start = 0;
 360    for ch in map
 361        .buffer_snapshot
 362        .text_for_range(trimmed_delete_range.clone())
 363        .flat_map(str::chars)
 364    {
 365        if ch.is_whitespace() {
 366            if whitespace_sequence_length == 0 {
 367                whitespace_sequence_start = current_offset;
 368            }
 369            whitespace_sequence_length += 1;
 370        } else {
 371            if whitespace_sequence_length >= 2 {
 372                whitespace_sequences.push((whitespace_sequence_start, current_offset));
 373            }
 374            whitespace_sequence_start = 0;
 375            whitespace_sequence_length = 0;
 376        }
 377        current_offset += ch.len_utf8();
 378    }
 379    if whitespace_sequence_length >= 2 {
 380        whitespace_sequences.push((whitespace_sequence_start, current_offset));
 381    }
 382
 383    let closest_whitespace_end = if is_backward {
 384        whitespace_sequences.last().map(|&(start, _)| start)
 385    } else {
 386        whitespace_sequences.first().map(|&(_, end)| end)
 387    };
 388
 389    closest_whitespace_end
 390        .unwrap_or_else(|| {
 391            if is_backward {
 392                trimmed_delete_range.start
 393            } else {
 394                trimmed_delete_range.end
 395            }
 396        })
 397        .to_display_point(map)
 398}
 399
 400/// Returns a position of the previous subword boundary, where a subword is defined as a run of
 401/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
 402/// lowerspace characters and uppercase characters.
 403pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
 404    let raw_point = point.to_point(map);
 405    let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
 406
 407    find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
 408        is_subword_start(left, right, &classifier) || left == '\n'
 409    })
 410}
 411
 412pub fn is_subword_start(left: char, right: char, classifier: &CharClassifier) -> bool {
 413    let is_word_start = classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
 414    let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
 415        || left == '_' && right != '_'
 416        || left.is_lowercase() && right.is_uppercase();
 417    is_word_start || is_subword_start
 418}
 419
 420/// Returns a position of the next word boundary, where a word character is defined as either
 421/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
 422pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
 423    let raw_point = point.to_point(map);
 424    let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
 425    let mut is_first_iteration = true;
 426    find_boundary(map, point, FindRange::MultiLine, |left, right| {
 427        // Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello|
 428        if is_first_iteration
 429            && classifier.is_punctuation(left)
 430            && !classifier.is_punctuation(right)
 431            && right != '\n'
 432        {
 433            is_first_iteration = false;
 434            return false;
 435        }
 436        is_first_iteration = false;
 437
 438        (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left))
 439            || right == '\n'
 440    })
 441}
 442
 443/// Returns a position of the next word boundary, where a word character is defined as either
 444/// uppercase letter, lowercase letter, '_' character, language-specific word character (like '-' in CSS) or newline.
 445pub fn next_word_end_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
 446    let raw_point = point.to_point(map);
 447    let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
 448
 449    let mut on_starting_row = true;
 450    find_boundary(map, point, FindRange::MultiLine, |left, right| {
 451        if left == '\n' {
 452            on_starting_row = false;
 453        }
 454        (classifier.kind(left) != classifier.kind(right)
 455            && ((on_starting_row && !left.is_whitespace())
 456                || (!on_starting_row && !right.is_whitespace())))
 457            || right == '\n'
 458    })
 459}
 460
 461/// Returns a position of the next subword boundary, where a subword is defined as a run of
 462/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
 463/// lowerspace characters and uppercase characters.
 464pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
 465    let raw_point = point.to_point(map);
 466    let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
 467
 468    find_boundary(map, point, FindRange::MultiLine, |left, right| {
 469        is_subword_end(left, right, &classifier) || right == '\n'
 470    })
 471}
 472
 473pub fn is_subword_end(left: char, right: char, classifier: &CharClassifier) -> bool {
 474    let is_word_end =
 475        (classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
 476    let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
 477        || left != '_' && right == '_'
 478        || left.is_lowercase() && right.is_uppercase();
 479    is_word_end || is_subword_end
 480}
 481
 482/// Returns a position of the start of the current paragraph, where a paragraph
 483/// is defined as a run of non-blank lines.
 484pub fn start_of_paragraph(
 485    map: &DisplaySnapshot,
 486    display_point: DisplayPoint,
 487    mut count: usize,
 488) -> DisplayPoint {
 489    let point = display_point.to_point(map);
 490    if point.row == 0 {
 491        return DisplayPoint::zero();
 492    }
 493
 494    let mut found_non_blank_line = false;
 495    for row in (0..point.row + 1).rev() {
 496        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
 497        if found_non_blank_line && blank {
 498            if count <= 1 {
 499                return Point::new(row, 0).to_display_point(map);
 500            }
 501            count -= 1;
 502            found_non_blank_line = false;
 503        }
 504
 505        found_non_blank_line |= !blank;
 506    }
 507
 508    DisplayPoint::zero()
 509}
 510
 511/// Returns a position of the end of the current paragraph, where a paragraph
 512/// is defined as a run of non-blank lines.
 513pub fn end_of_paragraph(
 514    map: &DisplaySnapshot,
 515    display_point: DisplayPoint,
 516    mut count: usize,
 517) -> DisplayPoint {
 518    let point = display_point.to_point(map);
 519    if point.row == map.buffer_snapshot.max_row().0 {
 520        return map.max_point();
 521    }
 522
 523    let mut found_non_blank_line = false;
 524    for row in point.row..=map.buffer_snapshot.max_row().0 {
 525        let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
 526        if found_non_blank_line && blank {
 527            if count <= 1 {
 528                return Point::new(row, 0).to_display_point(map);
 529            }
 530            count -= 1;
 531            found_non_blank_line = false;
 532        }
 533
 534        found_non_blank_line |= !blank;
 535    }
 536
 537    map.max_point()
 538}
 539
 540pub fn start_of_excerpt(
 541    map: &DisplaySnapshot,
 542    display_point: DisplayPoint,
 543    direction: Direction,
 544) -> DisplayPoint {
 545    let point = map.display_point_to_point(display_point, Bias::Left);
 546    let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
 547        return display_point;
 548    };
 549    match direction {
 550        Direction::Prev => {
 551            let mut start = excerpt.start_anchor().to_display_point(map);
 552            if start >= display_point && start.row() > DisplayRow(0) {
 553                let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
 554                    return display_point;
 555                };
 556                start = excerpt.start_anchor().to_display_point(map);
 557            }
 558            start
 559        }
 560        Direction::Next => {
 561            let mut end = excerpt.end_anchor().to_display_point(map);
 562            *end.row_mut() += 1;
 563            map.clip_point(end, Bias::Right)
 564        }
 565    }
 566}
 567
 568pub fn end_of_excerpt(
 569    map: &DisplaySnapshot,
 570    display_point: DisplayPoint,
 571    direction: Direction,
 572) -> DisplayPoint {
 573    let point = map.display_point_to_point(display_point, Bias::Left);
 574    let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
 575        return display_point;
 576    };
 577    match direction {
 578        Direction::Prev => {
 579            let mut start = excerpt.start_anchor().to_display_point(map);
 580            if start.row() > DisplayRow(0) {
 581                *start.row_mut() -= 1;
 582            }
 583            start = map.clip_point(start, Bias::Left);
 584            *start.column_mut() = 0;
 585            start
 586        }
 587        Direction::Next => {
 588            let mut end = excerpt.end_anchor().to_display_point(map);
 589            *end.column_mut() = 0;
 590            if end <= display_point {
 591                *end.row_mut() += 1;
 592                let point_end = map.display_point_to_point(end, Bias::Right);
 593                let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point_end..point_end)
 594                else {
 595                    return display_point;
 596                };
 597                end = excerpt.end_anchor().to_display_point(map);
 598                *end.column_mut() = 0;
 599            }
 600            end
 601        }
 602    }
 603}
 604
 605/// Scans for a boundary preceding the given start point `from` until a boundary is found,
 606/// indicated by the given predicate returning true.
 607/// The predicate is called with the character to the left and right of the candidate boundary location.
 608/// 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.
 609pub fn find_preceding_boundary_point(
 610    buffer_snapshot: &MultiBufferSnapshot,
 611    from: Point,
 612    find_range: FindRange,
 613    mut is_boundary: impl FnMut(char, char) -> bool,
 614) -> Point {
 615    let mut prev_ch = None;
 616    let mut offset = from.to_offset(buffer_snapshot);
 617
 618    for ch in buffer_snapshot.reversed_chars_at(offset) {
 619        if find_range == FindRange::SingleLine && ch == '\n' {
 620            break;
 621        }
 622        if let Some(prev_ch) = prev_ch
 623            && is_boundary(ch, prev_ch)
 624        {
 625            break;
 626        }
 627
 628        offset -= ch.len_utf8();
 629        prev_ch = Some(ch);
 630    }
 631
 632    offset.to_point(buffer_snapshot)
 633}
 634
 635/// Scans for a boundary preceding the given start point `from` until a boundary is found,
 636/// indicated by the given predicate returning true.
 637/// The predicate is called with the character to the left and right of the candidate boundary location.
 638/// 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.
 639pub fn find_preceding_boundary_display_point(
 640    map: &DisplaySnapshot,
 641    from: DisplayPoint,
 642    find_range: FindRange,
 643    is_boundary: impl FnMut(char, char) -> bool,
 644) -> DisplayPoint {
 645    let result = find_preceding_boundary_point(
 646        &map.buffer_snapshot,
 647        from.to_point(map),
 648        find_range,
 649        is_boundary,
 650    );
 651    map.clip_point(result.to_display_point(map), Bias::Left)
 652}
 653
 654/// Scans for a boundary following the given start point until a boundary is found, indicated by the
 655/// given predicate returning true. The predicate is called with the character to the left and right
 656/// of the candidate boundary location, and will be called with `\n` characters indicating the start
 657/// or end of a line. The function supports optionally returning the point just before the boundary
 658/// is found via return_point_before_boundary.
 659pub fn find_boundary_point(
 660    map: &DisplaySnapshot,
 661    from: DisplayPoint,
 662    find_range: FindRange,
 663    mut is_boundary: impl FnMut(char, char) -> bool,
 664    return_point_before_boundary: bool,
 665) -> DisplayPoint {
 666    let mut offset = from.to_offset(map, Bias::Right);
 667    let mut prev_offset = offset;
 668    let mut prev_ch = None;
 669
 670    for ch in map.buffer_snapshot.chars_at(offset) {
 671        if find_range == FindRange::SingleLine && ch == '\n' {
 672            break;
 673        }
 674        if let Some(prev_ch) = prev_ch
 675            && is_boundary(prev_ch, ch)
 676        {
 677            if return_point_before_boundary {
 678                return map.clip_point(prev_offset.to_display_point(map), Bias::Right);
 679            } else {
 680                break;
 681            }
 682        }
 683        prev_offset = offset;
 684        offset += ch.len_utf8();
 685        prev_ch = Some(ch);
 686    }
 687    map.clip_point(offset.to_display_point(map), Bias::Right)
 688}
 689
 690pub fn find_preceding_boundary_trail(
 691    map: &DisplaySnapshot,
 692    head: DisplayPoint,
 693    mut is_boundary: impl FnMut(char, char) -> bool,
 694) -> (Option<DisplayPoint>, DisplayPoint) {
 695    let mut offset = head.to_offset(map, Bias::Left);
 696    let mut trail_offset = None;
 697
 698    let mut prev_ch = map.buffer_snapshot.chars_at(offset).next();
 699    let mut forward = map.buffer_snapshot.reversed_chars_at(offset).peekable();
 700
 701    // Skip newlines
 702    while let Some(&ch) = forward.peek() {
 703        if ch == '\n' {
 704            prev_ch = forward.next();
 705            offset -= ch.len_utf8();
 706            trail_offset = Some(offset);
 707        } else {
 708            break;
 709        }
 710    }
 711
 712    // Find the boundary
 713    let start_offset = offset;
 714    for ch in forward {
 715        if let Some(prev_ch) = prev_ch
 716            && is_boundary(prev_ch, ch)
 717        {
 718            if start_offset == offset {
 719                trail_offset = Some(offset);
 720            } else {
 721                break;
 722            }
 723        }
 724        offset -= ch.len_utf8();
 725        prev_ch = Some(ch);
 726    }
 727
 728    let trail = trail_offset
 729        .map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Left));
 730
 731    (
 732        trail,
 733        map.clip_point(offset.to_display_point(map), Bias::Left),
 734    )
 735}
 736
 737/// Finds the location of a boundary
 738pub fn find_boundary_trail(
 739    map: &DisplaySnapshot,
 740    head: DisplayPoint,
 741    mut is_boundary: impl FnMut(char, char) -> bool,
 742) -> (Option<DisplayPoint>, DisplayPoint) {
 743    let mut offset = head.to_offset(map, Bias::Right);
 744    let mut trail_offset = None;
 745
 746    let mut prev_ch = map.buffer_snapshot.reversed_chars_at(offset).next();
 747    let mut forward = map.buffer_snapshot.chars_at(offset).peekable();
 748
 749    // Skip newlines
 750    while let Some(&ch) = forward.peek() {
 751        if ch == '\n' {
 752            prev_ch = forward.next();
 753            offset += ch.len_utf8();
 754            trail_offset = Some(offset);
 755        } else {
 756            break;
 757        }
 758    }
 759
 760    // Find the boundary
 761    let start_offset = offset;
 762    for ch in forward {
 763        if let Some(prev_ch) = prev_ch
 764            && is_boundary(prev_ch, ch)
 765        {
 766            if start_offset == offset {
 767                trail_offset = Some(offset);
 768            } else {
 769                break;
 770            }
 771        }
 772        offset += ch.len_utf8();
 773        prev_ch = Some(ch);
 774    }
 775
 776    let trail = trail_offset
 777        .map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Right));
 778
 779    (
 780        trail,
 781        map.clip_point(offset.to_display_point(map), Bias::Right),
 782    )
 783}
 784
 785pub fn find_boundary(
 786    map: &DisplaySnapshot,
 787    from: DisplayPoint,
 788    find_range: FindRange,
 789    is_boundary: impl FnMut(char, char) -> bool,
 790) -> DisplayPoint {
 791    find_boundary_point(map, from, find_range, is_boundary, false)
 792}
 793
 794pub fn find_boundary_exclusive(
 795    map: &DisplaySnapshot,
 796    from: DisplayPoint,
 797    find_range: FindRange,
 798    is_boundary: impl FnMut(char, char) -> bool,
 799) -> DisplayPoint {
 800    find_boundary_point(map, from, find_range, is_boundary, true)
 801}
 802
 803/// Returns an iterator over the characters following a given offset in the [`DisplaySnapshot`].
 804/// The returned value also contains a range of the start/end of a returned character in
 805/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
 806pub fn chars_after(
 807    map: &DisplaySnapshot,
 808    mut offset: usize,
 809) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
 810    map.buffer_snapshot.chars_at(offset).map(move |ch| {
 811        let before = offset;
 812        offset += ch.len_utf8();
 813        (ch, before..offset)
 814    })
 815}
 816
 817/// Returns a reverse iterator over the characters following a given offset in the [`DisplaySnapshot`].
 818/// The returned value also contains a range of the start/end of a returned character in
 819/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
 820pub fn chars_before(
 821    map: &DisplaySnapshot,
 822    mut offset: usize,
 823) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
 824    map.buffer_snapshot
 825        .reversed_chars_at(offset)
 826        .map(move |ch| {
 827            let after = offset;
 828            offset -= ch.len_utf8();
 829            (ch, offset..after)
 830        })
 831}
 832
 833/// Returns a list of lines (represented as a [`DisplayPoint`] range) contained
 834/// within a passed range.
 835///
 836/// The line ranges are **always* going to be in bounds of a requested range, which means that
 837/// the first and the last lines might not necessarily represent the
 838/// full range of a logical line (as their `.start`/`.end` values are clipped to those of a passed in range).
 839pub fn split_display_range_by_lines(
 840    map: &DisplaySnapshot,
 841    range: Range<DisplayPoint>,
 842) -> Vec<Range<DisplayPoint>> {
 843    let mut result = Vec::new();
 844
 845    let mut start = range.start;
 846    // Loop over all the covered rows until the one containing the range end
 847    for row in range.start.row().0..range.end.row().0 {
 848        let row_end_column = map.line_len(DisplayRow(row));
 849        let end = map.clip_point(
 850            DisplayPoint::new(DisplayRow(row), row_end_column),
 851            Bias::Left,
 852        );
 853        if start != end {
 854            result.push(start..end);
 855        }
 856        start = map.clip_point(DisplayPoint::new(DisplayRow(row + 1), 0), Bias::Left);
 857    }
 858
 859    // Add the final range from the start of the last end to the original range end.
 860    result.push(start..range.end);
 861
 862    result
 863}
 864
 865#[cfg(test)]
 866mod tests {
 867    use super::*;
 868    use crate::{
 869        Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer,
 870        display_map::Inlay,
 871        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
 872    };
 873    use gpui::{AppContext as _, font, px};
 874    use language::Capability;
 875    use project::{Project, project_settings::DiagnosticSeverity};
 876    use settings::SettingsStore;
 877    use util::post_inc;
 878
 879    #[gpui::test]
 880    fn test_previous_word_start(cx: &mut gpui::App) {
 881        init_test(cx);
 882
 883        fn assert(marked_text: &str, cx: &mut gpui::App) {
 884            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 885            let actual = previous_word_start(&snapshot, display_points[1]);
 886            let expected = display_points[0];
 887            if actual != expected {
 888                eprintln!(
 889                    "previous_word_start mismatch for '{}': actual={:?}, expected={:?}",
 890                    marked_text, actual, expected
 891                );
 892            }
 893            assert_eq!(actual, expected);
 894        }
 895
 896        assert("\nˇ   ˇlorem", cx);
 897        assert("ˇ\nˇ   lorem", cx);
 898        assert("    ˇloremˇ", cx);
 899        assert("ˇ    ˇlorem", cx);
 900        assert("    ˇlorˇem", cx);
 901        assert("\nlorem\nˇ   ˇipsum", cx);
 902        assert("\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(" ˇdefγˇ", cx);
 908        assert(" ˇbcΔˇ", cx);
 909        // Test punctuation skipping behavior
 910        assert("ˇhello.ˇ", cx);
 911        assert("helloˇ...ˇ", cx);
 912        assert("helloˇ.---..ˇtest", cx);
 913        assert("test  ˇ.--ˇtest", cx);
 914        assert("oneˇ,;:!?ˇtwo", cx);
 915    }
 916
 917    #[gpui::test]
 918    fn test_previous_subword_start(cx: &mut gpui::App) {
 919        init_test(cx);
 920
 921        fn assert(marked_text: &str, cx: &mut gpui::App) {
 922            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 923            assert_eq!(
 924                previous_subword_start(&snapshot, display_points[1]),
 925                display_points[0]
 926            );
 927        }
 928
 929        // Subword boundaries are respected
 930        assert("lorem_ˇipˇsum", cx);
 931        assert("lorem_ˇipsumˇ", cx);
 932        assert("ˇlorem_ˇipsum", cx);
 933        assert("lorem_ˇipsum_ˇdolor", cx);
 934        assert("loremˇIpˇsum", cx);
 935        assert("loremˇIpsumˇ", cx);
 936
 937        // Word boundaries are still respected
 938        assert("\nˇ   ˇlorem", cx);
 939        assert("    ˇloremˇ", cx);
 940        assert("    ˇlorˇem", cx);
 941        assert("\nlorem\nˇ   ˇipsum", cx);
 942        assert("\n\nˇ\nˇ", cx);
 943        assert("    ˇlorem  ˇipsum", cx);
 944        assert("loremˇ-ˇipsum", cx);
 945        assert("loremˇ-#$@ˇipsum", cx);
 946        assert(" ˇdefγˇ", cx);
 947        assert(" bcˇΔˇ", cx);
 948        assert(" ˇbcδˇ", cx);
 949        assert(" abˇ——ˇcd", cx);
 950    }
 951
 952    #[gpui::test]
 953    fn test_find_preceding_boundary(cx: &mut gpui::App) {
 954        init_test(cx);
 955
 956        fn assert(
 957            marked_text: &str,
 958            cx: &mut gpui::App,
 959            is_boundary: impl FnMut(char, char) -> bool,
 960        ) {
 961            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
 962            assert_eq!(
 963                find_preceding_boundary_display_point(
 964                    &snapshot,
 965                    display_points[1],
 966                    FindRange::MultiLine,
 967                    is_boundary
 968                ),
 969                display_points[0]
 970            );
 971        }
 972
 973        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
 974            left == 'c' && right == 'd'
 975        });
 976        assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
 977            left == '\n' && right == 'g'
 978        });
 979        let mut line_count = 0;
 980        assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
 981            if left == '\n' {
 982                line_count += 1;
 983                line_count == 2
 984            } else {
 985                false
 986            }
 987        });
 988    }
 989
 990    #[gpui::test]
 991    fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::App) {
 992        init_test(cx);
 993
 994        let input_text = "abcdefghijklmnopqrstuvwxys";
 995        let font = font("Helvetica");
 996        let font_size = px(14.0);
 997        let buffer = MultiBuffer::build_simple(input_text, cx);
 998        let buffer_snapshot = buffer.read(cx).snapshot(cx);
 999
1000        let display_map = cx.new(|cx| {
1001            DisplayMap::new(
1002                buffer,
1003                font,
1004                font_size,
1005                None,
1006                1,
1007                1,
1008                FoldPlaceholder::test(),
1009                DiagnosticSeverity::Warning,
1010                cx,
1011            )
1012        });
1013
1014        // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
1015        let mut id = 0;
1016        let inlays = (0..buffer_snapshot.len())
1017            .flat_map(|offset| {
1018                [
1019                    Inlay::edit_prediction(
1020                        post_inc(&mut id),
1021                        buffer_snapshot.anchor_before(offset),
1022                        "test",
1023                    ),
1024                    Inlay::edit_prediction(
1025                        post_inc(&mut id),
1026                        buffer_snapshot.anchor_after(offset),
1027                        "test",
1028                    ),
1029                    Inlay::mock_hint(
1030                        post_inc(&mut id),
1031                        buffer_snapshot.anchor_before(offset),
1032                        "test",
1033                    ),
1034                    Inlay::mock_hint(
1035                        post_inc(&mut id),
1036                        buffer_snapshot.anchor_after(offset),
1037                        "test",
1038                    ),
1039                ]
1040            })
1041            .collect();
1042        let snapshot = display_map.update(cx, |map, cx| {
1043            map.splice_inlays(&[], inlays, cx);
1044            map.snapshot(cx)
1045        });
1046
1047        assert_eq!(
1048            find_preceding_boundary_display_point(
1049                &snapshot,
1050                buffer_snapshot.len().to_display_point(&snapshot),
1051                FindRange::MultiLine,
1052                |left, _| left == 'e',
1053            ),
1054            snapshot
1055                .buffer_snapshot
1056                .offset_to_point(5)
1057                .to_display_point(&snapshot),
1058            "Should not stop at inlays when looking for boundaries"
1059        );
1060    }
1061
1062    #[gpui::test]
1063    fn test_next_word_end(cx: &mut gpui::App) {
1064        init_test(cx);
1065
1066        fn assert(marked_text: &str, cx: &mut gpui::App) {
1067            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
1068            let actual = next_word_end(&snapshot, display_points[0]);
1069            let expected = display_points[1];
1070            if actual != expected {
1071                eprintln!(
1072                    "next_word_end mismatch for '{}': actual={:?}, expected={:?}",
1073                    marked_text, actual, expected
1074                );
1075            }
1076            assert_eq!(actual, expected);
1077        }
1078
1079        assert("\nˇ   loremˇ", cx);
1080        assert("    ˇloremˇ", cx);
1081        assert("    lorˇemˇ", cx);
1082        assert("    loremˇ    ˇ\nipsum\n", cx);
1083        assert("\nˇ\nˇ\n\n", cx);
1084        assert("loremˇ    ipsumˇ   ", cx);
1085        assert("loremˇ-ipsumˇ", cx);
1086        assert("loremˇ#$@-ˇipsum", cx);
1087        assert("loremˇ_ipsumˇ", cx);
1088        assert(" ˇbcΔˇ", cx);
1089        assert(" abˇ——ˇcd", cx);
1090        // Test punctuation skipping behavior
1091        assert("ˇ.helloˇ", cx);
1092        assert("display_pointsˇ[0ˇ]", cx);
1093        assert("ˇ...ˇhello", cx);
1094        assert("helloˇ.---..ˇtest", cx);
1095        assert("testˇ.--ˇ test", cx);
1096        assert("oneˇ,;:!?ˇtwo", cx);
1097    }
1098
1099    #[gpui::test]
1100    fn test_next_subword_end(cx: &mut gpui::App) {
1101        init_test(cx);
1102
1103        fn assert(marked_text: &str, cx: &mut gpui::App) {
1104            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
1105            assert_eq!(
1106                next_subword_end(&snapshot, display_points[0]),
1107                display_points[1]
1108            );
1109        }
1110
1111        // Subword boundaries are respected
1112        assert("loˇremˇ_ipsum", cx);
1113        assert("ˇloremˇ_ipsum", cx);
1114        assert("loremˇ_ipsumˇ", cx);
1115        assert("loremˇ_ipsumˇ_dolor", cx);
1116        assert("loˇremˇIpsum", cx);
1117        assert("loremˇIpsumˇDolor", cx);
1118
1119        // Word boundaries are still respected
1120        assert("\nˇ   loremˇ", cx);
1121        assert("    ˇloremˇ", cx);
1122        assert("    lorˇemˇ", cx);
1123        assert("    loremˇ    ˇ\nipsum\n", cx);
1124        assert("\nˇ\nˇ\n\n", cx);
1125        assert("loremˇ    ipsumˇ   ", cx);
1126        assert("loremˇ-ˇipsum", cx);
1127        assert("loremˇ#$@-ˇipsum", cx);
1128        assert("loremˇ_ipsumˇ", cx);
1129        assert(" ˇbcˇΔ", cx);
1130        assert(" abˇ——ˇcd", cx);
1131    }
1132
1133    #[gpui::test]
1134    fn test_find_boundary(cx: &mut gpui::App) {
1135        init_test(cx);
1136
1137        fn assert(
1138            marked_text: &str,
1139            cx: &mut gpui::App,
1140            is_boundary: impl FnMut(char, char) -> bool,
1141        ) {
1142            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
1143            assert_eq!(
1144                find_boundary(
1145                    &snapshot,
1146                    display_points[0],
1147                    FindRange::MultiLine,
1148                    is_boundary,
1149                ),
1150                display_points[1]
1151            );
1152        }
1153
1154        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
1155            left == 'j' && right == 'k'
1156        });
1157        assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
1158            left == '\n' && right == 'i'
1159        });
1160        let mut line_count = 0;
1161        assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
1162            if left == '\n' {
1163                line_count += 1;
1164                line_count == 2
1165            } else {
1166                false
1167            }
1168        });
1169    }
1170
1171    #[gpui::test]
1172    async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
1173        cx.update(|cx| {
1174            init_test(cx);
1175        });
1176
1177        let mut cx = EditorTestContext::new(cx).await;
1178        let editor = cx.editor.clone();
1179        let window = cx.window;
1180        _ = cx.update_window(window, |_, window, cx| {
1181            let text_layout_details = editor.read(cx).text_layout_details(window);
1182
1183            let font = font("Helvetica");
1184
1185            let buffer = cx.new(|cx| Buffer::local("abc\ndefg\nhijkl\nmn", cx));
1186            let multibuffer = cx.new(|cx| {
1187                let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1188                multibuffer.push_excerpts(
1189                    buffer.clone(),
1190                    [
1191                        ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
1192                        ExcerptRange::new(Point::new(2, 0)..Point::new(3, 2)),
1193                    ],
1194                    cx,
1195                );
1196                multibuffer
1197            });
1198            let display_map = cx.new(|cx| {
1199                DisplayMap::new(
1200                    multibuffer,
1201                    font,
1202                    px(14.0),
1203                    None,
1204                    0,
1205                    1,
1206                    FoldPlaceholder::test(),
1207                    DiagnosticSeverity::Warning,
1208                    cx,
1209                )
1210            });
1211            let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
1212
1213            assert_eq!(snapshot.text(), "abc\ndefg\n\nhijkl\nmn");
1214
1215            let col_2_x = snapshot
1216                .x_for_display_point(DisplayPoint::new(DisplayRow(0), 2), &text_layout_details);
1217
1218            // Can't move up into the first excerpt's header
1219            assert_eq!(
1220                up(
1221                    &snapshot,
1222                    DisplayPoint::new(DisplayRow(0), 2),
1223                    SelectionGoal::HorizontalPosition(col_2_x.0),
1224                    false,
1225                    &text_layout_details
1226                ),
1227                (
1228                    DisplayPoint::new(DisplayRow(0), 0),
1229                    SelectionGoal::HorizontalPosition(col_2_x.0),
1230                ),
1231            );
1232            assert_eq!(
1233                up(
1234                    &snapshot,
1235                    DisplayPoint::new(DisplayRow(0), 0),
1236                    SelectionGoal::None,
1237                    false,
1238                    &text_layout_details
1239                ),
1240                (
1241                    DisplayPoint::new(DisplayRow(0), 0),
1242                    SelectionGoal::HorizontalPosition(0.0),
1243                ),
1244            );
1245
1246            let col_4_x = snapshot
1247                .x_for_display_point(DisplayPoint::new(DisplayRow(1), 4), &text_layout_details);
1248
1249            // Move up and down within first excerpt
1250            assert_eq!(
1251                up(
1252                    &snapshot,
1253                    DisplayPoint::new(DisplayRow(1), 4),
1254                    SelectionGoal::HorizontalPosition(col_4_x.0),
1255                    false,
1256                    &text_layout_details
1257                ),
1258                (
1259                    DisplayPoint::new(DisplayRow(0), 3),
1260                    SelectionGoal::HorizontalPosition(col_4_x.0)
1261                ),
1262            );
1263            assert_eq!(
1264                down(
1265                    &snapshot,
1266                    DisplayPoint::new(DisplayRow(0), 3),
1267                    SelectionGoal::HorizontalPosition(col_4_x.0),
1268                    false,
1269                    &text_layout_details
1270                ),
1271                (
1272                    DisplayPoint::new(DisplayRow(1), 4),
1273                    SelectionGoal::HorizontalPosition(col_4_x.0)
1274                ),
1275            );
1276
1277            let col_5_x = snapshot
1278                .x_for_display_point(DisplayPoint::new(DisplayRow(3), 5), &text_layout_details);
1279
1280            // Move up and down across second excerpt's header
1281            assert_eq!(
1282                up(
1283                    &snapshot,
1284                    DisplayPoint::new(DisplayRow(3), 5),
1285                    SelectionGoal::HorizontalPosition(col_5_x.0),
1286                    false,
1287                    &text_layout_details
1288                ),
1289                (
1290                    DisplayPoint::new(DisplayRow(1), 4),
1291                    SelectionGoal::HorizontalPosition(col_5_x.0)
1292                ),
1293            );
1294            assert_eq!(
1295                down(
1296                    &snapshot,
1297                    DisplayPoint::new(DisplayRow(1), 4),
1298                    SelectionGoal::HorizontalPosition(col_5_x.0),
1299                    false,
1300                    &text_layout_details
1301                ),
1302                (
1303                    DisplayPoint::new(DisplayRow(3), 5),
1304                    SelectionGoal::HorizontalPosition(col_5_x.0)
1305                ),
1306            );
1307
1308            let max_point_x = snapshot
1309                .x_for_display_point(DisplayPoint::new(DisplayRow(4), 2), &text_layout_details);
1310
1311            // Can't move down off the end, and attempting to do so leaves the selection goal unchanged
1312            assert_eq!(
1313                down(
1314                    &snapshot,
1315                    DisplayPoint::new(DisplayRow(4), 0),
1316                    SelectionGoal::HorizontalPosition(0.0),
1317                    false,
1318                    &text_layout_details
1319                ),
1320                (
1321                    DisplayPoint::new(DisplayRow(4), 2),
1322                    SelectionGoal::HorizontalPosition(0.0)
1323                ),
1324            );
1325            assert_eq!(
1326                down(
1327                    &snapshot,
1328                    DisplayPoint::new(DisplayRow(4), 2),
1329                    SelectionGoal::HorizontalPosition(max_point_x.0),
1330                    false,
1331                    &text_layout_details
1332                ),
1333                (
1334                    DisplayPoint::new(DisplayRow(4), 2),
1335                    SelectionGoal::HorizontalPosition(max_point_x.0)
1336                ),
1337            );
1338        });
1339    }
1340
1341    fn init_test(cx: &mut gpui::App) {
1342        let settings_store = SettingsStore::test(cx);
1343        cx.set_global(settings_store);
1344        workspace::init_settings(cx);
1345        theme::init(theme::LoadThemes::JustBase, cx);
1346        language::init(cx);
1347        crate::init(cx);
1348        Project::init_settings(cx);
1349    }
1350}