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    NextWordStart { ignore_punctuation: bool },
  27    NextWordEnd { ignore_punctuation: bool },
  28    PreviousWordStart { ignore_punctuation: bool },
  29    FirstNonWhitespace { display_lines: bool },
  30    CurrentLine,
  31    StartOfLine { display_lines: bool },
  32    EndOfLine { display_lines: bool },
  33    StartOfParagraph,
  34    EndOfParagraph,
  35    StartOfDocument,
  36    EndOfDocument,
  37    Matching,
  38    FindForward { before: bool, char: char },
  39    FindBackward { after: bool, char: char },
  40    RepeatFind { last_find: Box<Motion> },
  41    RepeatFindReversed { last_find: Box<Motion> },
  42    NextLineStart,
  43    StartOfLineDownward,
  44    EndOfLineDownward,
  45    GoToColumn,
  46    WindowTop,
  47    WindowMiddle,
  48    WindowBottom,
  49}
  50
  51#[derive(Clone, Deserialize, PartialEq)]
  52#[serde(rename_all = "camelCase")]
  53struct NextWordStart {
  54    #[serde(default)]
  55    ignore_punctuation: bool,
  56}
  57
  58#[derive(Clone, Deserialize, PartialEq)]
  59#[serde(rename_all = "camelCase")]
  60struct NextWordEnd {
  61    #[serde(default)]
  62    ignore_punctuation: bool,
  63}
  64
  65#[derive(Clone, Deserialize, PartialEq)]
  66#[serde(rename_all = "camelCase")]
  67struct PreviousWordStart {
  68    #[serde(default)]
  69    ignore_punctuation: bool,
  70}
  71
  72#[derive(Clone, Deserialize, PartialEq)]
  73#[serde(rename_all = "camelCase")]
  74pub(crate) struct Up {
  75    #[serde(default)]
  76    pub(crate) display_lines: bool,
  77}
  78
  79#[derive(Clone, Deserialize, PartialEq)]
  80#[serde(rename_all = "camelCase")]
  81pub(crate) struct Down {
  82    #[serde(default)]
  83    pub(crate) display_lines: bool,
  84}
  85
  86#[derive(Clone, Deserialize, PartialEq)]
  87#[serde(rename_all = "camelCase")]
  88struct FirstNonWhitespace {
  89    #[serde(default)]
  90    display_lines: bool,
  91}
  92
  93#[derive(Clone, Deserialize, PartialEq)]
  94#[serde(rename_all = "camelCase")]
  95struct EndOfLine {
  96    #[serde(default)]
  97    display_lines: bool,
  98}
  99
 100#[derive(Clone, Deserialize, PartialEq)]
 101#[serde(rename_all = "camelCase")]
 102pub struct StartOfLine {
 103    #[serde(default)]
 104    pub(crate) display_lines: bool,
 105}
 106
 107impl_actions!(
 108    vim,
 109    [
 110        StartOfLine,
 111        EndOfLine,
 112        FirstNonWhitespace,
 113        Down,
 114        Up,
 115        PreviousWordStart,
 116        NextWordEnd,
 117        NextWordStart
 118    ]
 119);
 120
 121actions!(
 122    vim,
 123    [
 124        Left,
 125        Backspace,
 126        Right,
 127        CurrentLine,
 128        StartOfParagraph,
 129        EndOfParagraph,
 130        StartOfDocument,
 131        EndOfDocument,
 132        Matching,
 133        NextLineStart,
 134        StartOfLineDownward,
 135        EndOfLineDownward,
 136        GoToColumn,
 137        RepeatFind,
 138        RepeatFindReversed,
 139        WindowTop,
 140        WindowMiddle,
 141        WindowBottom,
 142    ]
 143);
 144
 145pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 146    workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 147    workspace
 148        .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
 149    workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
 150        motion(
 151            Motion::Down {
 152                display_lines: action.display_lines,
 153            },
 154            cx,
 155        )
 156    });
 157    workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
 158        motion(
 159            Motion::Up {
 160                display_lines: action.display_lines,
 161            },
 162            cx,
 163        )
 164    });
 165    workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 166    workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
 167        motion(
 168            Motion::FirstNonWhitespace {
 169                display_lines: action.display_lines,
 170            },
 171            cx,
 172        )
 173    });
 174    workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
 175        motion(
 176            Motion::StartOfLine {
 177                display_lines: action.display_lines,
 178            },
 179            cx,
 180        )
 181    });
 182    workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
 183        motion(
 184            Motion::EndOfLine {
 185                display_lines: action.display_lines,
 186            },
 187            cx,
 188        )
 189    });
 190    workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
 191        motion(Motion::CurrentLine, cx)
 192    });
 193    workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
 194        motion(Motion::StartOfParagraph, cx)
 195    });
 196    workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
 197        motion(Motion::EndOfParagraph, cx)
 198    });
 199    workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
 200        motion(Motion::StartOfDocument, cx)
 201    });
 202    workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
 203        motion(Motion::EndOfDocument, cx)
 204    });
 205    workspace
 206        .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
 207
 208    workspace.register_action(
 209        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
 210            motion(Motion::NextWordStart { ignore_punctuation }, cx)
 211        },
 212    );
 213    workspace.register_action(
 214        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
 215            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
 216        },
 217    );
 218    workspace.register_action(
 219        |_: &mut Workspace,
 220         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
 221         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
 222    );
 223    workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
 224        motion(Motion::NextLineStart, cx)
 225    });
 226    workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
 227        motion(Motion::StartOfLineDownward, cx)
 228    });
 229    workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
 230        motion(Motion::EndOfLineDownward, cx)
 231    });
 232    workspace
 233        .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
 234
 235    workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
 236        if let Some(last_find) = Vim::read(cx)
 237            .workspace_state
 238            .last_find
 239            .clone()
 240            .map(Box::new)
 241        {
 242            motion(Motion::RepeatFind { last_find }, cx);
 243        }
 244    });
 245
 246    workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
 247        if let Some(last_find) = Vim::read(cx)
 248            .workspace_state
 249            .last_find
 250            .clone()
 251            .map(Box::new)
 252        {
 253            motion(Motion::RepeatFindReversed { last_find }, cx);
 254        }
 255    });
 256    workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
 257    workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
 258        motion(Motion::WindowMiddle, cx)
 259    });
 260    workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
 261        motion(Motion::WindowBottom, cx)
 262    });
 263}
 264
 265pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 266    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
 267        Vim::read(cx).active_operator()
 268    {
 269        Vim::update(cx, |vim, cx| vim.pop_operator(cx));
 270    }
 271
 272    let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
 273    let operator = Vim::read(cx).active_operator();
 274    match Vim::read(cx).state().mode {
 275        Mode::Normal => normal_motion(motion, operator, count, cx),
 276        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
 277        Mode::Insert => {
 278            // Shouldn't execute a motion in insert mode. Ignoring
 279        }
 280    }
 281    Vim::update(cx, |vim, cx| vim.clear_operator(cx));
 282}
 283
 284// Motion handling is specified here:
 285// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 286impl Motion {
 287    pub fn linewise(&self) -> bool {
 288        use Motion::*;
 289        match self {
 290            Down { .. }
 291            | Up { .. }
 292            | StartOfDocument
 293            | EndOfDocument
 294            | CurrentLine
 295            | NextLineStart
 296            | StartOfLineDownward
 297            | StartOfParagraph
 298            | WindowTop
 299            | WindowMiddle
 300            | WindowBottom
 301            | EndOfParagraph => true,
 302            EndOfLine { .. }
 303            | NextWordEnd { .. }
 304            | Matching
 305            | FindForward { .. }
 306            | Left
 307            | Backspace
 308            | Right
 309            | StartOfLine { .. }
 310            | EndOfLineDownward
 311            | GoToColumn
 312            | NextWordStart { .. }
 313            | PreviousWordStart { .. }
 314            | FirstNonWhitespace { .. }
 315            | FindBackward { .. }
 316            | RepeatFind { .. }
 317            | RepeatFindReversed { .. } => false,
 318        }
 319    }
 320
 321    pub fn infallible(&self) -> bool {
 322        use Motion::*;
 323        match self {
 324            StartOfDocument | EndOfDocument | CurrentLine => true,
 325            Down { .. }
 326            | Up { .. }
 327            | EndOfLine { .. }
 328            | NextWordEnd { .. }
 329            | Matching
 330            | FindForward { .. }
 331            | RepeatFind { .. }
 332            | Left
 333            | Backspace
 334            | Right
 335            | StartOfLine { .. }
 336            | StartOfParagraph
 337            | EndOfParagraph
 338            | StartOfLineDownward
 339            | EndOfLineDownward
 340            | GoToColumn
 341            | NextWordStart { .. }
 342            | PreviousWordStart { .. }
 343            | FirstNonWhitespace { .. }
 344            | FindBackward { .. }
 345            | RepeatFindReversed { .. }
 346            | WindowTop
 347            | WindowMiddle
 348            | WindowBottom
 349            | NextLineStart => false,
 350        }
 351    }
 352
 353    pub fn inclusive(&self) -> bool {
 354        use Motion::*;
 355        match self {
 356            Down { .. }
 357            | Up { .. }
 358            | StartOfDocument
 359            | EndOfDocument
 360            | CurrentLine
 361            | EndOfLine { .. }
 362            | EndOfLineDownward
 363            | NextWordEnd { .. }
 364            | Matching
 365            | FindForward { .. }
 366            | WindowTop
 367            | WindowMiddle
 368            | WindowBottom
 369            | NextLineStart => true,
 370            Left
 371            | Backspace
 372            | Right
 373            | StartOfLine { .. }
 374            | StartOfLineDownward
 375            | StartOfParagraph
 376            | EndOfParagraph
 377            | GoToColumn
 378            | NextWordStart { .. }
 379            | PreviousWordStart { .. }
 380            | FirstNonWhitespace { .. }
 381            | FindBackward { .. } => false,
 382            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 383                motion.inclusive()
 384            }
 385        }
 386    }
 387
 388    pub fn move_point(
 389        &self,
 390        map: &DisplaySnapshot,
 391        point: DisplayPoint,
 392        goal: SelectionGoal,
 393        maybe_times: Option<usize>,
 394        text_layout_details: &TextLayoutDetails,
 395    ) -> Option<(DisplayPoint, SelectionGoal)> {
 396        let times = maybe_times.unwrap_or(1);
 397        use Motion::*;
 398        let infallible = self.infallible();
 399        let (new_point, goal) = match self {
 400            Left => (left(map, point, times), SelectionGoal::None),
 401            Backspace => (backspace(map, point, times), SelectionGoal::None),
 402            Down {
 403                display_lines: false,
 404            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
 405            Down {
 406                display_lines: true,
 407            } => down_display(map, point, goal, times, &text_layout_details),
 408            Up {
 409                display_lines: false,
 410            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
 411            Up {
 412                display_lines: true,
 413            } => up_display(map, point, goal, times, &text_layout_details),
 414            Right => (right(map, point, times), SelectionGoal::None),
 415            NextWordStart { ignore_punctuation } => (
 416                next_word_start(map, point, *ignore_punctuation, times),
 417                SelectionGoal::None,
 418            ),
 419            NextWordEnd { ignore_punctuation } => (
 420                next_word_end(map, point, *ignore_punctuation, times),
 421                SelectionGoal::None,
 422            ),
 423            PreviousWordStart { ignore_punctuation } => (
 424                previous_word_start(map, point, *ignore_punctuation, times),
 425                SelectionGoal::None,
 426            ),
 427            FirstNonWhitespace { display_lines } => (
 428                first_non_whitespace(map, *display_lines, point),
 429                SelectionGoal::None,
 430            ),
 431            StartOfLine { display_lines } => (
 432                start_of_line(map, *display_lines, point),
 433                SelectionGoal::None,
 434            ),
 435            EndOfLine { display_lines } => {
 436                (end_of_line(map, *display_lines, point), SelectionGoal::None)
 437            }
 438            StartOfParagraph => (
 439                movement::start_of_paragraph(map, point, times),
 440                SelectionGoal::None,
 441            ),
 442            EndOfParagraph => (
 443                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 444                SelectionGoal::None,
 445            ),
 446            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 447            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 448            EndOfDocument => (
 449                end_of_document(map, point, maybe_times),
 450                SelectionGoal::None,
 451            ),
 452            Matching => (matching(map, point), SelectionGoal::None),
 453            // t f
 454            FindForward { before, char } => {
 455                return find_forward(map, point, *before, *char, times)
 456                    .map(|new_point| (new_point, SelectionGoal::None))
 457            }
 458            // T F
 459            FindBackward { after, char } => (
 460                find_backward(map, point, *after, *char, times),
 461                SelectionGoal::None,
 462            ),
 463            // ; -- repeat the last find done with t, f, T, F
 464            RepeatFind { last_find } => match **last_find {
 465                Motion::FindForward { before, char } => {
 466                    let mut new_point = find_forward(map, point, before, char, times);
 467                    if new_point == Some(point) {
 468                        new_point = find_forward(map, point, before, char, times + 1);
 469                    }
 470
 471                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 472                }
 473
 474                Motion::FindBackward { after, char } => {
 475                    let mut new_point = find_backward(map, point, after, char, times);
 476                    if new_point == point {
 477                        new_point = find_backward(map, point, after, char, times + 1);
 478                    }
 479
 480                    (new_point, SelectionGoal::None)
 481                }
 482                _ => return None,
 483            },
 484            // , -- repeat the last find done with t, f, T, F, in opposite direction
 485            RepeatFindReversed { last_find } => match **last_find {
 486                Motion::FindForward { before, char } => {
 487                    let mut new_point = find_backward(map, point, before, char, times);
 488                    if new_point == point {
 489                        new_point = find_backward(map, point, before, char, times + 1);
 490                    }
 491
 492                    (new_point, SelectionGoal::None)
 493                }
 494
 495                Motion::FindBackward { after, char } => {
 496                    let mut new_point = find_forward(map, point, after, char, times);
 497                    if new_point == Some(point) {
 498                        new_point = find_forward(map, point, after, char, times + 1);
 499                    }
 500
 501                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 502                }
 503                _ => return None,
 504            },
 505            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 506            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 507            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
 508            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
 509            WindowTop => window_top(map, point, &text_layout_details),
 510            WindowMiddle => window_middle(map, point, &text_layout_details),
 511            WindowBottom => window_bottom(map, point, &text_layout_details),
 512        };
 513
 514        (new_point != point || infallible).then_some((new_point, goal))
 515    }
 516
 517    // Expands a selection using self motion for an operator
 518    pub fn expand_selection(
 519        &self,
 520        map: &DisplaySnapshot,
 521        selection: &mut Selection<DisplayPoint>,
 522        times: Option<usize>,
 523        expand_to_surrounding_newline: bool,
 524        text_layout_details: &TextLayoutDetails,
 525    ) -> bool {
 526        if let Some((new_head, goal)) = self.move_point(
 527            map,
 528            selection.head(),
 529            selection.goal,
 530            times,
 531            &text_layout_details,
 532        ) {
 533            selection.set_head(new_head, goal);
 534
 535            if self.linewise() {
 536                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 537
 538                if expand_to_surrounding_newline {
 539                    if selection.end.row() < map.max_point().row() {
 540                        *selection.end.row_mut() += 1;
 541                        *selection.end.column_mut() = 0;
 542                        selection.end = map.clip_point(selection.end, Bias::Right);
 543                        // Don't reset the end here
 544                        return true;
 545                    } else if selection.start.row() > 0 {
 546                        *selection.start.row_mut() -= 1;
 547                        *selection.start.column_mut() = map.line_len(selection.start.row());
 548                        selection.start = map.clip_point(selection.start, Bias::Left);
 549                    }
 550                }
 551
 552                (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
 553            } else {
 554                // Another special case: When using the "w" motion in combination with an
 555                // operator and the last word moved over is at the end of a line, the end of
 556                // that word becomes the end of the operated text, not the first word in the
 557                // next line.
 558                if let Motion::NextWordStart {
 559                    ignore_punctuation: _,
 560                } = self
 561                {
 562                    let start_row = selection.start.to_point(&map).row;
 563                    if selection.end.to_point(&map).row > start_row {
 564                        selection.end =
 565                            Point::new(start_row, map.buffer_snapshot.line_len(start_row))
 566                                .to_display_point(&map)
 567                    }
 568                }
 569
 570                // If the motion is exclusive and the end of the motion is in column 1, the
 571                // end of the motion is moved to the end of the previous line and the motion
 572                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
 573                // but "d}" will not include that line.
 574                let mut inclusive = self.inclusive();
 575                if !inclusive
 576                    && self != &Motion::Backspace
 577                    && selection.end.row() > selection.start.row()
 578                    && selection.end.column() == 0
 579                {
 580                    inclusive = true;
 581                    *selection.end.row_mut() -= 1;
 582                    *selection.end.column_mut() = 0;
 583                    selection.end = map.clip_point(
 584                        map.next_line_boundary(selection.end.to_point(map)).1,
 585                        Bias::Left,
 586                    );
 587                }
 588
 589                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 590                    *selection.end.column_mut() += 1;
 591                }
 592            }
 593            true
 594        } else {
 595            false
 596        }
 597    }
 598}
 599
 600fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 601    for _ in 0..times {
 602        point = movement::saturating_left(map, point);
 603        if point.column() == 0 {
 604            break;
 605        }
 606    }
 607    point
 608}
 609
 610fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 611    for _ in 0..times {
 612        point = movement::left(map, point);
 613    }
 614    point
 615}
 616
 617pub(crate) fn start_of_relative_buffer_row(
 618    map: &DisplaySnapshot,
 619    point: DisplayPoint,
 620    times: isize,
 621) -> DisplayPoint {
 622    let start = map.display_point_to_fold_point(point, Bias::Left);
 623    let target = start.row() as isize + times;
 624    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 625
 626    map.clip_point(
 627        map.fold_point_to_display_point(
 628            map.fold_snapshot
 629                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 630        ),
 631        Bias::Right,
 632    )
 633}
 634
 635fn up_down_buffer_rows(
 636    map: &DisplaySnapshot,
 637    point: DisplayPoint,
 638    mut goal: SelectionGoal,
 639    times: isize,
 640    text_layout_details: &TextLayoutDetails,
 641) -> (DisplayPoint, SelectionGoal) {
 642    let start = map.display_point_to_fold_point(point, Bias::Left);
 643    let begin_folded_line = map.fold_point_to_display_point(
 644        map.fold_snapshot
 645            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
 646    );
 647    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 648
 649    let (goal_wrap, goal_x) = match goal {
 650        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
 651        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 652        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 653        _ => {
 654            let x = map.x_for_display_point(point, text_layout_details);
 655            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
 656            (select_nth_wrapped_row, x.0)
 657        }
 658    };
 659
 660    let target = start.row() as isize + times;
 661    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 662
 663    let mut begin_folded_line = map.fold_point_to_display_point(
 664        map.fold_snapshot
 665            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
 666    );
 667
 668    let mut i = 0;
 669    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
 670        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
 671        if map
 672            .display_point_to_fold_point(next_folded_line, Bias::Right)
 673            .row()
 674            == new_row
 675        {
 676            i += 1;
 677            begin_folded_line = next_folded_line;
 678        } else {
 679            break;
 680        }
 681    }
 682
 683    let new_col = if i == goal_wrap {
 684        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
 685    } else {
 686        map.line_len(begin_folded_line.row())
 687    };
 688
 689    (
 690        map.clip_point(
 691            DisplayPoint::new(begin_folded_line.row(), new_col),
 692            Bias::Left,
 693        ),
 694        goal,
 695    )
 696}
 697
 698fn down_display(
 699    map: &DisplaySnapshot,
 700    mut point: DisplayPoint,
 701    mut goal: SelectionGoal,
 702    times: usize,
 703    text_layout_details: &TextLayoutDetails,
 704) -> (DisplayPoint, SelectionGoal) {
 705    for _ in 0..times {
 706        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
 707    }
 708
 709    (point, goal)
 710}
 711
 712fn up_display(
 713    map: &DisplaySnapshot,
 714    mut point: DisplayPoint,
 715    mut goal: SelectionGoal,
 716    times: usize,
 717    text_layout_details: &TextLayoutDetails,
 718) -> (DisplayPoint, SelectionGoal) {
 719    for _ in 0..times {
 720        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
 721    }
 722
 723    (point, goal)
 724}
 725
 726pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 727    for _ in 0..times {
 728        let new_point = movement::saturating_right(map, point);
 729        if point == new_point {
 730            break;
 731        }
 732        point = new_point;
 733    }
 734    point
 735}
 736
 737pub(crate) fn next_word_start(
 738    map: &DisplaySnapshot,
 739    mut point: DisplayPoint,
 740    ignore_punctuation: bool,
 741    times: usize,
 742) -> DisplayPoint {
 743    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 744    for _ in 0..times {
 745        let mut crossed_newline = false;
 746        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 747            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 748            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 749            let at_newline = right == '\n';
 750
 751            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
 752                || at_newline && crossed_newline
 753                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 754
 755            crossed_newline |= at_newline;
 756            found
 757        })
 758    }
 759    point
 760}
 761
 762fn next_word_end(
 763    map: &DisplaySnapshot,
 764    mut point: DisplayPoint,
 765    ignore_punctuation: bool,
 766    times: usize,
 767) -> DisplayPoint {
 768    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 769    for _ in 0..times {
 770        if point.column() < map.line_len(point.row()) {
 771            *point.column_mut() += 1;
 772        } else if point.row() < map.max_buffer_row() {
 773            *point.row_mut() += 1;
 774            *point.column_mut() = 0;
 775        }
 776        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
 777            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 778            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 779
 780            left_kind != right_kind && left_kind != CharKind::Whitespace
 781        });
 782
 783        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
 784        // we have backtracked already
 785        if !map
 786            .chars_at(point)
 787            .nth(1)
 788            .map(|(c, _)| c == '\n')
 789            .unwrap_or(true)
 790        {
 791            *point.column_mut() = point.column().saturating_sub(1);
 792        }
 793        point = map.clip_point(point, Bias::Left);
 794    }
 795    point
 796}
 797
 798fn previous_word_start(
 799    map: &DisplaySnapshot,
 800    mut point: DisplayPoint,
 801    ignore_punctuation: bool,
 802    times: usize,
 803) -> DisplayPoint {
 804    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
 805    for _ in 0..times {
 806        // This works even though find_preceding_boundary is called for every character in the line containing
 807        // cursor because the newline is checked only once.
 808        point =
 809            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
 810                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
 811                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 812
 813                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
 814            });
 815    }
 816    point
 817}
 818
 819pub(crate) fn first_non_whitespace(
 820    map: &DisplaySnapshot,
 821    display_lines: bool,
 822    from: DisplayPoint,
 823) -> DisplayPoint {
 824    let mut last_point = start_of_line(map, display_lines, from);
 825    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
 826    for (ch, point) in map.chars_at(last_point) {
 827        if ch == '\n' {
 828            return from;
 829        }
 830
 831        last_point = point;
 832
 833        if char_kind(&scope, ch) != CharKind::Whitespace {
 834            break;
 835        }
 836    }
 837
 838    map.clip_point(last_point, Bias::Left)
 839}
 840
 841pub(crate) fn start_of_line(
 842    map: &DisplaySnapshot,
 843    display_lines: bool,
 844    point: DisplayPoint,
 845) -> DisplayPoint {
 846    if display_lines {
 847        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
 848    } else {
 849        map.prev_line_boundary(point.to_point(map)).1
 850    }
 851}
 852
 853pub(crate) fn end_of_line(
 854    map: &DisplaySnapshot,
 855    display_lines: bool,
 856    point: DisplayPoint,
 857) -> DisplayPoint {
 858    if display_lines {
 859        map.clip_point(
 860            DisplayPoint::new(point.row(), map.line_len(point.row())),
 861            Bias::Left,
 862        )
 863    } else {
 864        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
 865    }
 866}
 867
 868fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
 869    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
 870    *new_point.column_mut() = point.column();
 871    map.clip_point(new_point, Bias::Left)
 872}
 873
 874fn end_of_document(
 875    map: &DisplaySnapshot,
 876    point: DisplayPoint,
 877    line: Option<usize>,
 878) -> DisplayPoint {
 879    let new_row = if let Some(line) = line {
 880        (line - 1) as u32
 881    } else {
 882        map.max_buffer_row()
 883    };
 884
 885    let new_point = Point::new(new_row, point.column());
 886    map.clip_point(new_point.to_display_point(map), Bias::Left)
 887}
 888
 889fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
 890    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
 891    let point = display_point.to_point(map);
 892    let offset = point.to_offset(&map.buffer_snapshot);
 893
 894    // Ensure the range is contained by the current line.
 895    let mut line_end = map.next_line_boundary(point).0;
 896    if line_end == point {
 897        line_end = map.max_point().to_point(map);
 898    }
 899
 900    let line_range = map.prev_line_boundary(point).0..line_end;
 901    let visible_line_range =
 902        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
 903    let ranges = map
 904        .buffer_snapshot
 905        .bracket_ranges(visible_line_range.clone());
 906    if let Some(ranges) = ranges {
 907        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
 908            ..line_range.end.to_offset(&map.buffer_snapshot);
 909        let mut closest_pair_destination = None;
 910        let mut closest_distance = usize::MAX;
 911
 912        for (open_range, close_range) in ranges {
 913            if open_range.start >= offset && line_range.contains(&open_range.start) {
 914                let distance = open_range.start - offset;
 915                if distance < closest_distance {
 916                    closest_pair_destination = Some(close_range.start);
 917                    closest_distance = distance;
 918                    continue;
 919                }
 920            }
 921
 922            if close_range.start >= offset && line_range.contains(&close_range.start) {
 923                let distance = close_range.start - offset;
 924                if distance < closest_distance {
 925                    closest_pair_destination = Some(open_range.start);
 926                    closest_distance = distance;
 927                    continue;
 928                }
 929            }
 930
 931            continue;
 932        }
 933
 934        closest_pair_destination
 935            .map(|destination| destination.to_display_point(map))
 936            .unwrap_or(display_point)
 937    } else {
 938        display_point
 939    }
 940}
 941
 942fn find_forward(
 943    map: &DisplaySnapshot,
 944    from: DisplayPoint,
 945    before: bool,
 946    target: char,
 947    times: usize,
 948) -> Option<DisplayPoint> {
 949    let mut to = from;
 950    let mut found = false;
 951
 952    for _ in 0..times {
 953        found = false;
 954        to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
 955            found = right == target;
 956            found
 957        });
 958    }
 959
 960    if found {
 961        if before && to.column() > 0 {
 962            *to.column_mut() -= 1;
 963            Some(map.clip_point(to, Bias::Left))
 964        } else {
 965            Some(to)
 966        }
 967    } else {
 968        None
 969    }
 970}
 971
 972fn find_backward(
 973    map: &DisplaySnapshot,
 974    from: DisplayPoint,
 975    after: bool,
 976    target: char,
 977    times: usize,
 978) -> DisplayPoint {
 979    let mut to = from;
 980
 981    for _ in 0..times {
 982        to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
 983    }
 984
 985    if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
 986        if after {
 987            *to.column_mut() += 1;
 988            map.clip_point(to, Bias::Right)
 989        } else {
 990            to
 991        }
 992    } else {
 993        from
 994    }
 995}
 996
 997fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
 998    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
 999    first_non_whitespace(map, false, correct_line)
1000}
1001
1002fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1003    let correct_line = start_of_relative_buffer_row(map, point, 0);
1004    right(map, correct_line, times.saturating_sub(1))
1005}
1006
1007pub(crate) fn next_line_end(
1008    map: &DisplaySnapshot,
1009    mut point: DisplayPoint,
1010    times: usize,
1011) -> DisplayPoint {
1012    if times > 1 {
1013        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1014    }
1015    end_of_line(map, false, point)
1016}
1017
1018fn window_top(
1019    map: &DisplaySnapshot,
1020    point: DisplayPoint,
1021    text_layout_details: &TextLayoutDetails,
1022) -> (DisplayPoint, SelectionGoal) {
1023    let first_visible_line = text_layout_details.anchor.to_display_point(map);
1024    let new_col = point.column().min(map.line_len(first_visible_line.row()));
1025    let new_point = DisplayPoint::new(first_visible_line.row(), new_col);
1026    (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1027}
1028
1029fn window_middle(
1030    map: &DisplaySnapshot,
1031    point: DisplayPoint,
1032    text_layout_details: &TextLayoutDetails,
1033) -> (DisplayPoint, SelectionGoal) {
1034    if let Some(visible_rows) = text_layout_details.visible_rows {
1035        let first_visible_line = text_layout_details.anchor.to_display_point(map);
1036        let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1037        let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1038        let new_col = point.column().min(map.line_len(new_row));
1039        let new_point = DisplayPoint::new(new_row, new_col);
1040        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1041    } else {
1042        (point, SelectionGoal::None)
1043    }
1044}
1045
1046fn window_bottom(
1047    map: &DisplaySnapshot,
1048    point: DisplayPoint,
1049    text_layout_details: &TextLayoutDetails,
1050) -> (DisplayPoint, SelectionGoal) {
1051    if let Some(visible_rows) = text_layout_details.visible_rows {
1052        let first_visible_line = text_layout_details.anchor.to_display_point(map);
1053        let bottom_row = first_visible_line.row() + (visible_rows) as u32;
1054        let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1055        let new_col = point.column().min(map.line_len(bottom_row_capped));
1056        let new_point = DisplayPoint::new(bottom_row_capped, new_col);
1057        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1058    } else {
1059        (point, SelectionGoal::None)
1060    }
1061}
1062
1063#[cfg(test)]
1064mod test {
1065
1066    use crate::test::NeovimBackedTestContext;
1067    use indoc::indoc;
1068
1069    #[gpui::test]
1070    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1071        let mut cx = NeovimBackedTestContext::new(cx).await;
1072
1073        let initial_state = indoc! {r"ˇabc
1074            def
1075
1076            paragraph
1077            the second
1078
1079
1080
1081            third and
1082            final"};
1083
1084        // goes down once
1085        cx.set_shared_state(initial_state).await;
1086        cx.simulate_shared_keystrokes(["}"]).await;
1087        cx.assert_shared_state(indoc! {r"abc
1088            def
1089            ˇ
1090            paragraph
1091            the second
1092
1093
1094
1095            third and
1096            final"})
1097            .await;
1098
1099        // goes up once
1100        cx.simulate_shared_keystrokes(["{"]).await;
1101        cx.assert_shared_state(initial_state).await;
1102
1103        // goes down twice
1104        cx.simulate_shared_keystrokes(["2", "}"]).await;
1105        cx.assert_shared_state(indoc! {r"abc
1106            def
1107
1108            paragraph
1109            the second
1110            ˇ
1111
1112
1113            third and
1114            final"})
1115            .await;
1116
1117        // goes down over multiple blanks
1118        cx.simulate_shared_keystrokes(["}"]).await;
1119        cx.assert_shared_state(indoc! {r"abc
1120                def
1121
1122                paragraph
1123                the second
1124
1125
1126
1127                third and
1128                finaˇl"})
1129            .await;
1130
1131        // goes up twice
1132        cx.simulate_shared_keystrokes(["2", "{"]).await;
1133        cx.assert_shared_state(indoc! {r"abc
1134                def
1135                ˇ
1136                paragraph
1137                the second
1138
1139
1140
1141                third and
1142                final"})
1143            .await
1144    }
1145
1146    #[gpui::test]
1147    async fn test_matching(cx: &mut gpui::TestAppContext) {
1148        let mut cx = NeovimBackedTestContext::new(cx).await;
1149
1150        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1151                do(something(with<Types>.and_arrays[0, 2]))
1152            }"})
1153            .await;
1154        cx.simulate_shared_keystrokes(["%"]).await;
1155        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1156                do(something(with<Types>.and_arrays[0, 2]))
1157            }"})
1158            .await;
1159
1160        // test it works on the last character of the line
1161        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1162            do(something(with<Types>.and_arrays[0, 2]))
1163            }"})
1164            .await;
1165        cx.simulate_shared_keystrokes(["%"]).await;
1166        cx.assert_shared_state(indoc! {r"func (a string) {
1167            do(something(with<Types>.and_arrays[0, 2]))
1168            ˇ}"})
1169            .await;
1170
1171        // test it works on immediate nesting
1172        cx.set_shared_state("ˇ{()}").await;
1173        cx.simulate_shared_keystrokes(["%"]).await;
1174        cx.assert_shared_state("{()ˇ}").await;
1175        cx.simulate_shared_keystrokes(["%"]).await;
1176        cx.assert_shared_state("ˇ{()}").await;
1177
1178        // test it works on immediate nesting inside braces
1179        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1180        cx.simulate_shared_keystrokes(["%"]).await;
1181        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1182
1183        // test it jumps to the next paren on a line
1184        cx.set_shared_state("func ˇboop() {\n}").await;
1185        cx.simulate_shared_keystrokes(["%"]).await;
1186        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1187    }
1188
1189    #[gpui::test]
1190    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1191        let mut cx = NeovimBackedTestContext::new(cx).await;
1192
1193        // f and F
1194        cx.set_shared_state("ˇone two three four").await;
1195        cx.simulate_shared_keystrokes(["f", "o"]).await;
1196        cx.assert_shared_state("one twˇo three four").await;
1197        cx.simulate_shared_keystrokes([","]).await;
1198        cx.assert_shared_state("ˇone two three four").await;
1199        cx.simulate_shared_keystrokes(["2", ";"]).await;
1200        cx.assert_shared_state("one two three fˇour").await;
1201        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1202        cx.assert_shared_state("one two threˇe four").await;
1203        cx.simulate_shared_keystrokes(["2", ";"]).await;
1204        cx.assert_shared_state("onˇe two three four").await;
1205        cx.simulate_shared_keystrokes([","]).await;
1206        cx.assert_shared_state("one two thrˇee four").await;
1207
1208        // t and T
1209        cx.set_shared_state("ˇone two three four").await;
1210        cx.simulate_shared_keystrokes(["t", "o"]).await;
1211        cx.assert_shared_state("one tˇwo three four").await;
1212        cx.simulate_shared_keystrokes([","]).await;
1213        cx.assert_shared_state("oˇne two three four").await;
1214        cx.simulate_shared_keystrokes(["2", ";"]).await;
1215        cx.assert_shared_state("one two three ˇfour").await;
1216        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1217        cx.assert_shared_state("one two threeˇ four").await;
1218        cx.simulate_shared_keystrokes(["3", ";"]).await;
1219        cx.assert_shared_state("oneˇ two three four").await;
1220        cx.simulate_shared_keystrokes([","]).await;
1221        cx.assert_shared_state("one two thˇree four").await;
1222    }
1223
1224    #[gpui::test]
1225    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1226        let mut cx = NeovimBackedTestContext::new(cx).await;
1227        cx.set_shared_state("ˇone\n  two\nthree").await;
1228        cx.simulate_shared_keystrokes(["enter"]).await;
1229        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1230    }
1231
1232    #[gpui::test]
1233    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1234        let mut cx = NeovimBackedTestContext::new(cx).await;
1235        let initial_state = indoc! {r"abc
1236          def
1237          paragraph
1238          the second
1239          third ˇand
1240          final"};
1241
1242        cx.set_shared_state(initial_state).await;
1243        cx.simulate_shared_keystrokes(["shift-h"]).await;
1244        cx.assert_shared_state(indoc! {r"abˇc
1245          def
1246          paragraph
1247          the second
1248          third and
1249          final"})
1250            .await;
1251
1252        // clip point
1253        cx.set_shared_state(indoc! {r"
1254          1 2 3
1255          4 5 6
1256          7 8 ˇ9
1257          "})
1258            .await;
1259        cx.simulate_shared_keystrokes(["shift-h"]).await;
1260        cx.assert_shared_state(indoc! {r"
1261          1 2 ˇ3
1262          4 5 6
1263          7 8 9
1264          "})
1265            .await;
1266
1267        cx.set_shared_state(indoc! {r"
1268          1 2 3
1269          4 5 6
1270          ˇ7 8 9
1271          "})
1272            .await;
1273        cx.simulate_shared_keystrokes(["shift-h"]).await;
1274        cx.assert_shared_state(indoc! {r"
1275          ˇ1 2 3
1276          4 5 6
1277          7 8 9
1278          "})
1279            .await;
1280    }
1281
1282    #[gpui::test]
1283    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1284        let mut cx = NeovimBackedTestContext::new(cx).await;
1285        let initial_state = indoc! {r"abˇc
1286          def
1287          paragraph
1288          the second
1289          third and
1290          final"};
1291
1292        cx.set_shared_state(initial_state).await;
1293        cx.simulate_shared_keystrokes(["shift-m"]).await;
1294        cx.assert_shared_state(indoc! {r"abc
1295          def
1296          paˇragraph
1297          the second
1298          third and
1299          final"})
1300            .await;
1301
1302        cx.set_shared_state(indoc! {r"
1303          1 2 3
1304          4 5 6
1305          7 8 ˇ9
1306          "})
1307            .await;
1308        cx.simulate_shared_keystrokes(["shift-m"]).await;
1309        cx.assert_shared_state(indoc! {r"
1310          1 2 3
1311          4 5 ˇ6
1312          7 8 9
1313          "})
1314            .await;
1315        cx.set_shared_state(indoc! {r"
1316          1 2 3
1317          4 5 6
1318          ˇ7 8 9
1319          "})
1320            .await;
1321        cx.simulate_shared_keystrokes(["shift-m"]).await;
1322        cx.assert_shared_state(indoc! {r"
1323          1 2 3
1324          ˇ4 5 6
1325          7 8 9
1326          "})
1327            .await;
1328        cx.set_shared_state(indoc! {r"
1329          ˇ1 2 3
1330          4 5 6
1331          7 8 9
1332          "})
1333            .await;
1334        cx.simulate_shared_keystrokes(["shift-m"]).await;
1335        cx.assert_shared_state(indoc! {r"
1336          1 2 3
1337          ˇ4 5 6
1338          7 8 9
1339          "})
1340            .await;
1341        cx.set_shared_state(indoc! {r"
1342          1 2 3
1343          ˇ4 5 6
1344          7 8 9
1345          "})
1346            .await;
1347        cx.simulate_shared_keystrokes(["shift-m"]).await;
1348        cx.assert_shared_state(indoc! {r"
1349          1 2 3
1350          ˇ4 5 6
1351          7 8 9
1352          "})
1353            .await;
1354        cx.set_shared_state(indoc! {r"
1355          1 2 3
1356          4 5 ˇ6
1357          7 8 9
1358          "})
1359            .await;
1360        cx.simulate_shared_keystrokes(["shift-m"]).await;
1361        cx.assert_shared_state(indoc! {r"
1362          1 2 3
1363          4 5 ˇ6
1364          7 8 9
1365          "})
1366            .await;
1367    }
1368
1369    #[gpui::test]
1370    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1371        let mut cx = NeovimBackedTestContext::new(cx).await;
1372        let initial_state = indoc! {r"abc
1373          deˇf
1374          paragraph
1375          the second
1376          third and
1377          final"};
1378
1379        cx.set_shared_state(initial_state).await;
1380        cx.simulate_shared_keystrokes(["shift-l"]).await;
1381        cx.assert_shared_state(indoc! {r"abc
1382          def
1383          paragraph
1384          the second
1385          third and
1386          fiˇnal"})
1387            .await;
1388
1389        cx.set_shared_state(indoc! {r"
1390          1 2 3
1391          4 5 ˇ6
1392          7 8 9
1393          "})
1394            .await;
1395        cx.simulate_shared_keystrokes(["shift-l"]).await;
1396        cx.assert_shared_state(indoc! {r"
1397          1 2 3
1398          4 5 6
1399          7 8 9
1400          ˇ"})
1401            .await;
1402
1403        cx.set_shared_state(indoc! {r"
1404          1 2 3
1405          ˇ4 5 6
1406          7 8 9
1407          "})
1408            .await;
1409        cx.simulate_shared_keystrokes(["shift-l"]).await;
1410        cx.assert_shared_state(indoc! {r"
1411          1 2 3
1412          4 5 6
1413          7 8 9
1414          ˇ"})
1415            .await;
1416
1417        cx.set_shared_state(indoc! {r"
1418          1 2 ˇ3
1419          4 5 6
1420          7 8 9
1421          "})
1422            .await;
1423        cx.simulate_shared_keystrokes(["shift-l"]).await;
1424        cx.assert_shared_state(indoc! {r"
1425          1 2 3
1426          4 5 6
1427          7 8 9
1428          ˇ"})
1429            .await;
1430
1431        cx.set_shared_state(indoc! {r"
1432          ˇ1 2 3
1433          4 5 6
1434          7 8 9
1435          "})
1436            .await;
1437        cx.simulate_shared_keystrokes(["shift-l"]).await;
1438        cx.assert_shared_state(indoc! {r"
1439          1 2 3
1440          4 5 6
1441          7 8 9
1442          ˇ"})
1443            .await;
1444    }
1445}