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