motion.rs

   1use editor::{
   2    display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
   3    movement::{
   4        self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
   5    },
   6    scroll::Autoscroll,
   7    Anchor, Bias, DisplayPoint, RowExt, ToOffset,
   8};
   9use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
  10use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
  11use multi_buffer::MultiBufferRow;
  12use serde::Deserialize;
  13use std::ops::Range;
  14use workspace::Workspace;
  15
  16use crate::{
  17    normal::{mark, normal_motion},
  18    state::{Mode, Operator},
  19    surrounds::SurroundsType,
  20    utils::coerce_punctuation,
  21    visual::visual_motion,
  22    Vim,
  23};
  24
  25#[derive(Clone, Debug, PartialEq, Eq)]
  26pub enum Motion {
  27    Left,
  28    Backspace,
  29    Down {
  30        display_lines: bool,
  31    },
  32    Up {
  33        display_lines: bool,
  34    },
  35    Right,
  36    Space,
  37    NextWordStart {
  38        ignore_punctuation: bool,
  39    },
  40    NextWordEnd {
  41        ignore_punctuation: bool,
  42    },
  43    PreviousWordStart {
  44        ignore_punctuation: bool,
  45    },
  46    PreviousWordEnd {
  47        ignore_punctuation: bool,
  48    },
  49    NextSubwordStart {
  50        ignore_punctuation: bool,
  51    },
  52    NextSubwordEnd {
  53        ignore_punctuation: bool,
  54    },
  55    PreviousSubwordStart {
  56        ignore_punctuation: bool,
  57    },
  58    PreviousSubwordEnd {
  59        ignore_punctuation: bool,
  60    },
  61    FirstNonWhitespace {
  62        display_lines: bool,
  63    },
  64    CurrentLine,
  65    StartOfLine {
  66        display_lines: bool,
  67    },
  68    EndOfLine {
  69        display_lines: bool,
  70    },
  71    StartOfParagraph,
  72    EndOfParagraph,
  73    StartOfDocument,
  74    EndOfDocument,
  75    Matching,
  76    FindForward {
  77        before: bool,
  78        char: char,
  79        mode: FindRange,
  80        smartcase: bool,
  81    },
  82    FindBackward {
  83        after: bool,
  84        char: char,
  85        mode: FindRange,
  86        smartcase: bool,
  87    },
  88    RepeatFind {
  89        last_find: Box<Motion>,
  90    },
  91    RepeatFindReversed {
  92        last_find: Box<Motion>,
  93    },
  94    NextLineStart,
  95    StartOfLineDownward,
  96    EndOfLineDownward,
  97    GoToColumn,
  98    WindowTop,
  99    WindowMiddle,
 100    WindowBottom,
 101
 102    // we don't have a good way to run a search syncronously, so
 103    // we handle search motions by running the search async and then
 104    // calling back into motion with this
 105    ZedSearchResult {
 106        prior_selections: Vec<Range<Anchor>>,
 107        new_selections: Vec<Range<Anchor>>,
 108    },
 109    Jump {
 110        anchor: Anchor,
 111        line: bool,
 112    },
 113}
 114
 115#[derive(Clone, Deserialize, PartialEq)]
 116#[serde(rename_all = "camelCase")]
 117struct NextWordStart {
 118    #[serde(default)]
 119    ignore_punctuation: bool,
 120}
 121
 122#[derive(Clone, Deserialize, PartialEq)]
 123#[serde(rename_all = "camelCase")]
 124struct NextWordEnd {
 125    #[serde(default)]
 126    ignore_punctuation: bool,
 127}
 128
 129#[derive(Clone, Deserialize, PartialEq)]
 130#[serde(rename_all = "camelCase")]
 131struct PreviousWordStart {
 132    #[serde(default)]
 133    ignore_punctuation: bool,
 134}
 135
 136#[derive(Clone, Deserialize, PartialEq)]
 137#[serde(rename_all = "camelCase")]
 138struct PreviousWordEnd {
 139    #[serde(default)]
 140    ignore_punctuation: bool,
 141}
 142
 143#[derive(Clone, Deserialize, PartialEq)]
 144#[serde(rename_all = "camelCase")]
 145pub(crate) struct NextSubwordStart {
 146    #[serde(default)]
 147    pub(crate) ignore_punctuation: bool,
 148}
 149
 150#[derive(Clone, Deserialize, PartialEq)]
 151#[serde(rename_all = "camelCase")]
 152pub(crate) struct NextSubwordEnd {
 153    #[serde(default)]
 154    pub(crate) ignore_punctuation: bool,
 155}
 156
 157#[derive(Clone, Deserialize, PartialEq)]
 158#[serde(rename_all = "camelCase")]
 159pub(crate) struct PreviousSubwordStart {
 160    #[serde(default)]
 161    pub(crate) ignore_punctuation: bool,
 162}
 163
 164#[derive(Clone, Deserialize, PartialEq)]
 165#[serde(rename_all = "camelCase")]
 166pub(crate) struct PreviousSubwordEnd {
 167    #[serde(default)]
 168    pub(crate) ignore_punctuation: bool,
 169}
 170
 171#[derive(Clone, Deserialize, PartialEq)]
 172#[serde(rename_all = "camelCase")]
 173pub(crate) struct Up {
 174    #[serde(default)]
 175    pub(crate) display_lines: bool,
 176}
 177
 178#[derive(Clone, Deserialize, PartialEq)]
 179#[serde(rename_all = "camelCase")]
 180pub(crate) struct Down {
 181    #[serde(default)]
 182    pub(crate) display_lines: bool,
 183}
 184
 185#[derive(Clone, Deserialize, PartialEq)]
 186#[serde(rename_all = "camelCase")]
 187struct FirstNonWhitespace {
 188    #[serde(default)]
 189    display_lines: bool,
 190}
 191
 192#[derive(Clone, Deserialize, PartialEq)]
 193#[serde(rename_all = "camelCase")]
 194struct EndOfLine {
 195    #[serde(default)]
 196    display_lines: bool,
 197}
 198
 199#[derive(Clone, Deserialize, PartialEq)]
 200#[serde(rename_all = "camelCase")]
 201pub struct StartOfLine {
 202    #[serde(default)]
 203    pub(crate) display_lines: bool,
 204}
 205
 206impl_actions!(
 207    vim,
 208    [
 209        StartOfLine,
 210        EndOfLine,
 211        FirstNonWhitespace,
 212        Down,
 213        Up,
 214        NextWordStart,
 215        NextWordEnd,
 216        PreviousWordStart,
 217        PreviousWordEnd,
 218        NextSubwordStart,
 219        NextSubwordEnd,
 220        PreviousSubwordStart,
 221        PreviousSubwordEnd,
 222    ]
 223);
 224
 225actions!(
 226    vim,
 227    [
 228        Left,
 229        Backspace,
 230        Right,
 231        Space,
 232        CurrentLine,
 233        StartOfParagraph,
 234        EndOfParagraph,
 235        StartOfDocument,
 236        EndOfDocument,
 237        Matching,
 238        NextLineStart,
 239        StartOfLineDownward,
 240        EndOfLineDownward,
 241        GoToColumn,
 242        RepeatFind,
 243        RepeatFindReversed,
 244        WindowTop,
 245        WindowMiddle,
 246        WindowBottom,
 247    ]
 248);
 249
 250pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 251    workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 252    workspace
 253        .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
 254    workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
 255        motion(
 256            Motion::Down {
 257                display_lines: action.display_lines,
 258            },
 259            cx,
 260        )
 261    });
 262    workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
 263        motion(
 264            Motion::Up {
 265                display_lines: action.display_lines,
 266            },
 267            cx,
 268        )
 269    });
 270    workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 271    workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx));
 272    workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
 273        motion(
 274            Motion::FirstNonWhitespace {
 275                display_lines: action.display_lines,
 276            },
 277            cx,
 278        )
 279    });
 280    workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
 281        motion(
 282            Motion::StartOfLine {
 283                display_lines: action.display_lines,
 284            },
 285            cx,
 286        )
 287    });
 288    workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
 289        motion(
 290            Motion::EndOfLine {
 291                display_lines: action.display_lines,
 292            },
 293            cx,
 294        )
 295    });
 296    workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
 297        motion(Motion::CurrentLine, cx)
 298    });
 299    workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
 300        motion(Motion::StartOfParagraph, cx)
 301    });
 302    workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
 303        motion(Motion::EndOfParagraph, cx)
 304    });
 305    workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
 306        motion(Motion::StartOfDocument, cx)
 307    });
 308    workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
 309        motion(Motion::EndOfDocument, cx)
 310    });
 311    workspace
 312        .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
 313
 314    workspace.register_action(
 315        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
 316            motion(Motion::NextWordStart { ignore_punctuation }, cx)
 317        },
 318    );
 319    workspace.register_action(
 320        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
 321            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
 322        },
 323    );
 324    workspace.register_action(
 325        |_: &mut Workspace,
 326         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
 327         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
 328    );
 329    workspace.register_action(
 330        |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
 331            motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
 332        },
 333    );
 334    workspace.register_action(
 335        |_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| {
 336            motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
 337        },
 338    );
 339    workspace.register_action(
 340        |_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| {
 341            motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
 342        },
 343    );
 344    workspace.register_action(
 345        |_: &mut Workspace,
 346         &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart,
 347         cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) },
 348    );
 349    workspace.register_action(
 350        |_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| {
 351            motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
 352        },
 353    );
 354    workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
 355        motion(Motion::NextLineStart, cx)
 356    });
 357    workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
 358        motion(Motion::StartOfLineDownward, cx)
 359    });
 360    workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
 361        motion(Motion::EndOfLineDownward, cx)
 362    });
 363    workspace
 364        .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
 365
 366    workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| {
 367        if let Some(last_find) = Vim::read(cx)
 368            .workspace_state
 369            .last_find
 370            .clone()
 371            .map(Box::new)
 372        {
 373            motion(Motion::RepeatFind { last_find }, cx);
 374        }
 375    });
 376
 377    workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| {
 378        if let Some(last_find) = Vim::read(cx)
 379            .workspace_state
 380            .last_find
 381            .clone()
 382            .map(Box::new)
 383        {
 384            motion(Motion::RepeatFindReversed { last_find }, cx);
 385        }
 386    });
 387    workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
 388    workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
 389        motion(Motion::WindowMiddle, cx)
 390    });
 391    workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
 392        motion(Motion::WindowBottom, cx)
 393    });
 394}
 395
 396pub(crate) fn search_motion(m: Motion, cx: &mut WindowContext) {
 397    if let Motion::ZedSearchResult {
 398        prior_selections, ..
 399    } = &m
 400    {
 401        match Vim::read(cx).state().mode {
 402            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 403                if !prior_selections.is_empty() {
 404                    Vim::update(cx, |vim, cx| {
 405                        vim.update_active_editor(cx, |_, editor, cx| {
 406                            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 407                                s.select_ranges(prior_selections.iter().cloned())
 408                            })
 409                        });
 410                    });
 411                }
 412            }
 413            Mode::Normal | Mode::Replace | Mode::Insert => {
 414                if Vim::read(cx).active_operator().is_none() {
 415                    return;
 416                }
 417            }
 418        }
 419    }
 420
 421    motion(m, cx)
 422}
 423
 424pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 425    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
 426        Vim::read(cx).active_operator()
 427    {
 428        Vim::update(cx, |vim, cx| vim.pop_operator(cx));
 429    }
 430
 431    let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
 432    let active_operator = Vim::read(cx).active_operator();
 433    let mut waiting_operator: Option<Operator> = None;
 434    match Vim::read(cx).state().mode {
 435        Mode::Normal | Mode::Replace => {
 436            if active_operator == Some(Operator::AddSurrounds { target: None }) {
 437                waiting_operator = Some(Operator::AddSurrounds {
 438                    target: Some(SurroundsType::Motion(motion)),
 439                });
 440            } else {
 441                normal_motion(motion.clone(), active_operator.clone(), count, cx)
 442            }
 443        }
 444        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 445            visual_motion(motion.clone(), count, cx)
 446        }
 447        Mode::Insert => {
 448            // Shouldn't execute a motion in insert mode. Ignoring
 449        }
 450    }
 451    Vim::update(cx, |vim, cx| {
 452        vim.clear_operator(cx);
 453        if let Some(operator) = waiting_operator {
 454            vim.push_operator(operator, cx);
 455            vim.update_state(|state| state.pre_count = count)
 456        }
 457    });
 458}
 459
 460// Motion handling is specified here:
 461// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 462impl Motion {
 463    pub fn linewise(&self) -> bool {
 464        use Motion::*;
 465        match self {
 466            Down { .. }
 467            | Up { .. }
 468            | StartOfDocument
 469            | EndOfDocument
 470            | CurrentLine
 471            | NextLineStart
 472            | StartOfLineDownward
 473            | StartOfParagraph
 474            | WindowTop
 475            | WindowMiddle
 476            | WindowBottom
 477            | Jump { line: true, .. }
 478            | EndOfParagraph => true,
 479            EndOfLine { .. }
 480            | Matching
 481            | FindForward { .. }
 482            | Left
 483            | Backspace
 484            | Right
 485            | Space
 486            | StartOfLine { .. }
 487            | EndOfLineDownward
 488            | GoToColumn
 489            | NextWordStart { .. }
 490            | NextWordEnd { .. }
 491            | PreviousWordStart { .. }
 492            | PreviousWordEnd { .. }
 493            | NextSubwordStart { .. }
 494            | NextSubwordEnd { .. }
 495            | PreviousSubwordStart { .. }
 496            | PreviousSubwordEnd { .. }
 497            | FirstNonWhitespace { .. }
 498            | FindBackward { .. }
 499            | RepeatFind { .. }
 500            | RepeatFindReversed { .. }
 501            | Jump { line: false, .. }
 502            | ZedSearchResult { .. } => false,
 503        }
 504    }
 505
 506    pub fn infallible(&self) -> bool {
 507        use Motion::*;
 508        match self {
 509            StartOfDocument | EndOfDocument | CurrentLine => true,
 510            Down { .. }
 511            | Up { .. }
 512            | EndOfLine { .. }
 513            | Matching
 514            | FindForward { .. }
 515            | RepeatFind { .. }
 516            | Left
 517            | Backspace
 518            | Right
 519            | Space
 520            | StartOfLine { .. }
 521            | StartOfParagraph
 522            | EndOfParagraph
 523            | StartOfLineDownward
 524            | EndOfLineDownward
 525            | GoToColumn
 526            | NextWordStart { .. }
 527            | NextWordEnd { .. }
 528            | PreviousWordStart { .. }
 529            | PreviousWordEnd { .. }
 530            | NextSubwordStart { .. }
 531            | NextSubwordEnd { .. }
 532            | PreviousSubwordStart { .. }
 533            | PreviousSubwordEnd { .. }
 534            | FirstNonWhitespace { .. }
 535            | FindBackward { .. }
 536            | RepeatFindReversed { .. }
 537            | WindowTop
 538            | WindowMiddle
 539            | WindowBottom
 540            | NextLineStart
 541            | ZedSearchResult { .. }
 542            | Jump { .. } => false,
 543        }
 544    }
 545
 546    pub fn inclusive(&self) -> bool {
 547        use Motion::*;
 548        match self {
 549            Down { .. }
 550            | Up { .. }
 551            | StartOfDocument
 552            | EndOfDocument
 553            | CurrentLine
 554            | EndOfLine { .. }
 555            | EndOfLineDownward
 556            | Matching
 557            | FindForward { .. }
 558            | WindowTop
 559            | WindowMiddle
 560            | WindowBottom
 561            | NextWordEnd { .. }
 562            | PreviousWordEnd { .. }
 563            | NextSubwordEnd { .. }
 564            | PreviousSubwordEnd { .. }
 565            | NextLineStart => true,
 566            Left
 567            | Backspace
 568            | Right
 569            | Space
 570            | StartOfLine { .. }
 571            | StartOfLineDownward
 572            | StartOfParagraph
 573            | EndOfParagraph
 574            | GoToColumn
 575            | NextWordStart { .. }
 576            | PreviousWordStart { .. }
 577            | NextSubwordStart { .. }
 578            | PreviousSubwordStart { .. }
 579            | FirstNonWhitespace { .. }
 580            | FindBackward { .. }
 581            | Jump { .. }
 582            | ZedSearchResult { .. } => false,
 583            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 584                motion.inclusive()
 585            }
 586        }
 587    }
 588
 589    pub fn move_point(
 590        &self,
 591        map: &DisplaySnapshot,
 592        point: DisplayPoint,
 593        goal: SelectionGoal,
 594        maybe_times: Option<usize>,
 595        text_layout_details: &TextLayoutDetails,
 596    ) -> Option<(DisplayPoint, SelectionGoal)> {
 597        let times = maybe_times.unwrap_or(1);
 598        use Motion::*;
 599        let infallible = self.infallible();
 600        let (new_point, goal) = match self {
 601            Left => (left(map, point, times), SelectionGoal::None),
 602            Backspace => (backspace(map, point, times), SelectionGoal::None),
 603            Down {
 604                display_lines: false,
 605            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
 606            Down {
 607                display_lines: true,
 608            } => down_display(map, point, goal, times, &text_layout_details),
 609            Up {
 610                display_lines: false,
 611            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
 612            Up {
 613                display_lines: true,
 614            } => up_display(map, point, goal, times, &text_layout_details),
 615            Right => (right(map, point, times), SelectionGoal::None),
 616            Space => (space(map, point, times), SelectionGoal::None),
 617            NextWordStart { ignore_punctuation } => (
 618                next_word_start(map, point, *ignore_punctuation, times),
 619                SelectionGoal::None,
 620            ),
 621            NextWordEnd { ignore_punctuation } => (
 622                next_word_end(map, point, *ignore_punctuation, times, true),
 623                SelectionGoal::None,
 624            ),
 625            PreviousWordStart { ignore_punctuation } => (
 626                previous_word_start(map, point, *ignore_punctuation, times),
 627                SelectionGoal::None,
 628            ),
 629            PreviousWordEnd { ignore_punctuation } => (
 630                previous_word_end(map, point, *ignore_punctuation, times),
 631                SelectionGoal::None,
 632            ),
 633            NextSubwordStart { ignore_punctuation } => (
 634                next_subword_start(map, point, *ignore_punctuation, times),
 635                SelectionGoal::None,
 636            ),
 637            NextSubwordEnd { ignore_punctuation } => (
 638                next_subword_end(map, point, *ignore_punctuation, times, true),
 639                SelectionGoal::None,
 640            ),
 641            PreviousSubwordStart { ignore_punctuation } => (
 642                previous_subword_start(map, point, *ignore_punctuation, times),
 643                SelectionGoal::None,
 644            ),
 645            PreviousSubwordEnd { ignore_punctuation } => (
 646                previous_subword_end(map, point, *ignore_punctuation, times),
 647                SelectionGoal::None,
 648            ),
 649            FirstNonWhitespace { display_lines } => (
 650                first_non_whitespace(map, *display_lines, point),
 651                SelectionGoal::None,
 652            ),
 653            StartOfLine { display_lines } => (
 654                start_of_line(map, *display_lines, point),
 655                SelectionGoal::None,
 656            ),
 657            EndOfLine { display_lines } => (
 658                end_of_line(map, *display_lines, point, times),
 659                SelectionGoal::None,
 660            ),
 661            StartOfParagraph => (
 662                movement::start_of_paragraph(map, point, times),
 663                SelectionGoal::None,
 664            ),
 665            EndOfParagraph => (
 666                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 667                SelectionGoal::None,
 668            ),
 669            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 670            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 671            EndOfDocument => (
 672                end_of_document(map, point, maybe_times),
 673                SelectionGoal::None,
 674            ),
 675            Matching => (matching(map, point), SelectionGoal::None),
 676            // t f
 677            FindForward {
 678                before,
 679                char,
 680                mode,
 681                smartcase,
 682            } => {
 683                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
 684                    .map(|new_point| (new_point, SelectionGoal::None))
 685            }
 686            // T F
 687            FindBackward {
 688                after,
 689                char,
 690                mode,
 691                smartcase,
 692            } => (
 693                find_backward(map, point, *after, *char, times, *mode, *smartcase),
 694                SelectionGoal::None,
 695            ),
 696            // ; -- repeat the last find done with t, f, T, F
 697            RepeatFind { last_find } => match **last_find {
 698                Motion::FindForward {
 699                    before,
 700                    char,
 701                    mode,
 702                    smartcase,
 703                } => {
 704                    let mut new_point =
 705                        find_forward(map, point, before, char, times, mode, smartcase);
 706                    if new_point == Some(point) {
 707                        new_point =
 708                            find_forward(map, point, before, char, times + 1, mode, smartcase);
 709                    }
 710
 711                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 712                }
 713
 714                Motion::FindBackward {
 715                    after,
 716                    char,
 717                    mode,
 718                    smartcase,
 719                } => {
 720                    let mut new_point =
 721                        find_backward(map, point, after, char, times, mode, smartcase);
 722                    if new_point == point {
 723                        new_point =
 724                            find_backward(map, point, after, char, times + 1, mode, smartcase);
 725                    }
 726
 727                    (new_point, SelectionGoal::None)
 728                }
 729                _ => return None,
 730            },
 731            // , -- repeat the last find done with t, f, T, F, in opposite direction
 732            RepeatFindReversed { last_find } => match **last_find {
 733                Motion::FindForward {
 734                    before,
 735                    char,
 736                    mode,
 737                    smartcase,
 738                } => {
 739                    let mut new_point =
 740                        find_backward(map, point, before, char, times, mode, smartcase);
 741                    if new_point == point {
 742                        new_point =
 743                            find_backward(map, point, before, char, times + 1, mode, smartcase);
 744                    }
 745
 746                    (new_point, SelectionGoal::None)
 747                }
 748
 749                Motion::FindBackward {
 750                    after,
 751                    char,
 752                    mode,
 753                    smartcase,
 754                } => {
 755                    let mut new_point =
 756                        find_forward(map, point, after, char, times, mode, smartcase);
 757                    if new_point == Some(point) {
 758                        new_point =
 759                            find_forward(map, point, after, char, times + 1, mode, smartcase);
 760                    }
 761
 762                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 763                }
 764                _ => return None,
 765            },
 766            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 767            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 768            EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
 769            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
 770            WindowTop => window_top(map, point, &text_layout_details, times - 1),
 771            WindowMiddle => window_middle(map, point, &text_layout_details),
 772            WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
 773            Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
 774            ZedSearchResult { new_selections, .. } => {
 775                // There will be only one selection, as
 776                // Search::SelectNextMatch selects a single match.
 777                if let Some(new_selection) = new_selections.first() {
 778                    (
 779                        new_selection.start.to_display_point(map),
 780                        SelectionGoal::None,
 781                    )
 782                } else {
 783                    return None;
 784                }
 785            }
 786        };
 787
 788        (new_point != point || infallible).then_some((new_point, goal))
 789    }
 790
 791    // Get the range value after self is applied to the specified selection.
 792    pub fn range(
 793        &self,
 794        map: &DisplaySnapshot,
 795        selection: Selection<DisplayPoint>,
 796        times: Option<usize>,
 797        expand_to_surrounding_newline: bool,
 798        text_layout_details: &TextLayoutDetails,
 799    ) -> Option<Range<DisplayPoint>> {
 800        if let Motion::ZedSearchResult {
 801            prior_selections,
 802            new_selections,
 803        } = self
 804        {
 805            if let Some((prior_selection, new_selection)) =
 806                prior_selections.first().zip(new_selections.first())
 807            {
 808                let start = prior_selection
 809                    .start
 810                    .to_display_point(map)
 811                    .min(new_selection.start.to_display_point(map));
 812                let end = new_selection
 813                    .end
 814                    .to_display_point(map)
 815                    .max(prior_selection.end.to_display_point(map));
 816
 817                if start < end {
 818                    return Some(start..end);
 819                } else {
 820                    return Some(end..start);
 821                }
 822            } else {
 823                return None;
 824            }
 825        }
 826
 827        if let Some((new_head, goal)) = self.move_point(
 828            map,
 829            selection.head(),
 830            selection.goal,
 831            times,
 832            &text_layout_details,
 833        ) {
 834            let mut selection = selection.clone();
 835            selection.set_head(new_head, goal);
 836
 837            if self.linewise() {
 838                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 839
 840                if expand_to_surrounding_newline {
 841                    if selection.end.row() < map.max_point().row() {
 842                        *selection.end.row_mut() += 1;
 843                        *selection.end.column_mut() = 0;
 844                        selection.end = map.clip_point(selection.end, Bias::Right);
 845                        // Don't reset the end here
 846                        return Some(selection.start..selection.end);
 847                    } else if selection.start.row().0 > 0 {
 848                        *selection.start.row_mut() -= 1;
 849                        *selection.start.column_mut() = map.line_len(selection.start.row());
 850                        selection.start = map.clip_point(selection.start, Bias::Left);
 851                    }
 852                }
 853
 854                selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
 855            } else {
 856                // Another special case: When using the "w" motion in combination with an
 857                // operator and the last word moved over is at the end of a line, the end of
 858                // that word becomes the end of the operated text, not the first word in the
 859                // next line.
 860                if let Motion::NextWordStart {
 861                    ignore_punctuation: _,
 862                } = self
 863                {
 864                    let start_row = MultiBufferRow(selection.start.to_point(&map).row);
 865                    if selection.end.to_point(&map).row > start_row.0 {
 866                        selection.end =
 867                            Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
 868                                .to_display_point(&map)
 869                    }
 870                }
 871
 872                // If the motion is exclusive and the end of the motion is in column 1, the
 873                // end of the motion is moved to the end of the previous line and the motion
 874                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
 875                // but "d}" will not include that line.
 876                let mut inclusive = self.inclusive();
 877                if !inclusive
 878                    && self != &Motion::Backspace
 879                    && selection.end.row() > selection.start.row()
 880                    && selection.end.column() == 0
 881                {
 882                    inclusive = true;
 883                    *selection.end.row_mut() -= 1;
 884                    *selection.end.column_mut() = 0;
 885                    selection.end = map.clip_point(
 886                        map.next_line_boundary(selection.end.to_point(map)).1,
 887                        Bias::Left,
 888                    );
 889                }
 890
 891                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 892                    *selection.end.column_mut() += 1;
 893                }
 894            }
 895            Some(selection.start..selection.end)
 896        } else {
 897            None
 898        }
 899    }
 900
 901    // Expands a selection using self for an operator
 902    pub fn expand_selection(
 903        &self,
 904        map: &DisplaySnapshot,
 905        selection: &mut Selection<DisplayPoint>,
 906        times: Option<usize>,
 907        expand_to_surrounding_newline: bool,
 908        text_layout_details: &TextLayoutDetails,
 909    ) -> bool {
 910        if let Some(range) = self.range(
 911            map,
 912            selection.clone(),
 913            times,
 914            expand_to_surrounding_newline,
 915            text_layout_details,
 916        ) {
 917            selection.start = range.start;
 918            selection.end = range.end;
 919            true
 920        } else {
 921            false
 922        }
 923    }
 924}
 925
 926fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 927    for _ in 0..times {
 928        point = movement::saturating_left(map, point);
 929        if point.column() == 0 {
 930            break;
 931        }
 932    }
 933    point
 934}
 935
 936pub(crate) fn backspace(
 937    map: &DisplaySnapshot,
 938    mut point: DisplayPoint,
 939    times: usize,
 940) -> DisplayPoint {
 941    for _ in 0..times {
 942        point = movement::left(map, point);
 943        if point.is_zero() {
 944            break;
 945        }
 946    }
 947    point
 948}
 949
 950fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 951    for _ in 0..times {
 952        point = wrapping_right(map, point);
 953        if point == map.max_point() {
 954            break;
 955        }
 956    }
 957    point
 958}
 959
 960fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 961    let max_column = map.line_len(point.row()).saturating_sub(1);
 962    if point.column() < max_column {
 963        *point.column_mut() += 1;
 964    } else if point.row() < map.max_point().row() {
 965        *point.row_mut() += 1;
 966        *point.column_mut() = 0;
 967    }
 968    point
 969}
 970
 971pub(crate) fn start_of_relative_buffer_row(
 972    map: &DisplaySnapshot,
 973    point: DisplayPoint,
 974    times: isize,
 975) -> DisplayPoint {
 976    let start = map.display_point_to_fold_point(point, Bias::Left);
 977    let target = start.row() as isize + times;
 978    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 979
 980    map.clip_point(
 981        map.fold_point_to_display_point(
 982            map.fold_snapshot
 983                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 984        ),
 985        Bias::Right,
 986    )
 987}
 988
 989fn up_down_buffer_rows(
 990    map: &DisplaySnapshot,
 991    point: DisplayPoint,
 992    mut goal: SelectionGoal,
 993    times: isize,
 994    text_layout_details: &TextLayoutDetails,
 995) -> (DisplayPoint, SelectionGoal) {
 996    let start = map.display_point_to_fold_point(point, Bias::Left);
 997    let begin_folded_line = map.fold_point_to_display_point(
 998        map.fold_snapshot
 999            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1000    );
1001    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1002
1003    let (goal_wrap, goal_x) = match goal {
1004        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1005        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1006        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1007        _ => {
1008            let x = map.x_for_display_point(point, text_layout_details);
1009            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1010            (select_nth_wrapped_row, x.0)
1011        }
1012    };
1013
1014    let target = start.row() as isize + times;
1015    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1016
1017    let mut begin_folded_line = map.fold_point_to_display_point(
1018        map.fold_snapshot
1019            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
1020    );
1021
1022    let mut i = 0;
1023    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1024        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1025        if map
1026            .display_point_to_fold_point(next_folded_line, Bias::Right)
1027            .row()
1028            == new_row
1029        {
1030            i += 1;
1031            begin_folded_line = next_folded_line;
1032        } else {
1033            break;
1034        }
1035    }
1036
1037    let new_col = if i == goal_wrap {
1038        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1039    } else {
1040        map.line_len(begin_folded_line.row())
1041    };
1042
1043    (
1044        map.clip_point(
1045            DisplayPoint::new(begin_folded_line.row(), new_col),
1046            Bias::Left,
1047        ),
1048        goal,
1049    )
1050}
1051
1052fn down_display(
1053    map: &DisplaySnapshot,
1054    mut point: DisplayPoint,
1055    mut goal: SelectionGoal,
1056    times: usize,
1057    text_layout_details: &TextLayoutDetails,
1058) -> (DisplayPoint, SelectionGoal) {
1059    for _ in 0..times {
1060        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1061    }
1062
1063    (point, goal)
1064}
1065
1066fn up_display(
1067    map: &DisplaySnapshot,
1068    mut point: DisplayPoint,
1069    mut goal: SelectionGoal,
1070    times: usize,
1071    text_layout_details: &TextLayoutDetails,
1072) -> (DisplayPoint, SelectionGoal) {
1073    for _ in 0..times {
1074        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
1075    }
1076
1077    (point, goal)
1078}
1079
1080pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1081    for _ in 0..times {
1082        let new_point = movement::saturating_right(map, point);
1083        if point == new_point {
1084            break;
1085        }
1086        point = new_point;
1087    }
1088    point
1089}
1090
1091pub(crate) fn next_char(
1092    map: &DisplaySnapshot,
1093    point: DisplayPoint,
1094    allow_cross_newline: bool,
1095) -> DisplayPoint {
1096    let mut new_point = point;
1097    let mut max_column = map.line_len(new_point.row());
1098    if !allow_cross_newline {
1099        max_column -= 1;
1100    }
1101    if new_point.column() < max_column {
1102        *new_point.column_mut() += 1;
1103    } else if new_point < map.max_point() && allow_cross_newline {
1104        *new_point.row_mut() += 1;
1105        *new_point.column_mut() = 0;
1106    }
1107    map.clip_ignoring_line_ends(new_point, Bias::Right)
1108}
1109
1110pub(crate) fn next_word_start(
1111    map: &DisplaySnapshot,
1112    mut point: DisplayPoint,
1113    ignore_punctuation: bool,
1114    times: usize,
1115) -> DisplayPoint {
1116    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1117    for _ in 0..times {
1118        let mut crossed_newline = false;
1119        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1120            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1121            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1122            let at_newline = right == '\n';
1123
1124            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1125                || at_newline && crossed_newline
1126                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1127
1128            crossed_newline |= at_newline;
1129            found
1130        });
1131        if point == new_point {
1132            break;
1133        }
1134        point = new_point;
1135    }
1136    point
1137}
1138
1139pub(crate) fn next_word_end(
1140    map: &DisplaySnapshot,
1141    mut point: DisplayPoint,
1142    ignore_punctuation: bool,
1143    times: usize,
1144    allow_cross_newline: bool,
1145) -> DisplayPoint {
1146    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1147    for _ in 0..times {
1148        let new_point = next_char(map, point, allow_cross_newline);
1149        let mut need_next_char = false;
1150        let new_point = movement::find_boundary_exclusive(
1151            map,
1152            new_point,
1153            FindRange::MultiLine,
1154            |left, right| {
1155                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1156                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1157                let at_newline = right == '\n';
1158
1159                if !allow_cross_newline && at_newline {
1160                    need_next_char = true;
1161                    return true;
1162                }
1163
1164                left_kind != right_kind && left_kind != CharKind::Whitespace
1165            },
1166        );
1167        let new_point = if need_next_char {
1168            next_char(map, new_point, true)
1169        } else {
1170            new_point
1171        };
1172        let new_point = map.clip_point(new_point, Bias::Left);
1173        if point == new_point {
1174            break;
1175        }
1176        point = new_point;
1177    }
1178    point
1179}
1180
1181fn previous_word_start(
1182    map: &DisplaySnapshot,
1183    mut point: DisplayPoint,
1184    ignore_punctuation: bool,
1185    times: usize,
1186) -> DisplayPoint {
1187    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1188    for _ in 0..times {
1189        // This works even though find_preceding_boundary is called for every character in the line containing
1190        // cursor because the newline is checked only once.
1191        let new_point = movement::find_preceding_boundary_display_point(
1192            map,
1193            point,
1194            FindRange::MultiLine,
1195            |left, right| {
1196                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1197                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1198
1199                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1200            },
1201        );
1202        if point == new_point {
1203            break;
1204        }
1205        point = new_point;
1206    }
1207    point
1208}
1209
1210fn previous_word_end(
1211    map: &DisplaySnapshot,
1212    point: DisplayPoint,
1213    ignore_punctuation: bool,
1214    times: usize,
1215) -> DisplayPoint {
1216    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1217    let mut point = point.to_point(map);
1218
1219    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1220        point.column += 1;
1221    }
1222    for _ in 0..times {
1223        let new_point = movement::find_preceding_boundary_point(
1224            &map.buffer_snapshot,
1225            point,
1226            FindRange::MultiLine,
1227            |left, right| {
1228                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1229                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1230                match (left_kind, right_kind) {
1231                    (CharKind::Punctuation, CharKind::Whitespace)
1232                    | (CharKind::Punctuation, CharKind::Word)
1233                    | (CharKind::Word, CharKind::Whitespace)
1234                    | (CharKind::Word, CharKind::Punctuation) => true,
1235                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1236                    _ => false,
1237                }
1238            },
1239        );
1240        if new_point == point {
1241            break;
1242        }
1243        point = new_point;
1244    }
1245    movement::saturating_left(map, point.to_display_point(map))
1246}
1247
1248fn next_subword_start(
1249    map: &DisplaySnapshot,
1250    mut point: DisplayPoint,
1251    ignore_punctuation: bool,
1252    times: usize,
1253) -> DisplayPoint {
1254    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1255    for _ in 0..times {
1256        let mut crossed_newline = false;
1257        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1258            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1259            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1260            let at_newline = right == '\n';
1261
1262            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1263            let is_subword_start =
1264                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1265
1266            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1267                || at_newline && crossed_newline
1268                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1269
1270            crossed_newline |= at_newline;
1271            found
1272        });
1273        if point == new_point {
1274            break;
1275        }
1276        point = new_point;
1277    }
1278    point
1279}
1280
1281pub(crate) fn next_subword_end(
1282    map: &DisplaySnapshot,
1283    mut point: DisplayPoint,
1284    ignore_punctuation: bool,
1285    times: usize,
1286    allow_cross_newline: bool,
1287) -> DisplayPoint {
1288    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1289    for _ in 0..times {
1290        let new_point = next_char(map, point, allow_cross_newline);
1291
1292        let mut crossed_newline = false;
1293        let mut need_backtrack = false;
1294        let new_point =
1295            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1296                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1297                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1298                let at_newline = right == '\n';
1299
1300                if !allow_cross_newline && at_newline {
1301                    return true;
1302                }
1303
1304                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1305                let is_subword_end =
1306                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1307
1308                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1309
1310                if found && (is_word_end || is_subword_end) {
1311                    need_backtrack = true;
1312                }
1313
1314                crossed_newline |= at_newline;
1315                found
1316            });
1317        let mut new_point = map.clip_point(new_point, Bias::Left);
1318        if need_backtrack {
1319            *new_point.column_mut() -= 1;
1320        }
1321        if point == new_point {
1322            break;
1323        }
1324        point = new_point;
1325    }
1326    point
1327}
1328
1329fn previous_subword_start(
1330    map: &DisplaySnapshot,
1331    mut point: DisplayPoint,
1332    ignore_punctuation: bool,
1333    times: usize,
1334) -> DisplayPoint {
1335    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1336    for _ in 0..times {
1337        let mut crossed_newline = false;
1338        // This works even though find_preceding_boundary is called for every character in the line containing
1339        // cursor because the newline is checked only once.
1340        let new_point = movement::find_preceding_boundary_display_point(
1341            map,
1342            point,
1343            FindRange::MultiLine,
1344            |left, right| {
1345                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1346                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1347                let at_newline = right == '\n';
1348
1349                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1350                let is_subword_start =
1351                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1352
1353                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1354                    || at_newline && crossed_newline
1355                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1356
1357                crossed_newline |= at_newline;
1358
1359                found
1360            },
1361        );
1362        if point == new_point {
1363            break;
1364        }
1365        point = new_point;
1366    }
1367    point
1368}
1369
1370fn previous_subword_end(
1371    map: &DisplaySnapshot,
1372    point: DisplayPoint,
1373    ignore_punctuation: bool,
1374    times: usize,
1375) -> DisplayPoint {
1376    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1377    let mut point = point.to_point(map);
1378
1379    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1380        point.column += 1;
1381    }
1382    for _ in 0..times {
1383        let new_point = movement::find_preceding_boundary_point(
1384            &map.buffer_snapshot,
1385            point,
1386            FindRange::MultiLine,
1387            |left, right| {
1388                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1389                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1390
1391                let is_subword_end =
1392                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1393
1394                if is_subword_end {
1395                    return true;
1396                }
1397
1398                match (left_kind, right_kind) {
1399                    (CharKind::Word, CharKind::Whitespace)
1400                    | (CharKind::Word, CharKind::Punctuation) => true,
1401                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1402                    _ => false,
1403                }
1404            },
1405        );
1406        if new_point == point {
1407            break;
1408        }
1409        point = new_point;
1410    }
1411    movement::saturating_left(map, point.to_display_point(map))
1412}
1413
1414pub(crate) fn first_non_whitespace(
1415    map: &DisplaySnapshot,
1416    display_lines: bool,
1417    from: DisplayPoint,
1418) -> DisplayPoint {
1419    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1420    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1421    for (ch, offset) in map.buffer_chars_at(start_offset) {
1422        if ch == '\n' {
1423            return from;
1424        }
1425
1426        start_offset = offset;
1427
1428        if char_kind(&scope, ch) != CharKind::Whitespace {
1429            break;
1430        }
1431    }
1432
1433    start_offset.to_display_point(map)
1434}
1435
1436pub(crate) fn last_non_whitespace(
1437    map: &DisplaySnapshot,
1438    from: DisplayPoint,
1439    count: usize,
1440) -> DisplayPoint {
1441    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1442    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1443    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1444        if ch == '\n' {
1445            break;
1446        }
1447        end_of_line = offset;
1448        if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
1449            break;
1450        }
1451    }
1452
1453    end_of_line.to_display_point(map)
1454}
1455
1456pub(crate) fn start_of_line(
1457    map: &DisplaySnapshot,
1458    display_lines: bool,
1459    point: DisplayPoint,
1460) -> DisplayPoint {
1461    if display_lines {
1462        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1463    } else {
1464        map.prev_line_boundary(point.to_point(map)).1
1465    }
1466}
1467
1468pub(crate) fn end_of_line(
1469    map: &DisplaySnapshot,
1470    display_lines: bool,
1471    mut point: DisplayPoint,
1472    times: usize,
1473) -> DisplayPoint {
1474    if times > 1 {
1475        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1476    }
1477    if display_lines {
1478        map.clip_point(
1479            DisplayPoint::new(point.row(), map.line_len(point.row())),
1480            Bias::Left,
1481        )
1482    } else {
1483        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1484    }
1485}
1486
1487fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1488    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1489    *new_point.column_mut() = point.column();
1490    map.clip_point(new_point, Bias::Left)
1491}
1492
1493fn end_of_document(
1494    map: &DisplaySnapshot,
1495    point: DisplayPoint,
1496    line: Option<usize>,
1497) -> DisplayPoint {
1498    let new_row = if let Some(line) = line {
1499        (line - 1) as u32
1500    } else {
1501        map.max_buffer_row().0
1502    };
1503
1504    let new_point = Point::new(new_row, point.column());
1505    map.clip_point(new_point.to_display_point(map), Bias::Left)
1506}
1507
1508fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1509    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1510    let display_point = map.clip_at_line_end(display_point);
1511    let point = display_point.to_point(map);
1512    let offset = point.to_offset(&map.buffer_snapshot);
1513
1514    // Ensure the range is contained by the current line.
1515    let mut line_end = map.next_line_boundary(point).0;
1516    if line_end == point {
1517        line_end = map.max_point().to_point(map);
1518    }
1519
1520    let line_range = map.prev_line_boundary(point).0..line_end;
1521    let visible_line_range =
1522        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1523    let ranges = map
1524        .buffer_snapshot
1525        .bracket_ranges(visible_line_range.clone());
1526    if let Some(ranges) = ranges {
1527        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1528            ..line_range.end.to_offset(&map.buffer_snapshot);
1529        let mut closest_pair_destination = None;
1530        let mut closest_distance = usize::MAX;
1531
1532        for (open_range, close_range) in ranges {
1533            if open_range.start >= offset && line_range.contains(&open_range.start) {
1534                let distance = open_range.start - offset;
1535                if distance < closest_distance {
1536                    closest_pair_destination = Some(close_range.start);
1537                    closest_distance = distance;
1538                    continue;
1539                }
1540            }
1541
1542            if close_range.start >= offset && line_range.contains(&close_range.start) {
1543                let distance = close_range.start - offset;
1544                if distance < closest_distance {
1545                    closest_pair_destination = Some(open_range.start);
1546                    closest_distance = distance;
1547                    continue;
1548                }
1549            }
1550
1551            continue;
1552        }
1553
1554        closest_pair_destination
1555            .map(|destination| destination.to_display_point(map))
1556            .unwrap_or(display_point)
1557    } else {
1558        display_point
1559    }
1560}
1561
1562fn find_forward(
1563    map: &DisplaySnapshot,
1564    from: DisplayPoint,
1565    before: bool,
1566    target: char,
1567    times: usize,
1568    mode: FindRange,
1569    smartcase: bool,
1570) -> Option<DisplayPoint> {
1571    let mut to = from;
1572    let mut found = false;
1573
1574    for _ in 0..times {
1575        found = false;
1576        let new_to = find_boundary(map, to, mode, |_, right| {
1577            found = is_character_match(target, right, smartcase);
1578            found
1579        });
1580        if to == new_to {
1581            break;
1582        }
1583        to = new_to;
1584    }
1585
1586    if found {
1587        if before && to.column() > 0 {
1588            *to.column_mut() -= 1;
1589            Some(map.clip_point(to, Bias::Left))
1590        } else {
1591            Some(to)
1592        }
1593    } else {
1594        None
1595    }
1596}
1597
1598fn find_backward(
1599    map: &DisplaySnapshot,
1600    from: DisplayPoint,
1601    after: bool,
1602    target: char,
1603    times: usize,
1604    mode: FindRange,
1605    smartcase: bool,
1606) -> DisplayPoint {
1607    let mut to = from;
1608
1609    for _ in 0..times {
1610        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1611            is_character_match(target, right, smartcase)
1612        });
1613        if to == new_to {
1614            break;
1615        }
1616        to = new_to;
1617    }
1618
1619    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1620    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1621        if after {
1622            *to.column_mut() += 1;
1623            map.clip_point(to, Bias::Right)
1624        } else {
1625            to
1626        }
1627    } else {
1628        from
1629    }
1630}
1631
1632fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1633    if smartcase {
1634        if target.is_uppercase() {
1635            target == other
1636        } else {
1637            target == other.to_ascii_lowercase()
1638        }
1639    } else {
1640        target == other
1641    }
1642}
1643
1644fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1645    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1646    first_non_whitespace(map, false, correct_line)
1647}
1648
1649fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1650    let correct_line = start_of_relative_buffer_row(map, point, 0);
1651    right(map, correct_line, times.saturating_sub(1))
1652}
1653
1654pub(crate) fn next_line_end(
1655    map: &DisplaySnapshot,
1656    mut point: DisplayPoint,
1657    times: usize,
1658) -> DisplayPoint {
1659    if times > 1 {
1660        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1661    }
1662    end_of_line(map, false, point, 1)
1663}
1664
1665fn window_top(
1666    map: &DisplaySnapshot,
1667    point: DisplayPoint,
1668    text_layout_details: &TextLayoutDetails,
1669    mut times: usize,
1670) -> (DisplayPoint, SelectionGoal) {
1671    let first_visible_line = text_layout_details
1672        .scroll_anchor
1673        .anchor
1674        .to_display_point(map);
1675
1676    if first_visible_line.row() != DisplayRow(0)
1677        && text_layout_details.vertical_scroll_margin as usize > times
1678    {
1679        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1680    }
1681
1682    if let Some(visible_rows) = text_layout_details.visible_rows {
1683        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1684        let new_row = (first_visible_line.row().0 + (times as u32))
1685            .min(bottom_row)
1686            .min(map.max_point().row().0);
1687        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1688
1689        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1690        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1691    } else {
1692        let new_row =
1693            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1694        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1695
1696        let new_point = DisplayPoint::new(new_row, new_col);
1697        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1698    }
1699}
1700
1701fn window_middle(
1702    map: &DisplaySnapshot,
1703    point: DisplayPoint,
1704    text_layout_details: &TextLayoutDetails,
1705) -> (DisplayPoint, SelectionGoal) {
1706    if let Some(visible_rows) = text_layout_details.visible_rows {
1707        let first_visible_line = text_layout_details
1708            .scroll_anchor
1709            .anchor
1710            .to_display_point(map);
1711
1712        let max_visible_rows =
1713            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1714
1715        let new_row =
1716            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1717        let new_row = DisplayRow(new_row);
1718        let new_col = point.column().min(map.line_len(new_row));
1719        let new_point = DisplayPoint::new(new_row, new_col);
1720        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1721    } else {
1722        (point, SelectionGoal::None)
1723    }
1724}
1725
1726fn window_bottom(
1727    map: &DisplaySnapshot,
1728    point: DisplayPoint,
1729    text_layout_details: &TextLayoutDetails,
1730    mut times: usize,
1731) -> (DisplayPoint, SelectionGoal) {
1732    if let Some(visible_rows) = text_layout_details.visible_rows {
1733        let first_visible_line = text_layout_details
1734            .scroll_anchor
1735            .anchor
1736            .to_display_point(map);
1737        let bottom_row = first_visible_line.row().0
1738            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1739        if bottom_row < map.max_point().row().0
1740            && text_layout_details.vertical_scroll_margin as usize > times
1741        {
1742            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1743        }
1744        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1745        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1746        {
1747            first_visible_line.row()
1748        } else {
1749            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1750        };
1751        let new_col = point.column().min(map.line_len(new_row));
1752        let new_point = DisplayPoint::new(new_row, new_col);
1753        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1754    } else {
1755        (point, SelectionGoal::None)
1756    }
1757}
1758
1759#[cfg(test)]
1760mod test {
1761
1762    use crate::test::NeovimBackedTestContext;
1763    use indoc::indoc;
1764
1765    #[gpui::test]
1766    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1767        let mut cx = NeovimBackedTestContext::new(cx).await;
1768
1769        let initial_state = indoc! {r"ˇabc
1770            def
1771
1772            paragraph
1773            the second
1774
1775
1776
1777            third and
1778            final"};
1779
1780        // goes down once
1781        cx.set_shared_state(initial_state).await;
1782        cx.simulate_shared_keystrokes("}").await;
1783        cx.shared_state().await.assert_eq(indoc! {r"abc
1784            def
1785            ˇ
1786            paragraph
1787            the second
1788
1789
1790
1791            third and
1792            final"});
1793
1794        // goes up once
1795        cx.simulate_shared_keystrokes("{").await;
1796        cx.shared_state().await.assert_eq(initial_state);
1797
1798        // goes down twice
1799        cx.simulate_shared_keystrokes("2 }").await;
1800        cx.shared_state().await.assert_eq(indoc! {r"abc
1801            def
1802
1803            paragraph
1804            the second
1805            ˇ
1806
1807
1808            third and
1809            final"});
1810
1811        // goes down over multiple blanks
1812        cx.simulate_shared_keystrokes("}").await;
1813        cx.shared_state().await.assert_eq(indoc! {r"abc
1814                def
1815
1816                paragraph
1817                the second
1818
1819
1820
1821                third and
1822                finaˇl"});
1823
1824        // goes up twice
1825        cx.simulate_shared_keystrokes("2 {").await;
1826        cx.shared_state().await.assert_eq(indoc! {r"abc
1827                def
1828                ˇ
1829                paragraph
1830                the second
1831
1832
1833
1834                third and
1835                final"});
1836    }
1837
1838    #[gpui::test]
1839    async fn test_matching(cx: &mut gpui::TestAppContext) {
1840        let mut cx = NeovimBackedTestContext::new(cx).await;
1841
1842        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1843                do(something(with<Types>.and_arrays[0, 2]))
1844            }"})
1845            .await;
1846        cx.simulate_shared_keystrokes("%").await;
1847        cx.shared_state()
1848            .await
1849            .assert_eq(indoc! {r"func (a stringˇ) {
1850                do(something(with<Types>.and_arrays[0, 2]))
1851            }"});
1852
1853        // test it works on the last character of the line
1854        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1855            do(something(with<Types>.and_arrays[0, 2]))
1856            }"})
1857            .await;
1858        cx.simulate_shared_keystrokes("%").await;
1859        cx.shared_state()
1860            .await
1861            .assert_eq(indoc! {r"func (a string) {
1862            do(something(with<Types>.and_arrays[0, 2]))
1863            ˇ}"});
1864
1865        // test it works on immediate nesting
1866        cx.set_shared_state("ˇ{()}").await;
1867        cx.simulate_shared_keystrokes("%").await;
1868        cx.shared_state().await.assert_eq("{()ˇ}");
1869        cx.simulate_shared_keystrokes("%").await;
1870        cx.shared_state().await.assert_eq("ˇ{()}");
1871
1872        // test it works on immediate nesting inside braces
1873        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1874        cx.simulate_shared_keystrokes("%").await;
1875        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
1876
1877        // test it jumps to the next paren on a line
1878        cx.set_shared_state("func ˇboop() {\n}").await;
1879        cx.simulate_shared_keystrokes("%").await;
1880        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
1881    }
1882
1883    #[gpui::test]
1884    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1885        let mut cx = NeovimBackedTestContext::new(cx).await;
1886
1887        // f and F
1888        cx.set_shared_state("ˇone two three four").await;
1889        cx.simulate_shared_keystrokes("f o").await;
1890        cx.shared_state().await.assert_eq("one twˇo three four");
1891        cx.simulate_shared_keystrokes(",").await;
1892        cx.shared_state().await.assert_eq("ˇone two three four");
1893        cx.simulate_shared_keystrokes("2 ;").await;
1894        cx.shared_state().await.assert_eq("one two three fˇour");
1895        cx.simulate_shared_keystrokes("shift-f e").await;
1896        cx.shared_state().await.assert_eq("one two threˇe four");
1897        cx.simulate_shared_keystrokes("2 ;").await;
1898        cx.shared_state().await.assert_eq("onˇe two three four");
1899        cx.simulate_shared_keystrokes(",").await;
1900        cx.shared_state().await.assert_eq("one two thrˇee four");
1901
1902        // t and T
1903        cx.set_shared_state("ˇone two three four").await;
1904        cx.simulate_shared_keystrokes("t o").await;
1905        cx.shared_state().await.assert_eq("one tˇwo three four");
1906        cx.simulate_shared_keystrokes(",").await;
1907        cx.shared_state().await.assert_eq("oˇne two three four");
1908        cx.simulate_shared_keystrokes("2 ;").await;
1909        cx.shared_state().await.assert_eq("one two three ˇfour");
1910        cx.simulate_shared_keystrokes("shift-t e").await;
1911        cx.shared_state().await.assert_eq("one two threeˇ four");
1912        cx.simulate_shared_keystrokes("3 ;").await;
1913        cx.shared_state().await.assert_eq("oneˇ two three four");
1914        cx.simulate_shared_keystrokes(",").await;
1915        cx.shared_state().await.assert_eq("one two thˇree four");
1916    }
1917
1918    #[gpui::test]
1919    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1920        let mut cx = NeovimBackedTestContext::new(cx).await;
1921        let initial_state = indoc! {r"something(ˇfoo)"};
1922        cx.set_shared_state(initial_state).await;
1923        cx.simulate_shared_keystrokes("}").await;
1924        cx.shared_state().await.assert_eq("something(fooˇ)");
1925    }
1926
1927    #[gpui::test]
1928    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1929        let mut cx = NeovimBackedTestContext::new(cx).await;
1930        cx.set_shared_state("ˇone\n  two\nthree").await;
1931        cx.simulate_shared_keystrokes("enter").await;
1932        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
1933    }
1934
1935    #[gpui::test]
1936    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1937        let mut cx = NeovimBackedTestContext::new(cx).await;
1938        cx.set_shared_state("ˇ one \n two \nthree").await;
1939        cx.simulate_shared_keystrokes("g _").await;
1940        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
1941        cx.simulate_shared_keystrokes("2 g _").await;
1942        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
1943    }
1944
1945    #[gpui::test]
1946    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1947        let mut cx = NeovimBackedTestContext::new(cx).await;
1948        let initial_state = indoc! {r"abc
1949          def
1950          paragraph
1951          the second
1952          third ˇand
1953          final"};
1954
1955        cx.set_shared_state(initial_state).await;
1956        cx.simulate_shared_keystrokes("shift-h").await;
1957        cx.shared_state().await.assert_eq(indoc! {r"abˇc
1958          def
1959          paragraph
1960          the second
1961          third and
1962          final"});
1963
1964        // clip point
1965        cx.set_shared_state(indoc! {r"
1966          1 2 3
1967          4 5 6
1968          7 8 ˇ9
1969          "})
1970            .await;
1971        cx.simulate_shared_keystrokes("shift-h").await;
1972        cx.shared_state().await.assert_eq(indoc! {"
1973          1 2 ˇ3
1974          4 5 6
1975          7 8 9
1976          "});
1977
1978        cx.set_shared_state(indoc! {r"
1979          1 2 3
1980          4 5 6
1981          ˇ7 8 9
1982          "})
1983            .await;
1984        cx.simulate_shared_keystrokes("shift-h").await;
1985        cx.shared_state().await.assert_eq(indoc! {"
1986          ˇ1 2 3
1987          4 5 6
1988          7 8 9
1989          "});
1990
1991        cx.set_shared_state(indoc! {r"
1992          1 2 3
1993          4 5 ˇ6
1994          7 8 9"})
1995            .await;
1996        cx.simulate_shared_keystrokes("9 shift-h").await;
1997        cx.shared_state().await.assert_eq(indoc! {"
1998          1 2 3
1999          4 5 6
2000          7 8 ˇ9"});
2001    }
2002
2003    #[gpui::test]
2004    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2005        let mut cx = NeovimBackedTestContext::new(cx).await;
2006        let initial_state = indoc! {r"abˇc
2007          def
2008          paragraph
2009          the second
2010          third and
2011          final"};
2012
2013        cx.set_shared_state(initial_state).await;
2014        cx.simulate_shared_keystrokes("shift-m").await;
2015        cx.shared_state().await.assert_eq(indoc! {r"abc
2016          def
2017          paˇragraph
2018          the second
2019          third and
2020          final"});
2021
2022        cx.set_shared_state(indoc! {r"
2023          1 2 3
2024          4 5 6
2025          7 8 ˇ9
2026          "})
2027            .await;
2028        cx.simulate_shared_keystrokes("shift-m").await;
2029        cx.shared_state().await.assert_eq(indoc! {"
2030          1 2 3
2031          4 5 ˇ6
2032          7 8 9
2033          "});
2034        cx.set_shared_state(indoc! {r"
2035          1 2 3
2036          4 5 6
2037          ˇ7 8 9
2038          "})
2039            .await;
2040        cx.simulate_shared_keystrokes("shift-m").await;
2041        cx.shared_state().await.assert_eq(indoc! {"
2042          1 2 3
2043          ˇ4 5 6
2044          7 8 9
2045          "});
2046        cx.set_shared_state(indoc! {r"
2047          ˇ1 2 3
2048          4 5 6
2049          7 8 9
2050          "})
2051            .await;
2052        cx.simulate_shared_keystrokes("shift-m").await;
2053        cx.shared_state().await.assert_eq(indoc! {"
2054          1 2 3
2055          ˇ4 5 6
2056          7 8 9
2057          "});
2058        cx.set_shared_state(indoc! {r"
2059          1 2 3
2060          ˇ4 5 6
2061          7 8 9
2062          "})
2063            .await;
2064        cx.simulate_shared_keystrokes("shift-m").await;
2065        cx.shared_state().await.assert_eq(indoc! {"
2066          1 2 3
2067          ˇ4 5 6
2068          7 8 9
2069          "});
2070        cx.set_shared_state(indoc! {r"
2071          1 2 3
2072          4 5 ˇ6
2073          7 8 9
2074          "})
2075            .await;
2076        cx.simulate_shared_keystrokes("shift-m").await;
2077        cx.shared_state().await.assert_eq(indoc! {"
2078          1 2 3
2079          4 5 ˇ6
2080          7 8 9
2081          "});
2082    }
2083
2084    #[gpui::test]
2085    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2086        let mut cx = NeovimBackedTestContext::new(cx).await;
2087        let initial_state = indoc! {r"abc
2088          deˇf
2089          paragraph
2090          the second
2091          third and
2092          final"};
2093
2094        cx.set_shared_state(initial_state).await;
2095        cx.simulate_shared_keystrokes("shift-l").await;
2096        cx.shared_state().await.assert_eq(indoc! {r"abc
2097          def
2098          paragraph
2099          the second
2100          third and
2101          fiˇnal"});
2102
2103        cx.set_shared_state(indoc! {r"
2104          1 2 3
2105          4 5 ˇ6
2106          7 8 9
2107          "})
2108            .await;
2109        cx.simulate_shared_keystrokes("shift-l").await;
2110        cx.shared_state().await.assert_eq(indoc! {"
2111          1 2 3
2112          4 5 6
2113          7 8 9
2114          ˇ"});
2115
2116        cx.set_shared_state(indoc! {r"
2117          1 2 3
2118          ˇ4 5 6
2119          7 8 9
2120          "})
2121            .await;
2122        cx.simulate_shared_keystrokes("shift-l").await;
2123        cx.shared_state().await.assert_eq(indoc! {"
2124          1 2 3
2125          4 5 6
2126          7 8 9
2127          ˇ"});
2128
2129        cx.set_shared_state(indoc! {r"
2130          1 2 ˇ3
2131          4 5 6
2132          7 8 9
2133          "})
2134            .await;
2135        cx.simulate_shared_keystrokes("shift-l").await;
2136        cx.shared_state().await.assert_eq(indoc! {"
2137          1 2 3
2138          4 5 6
2139          7 8 9
2140          ˇ"});
2141
2142        cx.set_shared_state(indoc! {r"
2143          ˇ1 2 3
2144          4 5 6
2145          7 8 9
2146          "})
2147            .await;
2148        cx.simulate_shared_keystrokes("shift-l").await;
2149        cx.shared_state().await.assert_eq(indoc! {"
2150          1 2 3
2151          4 5 6
2152          7 8 9
2153          ˇ"});
2154
2155        cx.set_shared_state(indoc! {r"
2156          1 2 3
2157          4 5 ˇ6
2158          7 8 9
2159          "})
2160            .await;
2161        cx.simulate_shared_keystrokes("9 shift-l").await;
2162        cx.shared_state().await.assert_eq(indoc! {"
2163          1 2 ˇ3
2164          4 5 6
2165          7 8 9
2166          "});
2167    }
2168
2169    #[gpui::test]
2170    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2171        let mut cx = NeovimBackedTestContext::new(cx).await;
2172        cx.set_shared_state(indoc! {r"
2173        456 5ˇ67 678
2174        "})
2175            .await;
2176        cx.simulate_shared_keystrokes("g e").await;
2177        cx.shared_state().await.assert_eq(indoc! {"
2178        45ˇ6 567 678
2179        "});
2180
2181        // Test times
2182        cx.set_shared_state(indoc! {r"
2183        123 234 345
2184        456 5ˇ67 678
2185        "})
2186            .await;
2187        cx.simulate_shared_keystrokes("4 g e").await;
2188        cx.shared_state().await.assert_eq(indoc! {"
2189        12ˇ3 234 345
2190        456 567 678
2191        "});
2192
2193        // With punctuation
2194        cx.set_shared_state(indoc! {r"
2195        123 234 345
2196        4;5.6 5ˇ67 678
2197        789 890 901
2198        "})
2199            .await;
2200        cx.simulate_shared_keystrokes("g e").await;
2201        cx.shared_state().await.assert_eq(indoc! {"
2202          123 234 345
2203          4;5.ˇ6 567 678
2204          789 890 901
2205        "});
2206
2207        // With punctuation and count
2208        cx.set_shared_state(indoc! {r"
2209        123 234 345
2210        4;5.6 5ˇ67 678
2211        789 890 901
2212        "})
2213            .await;
2214        cx.simulate_shared_keystrokes("5 g e").await;
2215        cx.shared_state().await.assert_eq(indoc! {"
2216          123 234 345
2217          ˇ4;5.6 567 678
2218          789 890 901
2219        "});
2220
2221        // newlines
2222        cx.set_shared_state(indoc! {r"
2223        123 234 345
2224
2225        78ˇ9 890 901
2226        "})
2227            .await;
2228        cx.simulate_shared_keystrokes("g e").await;
2229        cx.shared_state().await.assert_eq(indoc! {"
2230          123 234 345
2231          ˇ
2232          789 890 901
2233        "});
2234        cx.simulate_shared_keystrokes("g e").await;
2235        cx.shared_state().await.assert_eq(indoc! {"
2236          123 234 34ˇ5
2237
2238          789 890 901
2239        "});
2240
2241        // With punctuation
2242        cx.set_shared_state(indoc! {r"
2243        123 234 345
2244        4;5.ˇ6 567 678
2245        789 890 901
2246        "})
2247            .await;
2248        cx.simulate_shared_keystrokes("g shift-e").await;
2249        cx.shared_state().await.assert_eq(indoc! {"
2250          123 234 34ˇ5
2251          4;5.6 567 678
2252          789 890 901
2253        "});
2254    }
2255
2256    #[gpui::test]
2257    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2258        let mut cx = NeovimBackedTestContext::new(cx).await;
2259
2260        cx.set_shared_state(indoc! {"
2261            fn aˇ() {
2262              return
2263            }
2264        "})
2265            .await;
2266        cx.simulate_shared_keystrokes("v $ %").await;
2267        cx.shared_state().await.assert_eq(indoc! {"
2268            fn a«() {
2269              return
2270            }ˇ»
2271        "});
2272    }
2273}