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    utils::coerce_punctuation,
  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    WindowTop,
  45    WindowMiddle,
  46    WindowBottom,
  47}
  48
  49#[derive(Clone, Deserialize, PartialEq)]
  50#[serde(rename_all = "camelCase")]
  51struct NextWordStart {
  52    #[serde(default)]
  53    ignore_punctuation: bool,
  54}
  55
  56#[derive(Clone, Deserialize, PartialEq)]
  57#[serde(rename_all = "camelCase")]
  58struct NextWordEnd {
  59    #[serde(default)]
  60    ignore_punctuation: bool,
  61}
  62
  63#[derive(Clone, Deserialize, PartialEq)]
  64#[serde(rename_all = "camelCase")]
  65struct PreviousWordStart {
  66    #[serde(default)]
  67    ignore_punctuation: bool,
  68}
  69
  70#[derive(Clone, Deserialize, PartialEq)]
  71#[serde(rename_all = "camelCase")]
  72pub(crate) struct Up {
  73    #[serde(default)]
  74    pub(crate) display_lines: bool,
  75}
  76
  77#[derive(Clone, Deserialize, PartialEq)]
  78#[serde(rename_all = "camelCase")]
  79pub(crate) struct Down {
  80    #[serde(default)]
  81    pub(crate) display_lines: bool,
  82}
  83
  84#[derive(Clone, Deserialize, PartialEq)]
  85#[serde(rename_all = "camelCase")]
  86struct FirstNonWhitespace {
  87    #[serde(default)]
  88    display_lines: bool,
  89}
  90
  91#[derive(Clone, Deserialize, PartialEq)]
  92#[serde(rename_all = "camelCase")]
  93struct EndOfLine {
  94    #[serde(default)]
  95    display_lines: bool,
  96}
  97
  98#[derive(Clone, Deserialize, PartialEq)]
  99#[serde(rename_all = "camelCase")]
 100pub struct StartOfLine {
 101    #[serde(default)]
 102    pub(crate) display_lines: bool,
 103}
 104
 105#[derive(Clone, Deserialize, PartialEq)]
 106struct RepeatFind {
 107    #[serde(default)]
 108    backwards: bool,
 109}
 110
 111impl_actions!(
 112    vim,
 113    [
 114        RepeatFind,
 115        StartOfLine,
 116        EndOfLine,
 117        FirstNonWhitespace,
 118        Down,
 119        Up,
 120        PreviousWordStart,
 121        NextWordEnd,
 122        NextWordStart
 123    ]
 124);
 125
 126actions!(
 127    vim,
 128    [
 129        Left,
 130        Backspace,
 131        Right,
 132        CurrentLine,
 133        StartOfParagraph,
 134        EndOfParagraph,
 135        StartOfDocument,
 136        EndOfDocument,
 137        Matching,
 138        NextLineStart,
 139        StartOfLineDownward,
 140        EndOfLineDownward,
 141        GoToColumn,
 142        WindowTop,
 143        WindowMiddle,
 144        WindowBottom,
 145    ]
 146);
 147
 148pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 149    workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 150    workspace
 151        .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
 152    workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
 153        motion(
 154            Motion::Down {
 155                display_lines: action.display_lines,
 156            },
 157            cx,
 158        )
 159    });
 160    workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
 161        motion(
 162            Motion::Up {
 163                display_lines: action.display_lines,
 164            },
 165            cx,
 166        )
 167    });
 168    workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 169    workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
 170        motion(
 171            Motion::FirstNonWhitespace {
 172                display_lines: action.display_lines,
 173            },
 174            cx,
 175        )
 176    });
 177    workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
 178        motion(
 179            Motion::StartOfLine {
 180                display_lines: action.display_lines,
 181            },
 182            cx,
 183        )
 184    });
 185    workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
 186        motion(
 187            Motion::EndOfLine {
 188                display_lines: action.display_lines,
 189            },
 190            cx,
 191        )
 192    });
 193    workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
 194        motion(Motion::CurrentLine, cx)
 195    });
 196    workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
 197        motion(Motion::StartOfParagraph, cx)
 198    });
 199    workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
 200        motion(Motion::EndOfParagraph, cx)
 201    });
 202    workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
 203        motion(Motion::StartOfDocument, cx)
 204    });
 205    workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
 206        motion(Motion::EndOfDocument, cx)
 207    });
 208    workspace
 209        .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
 210
 211    workspace.register_action(
 212        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
 213            motion(Motion::NextWordStart { ignore_punctuation }, cx)
 214        },
 215    );
 216    workspace.register_action(
 217        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
 218            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
 219        },
 220    );
 221    workspace.register_action(
 222        |_: &mut Workspace,
 223         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
 224         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
 225    );
 226    workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
 227        motion(Motion::NextLineStart, cx)
 228    });
 229    workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
 230        motion(Motion::StartOfLineDownward, cx)
 231    });
 232    workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
 233        motion(Motion::EndOfLineDownward, cx)
 234    });
 235    workspace
 236        .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
 237    workspace.register_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
 238        repeat_motion(action.backwards, cx)
 239    });
 240    workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
 241    workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
 242        motion(Motion::WindowMiddle, cx)
 243    });
 244    workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
 245        motion(Motion::WindowBottom, cx)
 246    });
 247}
 248
 249pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 250    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
 251        Vim::read(cx).active_operator()
 252    {
 253        Vim::update(cx, |vim, cx| vim.pop_operator(cx));
 254    }
 255
 256    let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
 257    let operator = Vim::read(cx).active_operator();
 258    match Vim::read(cx).state().mode {
 259        Mode::Normal => normal_motion(motion, operator, count, cx),
 260        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
 261        Mode::Insert => {
 262            // Shouldn't execute a motion in insert mode. Ignoring
 263        }
 264    }
 265    Vim::update(cx, |vim, cx| vim.clear_operator(cx));
 266}
 267
 268fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
 269    let find = match Vim::read(cx).workspace_state.last_find.clone() {
 270        Some(Motion::FindForward { before, char }) => {
 271            if backwards {
 272                Motion::FindBackward {
 273                    after: before,
 274                    char,
 275                }
 276            } else {
 277                Motion::FindForward { before, char }
 278            }
 279        }
 280
 281        Some(Motion::FindBackward { after, char }) => {
 282            if backwards {
 283                Motion::FindForward {
 284                    before: after,
 285                    char,
 286                }
 287            } else {
 288                Motion::FindBackward { after, char }
 289            }
 290        }
 291        _ => return,
 292    };
 293
 294    motion(find, cx)
 295}
 296
 297// Motion handling is specified here:
 298// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 299impl Motion {
 300    pub fn linewise(&self) -> bool {
 301        use Motion::*;
 302        match self {
 303            Down { .. }
 304            | Up { .. }
 305            | StartOfDocument
 306            | EndOfDocument
 307            | CurrentLine
 308            | NextLineStart
 309            | StartOfLineDownward
 310            | StartOfParagraph
 311            | WindowTop
 312            | WindowMiddle
 313            | WindowBottom
 314            | EndOfParagraph => true,
 315            EndOfLine { .. }
 316            | NextWordEnd { .. }
 317            | Matching
 318            | FindForward { .. }
 319            | Left
 320            | Backspace
 321            | Right
 322            | StartOfLine { .. }
 323            | EndOfLineDownward
 324            | GoToColumn
 325            | NextWordStart { .. }
 326            | PreviousWordStart { .. }
 327            | FirstNonWhitespace { .. }
 328            | FindBackward { .. } => false,
 329        }
 330    }
 331
 332    pub fn infallible(&self) -> bool {
 333        use Motion::*;
 334        match self {
 335            StartOfDocument | EndOfDocument | CurrentLine => true,
 336            Down { .. }
 337            | Up { .. }
 338            | EndOfLine { .. }
 339            | NextWordEnd { .. }
 340            | Matching
 341            | FindForward { .. }
 342            | Left
 343            | Backspace
 344            | Right
 345            | StartOfLine { .. }
 346            | StartOfParagraph
 347            | EndOfParagraph
 348            | StartOfLineDownward
 349            | EndOfLineDownward
 350            | GoToColumn
 351            | NextWordStart { .. }
 352            | PreviousWordStart { .. }
 353            | FirstNonWhitespace { .. }
 354            | FindBackward { .. }
 355            | WindowTop
 356            | WindowMiddle
 357            | WindowBottom
 358            | NextLineStart => false,
 359        }
 360    }
 361
 362    pub fn inclusive(&self) -> bool {
 363        use Motion::*;
 364        match self {
 365            Down { .. }
 366            | Up { .. }
 367            | StartOfDocument
 368            | EndOfDocument
 369            | CurrentLine
 370            | EndOfLine { .. }
 371            | EndOfLineDownward
 372            | NextWordEnd { .. }
 373            | Matching
 374            | FindForward { .. }
 375            | WindowTop
 376            | WindowMiddle
 377            | WindowBottom
 378            | NextLineStart => true,
 379            Left
 380            | Backspace
 381            | Right
 382            | StartOfLine { .. }
 383            | StartOfLineDownward
 384            | StartOfParagraph
 385            | EndOfParagraph
 386            | GoToColumn
 387            | NextWordStart { .. }
 388            | PreviousWordStart { .. }
 389            | FirstNonWhitespace { .. }
 390            | FindBackward { .. } => false,
 391        }
 392    }
 393
 394    pub fn move_point(
 395        &self,
 396        map: &DisplaySnapshot,
 397        point: DisplayPoint,
 398        goal: SelectionGoal,
 399        maybe_times: Option<usize>,
 400        text_layout_details: &TextLayoutDetails,
 401    ) -> Option<(DisplayPoint, SelectionGoal)> {
 402        let times = maybe_times.unwrap_or(1);
 403        use Motion::*;
 404        let infallible = self.infallible();
 405        let (new_point, goal) = match self {
 406            Left => (left(map, point, times), SelectionGoal::None),
 407            Backspace => (backspace(map, point, times), SelectionGoal::None),
 408            Down {
 409                display_lines: false,
 410            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
 411            Down {
 412                display_lines: true,
 413            } => down_display(map, point, goal, times, &text_layout_details),
 414            Up {
 415                display_lines: false,
 416            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
 417            Up {
 418                display_lines: true,
 419            } => up_display(map, point, goal, times, &text_layout_details),
 420            Right => (right(map, point, times), SelectionGoal::None),
 421            NextWordStart { ignore_punctuation } => (
 422                next_word_start(map, point, *ignore_punctuation, times),
 423                SelectionGoal::None,
 424            ),
 425            NextWordEnd { ignore_punctuation } => (
 426                next_word_end(map, point, *ignore_punctuation, times),
 427                SelectionGoal::None,
 428            ),
 429            PreviousWordStart { ignore_punctuation } => (
 430                previous_word_start(map, point, *ignore_punctuation, times),
 431                SelectionGoal::None,
 432            ),
 433            FirstNonWhitespace { display_lines } => (
 434                first_non_whitespace(map, *display_lines, point),
 435                SelectionGoal::None,
 436            ),
 437            StartOfLine { display_lines } => (
 438                start_of_line(map, *display_lines, point),
 439                SelectionGoal::None,
 440            ),
 441            EndOfLine { display_lines } => {
 442                (end_of_line(map, *display_lines, point), SelectionGoal::None)
 443            }
 444            StartOfParagraph => (
 445                movement::start_of_paragraph(map, point, times),
 446                SelectionGoal::None,
 447            ),
 448            EndOfParagraph => (
 449                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 450                SelectionGoal::None,
 451            ),
 452            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 453            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 454            EndOfDocument => (
 455                end_of_document(map, point, maybe_times),
 456                SelectionGoal::None,
 457            ),
 458            Matching => (matching(map, point), SelectionGoal::None),
 459            FindForward { before, char } => {
 460                if let Some(new_point) = find_forward(map, point, *before, *char, times) {
 461                    return Some((new_point, SelectionGoal::None));
 462                } else {
 463                    return None;
 464                }
 465            }
 466            FindBackward { after, char } => (
 467                find_backward(map, point, *after, *char, times),
 468                SelectionGoal::None,
 469            ),
 470            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 471            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 472            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
 473            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
 474            WindowTop => window_top(map, point, &text_layout_details),
 475            WindowMiddle => window_middle(map, point, &text_layout_details),
 476            WindowBottom => window_bottom(map, point, &text_layout_details),
 477        };
 478
 479        (new_point != point || infallible).then_some((new_point, goal))
 480    }
 481
 482    // Expands a selection using self motion for an operator
 483    pub fn expand_selection(
 484        &self,
 485        map: &DisplaySnapshot,
 486        selection: &mut Selection<DisplayPoint>,
 487        times: Option<usize>,
 488        expand_to_surrounding_newline: bool,
 489        text_layout_details: &TextLayoutDetails,
 490    ) -> bool {
 491        if let Some((new_head, goal)) = self.move_point(
 492            map,
 493            selection.head(),
 494            selection.goal,
 495            times,
 496            &text_layout_details,
 497        ) {
 498            selection.set_head(new_head, goal);
 499
 500            if self.linewise() {
 501                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 502
 503                if expand_to_surrounding_newline {
 504                    if selection.end.row() < map.max_point().row() {
 505                        *selection.end.row_mut() += 1;
 506                        *selection.end.column_mut() = 0;
 507                        selection.end = map.clip_point(selection.end, Bias::Right);
 508                        // Don't reset the end here
 509                        return true;
 510                    } else if selection.start.row() > 0 {
 511                        *selection.start.row_mut() -= 1;
 512                        *selection.start.column_mut() = map.line_len(selection.start.row());
 513                        selection.start = map.clip_point(selection.start, Bias::Left);
 514                    }
 515                }
 516
 517                (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
 518            } else {
 519                // Another special case: When using the "w" motion in combination with an
 520                // operator and the last word moved over is at the end of a line, the end of
 521                // that word becomes the end of the operated text, not the first word in the
 522                // next line.
 523                if let Motion::NextWordStart {
 524                    ignore_punctuation: _,
 525                } = self
 526                {
 527                    let start_row = selection.start.to_point(&map).row;
 528                    if selection.end.to_point(&map).row > start_row {
 529                        selection.end =
 530                            Point::new(start_row, map.buffer_snapshot.line_len(start_row))
 531                                .to_display_point(&map)
 532                    }
 533                }
 534
 535                // If the motion is exclusive and the end of the motion is in column 1, the
 536                // end of the motion is moved to the end of the previous line and the motion
 537                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
 538                // but "d}" will not include that line.
 539                let mut inclusive = self.inclusive();
 540                if !inclusive
 541                    && self != &Motion::Backspace
 542                    && selection.end.row() > selection.start.row()
 543                    && selection.end.column() == 0
 544                {
 545                    inclusive = true;
 546                    *selection.end.row_mut() -= 1;
 547                    *selection.end.column_mut() = 0;
 548                    selection.end = map.clip_point(
 549                        map.next_line_boundary(selection.end.to_point(map)).1,
 550                        Bias::Left,
 551                    );
 552                }
 553
 554                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 555                    *selection.end.column_mut() += 1;
 556                }
 557            }
 558            true
 559        } else {
 560            false
 561        }
 562    }
 563}
 564
 565fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 566    for _ in 0..times {
 567        point = movement::saturating_left(map, point);
 568        if point.column() == 0 {
 569            break;
 570        }
 571    }
 572    point
 573}
 574
 575fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 576    for _ in 0..times {
 577        point = movement::left(map, point);
 578    }
 579    point
 580}
 581
 582pub(crate) fn start_of_relative_buffer_row(
 583    map: &DisplaySnapshot,
 584    point: DisplayPoint,
 585    times: isize,
 586) -> DisplayPoint {
 587    let start = map.display_point_to_fold_point(point, Bias::Left);
 588    let target = start.row() as isize + times;
 589    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 590
 591    map.clip_point(
 592        map.fold_point_to_display_point(
 593            map.fold_snapshot
 594                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 595        ),
 596        Bias::Right,
 597    )
 598}
 599
 600fn up_down_buffer_rows(
 601    map: &DisplaySnapshot,
 602    point: DisplayPoint,
 603    mut goal: SelectionGoal,
 604    times: isize,
 605    text_layout_details: &TextLayoutDetails,
 606) -> (DisplayPoint, SelectionGoal) {
 607    let start = map.display_point_to_fold_point(point, Bias::Left);
 608    let begin_folded_line = map.fold_point_to_display_point(
 609        map.fold_snapshot
 610            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
 611    );
 612    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 613
 614    let (goal_wrap, goal_x) = match goal {
 615        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
 616        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 617        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 618        _ => {
 619            let x = map.x_for_display_point(point, text_layout_details);
 620            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
 621            (select_nth_wrapped_row, x.0)
 622        }
 623    };
 624
 625    let target = start.row() as isize + times;
 626    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 627
 628    let mut begin_folded_line = map.fold_point_to_display_point(
 629        map.fold_snapshot
 630            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
 631    );
 632
 633    let mut i = 0;
 634    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
 635        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
 636        if map
 637            .display_point_to_fold_point(next_folded_line, Bias::Right)
 638            .row()
 639            == new_row
 640        {
 641            i += 1;
 642            begin_folded_line = next_folded_line;
 643        } else {
 644            break;
 645        }
 646    }
 647
 648    let new_col = if i == goal_wrap {
 649        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
 650    } else {
 651        map.line_len(begin_folded_line.row())
 652    };
 653
 654    (
 655        map.clip_point(
 656            DisplayPoint::new(begin_folded_line.row(), new_col),
 657            Bias::Left,
 658        ),
 659        goal,
 660    )
 661}
 662
 663fn down_display(
 664    map: &DisplaySnapshot,
 665    mut point: DisplayPoint,
 666    mut goal: SelectionGoal,
 667    times: usize,
 668    text_layout_details: &TextLayoutDetails,
 669) -> (DisplayPoint, SelectionGoal) {
 670    for _ in 0..times {
 671        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
 672    }
 673
 674    (point, goal)
 675}
 676
 677fn up_display(
 678    map: &DisplaySnapshot,
 679    mut point: DisplayPoint,
 680    mut goal: SelectionGoal,
 681    times: usize,
 682    text_layout_details: &TextLayoutDetails,
 683) -> (DisplayPoint, SelectionGoal) {
 684    for _ in 0..times {
 685        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
 686    }
 687
 688    (point, goal)
 689}
 690
 691pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 692    for _ in 0..times {
 693        let new_point = movement::saturating_right(map, point);
 694        if point == new_point {
 695            break;
 696        }
 697        point = new_point;
 698    }
 699    point
 700}
 701
 702pub(crate) fn next_word_start(
 703    map: &DisplaySnapshot,
 704    mut point: DisplayPoint,
 705    ignore_punctuation: bool,
 706    times: usize,
 707) -> DisplayPoint {
 708    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 709    for _ in 0..times {
 710        let mut crossed_newline = false;
 711        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 712            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 713            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 714            let at_newline = right == '\n';
 715
 716            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
 717                || at_newline && crossed_newline
 718                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 719
 720            crossed_newline |= at_newline;
 721            found
 722        })
 723    }
 724    point
 725}
 726
 727fn next_word_end(
 728    map: &DisplaySnapshot,
 729    mut point: DisplayPoint,
 730    ignore_punctuation: bool,
 731    times: usize,
 732) -> DisplayPoint {
 733    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 734    for _ in 0..times {
 735        if point.column() < map.line_len(point.row()) {
 736            *point.column_mut() += 1;
 737        } else if point.row() < map.max_buffer_row() {
 738            *point.row_mut() += 1;
 739            *point.column_mut() = 0;
 740        }
 741        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 742            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 743            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 744
 745            left_kind != right_kind && left_kind != CharKind::Whitespace
 746        });
 747
 748        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
 749        // we have backtracked already
 750        if !map
 751            .chars_at(point)
 752            .nth(1)
 753            .map(|(c, _)| c == '\n')
 754            .unwrap_or(true)
 755        {
 756            *point.column_mut() = point.column().saturating_sub(1);
 757        }
 758        point = map.clip_point(point, Bias::Left);
 759    }
 760    point
 761}
 762
 763fn previous_word_start(
 764    map: &DisplaySnapshot,
 765    mut point: DisplayPoint,
 766    ignore_punctuation: bool,
 767    times: usize,
 768) -> DisplayPoint {
 769    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 770    for _ in 0..times {
 771        // This works even though find_preceding_boundary is called for every character in the line containing
 772        // cursor because the newline is checked only once.
 773        point =
 774            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
 775                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 776                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 777
 778                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
 779            });
 780    }
 781    point
 782}
 783
 784pub(crate) fn first_non_whitespace(
 785    map: &DisplaySnapshot,
 786    display_lines: bool,
 787    from: DisplayPoint,
 788) -> DisplayPoint {
 789    let mut last_point = start_of_line(map, display_lines, from);
 790    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
 791    for (ch, point) in map.chars_at(last_point) {
 792        if ch == '\n' {
 793            return from;
 794        }
 795
 796        last_point = point;
 797
 798        if char_kind(&scope, ch) != CharKind::Whitespace {
 799            break;
 800        }
 801    }
 802
 803    map.clip_point(last_point, Bias::Left)
 804}
 805
 806pub(crate) fn start_of_line(
 807    map: &DisplaySnapshot,
 808    display_lines: bool,
 809    point: DisplayPoint,
 810) -> DisplayPoint {
 811    if display_lines {
 812        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
 813    } else {
 814        map.prev_line_boundary(point.to_point(map)).1
 815    }
 816}
 817
 818pub(crate) fn end_of_line(
 819    map: &DisplaySnapshot,
 820    display_lines: bool,
 821    point: DisplayPoint,
 822) -> DisplayPoint {
 823    if display_lines {
 824        map.clip_point(
 825            DisplayPoint::new(point.row(), map.line_len(point.row())),
 826            Bias::Left,
 827        )
 828    } else {
 829        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
 830    }
 831}
 832
 833fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
 834    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
 835    *new_point.column_mut() = point.column();
 836    map.clip_point(new_point, Bias::Left)
 837}
 838
 839fn end_of_document(
 840    map: &DisplaySnapshot,
 841    point: DisplayPoint,
 842    line: Option<usize>,
 843) -> DisplayPoint {
 844    let new_row = if let Some(line) = line {
 845        (line - 1) as u32
 846    } else {
 847        map.max_buffer_row()
 848    };
 849
 850    let new_point = Point::new(new_row, point.column());
 851    map.clip_point(new_point.to_display_point(map), Bias::Left)
 852}
 853
 854fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 855    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
 856    let point = display_point.to_point(map);
 857    let offset = point.to_offset(&map.buffer_snapshot);
 858
 859    // Ensure the range is contained by the current line.
 860    let mut line_end = map.next_line_boundary(point).0;
 861    if line_end == point {
 862        line_end = map.max_point().to_point(map);
 863    }
 864
 865    let line_range = map.prev_line_boundary(point).0..line_end;
 866    let visible_line_range =
 867        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
 868    let ranges = map
 869        .buffer_snapshot
 870        .bracket_ranges(visible_line_range.clone());
 871    if let Some(ranges) = ranges {
 872        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
 873            ..line_range.end.to_offset(&map.buffer_snapshot);
 874        let mut closest_pair_destination = None;
 875        let mut closest_distance = usize::MAX;
 876
 877        for (open_range, close_range) in ranges {
 878            if open_range.start >= offset && line_range.contains(&open_range.start) {
 879                let distance = open_range.start - offset;
 880                if distance < closest_distance {
 881                    closest_pair_destination = Some(close_range.start);
 882                    closest_distance = distance;
 883                    continue;
 884                }
 885            }
 886
 887            if close_range.start >= offset && line_range.contains(&close_range.start) {
 888                let distance = close_range.start - offset;
 889                if distance < closest_distance {
 890                    closest_pair_destination = Some(open_range.start);
 891                    closest_distance = distance;
 892                    continue;
 893                }
 894            }
 895
 896            continue;
 897        }
 898
 899        closest_pair_destination
 900            .map(|destination| destination.to_display_point(map))
 901            .unwrap_or(display_point)
 902    } else {
 903        display_point
 904    }
 905}
 906
 907fn find_forward(
 908    map: &DisplaySnapshot,
 909    from: DisplayPoint,
 910    before: bool,
 911    target: char,
 912    times: usize,
 913) -> Option<DisplayPoint> {
 914    let mut to = from;
 915    let mut found = false;
 916
 917    for _ in 0..times {
 918        found = false;
 919        to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
 920            found = right == target;
 921            found
 922        });
 923    }
 924
 925    if found {
 926        if before && to.column() > 0 {
 927            *to.column_mut() -= 1;
 928            Some(map.clip_point(to, Bias::Left))
 929        } else {
 930            Some(to)
 931        }
 932    } else {
 933        None
 934    }
 935}
 936
 937fn find_backward(
 938    map: &DisplaySnapshot,
 939    from: DisplayPoint,
 940    after: bool,
 941    target: char,
 942    times: usize,
 943) -> DisplayPoint {
 944    let mut to = from;
 945
 946    for _ in 0..times {
 947        to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
 948    }
 949
 950    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
 951        if after {
 952            *to.column_mut() += 1;
 953            map.clip_point(to, Bias::Right)
 954        } else {
 955            to
 956        }
 957    } else {
 958        from
 959    }
 960}
 961
 962fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
 963    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
 964    first_non_whitespace(map, false, correct_line)
 965}
 966
 967fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
 968    let correct_line = start_of_relative_buffer_row(map, point, 0);
 969    right(map, correct_line, times.saturating_sub(1))
 970}
 971
 972pub(crate) fn next_line_end(
 973    map: &DisplaySnapshot,
 974    mut point: DisplayPoint,
 975    times: usize,
 976) -> DisplayPoint {
 977    if times > 1 {
 978        point = start_of_relative_buffer_row(map, point, times as isize - 1);
 979    }
 980    end_of_line(map, false, point)
 981}
 982
 983fn window_top(
 984    map: &DisplaySnapshot,
 985    point: DisplayPoint,
 986    text_layout_details: &TextLayoutDetails,
 987) -> (DisplayPoint, SelectionGoal) {
 988    let first_visible_line = text_layout_details.anchor.to_display_point(map);
 989    let new_col = point.column().min(map.line_len(first_visible_line.row()));
 990    let new_point = DisplayPoint::new(first_visible_line.row(), new_col);
 991    (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
 992}
 993
 994fn window_middle(
 995    map: &DisplaySnapshot,
 996    point: DisplayPoint,
 997    text_layout_details: &TextLayoutDetails,
 998) -> (DisplayPoint, SelectionGoal) {
 999    if let Some(visible_rows) = text_layout_details.visible_rows {
1000        let first_visible_line = text_layout_details.anchor.to_display_point(map);
1001        let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1002        let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1003        let new_col = point.column().min(map.line_len(new_row));
1004        let new_point = DisplayPoint::new(new_row, new_col);
1005        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1006    } else {
1007        (point, SelectionGoal::None)
1008    }
1009}
1010
1011fn window_bottom(
1012    map: &DisplaySnapshot,
1013    point: DisplayPoint,
1014    text_layout_details: &TextLayoutDetails,
1015) -> (DisplayPoint, SelectionGoal) {
1016    if let Some(visible_rows) = text_layout_details.visible_rows {
1017        let first_visible_line = text_layout_details.anchor.to_display_point(map);
1018        let bottom_row = first_visible_line.row() + (visible_rows) as u32;
1019        let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1020        let new_col = point.column().min(map.line_len(bottom_row_capped));
1021        let new_point = DisplayPoint::new(bottom_row_capped, new_col);
1022        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1023    } else {
1024        (point, SelectionGoal::None)
1025    }
1026}
1027
1028#[cfg(test)]
1029mod test {
1030
1031    use crate::test::NeovimBackedTestContext;
1032    use indoc::indoc;
1033
1034    #[gpui::test]
1035    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1036        let mut cx = NeovimBackedTestContext::new(cx).await;
1037
1038        let initial_state = indoc! {r"ˇabc
1039            def
1040
1041            paragraph
1042            the second
1043
1044
1045
1046            third and
1047            final"};
1048
1049        // goes down once
1050        cx.set_shared_state(initial_state).await;
1051        cx.simulate_shared_keystrokes(["}"]).await;
1052        cx.assert_shared_state(indoc! {r"abc
1053            def
1054            ˇ
1055            paragraph
1056            the second
1057
1058
1059
1060            third and
1061            final"})
1062            .await;
1063
1064        // goes up once
1065        cx.simulate_shared_keystrokes(["{"]).await;
1066        cx.assert_shared_state(initial_state).await;
1067
1068        // goes down twice
1069        cx.simulate_shared_keystrokes(["2", "}"]).await;
1070        cx.assert_shared_state(indoc! {r"abc
1071            def
1072
1073            paragraph
1074            the second
1075            ˇ
1076
1077
1078            third and
1079            final"})
1080            .await;
1081
1082        // goes down over multiple blanks
1083        cx.simulate_shared_keystrokes(["}"]).await;
1084        cx.assert_shared_state(indoc! {r"abc
1085                def
1086
1087                paragraph
1088                the second
1089
1090
1091
1092                third and
1093                finaˇl"})
1094            .await;
1095
1096        // goes up twice
1097        cx.simulate_shared_keystrokes(["2", "{"]).await;
1098        cx.assert_shared_state(indoc! {r"abc
1099                def
1100                ˇ
1101                paragraph
1102                the second
1103
1104
1105
1106                third and
1107                final"})
1108            .await
1109    }
1110
1111    #[gpui::test]
1112    async fn test_matching(cx: &mut gpui::TestAppContext) {
1113        let mut cx = NeovimBackedTestContext::new(cx).await;
1114
1115        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1116                do(something(with<Types>.and_arrays[0, 2]))
1117            }"})
1118            .await;
1119        cx.simulate_shared_keystrokes(["%"]).await;
1120        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1121                do(something(with<Types>.and_arrays[0, 2]))
1122            }"})
1123            .await;
1124
1125        // test it works on the last character of the line
1126        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1127            do(something(with<Types>.and_arrays[0, 2]))
1128            }"})
1129            .await;
1130        cx.simulate_shared_keystrokes(["%"]).await;
1131        cx.assert_shared_state(indoc! {r"func (a string) {
1132            do(something(with<Types>.and_arrays[0, 2]))
1133            ˇ}"})
1134            .await;
1135
1136        // test it works on immediate nesting
1137        cx.set_shared_state("ˇ{()}").await;
1138        cx.simulate_shared_keystrokes(["%"]).await;
1139        cx.assert_shared_state("{()ˇ}").await;
1140        cx.simulate_shared_keystrokes(["%"]).await;
1141        cx.assert_shared_state("ˇ{()}").await;
1142
1143        // test it works on immediate nesting inside braces
1144        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1145        cx.simulate_shared_keystrokes(["%"]).await;
1146        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1147
1148        // test it jumps to the next paren on a line
1149        cx.set_shared_state("func ˇboop() {\n}").await;
1150        cx.simulate_shared_keystrokes(["%"]).await;
1151        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1152    }
1153
1154    #[gpui::test]
1155    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1156        let mut cx = NeovimBackedTestContext::new(cx).await;
1157
1158        cx.set_shared_state("ˇone two three four").await;
1159        cx.simulate_shared_keystrokes(["f", "o"]).await;
1160        cx.assert_shared_state("one twˇo three four").await;
1161        cx.simulate_shared_keystrokes([","]).await;
1162        cx.assert_shared_state("ˇone two three four").await;
1163        cx.simulate_shared_keystrokes(["2", ";"]).await;
1164        cx.assert_shared_state("one two three fˇour").await;
1165        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1166        cx.assert_shared_state("one two threeˇ four").await;
1167        cx.simulate_shared_keystrokes(["3", ";"]).await;
1168        cx.assert_shared_state("oneˇ two three four").await;
1169        cx.simulate_shared_keystrokes([","]).await;
1170        cx.assert_shared_state("one two thˇree four").await;
1171    }
1172
1173    #[gpui::test]
1174    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1175        let mut cx = NeovimBackedTestContext::new(cx).await;
1176        cx.set_shared_state("ˇone\n  two\nthree").await;
1177        cx.simulate_shared_keystrokes(["enter"]).await;
1178        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1179    }
1180
1181    #[gpui::test]
1182    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1183        let mut cx = NeovimBackedTestContext::new(cx).await;
1184        let initial_state = indoc! {r"abc
1185          def
1186          paragraph
1187          the second
1188          third ˇand
1189          final"};
1190
1191        cx.set_shared_state(initial_state).await;
1192        cx.simulate_shared_keystrokes(["shift-h"]).await;
1193        cx.assert_shared_state(indoc! {r"abˇc
1194          def
1195          paragraph
1196          the second
1197          third and
1198          final"})
1199            .await;
1200
1201        // clip point
1202        cx.set_shared_state(indoc! {r"
1203          1 2 3
1204          4 5 6
1205          7 8 ˇ9
1206          "})
1207            .await;
1208        cx.simulate_shared_keystrokes(["shift-h"]).await;
1209        cx.assert_shared_state(indoc! {r"
1210          1 2 ˇ3
1211          4 5 6
1212          7 8 9
1213          "})
1214            .await;
1215
1216        cx.set_shared_state(indoc! {r"
1217          1 2 3
1218          4 5 6
1219          ˇ7 8 9
1220          "})
1221            .await;
1222        cx.simulate_shared_keystrokes(["shift-h"]).await;
1223        cx.assert_shared_state(indoc! {r"
1224          ˇ1 2 3
1225          4 5 6
1226          7 8 9
1227          "})
1228            .await;
1229    }
1230
1231    #[gpui::test]
1232    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1233        let mut cx = NeovimBackedTestContext::new(cx).await;
1234        let initial_state = indoc! {r"abˇc
1235          def
1236          paragraph
1237          the second
1238          third and
1239          final"};
1240
1241        cx.set_shared_state(initial_state).await;
1242        cx.simulate_shared_keystrokes(["shift-m"]).await;
1243        cx.assert_shared_state(indoc! {r"abc
1244          def
1245          paˇragraph
1246          the second
1247          third and
1248          final"})
1249            .await;
1250
1251        cx.set_shared_state(indoc! {r"
1252          1 2 3
1253          4 5 6
1254          7 8 ˇ9
1255          "})
1256            .await;
1257        cx.simulate_shared_keystrokes(["shift-m"]).await;
1258        cx.assert_shared_state(indoc! {r"
1259          1 2 3
1260          4 5 ˇ6
1261          7 8 9
1262          "})
1263            .await;
1264        cx.set_shared_state(indoc! {r"
1265          1 2 3
1266          4 5 6
1267          ˇ7 8 9
1268          "})
1269            .await;
1270        cx.simulate_shared_keystrokes(["shift-m"]).await;
1271        cx.assert_shared_state(indoc! {r"
1272          1 2 3
1273          ˇ4 5 6
1274          7 8 9
1275          "})
1276            .await;
1277        cx.set_shared_state(indoc! {r"
1278          ˇ1 2 3
1279          4 5 6
1280          7 8 9
1281          "})
1282            .await;
1283        cx.simulate_shared_keystrokes(["shift-m"]).await;
1284        cx.assert_shared_state(indoc! {r"
1285          1 2 3
1286          ˇ4 5 6
1287          7 8 9
1288          "})
1289            .await;
1290        cx.set_shared_state(indoc! {r"
1291          1 2 3
1292          ˇ4 5 6
1293          7 8 9
1294          "})
1295            .await;
1296        cx.simulate_shared_keystrokes(["shift-m"]).await;
1297        cx.assert_shared_state(indoc! {r"
1298          1 2 3
1299          ˇ4 5 6
1300          7 8 9
1301          "})
1302            .await;
1303        cx.set_shared_state(indoc! {r"
1304          1 2 3
1305          4 5 ˇ6
1306          7 8 9
1307          "})
1308            .await;
1309        cx.simulate_shared_keystrokes(["shift-m"]).await;
1310        cx.assert_shared_state(indoc! {r"
1311          1 2 3
1312          4 5 ˇ6
1313          7 8 9
1314          "})
1315            .await;
1316    }
1317
1318    #[gpui::test]
1319    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1320        let mut cx = NeovimBackedTestContext::new(cx).await;
1321        let initial_state = indoc! {r"abc
1322          deˇf
1323          paragraph
1324          the second
1325          third and
1326          final"};
1327
1328        cx.set_shared_state(initial_state).await;
1329        cx.simulate_shared_keystrokes(["shift-l"]).await;
1330        cx.assert_shared_state(indoc! {r"abc
1331          def
1332          paragraph
1333          the second
1334          third and
1335          fiˇnal"})
1336            .await;
1337
1338        cx.set_shared_state(indoc! {r"
1339          1 2 3
1340          4 5 ˇ6
1341          7 8 9
1342          "})
1343            .await;
1344        cx.simulate_shared_keystrokes(["shift-l"]).await;
1345        cx.assert_shared_state(indoc! {r"
1346          1 2 3
1347          4 5 6
1348          7 8 9
1349          ˇ"})
1350            .await;
1351
1352        cx.set_shared_state(indoc! {r"
1353          1 2 3
1354          ˇ4 5 6
1355          7 8 9
1356          "})
1357            .await;
1358        cx.simulate_shared_keystrokes(["shift-l"]).await;
1359        cx.assert_shared_state(indoc! {r"
1360          1 2 3
1361          4 5 6
1362          7 8 9
1363          ˇ"})
1364            .await;
1365
1366        cx.set_shared_state(indoc! {r"
1367          1 2 ˇ3
1368          4 5 6
1369          7 8 9
1370          "})
1371            .await;
1372        cx.simulate_shared_keystrokes(["shift-l"]).await;
1373        cx.assert_shared_state(indoc! {r"
1374          1 2 3
1375          4 5 6
1376          7 8 9
1377          ˇ"})
1378            .await;
1379
1380        cx.set_shared_state(indoc! {r"
1381          ˇ1 2 3
1382          4 5 6
1383          7 8 9
1384          "})
1385            .await;
1386        cx.simulate_shared_keystrokes(["shift-l"]).await;
1387        cx.assert_shared_state(indoc! {r"
1388          1 2 3
1389          4 5 6
1390          7 8 9
1391          ˇ"})
1392            .await;
1393    }
1394}