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