motion.rs

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