motion.rs

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