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), SelectionGoal::None)
 497            }
 498            StartOfParagraph => (
 499                movement::start_of_paragraph(map, point, times),
 500                SelectionGoal::None,
 501            ),
 502            EndOfParagraph => (
 503                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 504                SelectionGoal::None,
 505            ),
 506            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 507            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 508            EndOfDocument => (
 509                end_of_document(map, point, maybe_times),
 510                SelectionGoal::None,
 511            ),
 512            Matching => (matching(map, point), SelectionGoal::None),
 513            // t f
 514            FindForward { before, char, mode } => {
 515                return find_forward(map, point, *before, *char, times, *mode)
 516                    .map(|new_point| (new_point, SelectionGoal::None))
 517            }
 518            // T F
 519            FindBackward { after, char, mode } => (
 520                find_backward(map, point, *after, *char, times, *mode),
 521                SelectionGoal::None,
 522            ),
 523            // ; -- repeat the last find done with t, f, T, F
 524            RepeatFind { last_find } => match **last_find {
 525                Motion::FindForward { before, char, mode } => {
 526                    let mut new_point = find_forward(map, point, before, char, times, mode);
 527                    if new_point == Some(point) {
 528                        new_point = find_forward(map, point, before, char, times + 1, mode);
 529                    }
 530
 531                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 532                }
 533
 534                Motion::FindBackward { after, char, mode } => {
 535                    let mut new_point = find_backward(map, point, after, char, times, mode);
 536                    if new_point == point {
 537                        new_point = find_backward(map, point, after, char, times + 1, mode);
 538                    }
 539
 540                    (new_point, SelectionGoal::None)
 541                }
 542                _ => return None,
 543            },
 544            // , -- repeat the last find done with t, f, T, F, in opposite direction
 545            RepeatFindReversed { last_find } => match **last_find {
 546                Motion::FindForward { before, char, mode } => {
 547                    let mut new_point = find_backward(map, point, before, char, times, mode);
 548                    if new_point == point {
 549                        new_point = find_backward(map, point, before, char, times + 1, mode);
 550                    }
 551
 552                    (new_point, SelectionGoal::None)
 553                }
 554
 555                Motion::FindBackward { after, char, mode } => {
 556                    let mut new_point = find_forward(map, point, after, char, times, mode);
 557                    if new_point == Some(point) {
 558                        new_point = find_forward(map, point, after, char, times + 1, mode);
 559                    }
 560
 561                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 562                }
 563                _ => return None,
 564            },
 565            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 566            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 567            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
 568            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
 569            WindowTop => window_top(map, point, &text_layout_details, times - 1),
 570            WindowMiddle => window_middle(map, point, &text_layout_details),
 571            WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
 572        };
 573
 574        (new_point != point || infallible).then_some((new_point, goal))
 575    }
 576
 577    // Expands a selection using self motion for an operator
 578    pub fn expand_selection(
 579        &self,
 580        map: &DisplaySnapshot,
 581        selection: &mut Selection<DisplayPoint>,
 582        times: Option<usize>,
 583        expand_to_surrounding_newline: bool,
 584        text_layout_details: &TextLayoutDetails,
 585    ) -> bool {
 586        if let Some((new_head, goal)) = self.move_point(
 587            map,
 588            selection.head(),
 589            selection.goal,
 590            times,
 591            &text_layout_details,
 592        ) {
 593            selection.set_head(new_head, goal);
 594
 595            if self.linewise() {
 596                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 597
 598                if expand_to_surrounding_newline {
 599                    if selection.end.row() < map.max_point().row() {
 600                        *selection.end.row_mut() += 1;
 601                        *selection.end.column_mut() = 0;
 602                        selection.end = map.clip_point(selection.end, Bias::Right);
 603                        // Don't reset the end here
 604                        return true;
 605                    } else if selection.start.row() > 0 {
 606                        *selection.start.row_mut() -= 1;
 607                        *selection.start.column_mut() = map.line_len(selection.start.row());
 608                        selection.start = map.clip_point(selection.start, Bias::Left);
 609                    }
 610                }
 611
 612                (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
 613            } else {
 614                // Another special case: When using the "w" motion in combination with an
 615                // operator and the last word moved over is at the end of a line, the end of
 616                // that word becomes the end of the operated text, not the first word in the
 617                // next line.
 618                if let Motion::NextWordStart {
 619                    ignore_punctuation: _,
 620                } = self
 621                {
 622                    let start_row = selection.start.to_point(&map).row;
 623                    if selection.end.to_point(&map).row > start_row {
 624                        selection.end =
 625                            Point::new(start_row, map.buffer_snapshot.line_len(start_row))
 626                                .to_display_point(&map)
 627                    }
 628                }
 629
 630                // If the motion is exclusive and the end of the motion is in column 1, the
 631                // end of the motion is moved to the end of the previous line and the motion
 632                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
 633                // but "d}" will not include that line.
 634                let mut inclusive = self.inclusive();
 635                if !inclusive
 636                    && self != &Motion::Backspace
 637                    && selection.end.row() > selection.start.row()
 638                    && selection.end.column() == 0
 639                {
 640                    inclusive = true;
 641                    *selection.end.row_mut() -= 1;
 642                    *selection.end.column_mut() = 0;
 643                    selection.end = map.clip_point(
 644                        map.next_line_boundary(selection.end.to_point(map)).1,
 645                        Bias::Left,
 646                    );
 647                }
 648
 649                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 650                    *selection.end.column_mut() += 1;
 651                }
 652            }
 653            true
 654        } else {
 655            false
 656        }
 657    }
 658}
 659
 660fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 661    for _ in 0..times {
 662        point = movement::saturating_left(map, point);
 663        if point.column() == 0 {
 664            break;
 665        }
 666    }
 667    point
 668}
 669
 670fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 671    for _ in 0..times {
 672        point = movement::left(map, point);
 673        if point.is_zero() {
 674            break;
 675        }
 676    }
 677    point
 678}
 679
 680fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 681    for _ in 0..times {
 682        point = wrapping_right(map, point);
 683        if point == map.max_point() {
 684            break;
 685        }
 686    }
 687    point
 688}
 689
 690fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 691    let max_column = map.line_len(point.row()).saturating_sub(1);
 692    if point.column() < max_column {
 693        *point.column_mut() += 1;
 694    } else if point.row() < map.max_point().row() {
 695        *point.row_mut() += 1;
 696        *point.column_mut() = 0;
 697    }
 698    point
 699}
 700
 701pub(crate) fn start_of_relative_buffer_row(
 702    map: &DisplaySnapshot,
 703    point: DisplayPoint,
 704    times: isize,
 705) -> DisplayPoint {
 706    let start = map.display_point_to_fold_point(point, Bias::Left);
 707    let target = start.row() as isize + times;
 708    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 709
 710    map.clip_point(
 711        map.fold_point_to_display_point(
 712            map.fold_snapshot
 713                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 714        ),
 715        Bias::Right,
 716    )
 717}
 718
 719fn up_down_buffer_rows(
 720    map: &DisplaySnapshot,
 721    point: DisplayPoint,
 722    mut goal: SelectionGoal,
 723    times: isize,
 724    text_layout_details: &TextLayoutDetails,
 725) -> (DisplayPoint, SelectionGoal) {
 726    let start = map.display_point_to_fold_point(point, Bias::Left);
 727    let begin_folded_line = map.fold_point_to_display_point(
 728        map.fold_snapshot
 729            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
 730    );
 731    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 732
 733    let (goal_wrap, goal_x) = match goal {
 734        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
 735        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 736        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 737        _ => {
 738            let x = map.x_for_display_point(point, text_layout_details);
 739            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
 740            (select_nth_wrapped_row, x.0)
 741        }
 742    };
 743
 744    let target = start.row() as isize + times;
 745    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 746
 747    let mut begin_folded_line = map.fold_point_to_display_point(
 748        map.fold_snapshot
 749            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
 750    );
 751
 752    let mut i = 0;
 753    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
 754        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
 755        if map
 756            .display_point_to_fold_point(next_folded_line, Bias::Right)
 757            .row()
 758            == new_row
 759        {
 760            i += 1;
 761            begin_folded_line = next_folded_line;
 762        } else {
 763            break;
 764        }
 765    }
 766
 767    let new_col = if i == goal_wrap {
 768        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
 769    } else {
 770        map.line_len(begin_folded_line.row())
 771    };
 772
 773    (
 774        map.clip_point(
 775            DisplayPoint::new(begin_folded_line.row(), new_col),
 776            Bias::Left,
 777        ),
 778        goal,
 779    )
 780}
 781
 782fn down_display(
 783    map: &DisplaySnapshot,
 784    mut point: DisplayPoint,
 785    mut goal: SelectionGoal,
 786    times: usize,
 787    text_layout_details: &TextLayoutDetails,
 788) -> (DisplayPoint, SelectionGoal) {
 789    for _ in 0..times {
 790        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
 791    }
 792
 793    (point, goal)
 794}
 795
 796fn up_display(
 797    map: &DisplaySnapshot,
 798    mut point: DisplayPoint,
 799    mut goal: SelectionGoal,
 800    times: usize,
 801    text_layout_details: &TextLayoutDetails,
 802) -> (DisplayPoint, SelectionGoal) {
 803    for _ in 0..times {
 804        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
 805    }
 806
 807    (point, goal)
 808}
 809
 810pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 811    for _ in 0..times {
 812        let new_point = movement::saturating_right(map, point);
 813        if point == new_point {
 814            break;
 815        }
 816        point = new_point;
 817    }
 818    point
 819}
 820
 821pub(crate) fn next_word_start(
 822    map: &DisplaySnapshot,
 823    mut point: DisplayPoint,
 824    ignore_punctuation: bool,
 825    times: usize,
 826) -> DisplayPoint {
 827    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 828    for _ in 0..times {
 829        let mut crossed_newline = false;
 830        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 831            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 832            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 833            let at_newline = right == '\n';
 834
 835            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
 836                || at_newline && crossed_newline
 837                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 838
 839            crossed_newline |= at_newline;
 840            found
 841        });
 842        if point == new_point {
 843            break;
 844        }
 845        point = new_point;
 846    }
 847    point
 848}
 849
 850fn next_word_end(
 851    map: &DisplaySnapshot,
 852    mut point: DisplayPoint,
 853    ignore_punctuation: bool,
 854    times: usize,
 855) -> DisplayPoint {
 856    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 857    for _ in 0..times {
 858        let mut new_point = point;
 859        if new_point.column() < map.line_len(new_point.row()) {
 860            *new_point.column_mut() += 1;
 861        } else if new_point < map.max_point() {
 862            *new_point.row_mut() += 1;
 863            *new_point.column_mut() = 0;
 864        }
 865
 866        let new_point = movement::find_boundary_exclusive(
 867            map,
 868            new_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 && left_kind != CharKind::Whitespace
 875            },
 876        );
 877        let new_point = map.clip_point(new_point, Bias::Left);
 878        if point == new_point {
 879            break;
 880        }
 881        point = new_point;
 882    }
 883    point
 884}
 885
 886fn previous_word_start(
 887    map: &DisplaySnapshot,
 888    mut point: DisplayPoint,
 889    ignore_punctuation: bool,
 890    times: usize,
 891) -> DisplayPoint {
 892    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 893    for _ in 0..times {
 894        // This works even though find_preceding_boundary is called for every character in the line containing
 895        // cursor because the newline is checked only once.
 896        let new_point = movement::find_preceding_boundary_display_point(
 897            map,
 898            point,
 899            FindRange::MultiLine,
 900            |left, right| {
 901                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 902                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 903
 904                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
 905            },
 906        );
 907        if point == new_point {
 908            break;
 909        }
 910        point = new_point;
 911    }
 912    point
 913}
 914
 915pub(crate) fn first_non_whitespace(
 916    map: &DisplaySnapshot,
 917    display_lines: bool,
 918    from: DisplayPoint,
 919) -> DisplayPoint {
 920    let mut last_point = start_of_line(map, display_lines, from);
 921    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
 922    for (ch, point) in map.chars_at(last_point) {
 923        if ch == '\n' {
 924            return from;
 925        }
 926
 927        last_point = point;
 928
 929        if char_kind(&scope, ch) != CharKind::Whitespace {
 930            break;
 931        }
 932    }
 933
 934    map.clip_point(last_point, Bias::Left)
 935}
 936
 937pub(crate) fn start_of_line(
 938    map: &DisplaySnapshot,
 939    display_lines: bool,
 940    point: DisplayPoint,
 941) -> DisplayPoint {
 942    if display_lines {
 943        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
 944    } else {
 945        map.prev_line_boundary(point.to_point(map)).1
 946    }
 947}
 948
 949pub(crate) fn end_of_line(
 950    map: &DisplaySnapshot,
 951    display_lines: bool,
 952    point: DisplayPoint,
 953) -> DisplayPoint {
 954    if display_lines {
 955        map.clip_point(
 956            DisplayPoint::new(point.row(), map.line_len(point.row())),
 957            Bias::Left,
 958        )
 959    } else {
 960        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
 961    }
 962}
 963
 964fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
 965    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
 966    *new_point.column_mut() = point.column();
 967    map.clip_point(new_point, Bias::Left)
 968}
 969
 970fn end_of_document(
 971    map: &DisplaySnapshot,
 972    point: DisplayPoint,
 973    line: Option<usize>,
 974) -> DisplayPoint {
 975    let new_row = if let Some(line) = line {
 976        (line - 1) as u32
 977    } else {
 978        map.max_buffer_row()
 979    };
 980
 981    let new_point = Point::new(new_row, point.column());
 982    map.clip_point(new_point.to_display_point(map), Bias::Left)
 983}
 984
 985fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 986    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
 987    let point = display_point.to_point(map);
 988    let offset = point.to_offset(&map.buffer_snapshot);
 989
 990    // Ensure the range is contained by the current line.
 991    let mut line_end = map.next_line_boundary(point).0;
 992    if line_end == point {
 993        line_end = map.max_point().to_point(map);
 994    }
 995
 996    let line_range = map.prev_line_boundary(point).0..line_end;
 997    let visible_line_range =
 998        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
 999    let ranges = map
1000        .buffer_snapshot
1001        .bracket_ranges(visible_line_range.clone());
1002    if let Some(ranges) = ranges {
1003        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1004            ..line_range.end.to_offset(&map.buffer_snapshot);
1005        let mut closest_pair_destination = None;
1006        let mut closest_distance = usize::MAX;
1007
1008        for (open_range, close_range) in ranges {
1009            if open_range.start >= offset && line_range.contains(&open_range.start) {
1010                let distance = open_range.start - offset;
1011                if distance < closest_distance {
1012                    closest_pair_destination = Some(close_range.start);
1013                    closest_distance = distance;
1014                    continue;
1015                }
1016            }
1017
1018            if close_range.start >= offset && line_range.contains(&close_range.start) {
1019                let distance = close_range.start - offset;
1020                if distance < closest_distance {
1021                    closest_pair_destination = Some(open_range.start);
1022                    closest_distance = distance;
1023                    continue;
1024                }
1025            }
1026
1027            continue;
1028        }
1029
1030        closest_pair_destination
1031            .map(|destination| destination.to_display_point(map))
1032            .unwrap_or(display_point)
1033    } else {
1034        display_point
1035    }
1036}
1037
1038fn find_forward(
1039    map: &DisplaySnapshot,
1040    from: DisplayPoint,
1041    before: bool,
1042    target: char,
1043    times: usize,
1044    mode: FindRange,
1045) -> Option<DisplayPoint> {
1046    let mut to = from;
1047    let mut found = false;
1048
1049    for _ in 0..times {
1050        found = false;
1051        let new_to = find_boundary(map, to, mode, |_, right| {
1052            found = right == target;
1053            found
1054        });
1055        if to == new_to {
1056            break;
1057        }
1058        to = new_to;
1059    }
1060
1061    if found {
1062        if before && to.column() > 0 {
1063            *to.column_mut() -= 1;
1064            Some(map.clip_point(to, Bias::Left))
1065        } else {
1066            Some(to)
1067        }
1068    } else {
1069        None
1070    }
1071}
1072
1073fn find_backward(
1074    map: &DisplaySnapshot,
1075    from: DisplayPoint,
1076    after: bool,
1077    target: char,
1078    times: usize,
1079    mode: FindRange,
1080) -> DisplayPoint {
1081    let mut to = from;
1082
1083    for _ in 0..times {
1084        let new_to =
1085            find_preceding_boundary_display_point(map, to, mode, |_, right| right == target);
1086        if to == new_to {
1087            break;
1088        }
1089        to = new_to;
1090    }
1091
1092    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
1093        if after {
1094            *to.column_mut() += 1;
1095            map.clip_point(to, Bias::Right)
1096        } else {
1097            to
1098        }
1099    } else {
1100        from
1101    }
1102}
1103
1104fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1105    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1106    first_non_whitespace(map, false, correct_line)
1107}
1108
1109fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1110    let correct_line = start_of_relative_buffer_row(map, point, 0);
1111    right(map, correct_line, times.saturating_sub(1))
1112}
1113
1114pub(crate) fn next_line_end(
1115    map: &DisplaySnapshot,
1116    mut point: DisplayPoint,
1117    times: usize,
1118) -> DisplayPoint {
1119    if times > 1 {
1120        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1121    }
1122    end_of_line(map, false, point)
1123}
1124
1125fn window_top(
1126    map: &DisplaySnapshot,
1127    point: DisplayPoint,
1128    text_layout_details: &TextLayoutDetails,
1129    mut times: usize,
1130) -> (DisplayPoint, SelectionGoal) {
1131    let first_visible_line = text_layout_details
1132        .scroll_anchor
1133        .anchor
1134        .to_display_point(map);
1135
1136    if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1137    {
1138        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1139    }
1140
1141    if let Some(visible_rows) = text_layout_details.visible_rows {
1142        let bottom_row = first_visible_line.row() + visible_rows as u32;
1143        let new_row = (first_visible_line.row() + (times as u32))
1144            .min(bottom_row)
1145            .min(map.max_point().row());
1146        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1147
1148        let new_point = DisplayPoint::new(new_row, new_col);
1149        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1150    } else {
1151        let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1152        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1153
1154        let new_point = DisplayPoint::new(new_row, new_col);
1155        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1156    }
1157}
1158
1159fn window_middle(
1160    map: &DisplaySnapshot,
1161    point: DisplayPoint,
1162    text_layout_details: &TextLayoutDetails,
1163) -> (DisplayPoint, SelectionGoal) {
1164    if let Some(visible_rows) = text_layout_details.visible_rows {
1165        let first_visible_line = text_layout_details
1166            .scroll_anchor
1167            .anchor
1168            .to_display_point(map);
1169
1170        let max_visible_rows =
1171            (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1172
1173        let new_row =
1174            (first_visible_line.row() + (max_visible_rows / 2) as u32).min(map.max_point().row());
1175        let new_col = point.column().min(map.line_len(new_row));
1176        let new_point = DisplayPoint::new(new_row, new_col);
1177        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1178    } else {
1179        (point, SelectionGoal::None)
1180    }
1181}
1182
1183fn window_bottom(
1184    map: &DisplaySnapshot,
1185    point: DisplayPoint,
1186    text_layout_details: &TextLayoutDetails,
1187    mut times: usize,
1188) -> (DisplayPoint, SelectionGoal) {
1189    if let Some(visible_rows) = text_layout_details.visible_rows {
1190        let first_visible_line = text_layout_details
1191            .scroll_anchor
1192            .anchor
1193            .to_display_point(map);
1194        let bottom_row = first_visible_line.row()
1195            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1196        if bottom_row < map.max_point().row()
1197            && text_layout_details.vertical_scroll_margin as usize > times
1198        {
1199            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1200        }
1201        let bottom_row_capped = bottom_row.min(map.max_point().row());
1202        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1203            first_visible_line.row()
1204        } else {
1205            bottom_row_capped.saturating_sub(times as u32)
1206        };
1207        let new_col = point.column().min(map.line_len(new_row));
1208        let new_point = DisplayPoint::new(new_row, new_col);
1209        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1210    } else {
1211        (point, SelectionGoal::None)
1212    }
1213}
1214
1215fn previous_word_end(
1216    map: &DisplaySnapshot,
1217    point: DisplayPoint,
1218    ignore_punctuation: bool,
1219    times: usize,
1220) -> DisplayPoint {
1221    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1222    let mut point = point.to_point(map);
1223
1224    if point.column < map.buffer_snapshot.line_len(point.row) {
1225        point.column += 1;
1226    }
1227    for _ in 0..times {
1228        let new_point = movement::find_preceding_boundary_point(
1229            &map.buffer_snapshot,
1230            point,
1231            FindRange::MultiLine,
1232            |left, right| {
1233                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1234                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1235                match (left_kind, right_kind) {
1236                    (CharKind::Punctuation, CharKind::Whitespace)
1237                    | (CharKind::Punctuation, CharKind::Word)
1238                    | (CharKind::Word, CharKind::Whitespace)
1239                    | (CharKind::Word, CharKind::Punctuation) => true,
1240                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1241                    _ => false,
1242                }
1243            },
1244        );
1245        if new_point == point {
1246            break;
1247        }
1248        point = new_point;
1249    }
1250    movement::saturating_left(map, point.to_display_point(map))
1251}
1252
1253#[cfg(test)]
1254mod test {
1255
1256    use crate::test::NeovimBackedTestContext;
1257    use indoc::indoc;
1258
1259    #[gpui::test]
1260    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1261        let mut cx = NeovimBackedTestContext::new(cx).await;
1262
1263        let initial_state = indoc! {r"ˇabc
1264            def
1265
1266            paragraph
1267            the second
1268
1269
1270
1271            third and
1272            final"};
1273
1274        // goes down once
1275        cx.set_shared_state(initial_state).await;
1276        cx.simulate_shared_keystrokes(["}"]).await;
1277        cx.assert_shared_state(indoc! {r"abc
1278            def
1279            ˇ
1280            paragraph
1281            the second
1282
1283
1284
1285            third and
1286            final"})
1287            .await;
1288
1289        // goes up once
1290        cx.simulate_shared_keystrokes(["{"]).await;
1291        cx.assert_shared_state(initial_state).await;
1292
1293        // goes down twice
1294        cx.simulate_shared_keystrokes(["2", "}"]).await;
1295        cx.assert_shared_state(indoc! {r"abc
1296            def
1297
1298            paragraph
1299            the second
1300            ˇ
1301
1302
1303            third and
1304            final"})
1305            .await;
1306
1307        // goes down over multiple blanks
1308        cx.simulate_shared_keystrokes(["}"]).await;
1309        cx.assert_shared_state(indoc! {r"abc
1310                def
1311
1312                paragraph
1313                the second
1314
1315
1316
1317                third and
1318                finaˇl"})
1319            .await;
1320
1321        // goes up twice
1322        cx.simulate_shared_keystrokes(["2", "{"]).await;
1323        cx.assert_shared_state(indoc! {r"abc
1324                def
1325                ˇ
1326                paragraph
1327                the second
1328
1329
1330
1331                third and
1332                final"})
1333            .await
1334    }
1335
1336    #[gpui::test]
1337    async fn test_matching(cx: &mut gpui::TestAppContext) {
1338        let mut cx = NeovimBackedTestContext::new(cx).await;
1339
1340        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1341                do(something(with<Types>.and_arrays[0, 2]))
1342            }"})
1343            .await;
1344        cx.simulate_shared_keystrokes(["%"]).await;
1345        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1346                do(something(with<Types>.and_arrays[0, 2]))
1347            }"})
1348            .await;
1349
1350        // test it works on the last character of the line
1351        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1352            do(something(with<Types>.and_arrays[0, 2]))
1353            }"})
1354            .await;
1355        cx.simulate_shared_keystrokes(["%"]).await;
1356        cx.assert_shared_state(indoc! {r"func (a string) {
1357            do(something(with<Types>.and_arrays[0, 2]))
1358            ˇ}"})
1359            .await;
1360
1361        // test it works on immediate nesting
1362        cx.set_shared_state("ˇ{()}").await;
1363        cx.simulate_shared_keystrokes(["%"]).await;
1364        cx.assert_shared_state("{()ˇ}").await;
1365        cx.simulate_shared_keystrokes(["%"]).await;
1366        cx.assert_shared_state("ˇ{()}").await;
1367
1368        // test it works on immediate nesting inside braces
1369        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1370        cx.simulate_shared_keystrokes(["%"]).await;
1371        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1372
1373        // test it jumps to the next paren on a line
1374        cx.set_shared_state("func ˇboop() {\n}").await;
1375        cx.simulate_shared_keystrokes(["%"]).await;
1376        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1377    }
1378
1379    #[gpui::test]
1380    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1381        let mut cx = NeovimBackedTestContext::new(cx).await;
1382
1383        // f and F
1384        cx.set_shared_state("ˇone two three four").await;
1385        cx.simulate_shared_keystrokes(["f", "o"]).await;
1386        cx.assert_shared_state("one twˇo three four").await;
1387        cx.simulate_shared_keystrokes([","]).await;
1388        cx.assert_shared_state("ˇone two three four").await;
1389        cx.simulate_shared_keystrokes(["2", ";"]).await;
1390        cx.assert_shared_state("one two three fˇour").await;
1391        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1392        cx.assert_shared_state("one two threˇe four").await;
1393        cx.simulate_shared_keystrokes(["2", ";"]).await;
1394        cx.assert_shared_state("onˇe two three four").await;
1395        cx.simulate_shared_keystrokes([","]).await;
1396        cx.assert_shared_state("one two thrˇee four").await;
1397
1398        // t and T
1399        cx.set_shared_state("ˇone two three four").await;
1400        cx.simulate_shared_keystrokes(["t", "o"]).await;
1401        cx.assert_shared_state("one tˇwo three four").await;
1402        cx.simulate_shared_keystrokes([","]).await;
1403        cx.assert_shared_state("oˇne two three four").await;
1404        cx.simulate_shared_keystrokes(["2", ";"]).await;
1405        cx.assert_shared_state("one two three ˇfour").await;
1406        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1407        cx.assert_shared_state("one two threeˇ four").await;
1408        cx.simulate_shared_keystrokes(["3", ";"]).await;
1409        cx.assert_shared_state("oneˇ two three four").await;
1410        cx.simulate_shared_keystrokes([","]).await;
1411        cx.assert_shared_state("one two thˇree four").await;
1412    }
1413
1414    #[gpui::test]
1415    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1416        let mut cx = NeovimBackedTestContext::new(cx).await;
1417        let initial_state = indoc! {r"something(ˇfoo)"};
1418        cx.set_shared_state(initial_state).await;
1419        cx.simulate_shared_keystrokes(["}"]).await;
1420        cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1421    }
1422
1423    #[gpui::test]
1424    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1425        let mut cx = NeovimBackedTestContext::new(cx).await;
1426        cx.set_shared_state("ˇone\n  two\nthree").await;
1427        cx.simulate_shared_keystrokes(["enter"]).await;
1428        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1429    }
1430
1431    #[gpui::test]
1432    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1433        let mut cx = NeovimBackedTestContext::new(cx).await;
1434        let initial_state = indoc! {r"abc
1435          def
1436          paragraph
1437          the second
1438          third ˇand
1439          final"};
1440
1441        cx.set_shared_state(initial_state).await;
1442        cx.simulate_shared_keystrokes(["shift-h"]).await;
1443        cx.assert_shared_state(indoc! {r"abˇc
1444          def
1445          paragraph
1446          the second
1447          third and
1448          final"})
1449            .await;
1450
1451        // clip point
1452        cx.set_shared_state(indoc! {r"
1453          1 2 3
1454          4 5 6
1455          7 8 ˇ9
1456          "})
1457            .await;
1458        cx.simulate_shared_keystrokes(["shift-h"]).await;
1459        cx.assert_shared_state(indoc! {r"
1460          1 2 ˇ3
1461          4 5 6
1462          7 8 9
1463          "})
1464            .await;
1465
1466        cx.set_shared_state(indoc! {r"
1467          1 2 3
1468          4 5 6
1469          ˇ7 8 9
1470          "})
1471            .await;
1472        cx.simulate_shared_keystrokes(["shift-h"]).await;
1473        cx.assert_shared_state(indoc! {r"
1474          ˇ1 2 3
1475          4 5 6
1476          7 8 9
1477          "})
1478            .await;
1479
1480        cx.set_shared_state(indoc! {r"
1481          1 2 3
1482          4 5 ˇ6
1483          7 8 9"})
1484            .await;
1485        cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1486        cx.assert_shared_state(indoc! {r"
1487          1 2 3
1488          4 5 6
1489          7 8 ˇ9"})
1490            .await;
1491    }
1492
1493    #[gpui::test]
1494    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1495        let mut cx = NeovimBackedTestContext::new(cx).await;
1496        let initial_state = indoc! {r"abˇc
1497          def
1498          paragraph
1499          the second
1500          third and
1501          final"};
1502
1503        cx.set_shared_state(initial_state).await;
1504        cx.simulate_shared_keystrokes(["shift-m"]).await;
1505        cx.assert_shared_state(indoc! {r"abc
1506          def
1507          paˇragraph
1508          the second
1509          third and
1510          final"})
1511            .await;
1512
1513        cx.set_shared_state(indoc! {r"
1514          1 2 3
1515          4 5 6
1516          7 8 ˇ9
1517          "})
1518            .await;
1519        cx.simulate_shared_keystrokes(["shift-m"]).await;
1520        cx.assert_shared_state(indoc! {r"
1521          1 2 3
1522          4 5 ˇ6
1523          7 8 9
1524          "})
1525            .await;
1526        cx.set_shared_state(indoc! {r"
1527          1 2 3
1528          4 5 6
1529          ˇ7 8 9
1530          "})
1531            .await;
1532        cx.simulate_shared_keystrokes(["shift-m"]).await;
1533        cx.assert_shared_state(indoc! {r"
1534          1 2 3
1535          ˇ4 5 6
1536          7 8 9
1537          "})
1538            .await;
1539        cx.set_shared_state(indoc! {r"
1540          ˇ1 2 3
1541          4 5 6
1542          7 8 9
1543          "})
1544            .await;
1545        cx.simulate_shared_keystrokes(["shift-m"]).await;
1546        cx.assert_shared_state(indoc! {r"
1547          1 2 3
1548          ˇ4 5 6
1549          7 8 9
1550          "})
1551            .await;
1552        cx.set_shared_state(indoc! {r"
1553          1 2 3
1554          ˇ4 5 6
1555          7 8 9
1556          "})
1557            .await;
1558        cx.simulate_shared_keystrokes(["shift-m"]).await;
1559        cx.assert_shared_state(indoc! {r"
1560          1 2 3
1561          ˇ4 5 6
1562          7 8 9
1563          "})
1564            .await;
1565        cx.set_shared_state(indoc! {r"
1566          1 2 3
1567          4 5 ˇ6
1568          7 8 9
1569          "})
1570            .await;
1571        cx.simulate_shared_keystrokes(["shift-m"]).await;
1572        cx.assert_shared_state(indoc! {r"
1573          1 2 3
1574          4 5 ˇ6
1575          7 8 9
1576          "})
1577            .await;
1578    }
1579
1580    #[gpui::test]
1581    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1582        let mut cx = NeovimBackedTestContext::new(cx).await;
1583        let initial_state = indoc! {r"abc
1584          deˇf
1585          paragraph
1586          the second
1587          third and
1588          final"};
1589
1590        cx.set_shared_state(initial_state).await;
1591        cx.simulate_shared_keystrokes(["shift-l"]).await;
1592        cx.assert_shared_state(indoc! {r"abc
1593          def
1594          paragraph
1595          the second
1596          third and
1597          fiˇnal"})
1598            .await;
1599
1600        cx.set_shared_state(indoc! {r"
1601          1 2 3
1602          4 5 ˇ6
1603          7 8 9
1604          "})
1605            .await;
1606        cx.simulate_shared_keystrokes(["shift-l"]).await;
1607        cx.assert_shared_state(indoc! {r"
1608          1 2 3
1609          4 5 6
1610          7 8 9
1611          ˇ"})
1612            .await;
1613
1614        cx.set_shared_state(indoc! {r"
1615          1 2 3
1616          ˇ4 5 6
1617          7 8 9
1618          "})
1619            .await;
1620        cx.simulate_shared_keystrokes(["shift-l"]).await;
1621        cx.assert_shared_state(indoc! {r"
1622          1 2 3
1623          4 5 6
1624          7 8 9
1625          ˇ"})
1626            .await;
1627
1628        cx.set_shared_state(indoc! {r"
1629          1 2 ˇ3
1630          4 5 6
1631          7 8 9
1632          "})
1633            .await;
1634        cx.simulate_shared_keystrokes(["shift-l"]).await;
1635        cx.assert_shared_state(indoc! {r"
1636          1 2 3
1637          4 5 6
1638          7 8 9
1639          ˇ"})
1640            .await;
1641
1642        cx.set_shared_state(indoc! {r"
1643          ˇ1 2 3
1644          4 5 6
1645          7 8 9
1646          "})
1647            .await;
1648        cx.simulate_shared_keystrokes(["shift-l"]).await;
1649        cx.assert_shared_state(indoc! {r"
1650          1 2 3
1651          4 5 6
1652          7 8 9
1653          ˇ"})
1654            .await;
1655
1656        cx.set_shared_state(indoc! {r"
1657          1 2 3
1658          4 5 ˇ6
1659          7 8 9
1660          "})
1661            .await;
1662        cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
1663        cx.assert_shared_state(indoc! {r"
1664          1 2 ˇ3
1665          4 5 6
1666          7 8 9
1667          "})
1668            .await;
1669    }
1670
1671    #[gpui::test]
1672    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
1673        let mut cx = NeovimBackedTestContext::new(cx).await;
1674        cx.set_shared_state(indoc! {r"
1675        456 5ˇ67 678
1676        "})
1677            .await;
1678        cx.simulate_shared_keystrokes(["g", "e"]).await;
1679        cx.assert_shared_state(indoc! {r"
1680        45ˇ6 567 678
1681        "})
1682            .await;
1683
1684        // Test times
1685        cx.set_shared_state(indoc! {r"
1686        123 234 345
1687        456 5ˇ67 678
1688        "})
1689            .await;
1690        cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
1691        cx.assert_shared_state(indoc! {r"
1692        12ˇ3 234 345
1693        456 567 678
1694        "})
1695            .await;
1696
1697        // With punctuation
1698        cx.set_shared_state(indoc! {r"
1699        123 234 345
1700        4;5.6 5ˇ67 678
1701        789 890 901
1702        "})
1703            .await;
1704        cx.simulate_shared_keystrokes(["g", "e"]).await;
1705        cx.assert_shared_state(indoc! {r"
1706          123 234 345
1707          4;5.ˇ6 567 678
1708          789 890 901
1709        "})
1710            .await;
1711
1712        // With punctuation and count
1713        cx.set_shared_state(indoc! {r"
1714        123 234 345
1715        4;5.6 5ˇ67 678
1716        789 890 901
1717        "})
1718            .await;
1719        cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
1720        cx.assert_shared_state(indoc! {r"
1721          123 234 345
1722          ˇ4;5.6 567 678
1723          789 890 901
1724        "})
1725            .await;
1726
1727        // newlines
1728        cx.set_shared_state(indoc! {r"
1729        123 234 345
1730
1731        78ˇ9 890 901
1732        "})
1733            .await;
1734        cx.simulate_shared_keystrokes(["g", "e"]).await;
1735        cx.assert_shared_state(indoc! {r"
1736          123 234 345
1737          ˇ
1738          789 890 901
1739        "})
1740            .await;
1741        cx.simulate_shared_keystrokes(["g", "e"]).await;
1742        cx.assert_shared_state(indoc! {r"
1743          123 234 34ˇ5
1744
1745          789 890 901
1746        "})
1747            .await;
1748
1749        // With punctuation
1750        cx.set_shared_state(indoc! {r"
1751        123 234 345
1752        4;5.ˇ6 567 678
1753        789 890 901
1754        "})
1755            .await;
1756        cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
1757        cx.assert_shared_state(indoc! {r"
1758          123 234 34ˇ5
1759          4;5.6 567 678
1760          789 890 901
1761        "})
1762            .await;
1763    }
1764}