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        if point.is_zero() {
 621            break;
 622        }
 623    }
 624    point
 625}
 626
 627fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 628    for _ in 0..times {
 629        point = wrapping_right(map, point);
 630        if point == map.max_point() {
 631            break;
 632        }
 633    }
 634    point
 635}
 636
 637fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 638    let max_column = map.line_len(point.row()).saturating_sub(1);
 639    if point.column() < max_column {
 640        *point.column_mut() += 1;
 641    } else if point.row() < map.max_point().row() {
 642        *point.row_mut() += 1;
 643        *point.column_mut() = 0;
 644    }
 645    point
 646}
 647
 648pub(crate) fn start_of_relative_buffer_row(
 649    map: &DisplaySnapshot,
 650    point: DisplayPoint,
 651    times: isize,
 652) -> DisplayPoint {
 653    let start = map.display_point_to_fold_point(point, Bias::Left);
 654    let target = start.row() as isize + times;
 655    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 656
 657    map.clip_point(
 658        map.fold_point_to_display_point(
 659            map.fold_snapshot
 660                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 661        ),
 662        Bias::Right,
 663    )
 664}
 665
 666fn up_down_buffer_rows(
 667    map: &DisplaySnapshot,
 668    point: DisplayPoint,
 669    mut goal: SelectionGoal,
 670    times: isize,
 671    text_layout_details: &TextLayoutDetails,
 672) -> (DisplayPoint, SelectionGoal) {
 673    let start = map.display_point_to_fold_point(point, Bias::Left);
 674    let begin_folded_line = map.fold_point_to_display_point(
 675        map.fold_snapshot
 676            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
 677    );
 678    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 679
 680    let (goal_wrap, goal_x) = match goal {
 681        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
 682        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 683        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 684        _ => {
 685            let x = map.x_for_display_point(point, text_layout_details);
 686            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
 687            (select_nth_wrapped_row, x.0)
 688        }
 689    };
 690
 691    let target = start.row() as isize + times;
 692    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 693
 694    let mut begin_folded_line = map.fold_point_to_display_point(
 695        map.fold_snapshot
 696            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
 697    );
 698
 699    let mut i = 0;
 700    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
 701        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
 702        if map
 703            .display_point_to_fold_point(next_folded_line, Bias::Right)
 704            .row()
 705            == new_row
 706        {
 707            i += 1;
 708            begin_folded_line = next_folded_line;
 709        } else {
 710            break;
 711        }
 712    }
 713
 714    let new_col = if i == goal_wrap {
 715        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
 716    } else {
 717        map.line_len(begin_folded_line.row())
 718    };
 719
 720    (
 721        map.clip_point(
 722            DisplayPoint::new(begin_folded_line.row(), new_col),
 723            Bias::Left,
 724        ),
 725        goal,
 726    )
 727}
 728
 729fn down_display(
 730    map: &DisplaySnapshot,
 731    mut point: DisplayPoint,
 732    mut goal: SelectionGoal,
 733    times: usize,
 734    text_layout_details: &TextLayoutDetails,
 735) -> (DisplayPoint, SelectionGoal) {
 736    for _ in 0..times {
 737        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
 738    }
 739
 740    (point, goal)
 741}
 742
 743fn up_display(
 744    map: &DisplaySnapshot,
 745    mut point: DisplayPoint,
 746    mut goal: SelectionGoal,
 747    times: usize,
 748    text_layout_details: &TextLayoutDetails,
 749) -> (DisplayPoint, SelectionGoal) {
 750    for _ in 0..times {
 751        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
 752    }
 753
 754    (point, goal)
 755}
 756
 757pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 758    for _ in 0..times {
 759        let new_point = movement::saturating_right(map, point);
 760        if point == new_point {
 761            break;
 762        }
 763        point = new_point;
 764    }
 765    point
 766}
 767
 768pub(crate) fn next_word_start(
 769    map: &DisplaySnapshot,
 770    mut point: DisplayPoint,
 771    ignore_punctuation: bool,
 772    times: usize,
 773) -> DisplayPoint {
 774    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 775    for _ in 0..times {
 776        let mut crossed_newline = false;
 777        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 778            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 779            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 780            let at_newline = right == '\n';
 781
 782            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
 783                || at_newline && crossed_newline
 784                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 785
 786            crossed_newline |= at_newline;
 787            found
 788        });
 789        if point == new_point {
 790            break;
 791        }
 792        point = new_point;
 793    }
 794    point
 795}
 796
 797fn next_word_end(
 798    map: &DisplaySnapshot,
 799    mut point: DisplayPoint,
 800    ignore_punctuation: bool,
 801    times: usize,
 802) -> DisplayPoint {
 803    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 804    for _ in 0..times {
 805        let mut new_point = point;
 806        if new_point.column() < map.line_len(new_point.row()) {
 807            *new_point.column_mut() += 1;
 808        } else if new_point < map.max_point() {
 809            *new_point.row_mut() += 1;
 810            *new_point.column_mut() = 0;
 811        }
 812
 813        let new_point = movement::find_boundary_exclusive(
 814            map,
 815            new_point,
 816            FindRange::MultiLine,
 817            |left, right| {
 818                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 819                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 820
 821                left_kind != right_kind && left_kind != CharKind::Whitespace
 822            },
 823        );
 824        let new_point = map.clip_point(new_point, Bias::Left);
 825        if point == new_point {
 826            break;
 827        }
 828        point = new_point;
 829    }
 830    point
 831}
 832
 833fn previous_word_start(
 834    map: &DisplaySnapshot,
 835    mut point: DisplayPoint,
 836    ignore_punctuation: bool,
 837    times: usize,
 838) -> DisplayPoint {
 839    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 840    for _ in 0..times {
 841        // This works even though find_preceding_boundary is called for every character in the line containing
 842        // cursor because the newline is checked only once.
 843        let new_point =
 844            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
 845                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 846                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 847
 848                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
 849            });
 850        if point == new_point {
 851            break;
 852        }
 853        point = new_point;
 854    }
 855    point
 856}
 857
 858pub(crate) fn first_non_whitespace(
 859    map: &DisplaySnapshot,
 860    display_lines: bool,
 861    from: DisplayPoint,
 862) -> DisplayPoint {
 863    let mut last_point = start_of_line(map, display_lines, from);
 864    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
 865    for (ch, point) in map.chars_at(last_point) {
 866        if ch == '\n' {
 867            return from;
 868        }
 869
 870        last_point = point;
 871
 872        if char_kind(&scope, ch) != CharKind::Whitespace {
 873            break;
 874        }
 875    }
 876
 877    map.clip_point(last_point, Bias::Left)
 878}
 879
 880pub(crate) fn start_of_line(
 881    map: &DisplaySnapshot,
 882    display_lines: bool,
 883    point: DisplayPoint,
 884) -> DisplayPoint {
 885    if display_lines {
 886        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
 887    } else {
 888        map.prev_line_boundary(point.to_point(map)).1
 889    }
 890}
 891
 892pub(crate) fn end_of_line(
 893    map: &DisplaySnapshot,
 894    display_lines: bool,
 895    point: DisplayPoint,
 896) -> DisplayPoint {
 897    if display_lines {
 898        map.clip_point(
 899            DisplayPoint::new(point.row(), map.line_len(point.row())),
 900            Bias::Left,
 901        )
 902    } else {
 903        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
 904    }
 905}
 906
 907fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
 908    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
 909    *new_point.column_mut() = point.column();
 910    map.clip_point(new_point, Bias::Left)
 911}
 912
 913fn end_of_document(
 914    map: &DisplaySnapshot,
 915    point: DisplayPoint,
 916    line: Option<usize>,
 917) -> DisplayPoint {
 918    let new_row = if let Some(line) = line {
 919        (line - 1) as u32
 920    } else {
 921        map.max_buffer_row()
 922    };
 923
 924    let new_point = Point::new(new_row, point.column());
 925    map.clip_point(new_point.to_display_point(map), Bias::Left)
 926}
 927
 928fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 929    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
 930    let point = display_point.to_point(map);
 931    let offset = point.to_offset(&map.buffer_snapshot);
 932
 933    // Ensure the range is contained by the current line.
 934    let mut line_end = map.next_line_boundary(point).0;
 935    if line_end == point {
 936        line_end = map.max_point().to_point(map);
 937    }
 938
 939    let line_range = map.prev_line_boundary(point).0..line_end;
 940    let visible_line_range =
 941        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
 942    let ranges = map
 943        .buffer_snapshot
 944        .bracket_ranges(visible_line_range.clone());
 945    if let Some(ranges) = ranges {
 946        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
 947            ..line_range.end.to_offset(&map.buffer_snapshot);
 948        let mut closest_pair_destination = None;
 949        let mut closest_distance = usize::MAX;
 950
 951        for (open_range, close_range) in ranges {
 952            if open_range.start >= offset && line_range.contains(&open_range.start) {
 953                let distance = open_range.start - offset;
 954                if distance < closest_distance {
 955                    closest_pair_destination = Some(close_range.start);
 956                    closest_distance = distance;
 957                    continue;
 958                }
 959            }
 960
 961            if close_range.start >= offset && line_range.contains(&close_range.start) {
 962                let distance = close_range.start - offset;
 963                if distance < closest_distance {
 964                    closest_pair_destination = Some(open_range.start);
 965                    closest_distance = distance;
 966                    continue;
 967                }
 968            }
 969
 970            continue;
 971        }
 972
 973        closest_pair_destination
 974            .map(|destination| destination.to_display_point(map))
 975            .unwrap_or(display_point)
 976    } else {
 977        display_point
 978    }
 979}
 980
 981fn find_forward(
 982    map: &DisplaySnapshot,
 983    from: DisplayPoint,
 984    before: bool,
 985    target: char,
 986    times: usize,
 987) -> Option<DisplayPoint> {
 988    let mut to = from;
 989    let mut found = false;
 990
 991    for _ in 0..times {
 992        found = false;
 993        let new_to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
 994            found = right == target;
 995            found
 996        });
 997        if to == new_to {
 998            break;
 999        }
1000        to = new_to;
1001    }
1002
1003    if found {
1004        if before && to.column() > 0 {
1005            *to.column_mut() -= 1;
1006            Some(map.clip_point(to, Bias::Left))
1007        } else {
1008            Some(to)
1009        }
1010    } else {
1011        None
1012    }
1013}
1014
1015fn find_backward(
1016    map: &DisplaySnapshot,
1017    from: DisplayPoint,
1018    after: bool,
1019    target: char,
1020    times: usize,
1021) -> DisplayPoint {
1022    let mut to = from;
1023
1024    for _ in 0..times {
1025        let new_to =
1026            find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
1027        if to == new_to {
1028            break;
1029        }
1030        to = new_to;
1031    }
1032
1033    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
1034        if after {
1035            *to.column_mut() += 1;
1036            map.clip_point(to, Bias::Right)
1037        } else {
1038            to
1039        }
1040    } else {
1041        from
1042    }
1043}
1044
1045fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1046    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1047    first_non_whitespace(map, false, correct_line)
1048}
1049
1050fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1051    let correct_line = start_of_relative_buffer_row(map, point, 0);
1052    right(map, correct_line, times.saturating_sub(1))
1053}
1054
1055pub(crate) fn next_line_end(
1056    map: &DisplaySnapshot,
1057    mut point: DisplayPoint,
1058    times: usize,
1059) -> DisplayPoint {
1060    if times > 1 {
1061        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1062    }
1063    end_of_line(map, false, point)
1064}
1065
1066fn window_top(
1067    map: &DisplaySnapshot,
1068    point: DisplayPoint,
1069    text_layout_details: &TextLayoutDetails,
1070    mut times: usize,
1071) -> (DisplayPoint, SelectionGoal) {
1072    let first_visible_line = text_layout_details
1073        .scroll_anchor
1074        .anchor
1075        .to_display_point(map);
1076
1077    if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1078    {
1079        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1080    }
1081
1082    if let Some(visible_rows) = text_layout_details.visible_rows {
1083        let bottom_row = first_visible_line.row() + visible_rows as u32;
1084        let new_row = (first_visible_line.row() + (times as u32))
1085            .min(bottom_row)
1086            .min(map.max_point().row());
1087        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1088
1089        let new_point = DisplayPoint::new(new_row, new_col);
1090        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1091    } else {
1092        let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1093        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1094
1095        let new_point = DisplayPoint::new(new_row, new_col);
1096        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1097    }
1098}
1099
1100fn window_middle(
1101    map: &DisplaySnapshot,
1102    point: DisplayPoint,
1103    text_layout_details: &TextLayoutDetails,
1104) -> (DisplayPoint, SelectionGoal) {
1105    if let Some(visible_rows) = text_layout_details.visible_rows {
1106        let first_visible_line = text_layout_details
1107            .scroll_anchor
1108            .anchor
1109            .to_display_point(map);
1110
1111        let max_visible_rows =
1112            (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1113
1114        let new_row =
1115            (first_visible_line.row() + (max_visible_rows / 2) as u32).min(map.max_point().row());
1116        let new_col = point.column().min(map.line_len(new_row));
1117        let new_point = DisplayPoint::new(new_row, new_col);
1118        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1119    } else {
1120        (point, SelectionGoal::None)
1121    }
1122}
1123
1124fn window_bottom(
1125    map: &DisplaySnapshot,
1126    point: DisplayPoint,
1127    text_layout_details: &TextLayoutDetails,
1128    mut times: usize,
1129) -> (DisplayPoint, SelectionGoal) {
1130    if let Some(visible_rows) = text_layout_details.visible_rows {
1131        let first_visible_line = text_layout_details
1132            .scroll_anchor
1133            .anchor
1134            .to_display_point(map);
1135        let bottom_row = first_visible_line.row()
1136            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1137        if bottom_row < map.max_point().row()
1138            && text_layout_details.vertical_scroll_margin as usize > times
1139        {
1140            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1141        }
1142        let bottom_row_capped = bottom_row.min(map.max_point().row());
1143        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1144            first_visible_line.row()
1145        } else {
1146            bottom_row_capped.saturating_sub(times as u32)
1147        };
1148        let new_col = point.column().min(map.line_len(new_row));
1149        let new_point = DisplayPoint::new(new_row, new_col);
1150        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1151    } else {
1152        (point, SelectionGoal::None)
1153    }
1154}
1155
1156#[cfg(test)]
1157mod test {
1158
1159    use crate::test::NeovimBackedTestContext;
1160    use indoc::indoc;
1161
1162    #[gpui::test]
1163    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1164        let mut cx = NeovimBackedTestContext::new(cx).await;
1165
1166        let initial_state = indoc! {r"ˇabc
1167            def
1168
1169            paragraph
1170            the second
1171
1172
1173
1174            third and
1175            final"};
1176
1177        // goes down once
1178        cx.set_shared_state(initial_state).await;
1179        cx.simulate_shared_keystrokes(["}"]).await;
1180        cx.assert_shared_state(indoc! {r"abc
1181            def
1182            ˇ
1183            paragraph
1184            the second
1185
1186
1187
1188            third and
1189            final"})
1190            .await;
1191
1192        // goes up once
1193        cx.simulate_shared_keystrokes(["{"]).await;
1194        cx.assert_shared_state(initial_state).await;
1195
1196        // goes down twice
1197        cx.simulate_shared_keystrokes(["2", "}"]).await;
1198        cx.assert_shared_state(indoc! {r"abc
1199            def
1200
1201            paragraph
1202            the second
1203            ˇ
1204
1205
1206            third and
1207            final"})
1208            .await;
1209
1210        // goes down over multiple blanks
1211        cx.simulate_shared_keystrokes(["}"]).await;
1212        cx.assert_shared_state(indoc! {r"abc
1213                def
1214
1215                paragraph
1216                the second
1217
1218
1219
1220                third and
1221                finaˇl"})
1222            .await;
1223
1224        // goes up twice
1225        cx.simulate_shared_keystrokes(["2", "{"]).await;
1226        cx.assert_shared_state(indoc! {r"abc
1227                def
1228                ˇ
1229                paragraph
1230                the second
1231
1232
1233
1234                third and
1235                final"})
1236            .await
1237    }
1238
1239    #[gpui::test]
1240    async fn test_matching(cx: &mut gpui::TestAppContext) {
1241        let mut cx = NeovimBackedTestContext::new(cx).await;
1242
1243        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1244                do(something(with<Types>.and_arrays[0, 2]))
1245            }"})
1246            .await;
1247        cx.simulate_shared_keystrokes(["%"]).await;
1248        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1249                do(something(with<Types>.and_arrays[0, 2]))
1250            }"})
1251            .await;
1252
1253        // test it works on the last character of the line
1254        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1255            do(something(with<Types>.and_arrays[0, 2]))
1256            }"})
1257            .await;
1258        cx.simulate_shared_keystrokes(["%"]).await;
1259        cx.assert_shared_state(indoc! {r"func (a string) {
1260            do(something(with<Types>.and_arrays[0, 2]))
1261            ˇ}"})
1262            .await;
1263
1264        // test it works on immediate nesting
1265        cx.set_shared_state("ˇ{()}").await;
1266        cx.simulate_shared_keystrokes(["%"]).await;
1267        cx.assert_shared_state("{()ˇ}").await;
1268        cx.simulate_shared_keystrokes(["%"]).await;
1269        cx.assert_shared_state("ˇ{()}").await;
1270
1271        // test it works on immediate nesting inside braces
1272        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1273        cx.simulate_shared_keystrokes(["%"]).await;
1274        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1275
1276        // test it jumps to the next paren on a line
1277        cx.set_shared_state("func ˇboop() {\n}").await;
1278        cx.simulate_shared_keystrokes(["%"]).await;
1279        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1280    }
1281
1282    #[gpui::test]
1283    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1284        let mut cx = NeovimBackedTestContext::new(cx).await;
1285
1286        // f and F
1287        cx.set_shared_state("ˇone two three four").await;
1288        cx.simulate_shared_keystrokes(["f", "o"]).await;
1289        cx.assert_shared_state("one twˇo three four").await;
1290        cx.simulate_shared_keystrokes([","]).await;
1291        cx.assert_shared_state("ˇone two three four").await;
1292        cx.simulate_shared_keystrokes(["2", ";"]).await;
1293        cx.assert_shared_state("one two three fˇour").await;
1294        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1295        cx.assert_shared_state("one two threˇe four").await;
1296        cx.simulate_shared_keystrokes(["2", ";"]).await;
1297        cx.assert_shared_state("onˇe two three four").await;
1298        cx.simulate_shared_keystrokes([","]).await;
1299        cx.assert_shared_state("one two thrˇee four").await;
1300
1301        // t and T
1302        cx.set_shared_state("ˇone two three four").await;
1303        cx.simulate_shared_keystrokes(["t", "o"]).await;
1304        cx.assert_shared_state("one tˇwo three four").await;
1305        cx.simulate_shared_keystrokes([","]).await;
1306        cx.assert_shared_state("oˇne two three four").await;
1307        cx.simulate_shared_keystrokes(["2", ";"]).await;
1308        cx.assert_shared_state("one two three ˇfour").await;
1309        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1310        cx.assert_shared_state("one two threeˇ four").await;
1311        cx.simulate_shared_keystrokes(["3", ";"]).await;
1312        cx.assert_shared_state("oneˇ two three four").await;
1313        cx.simulate_shared_keystrokes([","]).await;
1314        cx.assert_shared_state("one two thˇree four").await;
1315    }
1316
1317    #[gpui::test]
1318    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1319        let mut cx = NeovimBackedTestContext::new(cx).await;
1320        let initial_state = indoc! {r"something(ˇfoo)"};
1321        cx.set_shared_state(initial_state).await;
1322        cx.simulate_shared_keystrokes(["}"]).await;
1323        cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1324    }
1325
1326    #[gpui::test]
1327    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1328        let mut cx = NeovimBackedTestContext::new(cx).await;
1329        cx.set_shared_state("ˇone\n  two\nthree").await;
1330        cx.simulate_shared_keystrokes(["enter"]).await;
1331        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1332    }
1333
1334    #[gpui::test]
1335    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1336        let mut cx = NeovimBackedTestContext::new(cx).await;
1337        let initial_state = indoc! {r"abc
1338          def
1339          paragraph
1340          the second
1341          third ˇand
1342          final"};
1343
1344        cx.set_shared_state(initial_state).await;
1345        cx.simulate_shared_keystrokes(["shift-h"]).await;
1346        cx.assert_shared_state(indoc! {r"abˇc
1347          def
1348          paragraph
1349          the second
1350          third and
1351          final"})
1352            .await;
1353
1354        // clip point
1355        cx.set_shared_state(indoc! {r"
1356          1 2 3
1357          4 5 6
1358          7 8 ˇ9
1359          "})
1360            .await;
1361        cx.simulate_shared_keystrokes(["shift-h"]).await;
1362        cx.assert_shared_state(indoc! {r"
1363          1 2 ˇ3
1364          4 5 6
1365          7 8 9
1366          "})
1367            .await;
1368
1369        cx.set_shared_state(indoc! {r"
1370          1 2 3
1371          4 5 6
1372          ˇ7 8 9
1373          "})
1374            .await;
1375        cx.simulate_shared_keystrokes(["shift-h"]).await;
1376        cx.assert_shared_state(indoc! {r"
1377          ˇ1 2 3
1378          4 5 6
1379          7 8 9
1380          "})
1381            .await;
1382
1383        cx.set_shared_state(indoc! {r"
1384          1 2 3
1385          4 5 ˇ6
1386          7 8 9"})
1387            .await;
1388        cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1389        cx.assert_shared_state(indoc! {r"
1390          1 2 3
1391          4 5 6
1392          7 8 ˇ9"})
1393            .await;
1394    }
1395
1396    #[gpui::test]
1397    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1398        let mut cx = NeovimBackedTestContext::new(cx).await;
1399        let initial_state = indoc! {r"abˇc
1400          def
1401          paragraph
1402          the second
1403          third and
1404          final"};
1405
1406        cx.set_shared_state(initial_state).await;
1407        cx.simulate_shared_keystrokes(["shift-m"]).await;
1408        cx.assert_shared_state(indoc! {r"abc
1409          def
1410          paˇragraph
1411          the second
1412          third and
1413          final"})
1414            .await;
1415
1416        cx.set_shared_state(indoc! {r"
1417          1 2 3
1418          4 5 6
1419          7 8 ˇ9
1420          "})
1421            .await;
1422        cx.simulate_shared_keystrokes(["shift-m"]).await;
1423        cx.assert_shared_state(indoc! {r"
1424          1 2 3
1425          4 5 ˇ6
1426          7 8 9
1427          "})
1428            .await;
1429        cx.set_shared_state(indoc! {r"
1430          1 2 3
1431          4 5 6
1432          ˇ7 8 9
1433          "})
1434            .await;
1435        cx.simulate_shared_keystrokes(["shift-m"]).await;
1436        cx.assert_shared_state(indoc! {r"
1437          1 2 3
1438          ˇ4 5 6
1439          7 8 9
1440          "})
1441            .await;
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-m"]).await;
1449        cx.assert_shared_state(indoc! {r"
1450          1 2 3
1451          ˇ4 5 6
1452          7 8 9
1453          "})
1454            .await;
1455        cx.set_shared_state(indoc! {r"
1456          1 2 3
1457          ˇ4 5 6
1458          7 8 9
1459          "})
1460            .await;
1461        cx.simulate_shared_keystrokes(["shift-m"]).await;
1462        cx.assert_shared_state(indoc! {r"
1463          1 2 3
1464          ˇ4 5 6
1465          7 8 9
1466          "})
1467            .await;
1468        cx.set_shared_state(indoc! {r"
1469          1 2 3
1470          4 5 ˇ6
1471          7 8 9
1472          "})
1473            .await;
1474        cx.simulate_shared_keystrokes(["shift-m"]).await;
1475        cx.assert_shared_state(indoc! {r"
1476          1 2 3
1477          4 5 ˇ6
1478          7 8 9
1479          "})
1480            .await;
1481    }
1482
1483    #[gpui::test]
1484    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1485        let mut cx = NeovimBackedTestContext::new(cx).await;
1486        let initial_state = indoc! {r"abc
1487          deˇf
1488          paragraph
1489          the second
1490          third and
1491          final"};
1492
1493        cx.set_shared_state(initial_state).await;
1494        cx.simulate_shared_keystrokes(["shift-l"]).await;
1495        cx.assert_shared_state(indoc! {r"abc
1496          def
1497          paragraph
1498          the second
1499          third and
1500          fiˇnal"})
1501            .await;
1502
1503        cx.set_shared_state(indoc! {r"
1504          1 2 3
1505          4 5 ˇ6
1506          7 8 9
1507          "})
1508            .await;
1509        cx.simulate_shared_keystrokes(["shift-l"]).await;
1510        cx.assert_shared_state(indoc! {r"
1511          1 2 3
1512          4 5 6
1513          7 8 9
1514          ˇ"})
1515            .await;
1516
1517        cx.set_shared_state(indoc! {r"
1518          1 2 3
1519          ˇ4 5 6
1520          7 8 9
1521          "})
1522            .await;
1523        cx.simulate_shared_keystrokes(["shift-l"]).await;
1524        cx.assert_shared_state(indoc! {r"
1525          1 2 3
1526          4 5 6
1527          7 8 9
1528          ˇ"})
1529            .await;
1530
1531        cx.set_shared_state(indoc! {r"
1532          1 2 ˇ3
1533          4 5 6
1534          7 8 9
1535          "})
1536            .await;
1537        cx.simulate_shared_keystrokes(["shift-l"]).await;
1538        cx.assert_shared_state(indoc! {r"
1539          1 2 3
1540          4 5 6
1541          7 8 9
1542          ˇ"})
1543            .await;
1544
1545        cx.set_shared_state(indoc! {r"
1546          ˇ1 2 3
1547          4 5 6
1548          7 8 9
1549          "})
1550            .await;
1551        cx.simulate_shared_keystrokes(["shift-l"]).await;
1552        cx.assert_shared_state(indoc! {r"
1553          1 2 3
1554          4 5 6
1555          7 8 9
1556          ˇ"})
1557            .await;
1558
1559        cx.set_shared_state(indoc! {r"
1560          1 2 3
1561          4 5 ˇ6
1562          7 8 9
1563          "})
1564            .await;
1565        cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
1566        cx.assert_shared_state(indoc! {r"
1567          1 2 ˇ3
1568          4 5 6
1569          7 8 9
1570          "})
1571            .await;
1572    }
1573}