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),
 517            WindowMiddle => window_middle(map, point, &text_layout_details),
 518            WindowBottom => window_bottom(map, point, &text_layout_details),
 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) -> (DisplayPoint, SelectionGoal) {
1048    let first_visible_line = text_layout_details.anchor.to_display_point(map);
1049    let new_col = point.column().min(map.line_len(first_visible_line.row()));
1050    let new_point = DisplayPoint::new(first_visible_line.row(), new_col);
1051    (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1052}
1053
1054fn window_middle(
1055    map: &DisplaySnapshot,
1056    point: DisplayPoint,
1057    text_layout_details: &TextLayoutDetails,
1058) -> (DisplayPoint, SelectionGoal) {
1059    if let Some(visible_rows) = text_layout_details.visible_rows {
1060        let first_visible_line = text_layout_details.anchor.to_display_point(map);
1061        let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1062        let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1063        let new_col = point.column().min(map.line_len(new_row));
1064        let new_point = DisplayPoint::new(new_row, new_col);
1065        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1066    } else {
1067        (point, SelectionGoal::None)
1068    }
1069}
1070
1071fn window_bottom(
1072    map: &DisplaySnapshot,
1073    point: DisplayPoint,
1074    text_layout_details: &TextLayoutDetails,
1075) -> (DisplayPoint, SelectionGoal) {
1076    if let Some(visible_rows) = text_layout_details.visible_rows {
1077        let first_visible_line = text_layout_details.anchor.to_display_point(map);
1078        let bottom_row = first_visible_line.row() + (visible_rows) as u32;
1079        let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1080        let new_col = point.column().min(map.line_len(bottom_row_capped));
1081        let new_point = DisplayPoint::new(bottom_row_capped, new_col);
1082        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1083    } else {
1084        (point, SelectionGoal::None)
1085    }
1086}
1087
1088#[cfg(test)]
1089mod test {
1090
1091    use crate::test::NeovimBackedTestContext;
1092    use indoc::indoc;
1093
1094    #[gpui::test]
1095    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1096        let mut cx = NeovimBackedTestContext::new(cx).await;
1097
1098        let initial_state = indoc! {r"ˇabc
1099            def
1100
1101            paragraph
1102            the second
1103
1104
1105
1106            third and
1107            final"};
1108
1109        // goes down once
1110        cx.set_shared_state(initial_state).await;
1111        cx.simulate_shared_keystrokes(["}"]).await;
1112        cx.assert_shared_state(indoc! {r"abc
1113            def
1114            ˇ
1115            paragraph
1116            the second
1117
1118
1119
1120            third and
1121            final"})
1122            .await;
1123
1124        // goes up once
1125        cx.simulate_shared_keystrokes(["{"]).await;
1126        cx.assert_shared_state(initial_state).await;
1127
1128        // goes down twice
1129        cx.simulate_shared_keystrokes(["2", "}"]).await;
1130        cx.assert_shared_state(indoc! {r"abc
1131            def
1132
1133            paragraph
1134            the second
1135            ˇ
1136
1137
1138            third and
1139            final"})
1140            .await;
1141
1142        // goes down over multiple blanks
1143        cx.simulate_shared_keystrokes(["}"]).await;
1144        cx.assert_shared_state(indoc! {r"abc
1145                def
1146
1147                paragraph
1148                the second
1149
1150
1151
1152                third and
1153                finaˇl"})
1154            .await;
1155
1156        // goes up twice
1157        cx.simulate_shared_keystrokes(["2", "{"]).await;
1158        cx.assert_shared_state(indoc! {r"abc
1159                def
1160                ˇ
1161                paragraph
1162                the second
1163
1164
1165
1166                third and
1167                final"})
1168            .await
1169    }
1170
1171    #[gpui::test]
1172    async fn test_matching(cx: &mut gpui::TestAppContext) {
1173        let mut cx = NeovimBackedTestContext::new(cx).await;
1174
1175        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1176                do(something(with<Types>.and_arrays[0, 2]))
1177            }"})
1178            .await;
1179        cx.simulate_shared_keystrokes(["%"]).await;
1180        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1181                do(something(with<Types>.and_arrays[0, 2]))
1182            }"})
1183            .await;
1184
1185        // test it works on the last character of the line
1186        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1187            do(something(with<Types>.and_arrays[0, 2]))
1188            }"})
1189            .await;
1190        cx.simulate_shared_keystrokes(["%"]).await;
1191        cx.assert_shared_state(indoc! {r"func (a string) {
1192            do(something(with<Types>.and_arrays[0, 2]))
1193            ˇ}"})
1194            .await;
1195
1196        // test it works on immediate nesting
1197        cx.set_shared_state("ˇ{()}").await;
1198        cx.simulate_shared_keystrokes(["%"]).await;
1199        cx.assert_shared_state("{()ˇ}").await;
1200        cx.simulate_shared_keystrokes(["%"]).await;
1201        cx.assert_shared_state("ˇ{()}").await;
1202
1203        // test it works on immediate nesting inside braces
1204        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1205        cx.simulate_shared_keystrokes(["%"]).await;
1206        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1207
1208        // test it jumps to the next paren on a line
1209        cx.set_shared_state("func ˇboop() {\n}").await;
1210        cx.simulate_shared_keystrokes(["%"]).await;
1211        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1212    }
1213
1214    #[gpui::test]
1215    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1216        let mut cx = NeovimBackedTestContext::new(cx).await;
1217
1218        // f and F
1219        cx.set_shared_state("ˇone two three four").await;
1220        cx.simulate_shared_keystrokes(["f", "o"]).await;
1221        cx.assert_shared_state("one twˇo three four").await;
1222        cx.simulate_shared_keystrokes([","]).await;
1223        cx.assert_shared_state("ˇone two three four").await;
1224        cx.simulate_shared_keystrokes(["2", ";"]).await;
1225        cx.assert_shared_state("one two three fˇour").await;
1226        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1227        cx.assert_shared_state("one two threˇe four").await;
1228        cx.simulate_shared_keystrokes(["2", ";"]).await;
1229        cx.assert_shared_state("onˇe two three four").await;
1230        cx.simulate_shared_keystrokes([","]).await;
1231        cx.assert_shared_state("one two thrˇee four").await;
1232
1233        // t and T
1234        cx.set_shared_state("ˇone two three four").await;
1235        cx.simulate_shared_keystrokes(["t", "o"]).await;
1236        cx.assert_shared_state("one tˇwo three four").await;
1237        cx.simulate_shared_keystrokes([","]).await;
1238        cx.assert_shared_state("oˇne two three four").await;
1239        cx.simulate_shared_keystrokes(["2", ";"]).await;
1240        cx.assert_shared_state("one two three ˇfour").await;
1241        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1242        cx.assert_shared_state("one two threeˇ four").await;
1243        cx.simulate_shared_keystrokes(["3", ";"]).await;
1244        cx.assert_shared_state("oneˇ two three four").await;
1245        cx.simulate_shared_keystrokes([","]).await;
1246        cx.assert_shared_state("one two thˇree four").await;
1247    }
1248
1249    #[gpui::test]
1250    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1251        let mut cx = NeovimBackedTestContext::new(cx).await;
1252        cx.set_shared_state("ˇone\n  two\nthree").await;
1253        cx.simulate_shared_keystrokes(["enter"]).await;
1254        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1255    }
1256
1257    #[gpui::test]
1258    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1259        let mut cx = NeovimBackedTestContext::new(cx).await;
1260        let initial_state = indoc! {r"abc
1261          def
1262          paragraph
1263          the second
1264          third ˇand
1265          final"};
1266
1267        cx.set_shared_state(initial_state).await;
1268        cx.simulate_shared_keystrokes(["shift-h"]).await;
1269        cx.assert_shared_state(indoc! {r"abˇc
1270          def
1271          paragraph
1272          the second
1273          third and
1274          final"})
1275            .await;
1276
1277        // clip point
1278        cx.set_shared_state(indoc! {r"
1279          1 2 3
1280          4 5 6
1281          7 8 ˇ9
1282          "})
1283            .await;
1284        cx.simulate_shared_keystrokes(["shift-h"]).await;
1285        cx.assert_shared_state(indoc! {r"
1286          1 2 ˇ3
1287          4 5 6
1288          7 8 9
1289          "})
1290            .await;
1291
1292        cx.set_shared_state(indoc! {r"
1293          1 2 3
1294          4 5 6
1295          ˇ7 8 9
1296          "})
1297            .await;
1298        cx.simulate_shared_keystrokes(["shift-h"]).await;
1299        cx.assert_shared_state(indoc! {r"
1300          ˇ1 2 3
1301          4 5 6
1302          7 8 9
1303          "})
1304            .await;
1305    }
1306
1307    #[gpui::test]
1308    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1309        let mut cx = NeovimBackedTestContext::new(cx).await;
1310        let initial_state = indoc! {r"abˇc
1311          def
1312          paragraph
1313          the second
1314          third and
1315          final"};
1316
1317        cx.set_shared_state(initial_state).await;
1318        cx.simulate_shared_keystrokes(["shift-m"]).await;
1319        cx.assert_shared_state(indoc! {r"abc
1320          def
1321          paˇragraph
1322          the second
1323          third and
1324          final"})
1325            .await;
1326
1327        cx.set_shared_state(indoc! {r"
1328          1 2 3
1329          4 5 6
1330          7 8 ˇ9
1331          "})
1332            .await;
1333        cx.simulate_shared_keystrokes(["shift-m"]).await;
1334        cx.assert_shared_state(indoc! {r"
1335          1 2 3
1336          4 5 ˇ6
1337          7 8 9
1338          "})
1339            .await;
1340        cx.set_shared_state(indoc! {r"
1341          1 2 3
1342          4 5 6
1343          ˇ7 8 9
1344          "})
1345            .await;
1346        cx.simulate_shared_keystrokes(["shift-m"]).await;
1347        cx.assert_shared_state(indoc! {r"
1348          1 2 3
1349          ˇ4 5 6
1350          7 8 9
1351          "})
1352            .await;
1353        cx.set_shared_state(indoc! {r"
1354          ˇ1 2 3
1355          4 5 6
1356          7 8 9
1357          "})
1358            .await;
1359        cx.simulate_shared_keystrokes(["shift-m"]).await;
1360        cx.assert_shared_state(indoc! {r"
1361          1 2 3
1362          ˇ4 5 6
1363          7 8 9
1364          "})
1365            .await;
1366        cx.set_shared_state(indoc! {r"
1367          1 2 3
1368          ˇ4 5 6
1369          7 8 9
1370          "})
1371            .await;
1372        cx.simulate_shared_keystrokes(["shift-m"]).await;
1373        cx.assert_shared_state(indoc! {r"
1374          1 2 3
1375          ˇ4 5 6
1376          7 8 9
1377          "})
1378            .await;
1379        cx.set_shared_state(indoc! {r"
1380          1 2 3
1381          4 5 ˇ6
1382          7 8 9
1383          "})
1384            .await;
1385        cx.simulate_shared_keystrokes(["shift-m"]).await;
1386        cx.assert_shared_state(indoc! {r"
1387          1 2 3
1388          4 5 ˇ6
1389          7 8 9
1390          "})
1391            .await;
1392    }
1393
1394    #[gpui::test]
1395    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1396        let mut cx = NeovimBackedTestContext::new(cx).await;
1397        let initial_state = indoc! {r"abc
1398          deˇf
1399          paragraph
1400          the second
1401          third and
1402          final"};
1403
1404        cx.set_shared_state(initial_state).await;
1405        cx.simulate_shared_keystrokes(["shift-l"]).await;
1406        cx.assert_shared_state(indoc! {r"abc
1407          def
1408          paragraph
1409          the second
1410          third and
1411          fiˇnal"})
1412            .await;
1413
1414        cx.set_shared_state(indoc! {r"
1415          1 2 3
1416          4 5 ˇ6
1417          7 8 9
1418          "})
1419            .await;
1420        cx.simulate_shared_keystrokes(["shift-l"]).await;
1421        cx.assert_shared_state(indoc! {r"
1422          1 2 3
1423          4 5 6
1424          7 8 9
1425          ˇ"})
1426            .await;
1427
1428        cx.set_shared_state(indoc! {r"
1429          1 2 3
1430          ˇ4 5 6
1431          7 8 9
1432          "})
1433            .await;
1434        cx.simulate_shared_keystrokes(["shift-l"]).await;
1435        cx.assert_shared_state(indoc! {r"
1436          1 2 3
1437          4 5 6
1438          7 8 9
1439          ˇ"})
1440            .await;
1441
1442        cx.set_shared_state(indoc! {r"
1443          1 2 ˇ3
1444          4 5 6
1445          7 8 9
1446          "})
1447            .await;
1448        cx.simulate_shared_keystrokes(["shift-l"]).await;
1449        cx.assert_shared_state(indoc! {r"
1450          1 2 3
1451          4 5 6
1452          7 8 9
1453          ˇ"})
1454            .await;
1455
1456        cx.set_shared_state(indoc! {r"
1457          ˇ1 2 3
1458          4 5 6
1459          7 8 9
1460          "})
1461            .await;
1462        cx.simulate_shared_keystrokes(["shift-l"]).await;
1463        cx.assert_shared_state(indoc! {r"
1464          1 2 3
1465          4 5 6
1466          7 8 9
1467          ˇ"})
1468            .await;
1469    }
1470}