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