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::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 572        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 573        _ => {
 574            let x = map.x_for_point(point, text_layout_details);
 575            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
 576            (select_nth_wrapped_row, x)
 577        }
 578    };
 579
 580    let target = start.row() as isize + times;
 581    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 582
 583    let mut begin_folded_line = map.fold_point_to_display_point(
 584        map.fold_snapshot
 585            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
 586    );
 587
 588    let mut i = 0;
 589    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
 590        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
 591        if map
 592            .display_point_to_fold_point(next_folded_line, Bias::Right)
 593            .row()
 594            == new_row
 595        {
 596            i += 1;
 597            begin_folded_line = next_folded_line;
 598        } else {
 599            break;
 600        }
 601    }
 602
 603    let new_col = if i == goal_wrap {
 604        map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
 605    } else {
 606        map.line_len(begin_folded_line.row())
 607    };
 608
 609    (
 610        map.clip_point(
 611            DisplayPoint::new(begin_folded_line.row(), new_col),
 612            Bias::Left,
 613        ),
 614        goal,
 615    )
 616}
 617
 618fn down_display(
 619    map: &DisplaySnapshot,
 620    mut point: DisplayPoint,
 621    mut goal: SelectionGoal,
 622    times: usize,
 623    text_layout_details: &TextLayoutDetails,
 624) -> (DisplayPoint, SelectionGoal) {
 625    for _ in 0..times {
 626        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
 627    }
 628
 629    (point, goal)
 630}
 631
 632fn up_display(
 633    map: &DisplaySnapshot,
 634    mut point: DisplayPoint,
 635    mut goal: SelectionGoal,
 636    times: usize,
 637    text_layout_details: &TextLayoutDetails,
 638) -> (DisplayPoint, SelectionGoal) {
 639    for _ in 0..times {
 640        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
 641    }
 642
 643    (point, goal)
 644}
 645
 646pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 647    for _ in 0..times {
 648        let new_point = movement::saturating_right(map, point);
 649        if point == new_point {
 650            break;
 651        }
 652        point = new_point;
 653    }
 654    point
 655}
 656
 657pub(crate) fn next_word_start(
 658    map: &DisplaySnapshot,
 659    mut point: DisplayPoint,
 660    ignore_punctuation: bool,
 661    times: usize,
 662) -> DisplayPoint {
 663    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 664    for _ in 0..times {
 665        let mut crossed_newline = false;
 666        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 667            let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 668            let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 669            let at_newline = right == '\n';
 670
 671            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
 672                || at_newline && crossed_newline
 673                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 674
 675            crossed_newline |= at_newline;
 676            found
 677        })
 678    }
 679    point
 680}
 681
 682fn next_word_end(
 683    map: &DisplaySnapshot,
 684    mut point: DisplayPoint,
 685    ignore_punctuation: bool,
 686    times: usize,
 687) -> DisplayPoint {
 688    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 689    for _ in 0..times {
 690        if point.column() < map.line_len(point.row()) {
 691            *point.column_mut() += 1;
 692        } else if point.row() < map.max_buffer_row() {
 693            *point.row_mut() += 1;
 694            *point.column_mut() = 0;
 695        }
 696        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 697            let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 698            let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 699
 700            left_kind != right_kind && left_kind != CharKind::Whitespace
 701        });
 702
 703        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
 704        // we have backtracked already
 705        if !map
 706            .chars_at(point)
 707            .nth(1)
 708            .map(|(c, _)| c == '\n')
 709            .unwrap_or(true)
 710        {
 711            *point.column_mut() = point.column().saturating_sub(1);
 712        }
 713        point = map.clip_point(point, Bias::Left);
 714    }
 715    point
 716}
 717
 718fn previous_word_start(
 719    map: &DisplaySnapshot,
 720    mut point: DisplayPoint,
 721    ignore_punctuation: bool,
 722    times: usize,
 723) -> DisplayPoint {
 724    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 725    for _ in 0..times {
 726        // This works even though find_preceding_boundary is called for every character in the line containing
 727        // cursor because the newline is checked only once.
 728        point =
 729            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
 730                let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
 731                let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 732
 733                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
 734            });
 735    }
 736    point
 737}
 738
 739pub(crate) fn first_non_whitespace(
 740    map: &DisplaySnapshot,
 741    display_lines: bool,
 742    from: DisplayPoint,
 743) -> DisplayPoint {
 744    let mut last_point = start_of_line(map, display_lines, from);
 745    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
 746    for (ch, point) in map.chars_at(last_point) {
 747        if ch == '\n' {
 748            return from;
 749        }
 750
 751        last_point = point;
 752
 753        if char_kind(&scope, ch) != CharKind::Whitespace {
 754            break;
 755        }
 756    }
 757
 758    map.clip_point(last_point, Bias::Left)
 759}
 760
 761pub(crate) fn start_of_line(
 762    map: &DisplaySnapshot,
 763    display_lines: bool,
 764    point: DisplayPoint,
 765) -> DisplayPoint {
 766    if display_lines {
 767        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
 768    } else {
 769        map.prev_line_boundary(point.to_point(map)).1
 770    }
 771}
 772
 773pub(crate) fn end_of_line(
 774    map: &DisplaySnapshot,
 775    display_lines: bool,
 776    point: DisplayPoint,
 777) -> DisplayPoint {
 778    if display_lines {
 779        map.clip_point(
 780            DisplayPoint::new(point.row(), map.line_len(point.row())),
 781            Bias::Left,
 782        )
 783    } else {
 784        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
 785    }
 786}
 787
 788fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
 789    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
 790    *new_point.column_mut() = point.column();
 791    map.clip_point(new_point, Bias::Left)
 792}
 793
 794fn end_of_document(
 795    map: &DisplaySnapshot,
 796    point: DisplayPoint,
 797    line: Option<usize>,
 798) -> DisplayPoint {
 799    let new_row = if let Some(line) = line {
 800        (line - 1) as u32
 801    } else {
 802        map.max_buffer_row()
 803    };
 804
 805    let new_point = Point::new(new_row, point.column());
 806    map.clip_point(new_point.to_display_point(map), Bias::Left)
 807}
 808
 809fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 810    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
 811    let point = display_point.to_point(map);
 812    let offset = point.to_offset(&map.buffer_snapshot);
 813
 814    // Ensure the range is contained by the current line.
 815    let mut line_end = map.next_line_boundary(point).0;
 816    if line_end == point {
 817        line_end = map.max_point().to_point(map);
 818    }
 819
 820    let line_range = map.prev_line_boundary(point).0..line_end;
 821    let visible_line_range =
 822        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
 823    let ranges = map
 824        .buffer_snapshot
 825        .bracket_ranges(visible_line_range.clone());
 826    if let Some(ranges) = ranges {
 827        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
 828            ..line_range.end.to_offset(&map.buffer_snapshot);
 829        let mut closest_pair_destination = None;
 830        let mut closest_distance = usize::MAX;
 831
 832        for (open_range, close_range) in ranges {
 833            if open_range.start >= offset && line_range.contains(&open_range.start) {
 834                let distance = open_range.start - offset;
 835                if distance < closest_distance {
 836                    closest_pair_destination = Some(close_range.start);
 837                    closest_distance = distance;
 838                    continue;
 839                }
 840            }
 841
 842            if close_range.start >= offset && line_range.contains(&close_range.start) {
 843                let distance = close_range.start - offset;
 844                if distance < closest_distance {
 845                    closest_pair_destination = Some(open_range.start);
 846                    closest_distance = distance;
 847                    continue;
 848                }
 849            }
 850
 851            continue;
 852        }
 853
 854        closest_pair_destination
 855            .map(|destination| destination.to_display_point(map))
 856            .unwrap_or(display_point)
 857    } else {
 858        display_point
 859    }
 860}
 861
 862fn find_forward(
 863    map: &DisplaySnapshot,
 864    from: DisplayPoint,
 865    before: bool,
 866    target: char,
 867    times: usize,
 868) -> DisplayPoint {
 869    let mut to = from;
 870    let mut found = false;
 871
 872    for _ in 0..times {
 873        found = false;
 874        to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
 875            found = right == target;
 876            found
 877        });
 878    }
 879
 880    if found {
 881        if before && to.column() > 0 {
 882            *to.column_mut() -= 1;
 883            map.clip_point(to, Bias::Left)
 884        } else {
 885            to
 886        }
 887    } else {
 888        from
 889    }
 890}
 891
 892fn find_backward(
 893    map: &DisplaySnapshot,
 894    from: DisplayPoint,
 895    after: bool,
 896    target: char,
 897    times: usize,
 898) -> DisplayPoint {
 899    let mut to = from;
 900
 901    for _ in 0..times {
 902        to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
 903    }
 904
 905    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
 906        if after {
 907            *to.column_mut() += 1;
 908            map.clip_point(to, Bias::Right)
 909        } else {
 910            to
 911        }
 912    } else {
 913        from
 914    }
 915}
 916
 917fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
 918    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
 919    first_non_whitespace(map, false, correct_line)
 920}
 921
 922pub(crate) fn next_line_end(
 923    map: &DisplaySnapshot,
 924    mut point: DisplayPoint,
 925    times: usize,
 926) -> DisplayPoint {
 927    if times > 1 {
 928        point = start_of_relative_buffer_row(map, point, times as isize - 1);
 929    }
 930    end_of_line(map, false, point)
 931}
 932
 933#[cfg(test)]
 934
 935mod test {
 936
 937    use crate::test::NeovimBackedTestContext;
 938    use indoc::indoc;
 939
 940    #[gpui::test]
 941    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
 942        let mut cx = NeovimBackedTestContext::new(cx).await;
 943
 944        let initial_state = indoc! {r"ˇabc
 945            def
 946
 947            paragraph
 948            the second
 949
 950
 951
 952            third and
 953            final"};
 954
 955        // goes down once
 956        cx.set_shared_state(initial_state).await;
 957        cx.simulate_shared_keystrokes(["}"]).await;
 958        cx.assert_shared_state(indoc! {r"abc
 959            def
 960            ˇ
 961            paragraph
 962            the second
 963
 964
 965
 966            third and
 967            final"})
 968            .await;
 969
 970        // goes up once
 971        cx.simulate_shared_keystrokes(["{"]).await;
 972        cx.assert_shared_state(initial_state).await;
 973
 974        // goes down twice
 975        cx.simulate_shared_keystrokes(["2", "}"]).await;
 976        cx.assert_shared_state(indoc! {r"abc
 977            def
 978
 979            paragraph
 980            the second
 981            ˇ
 982
 983
 984            third and
 985            final"})
 986            .await;
 987
 988        // goes down over multiple blanks
 989        cx.simulate_shared_keystrokes(["}"]).await;
 990        cx.assert_shared_state(indoc! {r"abc
 991                def
 992
 993                paragraph
 994                the second
 995
 996
 997
 998                third and
 999                finaˇl"})
1000            .await;
1001
1002        // goes up twice
1003        cx.simulate_shared_keystrokes(["2", "{"]).await;
1004        cx.assert_shared_state(indoc! {r"abc
1005                def
1006                ˇ
1007                paragraph
1008                the second
1009
1010
1011
1012                third and
1013                final"})
1014            .await
1015    }
1016
1017    #[gpui::test]
1018    async fn test_matching(cx: &mut gpui::TestAppContext) {
1019        let mut cx = NeovimBackedTestContext::new(cx).await;
1020
1021        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1022                do(something(with<Types>.and_arrays[0, 2]))
1023            }"})
1024            .await;
1025        cx.simulate_shared_keystrokes(["%"]).await;
1026        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1027                do(something(with<Types>.and_arrays[0, 2]))
1028            }"})
1029            .await;
1030
1031        // test it works on the last character of the line
1032        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1033            do(something(with<Types>.and_arrays[0, 2]))
1034            }"})
1035            .await;
1036        cx.simulate_shared_keystrokes(["%"]).await;
1037        cx.assert_shared_state(indoc! {r"func (a string) {
1038            do(something(with<Types>.and_arrays[0, 2]))
1039            ˇ}"})
1040            .await;
1041
1042        // test it works on immediate nesting
1043        cx.set_shared_state("ˇ{()}").await;
1044        cx.simulate_shared_keystrokes(["%"]).await;
1045        cx.assert_shared_state("{()ˇ}").await;
1046        cx.simulate_shared_keystrokes(["%"]).await;
1047        cx.assert_shared_state("ˇ{()}").await;
1048
1049        // test it works on immediate nesting inside braces
1050        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1051        cx.simulate_shared_keystrokes(["%"]).await;
1052        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1053
1054        // test it jumps to the next paren on a line
1055        cx.set_shared_state("func ˇboop() {\n}").await;
1056        cx.simulate_shared_keystrokes(["%"]).await;
1057        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1058    }
1059
1060    #[gpui::test]
1061    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1062        let mut cx = NeovimBackedTestContext::new(cx).await;
1063
1064        cx.set_shared_state("ˇone two three four").await;
1065        cx.simulate_shared_keystrokes(["f", "o"]).await;
1066        cx.assert_shared_state("one twˇo three four").await;
1067        cx.simulate_shared_keystrokes([","]).await;
1068        cx.assert_shared_state("ˇone two three four").await;
1069        cx.simulate_shared_keystrokes(["2", ";"]).await;
1070        cx.assert_shared_state("one two three fˇour").await;
1071        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1072        cx.assert_shared_state("one two threeˇ four").await;
1073        cx.simulate_shared_keystrokes(["3", ";"]).await;
1074        cx.assert_shared_state("oneˇ two three four").await;
1075        cx.simulate_shared_keystrokes([","]).await;
1076        cx.assert_shared_state("one two thˇree four").await;
1077    }
1078
1079    #[gpui::test]
1080    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1081        let mut cx = NeovimBackedTestContext::new(cx).await;
1082        cx.set_shared_state("ˇone\n  two\nthree").await;
1083        cx.simulate_shared_keystrokes(["enter"]).await;
1084        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1085    }
1086}