motion.rs

   1use editor::{
   2    char_kind,
   3    display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
   4    movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
   5    Bias, CharKind, DisplayPoint, ToOffset,
   6};
   7use gpui::{actions, impl_actions, AppContext, WindowContext};
   8use language::{Point, Selection, SelectionGoal};
   9use serde::Deserialize;
  10use workspace::Workspace;
  11
  12use crate::{
  13    normal::normal_motion,
  14    state::{Mode, Operator},
  15    visual::visual_motion,
  16    Vim,
  17};
  18
  19#[derive(Clone, Debug, PartialEq, Eq)]
  20pub enum Motion {
  21    Left,
  22    Backspace,
  23    Down { display_lines: bool },
  24    Up { display_lines: bool },
  25    Right,
  26    NextWordStart { ignore_punctuation: bool },
  27    NextWordEnd { ignore_punctuation: bool },
  28    PreviousWordStart { ignore_punctuation: bool },
  29    FirstNonWhitespace { display_lines: bool },
  30    CurrentLine,
  31    StartOfLine { display_lines: bool },
  32    EndOfLine { display_lines: bool },
  33    StartOfParagraph,
  34    EndOfParagraph,
  35    StartOfDocument,
  36    EndOfDocument,
  37    Matching,
  38    FindForward { before: bool, char: char },
  39    FindBackward { after: bool, char: char },
  40    NextLineStart,
  41    StartOfLineDownward,
  42    EndOfLineDownward,
  43}
  44
  45#[derive(Clone, Deserialize, PartialEq)]
  46#[serde(rename_all = "camelCase")]
  47struct NextWordStart {
  48    #[serde(default)]
  49    ignore_punctuation: bool,
  50}
  51
  52#[derive(Clone, Deserialize, PartialEq)]
  53#[serde(rename_all = "camelCase")]
  54struct NextWordEnd {
  55    #[serde(default)]
  56    ignore_punctuation: bool,
  57}
  58
  59#[derive(Clone, Deserialize, PartialEq)]
  60#[serde(rename_all = "camelCase")]
  61struct PreviousWordStart {
  62    #[serde(default)]
  63    ignore_punctuation: bool,
  64}
  65
  66#[derive(Clone, Deserialize, PartialEq)]
  67#[serde(rename_all = "camelCase")]
  68pub(crate) struct Up {
  69    #[serde(default)]
  70    pub(crate) display_lines: bool,
  71}
  72
  73#[derive(Clone, Deserialize, PartialEq)]
  74#[serde(rename_all = "camelCase")]
  75struct Down {
  76    #[serde(default)]
  77    display_lines: bool,
  78}
  79
  80#[derive(Clone, Deserialize, PartialEq)]
  81#[serde(rename_all = "camelCase")]
  82struct FirstNonWhitespace {
  83    #[serde(default)]
  84    display_lines: bool,
  85}
  86
  87#[derive(Clone, Deserialize, PartialEq)]
  88#[serde(rename_all = "camelCase")]
  89struct EndOfLine {
  90    #[serde(default)]
  91    display_lines: bool,
  92}
  93
  94#[derive(Clone, Deserialize, PartialEq)]
  95#[serde(rename_all = "camelCase")]
  96pub struct StartOfLine {
  97    #[serde(default)]
  98    pub(crate) display_lines: bool,
  99}
 100
 101#[derive(Clone, Deserialize, PartialEq)]
 102struct RepeatFind {
 103    #[serde(default)]
 104    backwards: bool,
 105}
 106
 107actions!(
 108    vim,
 109    [
 110        Left,
 111        Backspace,
 112        Right,
 113        CurrentLine,
 114        StartOfParagraph,
 115        EndOfParagraph,
 116        StartOfDocument,
 117        EndOfDocument,
 118        Matching,
 119        NextLineStart,
 120        StartOfLineDownward,
 121        EndOfLineDownward,
 122    ]
 123);
 124impl_actions!(
 125    vim,
 126    [
 127        NextWordStart,
 128        NextWordEnd,
 129        PreviousWordStart,
 130        RepeatFind,
 131        Up,
 132        Down,
 133        FirstNonWhitespace,
 134        EndOfLine,
 135        StartOfLine,
 136    ]
 137);
 138
 139pub fn init(cx: &mut AppContext) {
 140    cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 141    cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
 142    cx.add_action(|_: &mut Workspace, action: &Down, cx: _| {
 143        motion(
 144            Motion::Down {
 145                display_lines: action.display_lines,
 146            },
 147            cx,
 148        )
 149    });
 150    cx.add_action(|_: &mut Workspace, action: &Up, cx: _| {
 151        motion(
 152            Motion::Up {
 153                display_lines: action.display_lines,
 154            },
 155            cx,
 156        )
 157    });
 158    cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 159    cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
 160        motion(
 161            Motion::FirstNonWhitespace {
 162                display_lines: action.display_lines,
 163            },
 164            cx,
 165        )
 166    });
 167    cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
 168        motion(
 169            Motion::StartOfLine {
 170                display_lines: action.display_lines,
 171            },
 172            cx,
 173        )
 174    });
 175    cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
 176        motion(
 177            Motion::EndOfLine {
 178                display_lines: action.display_lines,
 179            },
 180            cx,
 181        )
 182    });
 183    cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
 184    cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
 185        motion(Motion::StartOfParagraph, cx)
 186    });
 187    cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
 188        motion(Motion::EndOfParagraph, cx)
 189    });
 190    cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
 191        motion(Motion::StartOfDocument, cx)
 192    });
 193    cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
 194    cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
 195
 196    cx.add_action(
 197        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
 198            motion(Motion::NextWordStart { ignore_punctuation }, cx)
 199        },
 200    );
 201    cx.add_action(
 202        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
 203            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
 204        },
 205    );
 206    cx.add_action(
 207        |_: &mut Workspace,
 208         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
 209         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
 210    );
 211    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
 212    cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
 213        motion(Motion::StartOfLineDownward, cx)
 214    });
 215    cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
 216        motion(Motion::EndOfLineDownward, cx)
 217    });
 218    cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
 219        repeat_motion(action.backwards, cx)
 220    })
 221}
 222
 223pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 224    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
 225        Vim::read(cx).active_operator()
 226    {
 227        Vim::update(cx, |vim, cx| vim.pop_operator(cx));
 228    }
 229
 230    let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
 231    let operator = Vim::read(cx).active_operator();
 232    match Vim::read(cx).state().mode {
 233        Mode::Normal => normal_motion(motion, operator, count, cx),
 234        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
 235        Mode::Insert => {
 236            // Shouldn't execute a motion in insert mode. Ignoring
 237        }
 238    }
 239    Vim::update(cx, |vim, cx| vim.clear_operator(cx));
 240}
 241
 242fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
 243    let find = match Vim::read(cx).workspace_state.last_find.clone() {
 244        Some(Motion::FindForward { before, char }) => {
 245            if backwards {
 246                Motion::FindBackward {
 247                    after: before,
 248                    char,
 249                }
 250            } else {
 251                Motion::FindForward { before, char }
 252            }
 253        }
 254
 255        Some(Motion::FindBackward { after, char }) => {
 256            if backwards {
 257                Motion::FindForward {
 258                    before: after,
 259                    char,
 260                }
 261            } else {
 262                Motion::FindBackward { after, char }
 263            }
 264        }
 265        _ => return,
 266    };
 267
 268    motion(find, cx)
 269}
 270
 271// Motion handling is specified here:
 272// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 273impl Motion {
 274    pub fn linewise(&self) -> bool {
 275        use Motion::*;
 276        match self {
 277            Down { .. }
 278            | Up { .. }
 279            | StartOfDocument
 280            | EndOfDocument
 281            | CurrentLine
 282            | NextLineStart
 283            | StartOfLineDownward
 284            | StartOfParagraph
 285            | EndOfParagraph => true,
 286            EndOfLine { .. }
 287            | NextWordEnd { .. }
 288            | Matching
 289            | FindForward { .. }
 290            | Left
 291            | Backspace
 292            | Right
 293            | StartOfLine { .. }
 294            | EndOfLineDownward
 295            | NextWordStart { .. }
 296            | PreviousWordStart { .. }
 297            | FirstNonWhitespace { .. }
 298            | FindBackward { .. } => false,
 299        }
 300    }
 301
 302    pub fn infallible(&self) -> bool {
 303        use Motion::*;
 304        match self {
 305            StartOfDocument | EndOfDocument | CurrentLine => true,
 306            Down { .. }
 307            | Up { .. }
 308            | EndOfLine { .. }
 309            | NextWordEnd { .. }
 310            | Matching
 311            | FindForward { .. }
 312            | Left
 313            | Backspace
 314            | Right
 315            | StartOfLine { .. }
 316            | StartOfParagraph
 317            | EndOfParagraph
 318            | StartOfLineDownward
 319            | EndOfLineDownward
 320            | NextWordStart { .. }
 321            | PreviousWordStart { .. }
 322            | FirstNonWhitespace { .. }
 323            | FindBackward { .. }
 324            | NextLineStart => false,
 325        }
 326    }
 327
 328    pub fn inclusive(&self) -> bool {
 329        use Motion::*;
 330        match self {
 331            Down { .. }
 332            | Up { .. }
 333            | StartOfDocument
 334            | EndOfDocument
 335            | CurrentLine
 336            | EndOfLine { .. }
 337            | EndOfLineDownward
 338            | NextWordEnd { .. }
 339            | Matching
 340            | FindForward { .. }
 341            | NextLineStart => true,
 342            Left
 343            | Backspace
 344            | Right
 345            | StartOfLine { .. }
 346            | StartOfLineDownward
 347            | StartOfParagraph
 348            | EndOfParagraph
 349            | NextWordStart { .. }
 350            | PreviousWordStart { .. }
 351            | FirstNonWhitespace { .. }
 352            | FindBackward { .. } => false,
 353        }
 354    }
 355
 356    pub fn move_point(
 357        &self,
 358        map: &DisplaySnapshot,
 359        point: DisplayPoint,
 360        goal: SelectionGoal,
 361        maybe_times: Option<usize>,
 362        text_layout_details: &TextLayoutDetails,
 363    ) -> Option<(DisplayPoint, SelectionGoal)> {
 364        let times = maybe_times.unwrap_or(1);
 365        use Motion::*;
 366        let infallible = self.infallible();
 367        let (new_point, goal) = match self {
 368            Left => (left(map, point, times), SelectionGoal::None),
 369            Backspace => (backspace(map, point, times), SelectionGoal::None),
 370            Down {
 371                display_lines: false,
 372            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
 373            Down {
 374                display_lines: true,
 375            } => down_display(map, point, goal, times, &text_layout_details),
 376            Up {
 377                display_lines: false,
 378            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
 379            Up {
 380                display_lines: true,
 381            } => up_display(map, point, goal, times, &text_layout_details),
 382            Right => (right(map, point, times), SelectionGoal::None),
 383            NextWordStart { ignore_punctuation } => (
 384                next_word_start(map, point, *ignore_punctuation, times),
 385                SelectionGoal::None,
 386            ),
 387            NextWordEnd { ignore_punctuation } => (
 388                next_word_end(map, point, *ignore_punctuation, times),
 389                SelectionGoal::None,
 390            ),
 391            PreviousWordStart { ignore_punctuation } => (
 392                previous_word_start(map, point, *ignore_punctuation, times),
 393                SelectionGoal::None,
 394            ),
 395            FirstNonWhitespace { display_lines } => (
 396                first_non_whitespace(map, *display_lines, point),
 397                SelectionGoal::None,
 398            ),
 399            StartOfLine { display_lines } => (
 400                start_of_line(map, *display_lines, point),
 401                SelectionGoal::None,
 402            ),
 403            EndOfLine { display_lines } => {
 404                (end_of_line(map, *display_lines, point), SelectionGoal::None)
 405            }
 406            StartOfParagraph => (
 407                movement::start_of_paragraph(map, point, times),
 408                SelectionGoal::None,
 409            ),
 410            EndOfParagraph => (
 411                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 412                SelectionGoal::None,
 413            ),
 414            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 415            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 416            EndOfDocument => (
 417                end_of_document(map, point, maybe_times),
 418                SelectionGoal::None,
 419            ),
 420            Matching => (matching(map, point), SelectionGoal::None),
 421            FindForward { before, char } => (
 422                find_forward(map, point, *before, *char, times),
 423                SelectionGoal::None,
 424            ),
 425            FindBackward { after, char } => (
 426                find_backward(map, point, *after, *char, times),
 427                SelectionGoal::None,
 428            ),
 429            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 430            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 431            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
 432        };
 433
 434        (new_point != point || infallible).then_some((new_point, goal))
 435    }
 436
 437    // Expands a selection using self motion for an operator
 438    pub fn expand_selection(
 439        &self,
 440        map: &DisplaySnapshot,
 441        selection: &mut Selection<DisplayPoint>,
 442        times: Option<usize>,
 443        expand_to_surrounding_newline: bool,
 444        text_layout_details: &TextLayoutDetails,
 445    ) -> bool {
 446        if let Some((new_head, goal)) = self.move_point(
 447            map,
 448            selection.head(),
 449            selection.goal,
 450            times,
 451            &text_layout_details,
 452        ) {
 453            selection.set_head(new_head, goal);
 454
 455            if self.linewise() {
 456                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 457
 458                if expand_to_surrounding_newline {
 459                    if selection.end.row() < map.max_point().row() {
 460                        *selection.end.row_mut() += 1;
 461                        *selection.end.column_mut() = 0;
 462                        selection.end = map.clip_point(selection.end, Bias::Right);
 463                        // Don't reset the end here
 464                        return true;
 465                    } else if selection.start.row() > 0 {
 466                        *selection.start.row_mut() -= 1;
 467                        *selection.start.column_mut() = map.line_len(selection.start.row());
 468                        selection.start = map.clip_point(selection.start, Bias::Left);
 469                    }
 470                }
 471
 472                (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
 473            } else {
 474                // Another special case: When using the "w" motion in combination with an
 475                // operator and the last word moved over is at the end of a line, the end of
 476                // that word becomes the end of the operated text, not the first word in the
 477                // next line.
 478                if let Motion::NextWordStart {
 479                    ignore_punctuation: _,
 480                } = self
 481                {
 482                    let start_row = selection.start.to_point(&map).row;
 483                    if selection.end.to_point(&map).row > start_row {
 484                        selection.end =
 485                            Point::new(start_row, map.buffer_snapshot.line_len(start_row))
 486                                .to_display_point(&map)
 487                    }
 488                }
 489
 490                // If the motion is exclusive and the end of the motion is in column 1, the
 491                // end of the motion is moved to the end of the previous line and the motion
 492                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
 493                // but "d}" will not include that line.
 494                let mut inclusive = self.inclusive();
 495                if !inclusive
 496                    && self != &Motion::Backspace
 497                    && selection.end.row() > selection.start.row()
 498                    && selection.end.column() == 0
 499                {
 500                    inclusive = true;
 501                    *selection.end.row_mut() -= 1;
 502                    *selection.end.column_mut() = 0;
 503                    selection.end = map.clip_point(
 504                        map.next_line_boundary(selection.end.to_point(map)).1,
 505                        Bias::Left,
 506                    );
 507                }
 508
 509                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 510                    *selection.end.column_mut() += 1;
 511                }
 512            }
 513            true
 514        } else {
 515            false
 516        }
 517    }
 518}
 519
 520fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 521    for _ in 0..times {
 522        point = movement::saturating_left(map, point);
 523        if point.column() == 0 {
 524            break;
 525        }
 526    }
 527    point
 528}
 529
 530fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 531    for _ in 0..times {
 532        point = movement::left(map, point);
 533    }
 534    point
 535}
 536
 537pub(crate) fn start_of_relative_buffer_row(
 538    map: &DisplaySnapshot,
 539    point: DisplayPoint,
 540    times: isize,
 541) -> DisplayPoint {
 542    let start = map.display_point_to_fold_point(point, Bias::Left);
 543    let target = start.row() as isize + times;
 544    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 545
 546    map.clip_point(
 547        map.fold_point_to_display_point(
 548            map.fold_snapshot
 549                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 550        ),
 551        Bias::Right,
 552    )
 553}
 554
 555fn up_down_buffer_rows(
 556    map: &DisplaySnapshot,
 557    point: DisplayPoint,
 558    mut goal: SelectionGoal,
 559    times: isize,
 560    text_layout_details: &TextLayoutDetails,
 561) -> (DisplayPoint, SelectionGoal) {
 562    let start = map.display_point_to_fold_point(point, Bias::Left);
 563    let begin_folded_line = map.fold_point_to_display_point(
 564        map.fold_snapshot
 565            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
 566    );
 567    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 568
 569    let (goal_wrap, goal_x) = match goal {
 570        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
 571        SelectionGoal::WrappedHorizontalRange { end: (row, x), .. } => (row, x),
 572        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 573        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 574        _ => {
 575            let x = map.x_for_point(point, text_layout_details);
 576            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
 577            (select_nth_wrapped_row, x)
 578        }
 579    };
 580
 581    let target = start.row() as isize + times;
 582    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 583
 584    let mut begin_folded_line = map.fold_point_to_display_point(
 585        map.fold_snapshot
 586            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
 587    );
 588
 589    let mut i = 0;
 590    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
 591        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
 592        if map
 593            .display_point_to_fold_point(next_folded_line, Bias::Right)
 594            .row()
 595            == new_row
 596        {
 597            i += 1;
 598            begin_folded_line = next_folded_line;
 599        } else {
 600            break;
 601        }
 602    }
 603
 604    let new_col = if i == goal_wrap {
 605        map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
 606    } else {
 607        map.line_len(begin_folded_line.row())
 608    };
 609
 610    (
 611        map.clip_point(
 612            DisplayPoint::new(begin_folded_line.row(), new_col),
 613            Bias::Left,
 614        ),
 615        goal,
 616    )
 617}
 618
 619fn down_display(
 620    map: &DisplaySnapshot,
 621    mut point: DisplayPoint,
 622    mut goal: SelectionGoal,
 623    times: usize,
 624    text_layout_details: &TextLayoutDetails,
 625) -> (DisplayPoint, SelectionGoal) {
 626    for _ in 0..times {
 627        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
 628    }
 629
 630    (point, goal)
 631}
 632
 633fn up_display(
 634    map: &DisplaySnapshot,
 635    mut point: DisplayPoint,
 636    mut goal: SelectionGoal,
 637    times: usize,
 638    text_layout_details: &TextLayoutDetails,
 639) -> (DisplayPoint, SelectionGoal) {
 640    for _ in 0..times {
 641        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
 642    }
 643
 644    (point, goal)
 645}
 646
 647pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 648    for _ in 0..times {
 649        let new_point = movement::saturating_right(map, point);
 650        if point == new_point {
 651            break;
 652        }
 653        point = new_point;
 654    }
 655    point
 656}
 657
 658pub(crate) fn next_word_start(
 659    map: &DisplaySnapshot,
 660    mut point: DisplayPoint,
 661    ignore_punctuation: bool,
 662    times: usize,
 663) -> DisplayPoint {
 664    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 665    for _ in 0..times {
 666        let mut crossed_newline = false;
 667        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 668            let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 669            let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 670            let at_newline = right == '\n';
 671
 672            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
 673                || at_newline && crossed_newline
 674                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 675
 676            crossed_newline |= at_newline;
 677            found
 678        })
 679    }
 680    point
 681}
 682
 683fn next_word_end(
 684    map: &DisplaySnapshot,
 685    mut point: DisplayPoint,
 686    ignore_punctuation: bool,
 687    times: usize,
 688) -> DisplayPoint {
 689    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 690    for _ in 0..times {
 691        if point.column() < map.line_len(point.row()) {
 692            *point.column_mut() += 1;
 693        } else if point.row() < map.max_buffer_row() {
 694            *point.row_mut() += 1;
 695            *point.column_mut() = 0;
 696        }
 697        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 698            let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 699            let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 700
 701            left_kind != right_kind && left_kind != CharKind::Whitespace
 702        });
 703
 704        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
 705        // we have backtracked already
 706        if !map
 707            .chars_at(point)
 708            .nth(1)
 709            .map(|(c, _)| c == '\n')
 710            .unwrap_or(true)
 711        {
 712            *point.column_mut() = point.column().saturating_sub(1);
 713        }
 714        point = map.clip_point(point, Bias::Left);
 715    }
 716    point
 717}
 718
 719fn previous_word_start(
 720    map: &DisplaySnapshot,
 721    mut point: DisplayPoint,
 722    ignore_punctuation: bool,
 723    times: usize,
 724) -> DisplayPoint {
 725    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 726    for _ in 0..times {
 727        // This works even though find_preceding_boundary is called for every character in the line containing
 728        // cursor because the newline is checked only once.
 729        point =
 730            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
 731                let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 732                let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 733
 734                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
 735            });
 736    }
 737    point
 738}
 739
 740pub(crate) fn first_non_whitespace(
 741    map: &DisplaySnapshot,
 742    display_lines: bool,
 743    from: DisplayPoint,
 744) -> DisplayPoint {
 745    let mut last_point = start_of_line(map, display_lines, from);
 746    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
 747    for (ch, point) in map.chars_at(last_point) {
 748        if ch == '\n' {
 749            return from;
 750        }
 751
 752        last_point = point;
 753
 754        if char_kind(&scope, ch) != CharKind::Whitespace {
 755            break;
 756        }
 757    }
 758
 759    map.clip_point(last_point, Bias::Left)
 760}
 761
 762pub(crate) fn start_of_line(
 763    map: &DisplaySnapshot,
 764    display_lines: bool,
 765    point: DisplayPoint,
 766) -> DisplayPoint {
 767    if display_lines {
 768        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
 769    } else {
 770        map.prev_line_boundary(point.to_point(map)).1
 771    }
 772}
 773
 774pub(crate) fn end_of_line(
 775    map: &DisplaySnapshot,
 776    display_lines: bool,
 777    point: DisplayPoint,
 778) -> DisplayPoint {
 779    if display_lines {
 780        map.clip_point(
 781            DisplayPoint::new(point.row(), map.line_len(point.row())),
 782            Bias::Left,
 783        )
 784    } else {
 785        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
 786    }
 787}
 788
 789fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
 790    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
 791    *new_point.column_mut() = point.column();
 792    map.clip_point(new_point, Bias::Left)
 793}
 794
 795fn end_of_document(
 796    map: &DisplaySnapshot,
 797    point: DisplayPoint,
 798    line: Option<usize>,
 799) -> DisplayPoint {
 800    let new_row = if let Some(line) = line {
 801        (line - 1) as u32
 802    } else {
 803        map.max_buffer_row()
 804    };
 805
 806    let new_point = Point::new(new_row, point.column());
 807    map.clip_point(new_point.to_display_point(map), Bias::Left)
 808}
 809
 810fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 811    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
 812    let point = display_point.to_point(map);
 813    let offset = point.to_offset(&map.buffer_snapshot);
 814
 815    // Ensure the range is contained by the current line.
 816    let mut line_end = map.next_line_boundary(point).0;
 817    if line_end == point {
 818        line_end = map.max_point().to_point(map);
 819    }
 820
 821    let line_range = map.prev_line_boundary(point).0..line_end;
 822    let visible_line_range =
 823        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
 824    let ranges = map
 825        .buffer_snapshot
 826        .bracket_ranges(visible_line_range.clone());
 827    if let Some(ranges) = ranges {
 828        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
 829            ..line_range.end.to_offset(&map.buffer_snapshot);
 830        let mut closest_pair_destination = None;
 831        let mut closest_distance = usize::MAX;
 832
 833        for (open_range, close_range) in ranges {
 834            if open_range.start >= offset && line_range.contains(&open_range.start) {
 835                let distance = open_range.start - offset;
 836                if distance < closest_distance {
 837                    closest_pair_destination = Some(close_range.start);
 838                    closest_distance = distance;
 839                    continue;
 840                }
 841            }
 842
 843            if close_range.start >= offset && line_range.contains(&close_range.start) {
 844                let distance = close_range.start - offset;
 845                if distance < closest_distance {
 846                    closest_pair_destination = Some(open_range.start);
 847                    closest_distance = distance;
 848                    continue;
 849                }
 850            }
 851
 852            continue;
 853        }
 854
 855        closest_pair_destination
 856            .map(|destination| destination.to_display_point(map))
 857            .unwrap_or(display_point)
 858    } else {
 859        display_point
 860    }
 861}
 862
 863fn find_forward(
 864    map: &DisplaySnapshot,
 865    from: DisplayPoint,
 866    before: bool,
 867    target: char,
 868    times: usize,
 869) -> DisplayPoint {
 870    let mut to = from;
 871    let mut found = false;
 872
 873    for _ in 0..times {
 874        found = false;
 875        to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
 876            found = right == target;
 877            found
 878        });
 879    }
 880
 881    if found {
 882        if before && to.column() > 0 {
 883            *to.column_mut() -= 1;
 884            map.clip_point(to, Bias::Left)
 885        } else {
 886            to
 887        }
 888    } else {
 889        from
 890    }
 891}
 892
 893fn find_backward(
 894    map: &DisplaySnapshot,
 895    from: DisplayPoint,
 896    after: bool,
 897    target: char,
 898    times: usize,
 899) -> DisplayPoint {
 900    let mut to = from;
 901
 902    for _ in 0..times {
 903        to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
 904    }
 905
 906    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
 907        if after {
 908            *to.column_mut() += 1;
 909            map.clip_point(to, Bias::Right)
 910        } else {
 911            to
 912        }
 913    } else {
 914        from
 915    }
 916}
 917
 918fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
 919    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
 920    first_non_whitespace(map, false, correct_line)
 921}
 922
 923pub(crate) fn next_line_end(
 924    map: &DisplaySnapshot,
 925    mut point: DisplayPoint,
 926    times: usize,
 927) -> DisplayPoint {
 928    if times > 1 {
 929        point = start_of_relative_buffer_row(map, point, times as isize - 1);
 930    }
 931    end_of_line(map, false, point)
 932}
 933
 934#[cfg(test)]
 935
 936mod test {
 937
 938    use crate::test::NeovimBackedTestContext;
 939    use indoc::indoc;
 940
 941    #[gpui::test]
 942    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
 943        let mut cx = NeovimBackedTestContext::new(cx).await;
 944
 945        let initial_state = indoc! {r"ˇabc
 946            def
 947
 948            paragraph
 949            the second
 950
 951
 952
 953            third and
 954            final"};
 955
 956        // goes down once
 957        cx.set_shared_state(initial_state).await;
 958        cx.simulate_shared_keystrokes(["}"]).await;
 959        cx.assert_shared_state(indoc! {r"abc
 960            def
 961            ˇ
 962            paragraph
 963            the second
 964
 965
 966
 967            third and
 968            final"})
 969            .await;
 970
 971        // goes up once
 972        cx.simulate_shared_keystrokes(["{"]).await;
 973        cx.assert_shared_state(initial_state).await;
 974
 975        // goes down twice
 976        cx.simulate_shared_keystrokes(["2", "}"]).await;
 977        cx.assert_shared_state(indoc! {r"abc
 978            def
 979
 980            paragraph
 981            the second
 982            ˇ
 983
 984
 985            third and
 986            final"})
 987            .await;
 988
 989        // goes down over multiple blanks
 990        cx.simulate_shared_keystrokes(["}"]).await;
 991        cx.assert_shared_state(indoc! {r"abc
 992                def
 993
 994                paragraph
 995                the second
 996
 997
 998
 999                third and
1000                finaˇl"})
1001            .await;
1002
1003        // goes up twice
1004        cx.simulate_shared_keystrokes(["2", "{"]).await;
1005        cx.assert_shared_state(indoc! {r"abc
1006                def
1007                ˇ
1008                paragraph
1009                the second
1010
1011
1012
1013                third and
1014                final"})
1015            .await
1016    }
1017
1018    #[gpui::test]
1019    async fn test_matching(cx: &mut gpui::TestAppContext) {
1020        let mut cx = NeovimBackedTestContext::new(cx).await;
1021
1022        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1023                do(something(with<Types>.and_arrays[0, 2]))
1024            }"})
1025            .await;
1026        cx.simulate_shared_keystrokes(["%"]).await;
1027        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1028                do(something(with<Types>.and_arrays[0, 2]))
1029            }"})
1030            .await;
1031
1032        // test it works on the last character of the line
1033        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1034            do(something(with<Types>.and_arrays[0, 2]))
1035            }"})
1036            .await;
1037        cx.simulate_shared_keystrokes(["%"]).await;
1038        cx.assert_shared_state(indoc! {r"func (a string) {
1039            do(something(with<Types>.and_arrays[0, 2]))
1040            ˇ}"})
1041            .await;
1042
1043        // test it works on immediate nesting
1044        cx.set_shared_state("ˇ{()}").await;
1045        cx.simulate_shared_keystrokes(["%"]).await;
1046        cx.assert_shared_state("{()ˇ}").await;
1047        cx.simulate_shared_keystrokes(["%"]).await;
1048        cx.assert_shared_state("ˇ{()}").await;
1049
1050        // test it works on immediate nesting inside braces
1051        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1052        cx.simulate_shared_keystrokes(["%"]).await;
1053        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1054
1055        // test it jumps to the next paren on a line
1056        cx.set_shared_state("func ˇboop() {\n}").await;
1057        cx.simulate_shared_keystrokes(["%"]).await;
1058        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1059    }
1060
1061    #[gpui::test]
1062    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1063        let mut cx = NeovimBackedTestContext::new(cx).await;
1064
1065        cx.set_shared_state("ˇone two three four").await;
1066        cx.simulate_shared_keystrokes(["f", "o"]).await;
1067        cx.assert_shared_state("one twˇo three four").await;
1068        cx.simulate_shared_keystrokes([","]).await;
1069        cx.assert_shared_state("ˇone two three four").await;
1070        cx.simulate_shared_keystrokes(["2", ";"]).await;
1071        cx.assert_shared_state("one two three fˇour").await;
1072        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1073        cx.assert_shared_state("one two threeˇ four").await;
1074        cx.simulate_shared_keystrokes(["3", ";"]).await;
1075        cx.assert_shared_state("oneˇ two three four").await;
1076        cx.simulate_shared_keystrokes([","]).await;
1077        cx.assert_shared_state("one two thˇree four").await;
1078    }
1079
1080    #[gpui::test]
1081    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1082        let mut cx = NeovimBackedTestContext::new(cx).await;
1083        cx.set_shared_state("ˇone\n  two\nthree").await;
1084        cx.simulate_shared_keystrokes(["enter"]).await;
1085        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1086    }
1087}