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        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 802            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 803            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 804
 805            left_kind != right_kind && left_kind != CharKind::Whitespace
 806        });
 807
 808        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
 809        // we have backtracked already
 810        if !map
 811            .chars_at(point)
 812            .nth(1)
 813            .map(|(c, _)| c == '\n')
 814            .unwrap_or(true)
 815        {
 816            *point.column_mut() = point.column().saturating_sub(1);
 817        }
 818        point = map.clip_point(point, Bias::Left);
 819    }
 820    point
 821}
 822
 823fn previous_word_start(
 824    map: &DisplaySnapshot,
 825    mut point: DisplayPoint,
 826    ignore_punctuation: bool,
 827    times: usize,
 828) -> DisplayPoint {
 829    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 830    for _ in 0..times {
 831        // This works even though find_preceding_boundary is called for every character in the line containing
 832        // cursor because the newline is checked only once.
 833        point =
 834            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
 835                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 836                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 837
 838                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
 839            });
 840    }
 841    point
 842}
 843
 844pub(crate) fn first_non_whitespace(
 845    map: &DisplaySnapshot,
 846    display_lines: bool,
 847    from: DisplayPoint,
 848) -> DisplayPoint {
 849    let mut last_point = start_of_line(map, display_lines, from);
 850    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
 851    for (ch, point) in map.chars_at(last_point) {
 852        if ch == '\n' {
 853            return from;
 854        }
 855
 856        last_point = point;
 857
 858        if char_kind(&scope, ch) != CharKind::Whitespace {
 859            break;
 860        }
 861    }
 862
 863    map.clip_point(last_point, Bias::Left)
 864}
 865
 866pub(crate) fn start_of_line(
 867    map: &DisplaySnapshot,
 868    display_lines: bool,
 869    point: DisplayPoint,
 870) -> DisplayPoint {
 871    if display_lines {
 872        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
 873    } else {
 874        map.prev_line_boundary(point.to_point(map)).1
 875    }
 876}
 877
 878pub(crate) fn end_of_line(
 879    map: &DisplaySnapshot,
 880    display_lines: bool,
 881    point: DisplayPoint,
 882) -> DisplayPoint {
 883    if display_lines {
 884        map.clip_point(
 885            DisplayPoint::new(point.row(), map.line_len(point.row())),
 886            Bias::Left,
 887        )
 888    } else {
 889        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
 890    }
 891}
 892
 893fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
 894    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
 895    *new_point.column_mut() = point.column();
 896    map.clip_point(new_point, Bias::Left)
 897}
 898
 899fn end_of_document(
 900    map: &DisplaySnapshot,
 901    point: DisplayPoint,
 902    line: Option<usize>,
 903) -> DisplayPoint {
 904    let new_row = if let Some(line) = line {
 905        (line - 1) as u32
 906    } else {
 907        map.max_buffer_row()
 908    };
 909
 910    let new_point = Point::new(new_row, point.column());
 911    map.clip_point(new_point.to_display_point(map), Bias::Left)
 912}
 913
 914fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 915    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
 916    let point = display_point.to_point(map);
 917    let offset = point.to_offset(&map.buffer_snapshot);
 918
 919    // Ensure the range is contained by the current line.
 920    let mut line_end = map.next_line_boundary(point).0;
 921    if line_end == point {
 922        line_end = map.max_point().to_point(map);
 923    }
 924
 925    let line_range = map.prev_line_boundary(point).0..line_end;
 926    let visible_line_range =
 927        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
 928    let ranges = map
 929        .buffer_snapshot
 930        .bracket_ranges(visible_line_range.clone());
 931    if let Some(ranges) = ranges {
 932        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
 933            ..line_range.end.to_offset(&map.buffer_snapshot);
 934        let mut closest_pair_destination = None;
 935        let mut closest_distance = usize::MAX;
 936
 937        for (open_range, close_range) in ranges {
 938            if open_range.start >= offset && line_range.contains(&open_range.start) {
 939                let distance = open_range.start - offset;
 940                if distance < closest_distance {
 941                    closest_pair_destination = Some(close_range.start);
 942                    closest_distance = distance;
 943                    continue;
 944                }
 945            }
 946
 947            if close_range.start >= offset && line_range.contains(&close_range.start) {
 948                let distance = close_range.start - offset;
 949                if distance < closest_distance {
 950                    closest_pair_destination = Some(open_range.start);
 951                    closest_distance = distance;
 952                    continue;
 953                }
 954            }
 955
 956            continue;
 957        }
 958
 959        closest_pair_destination
 960            .map(|destination| destination.to_display_point(map))
 961            .unwrap_or(display_point)
 962    } else {
 963        display_point
 964    }
 965}
 966
 967fn find_forward(
 968    map: &DisplaySnapshot,
 969    from: DisplayPoint,
 970    before: bool,
 971    target: char,
 972    times: usize,
 973) -> Option<DisplayPoint> {
 974    let mut to = from;
 975    let mut found = false;
 976
 977    for _ in 0..times {
 978        found = false;
 979        to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
 980            found = right == target;
 981            found
 982        });
 983    }
 984
 985    if found {
 986        if before && to.column() > 0 {
 987            *to.column_mut() -= 1;
 988            Some(map.clip_point(to, Bias::Left))
 989        } else {
 990            Some(to)
 991        }
 992    } else {
 993        None
 994    }
 995}
 996
 997fn find_backward(
 998    map: &DisplaySnapshot,
 999    from: DisplayPoint,
1000    after: bool,
1001    target: char,
1002    times: usize,
1003) -> DisplayPoint {
1004    let mut to = from;
1005
1006    for _ in 0..times {
1007        to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
1008    }
1009
1010    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
1011        if after {
1012            *to.column_mut() += 1;
1013            map.clip_point(to, Bias::Right)
1014        } else {
1015            to
1016        }
1017    } else {
1018        from
1019    }
1020}
1021
1022fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1023    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1024    first_non_whitespace(map, false, correct_line)
1025}
1026
1027fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1028    let correct_line = start_of_relative_buffer_row(map, point, 0);
1029    right(map, correct_line, times.saturating_sub(1))
1030}
1031
1032pub(crate) fn next_line_end(
1033    map: &DisplaySnapshot,
1034    mut point: DisplayPoint,
1035    times: usize,
1036) -> DisplayPoint {
1037    if times > 1 {
1038        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1039    }
1040    end_of_line(map, false, point)
1041}
1042
1043fn window_top(
1044    map: &DisplaySnapshot,
1045    point: DisplayPoint,
1046    text_layout_details: &TextLayoutDetails,
1047    mut times: usize,
1048) -> (DisplayPoint, SelectionGoal) {
1049    let first_visible_line = text_layout_details
1050        .scroll_anchor
1051        .anchor
1052        .to_display_point(map);
1053
1054    if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1055    {
1056        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1057    }
1058
1059    if let Some(visible_rows) = text_layout_details.visible_rows {
1060        let bottom_row = first_visible_line.row() + visible_rows as u32;
1061        let new_row = (first_visible_line.row() + (times as u32)).min(bottom_row);
1062        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1063
1064        let new_point = DisplayPoint::new(new_row, new_col);
1065        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1066    } else {
1067        let new_row = first_visible_line.row() + (times as u32);
1068        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1069
1070        let new_point = DisplayPoint::new(new_row, new_col);
1071        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1072    }
1073}
1074
1075fn window_middle(
1076    map: &DisplaySnapshot,
1077    point: DisplayPoint,
1078    text_layout_details: &TextLayoutDetails,
1079) -> (DisplayPoint, SelectionGoal) {
1080    if let Some(visible_rows) = text_layout_details.visible_rows {
1081        let first_visible_line = text_layout_details
1082            .scroll_anchor
1083            .anchor
1084            .to_display_point(map);
1085        let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1086        let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1087        let new_col = point.column().min(map.line_len(new_row));
1088        let new_point = DisplayPoint::new(new_row, new_col);
1089        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1090    } else {
1091        (point, SelectionGoal::None)
1092    }
1093}
1094
1095fn window_bottom(
1096    map: &DisplaySnapshot,
1097    point: DisplayPoint,
1098    text_layout_details: &TextLayoutDetails,
1099    mut times: usize,
1100) -> (DisplayPoint, SelectionGoal) {
1101    if let Some(visible_rows) = text_layout_details.visible_rows {
1102        let first_visible_line = text_layout_details
1103            .scroll_anchor
1104            .anchor
1105            .to_display_point(map);
1106        let bottom_row = first_visible_line.row()
1107            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1108        if bottom_row < map.max_buffer_row()
1109            && text_layout_details.vertical_scroll_margin as usize > times
1110        {
1111            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1112        }
1113        let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1114        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1115            first_visible_line.row()
1116        } else {
1117            bottom_row_capped.saturating_sub(times as u32)
1118        };
1119        let new_col = point.column().min(map.line_len(new_row));
1120        let new_point = DisplayPoint::new(new_row, new_col);
1121        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1122    } else {
1123        (point, SelectionGoal::None)
1124    }
1125}
1126
1127#[cfg(test)]
1128mod test {
1129
1130    use crate::test::NeovimBackedTestContext;
1131    use indoc::indoc;
1132
1133    #[gpui::test]
1134    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1135        let mut cx = NeovimBackedTestContext::new(cx).await;
1136
1137        let initial_state = indoc! {r"ˇabc
1138            def
1139
1140            paragraph
1141            the second
1142
1143
1144
1145            third and
1146            final"};
1147
1148        // goes down once
1149        cx.set_shared_state(initial_state).await;
1150        cx.simulate_shared_keystrokes(["}"]).await;
1151        cx.assert_shared_state(indoc! {r"abc
1152            def
1153            ˇ
1154            paragraph
1155            the second
1156
1157
1158
1159            third and
1160            final"})
1161            .await;
1162
1163        // goes up once
1164        cx.simulate_shared_keystrokes(["{"]).await;
1165        cx.assert_shared_state(initial_state).await;
1166
1167        // goes down twice
1168        cx.simulate_shared_keystrokes(["2", "}"]).await;
1169        cx.assert_shared_state(indoc! {r"abc
1170            def
1171
1172            paragraph
1173            the second
1174            ˇ
1175
1176
1177            third and
1178            final"})
1179            .await;
1180
1181        // goes down over multiple blanks
1182        cx.simulate_shared_keystrokes(["}"]).await;
1183        cx.assert_shared_state(indoc! {r"abc
1184                def
1185
1186                paragraph
1187                the second
1188
1189
1190
1191                third and
1192                finaˇl"})
1193            .await;
1194
1195        // goes up twice
1196        cx.simulate_shared_keystrokes(["2", "{"]).await;
1197        cx.assert_shared_state(indoc! {r"abc
1198                def
1199                ˇ
1200                paragraph
1201                the second
1202
1203
1204
1205                third and
1206                final"})
1207            .await
1208    }
1209
1210    #[gpui::test]
1211    async fn test_matching(cx: &mut gpui::TestAppContext) {
1212        let mut cx = NeovimBackedTestContext::new(cx).await;
1213
1214        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1215                do(something(with<Types>.and_arrays[0, 2]))
1216            }"})
1217            .await;
1218        cx.simulate_shared_keystrokes(["%"]).await;
1219        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1220                do(something(with<Types>.and_arrays[0, 2]))
1221            }"})
1222            .await;
1223
1224        // test it works on the last character of the line
1225        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1226            do(something(with<Types>.and_arrays[0, 2]))
1227            }"})
1228            .await;
1229        cx.simulate_shared_keystrokes(["%"]).await;
1230        cx.assert_shared_state(indoc! {r"func (a string) {
1231            do(something(with<Types>.and_arrays[0, 2]))
1232            ˇ}"})
1233            .await;
1234
1235        // test it works on immediate nesting
1236        cx.set_shared_state("ˇ{()}").await;
1237        cx.simulate_shared_keystrokes(["%"]).await;
1238        cx.assert_shared_state("{()ˇ}").await;
1239        cx.simulate_shared_keystrokes(["%"]).await;
1240        cx.assert_shared_state("ˇ{()}").await;
1241
1242        // test it works on immediate nesting inside braces
1243        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1244        cx.simulate_shared_keystrokes(["%"]).await;
1245        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1246
1247        // test it jumps to the next paren on a line
1248        cx.set_shared_state("func ˇboop() {\n}").await;
1249        cx.simulate_shared_keystrokes(["%"]).await;
1250        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1251    }
1252
1253    #[gpui::test]
1254    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1255        let mut cx = NeovimBackedTestContext::new(cx).await;
1256
1257        // f and F
1258        cx.set_shared_state("ˇone two three four").await;
1259        cx.simulate_shared_keystrokes(["f", "o"]).await;
1260        cx.assert_shared_state("one twˇo three four").await;
1261        cx.simulate_shared_keystrokes([","]).await;
1262        cx.assert_shared_state("ˇone two three four").await;
1263        cx.simulate_shared_keystrokes(["2", ";"]).await;
1264        cx.assert_shared_state("one two three fˇour").await;
1265        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1266        cx.assert_shared_state("one two threˇe four").await;
1267        cx.simulate_shared_keystrokes(["2", ";"]).await;
1268        cx.assert_shared_state("onˇe two three four").await;
1269        cx.simulate_shared_keystrokes([","]).await;
1270        cx.assert_shared_state("one two thrˇee four").await;
1271
1272        // t and T
1273        cx.set_shared_state("ˇone two three four").await;
1274        cx.simulate_shared_keystrokes(["t", "o"]).await;
1275        cx.assert_shared_state("one tˇwo three four").await;
1276        cx.simulate_shared_keystrokes([","]).await;
1277        cx.assert_shared_state("oˇne two three four").await;
1278        cx.simulate_shared_keystrokes(["2", ";"]).await;
1279        cx.assert_shared_state("one two three ˇfour").await;
1280        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1281        cx.assert_shared_state("one two threeˇ four").await;
1282        cx.simulate_shared_keystrokes(["3", ";"]).await;
1283        cx.assert_shared_state("oneˇ two three four").await;
1284        cx.simulate_shared_keystrokes([","]).await;
1285        cx.assert_shared_state("one two thˇree four").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}