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