motion.rs

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