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.assert_shared_state(indoc! {r"abc
1784            def
1785            ˇ
1786            paragraph
1787            the second
1788
1789
1790
1791            third and
1792            final"})
1793            .await;
1794
1795        // goes up once
1796        cx.simulate_shared_keystrokes(["{"]).await;
1797        cx.assert_shared_state(initial_state).await;
1798
1799        // goes down twice
1800        cx.simulate_shared_keystrokes(["2", "}"]).await;
1801        cx.assert_shared_state(indoc! {r"abc
1802            def
1803
1804            paragraph
1805            the second
1806            ˇ
1807
1808
1809            third and
1810            final"})
1811            .await;
1812
1813        // goes down over multiple blanks
1814        cx.simulate_shared_keystrokes(["}"]).await;
1815        cx.assert_shared_state(indoc! {r"abc
1816                def
1817
1818                paragraph
1819                the second
1820
1821
1822
1823                third and
1824                finaˇl"})
1825            .await;
1826
1827        // goes up twice
1828        cx.simulate_shared_keystrokes(["2", "{"]).await;
1829        cx.assert_shared_state(indoc! {r"abc
1830                def
1831                ˇ
1832                paragraph
1833                the second
1834
1835
1836
1837                third and
1838                final"})
1839            .await
1840    }
1841
1842    #[gpui::test]
1843    async fn test_matching(cx: &mut gpui::TestAppContext) {
1844        let mut cx = NeovimBackedTestContext::new(cx).await;
1845
1846        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1847                do(something(with<Types>.and_arrays[0, 2]))
1848            }"})
1849            .await;
1850        cx.simulate_shared_keystrokes(["%"]).await;
1851        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1852                do(something(with<Types>.and_arrays[0, 2]))
1853            }"})
1854            .await;
1855
1856        // test it works on the last character of the line
1857        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1858            do(something(with<Types>.and_arrays[0, 2]))
1859            }"})
1860            .await;
1861        cx.simulate_shared_keystrokes(["%"]).await;
1862        cx.assert_shared_state(indoc! {r"func (a string) {
1863            do(something(with<Types>.and_arrays[0, 2]))
1864            ˇ}"})
1865            .await;
1866
1867        // test it works on immediate nesting
1868        cx.set_shared_state("ˇ{()}").await;
1869        cx.simulate_shared_keystrokes(["%"]).await;
1870        cx.assert_shared_state("{()ˇ}").await;
1871        cx.simulate_shared_keystrokes(["%"]).await;
1872        cx.assert_shared_state("ˇ{()}").await;
1873
1874        // test it works on immediate nesting inside braces
1875        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1876        cx.simulate_shared_keystrokes(["%"]).await;
1877        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1878
1879        // test it jumps to the next paren on a line
1880        cx.set_shared_state("func ˇboop() {\n}").await;
1881        cx.simulate_shared_keystrokes(["%"]).await;
1882        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1883    }
1884
1885    #[gpui::test]
1886    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1887        let mut cx = NeovimBackedTestContext::new(cx).await;
1888
1889        // f and F
1890        cx.set_shared_state("ˇone two three four").await;
1891        cx.simulate_shared_keystrokes(["f", "o"]).await;
1892        cx.assert_shared_state("one twˇo three four").await;
1893        cx.simulate_shared_keystrokes([","]).await;
1894        cx.assert_shared_state("ˇone two three four").await;
1895        cx.simulate_shared_keystrokes(["2", ";"]).await;
1896        cx.assert_shared_state("one two three fˇour").await;
1897        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1898        cx.assert_shared_state("one two threˇe four").await;
1899        cx.simulate_shared_keystrokes(["2", ";"]).await;
1900        cx.assert_shared_state("onˇe two three four").await;
1901        cx.simulate_shared_keystrokes([","]).await;
1902        cx.assert_shared_state("one two thrˇee four").await;
1903
1904        // t and T
1905        cx.set_shared_state("ˇone two three four").await;
1906        cx.simulate_shared_keystrokes(["t", "o"]).await;
1907        cx.assert_shared_state("one tˇwo three four").await;
1908        cx.simulate_shared_keystrokes([","]).await;
1909        cx.assert_shared_state("oˇne two three four").await;
1910        cx.simulate_shared_keystrokes(["2", ";"]).await;
1911        cx.assert_shared_state("one two three ˇfour").await;
1912        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1913        cx.assert_shared_state("one two threeˇ four").await;
1914        cx.simulate_shared_keystrokes(["3", ";"]).await;
1915        cx.assert_shared_state("oneˇ two three four").await;
1916        cx.simulate_shared_keystrokes([","]).await;
1917        cx.assert_shared_state("one two thˇree four").await;
1918    }
1919
1920    #[gpui::test]
1921    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1922        let mut cx = NeovimBackedTestContext::new(cx).await;
1923        let initial_state = indoc! {r"something(ˇfoo)"};
1924        cx.set_shared_state(initial_state).await;
1925        cx.simulate_shared_keystrokes(["}"]).await;
1926        cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1927    }
1928
1929    #[gpui::test]
1930    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1931        let mut cx = NeovimBackedTestContext::new(cx).await;
1932        cx.set_shared_state("ˇone\n  two\nthree").await;
1933        cx.simulate_shared_keystrokes(["enter"]).await;
1934        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1935    }
1936
1937    #[gpui::test]
1938    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1939        let mut cx = NeovimBackedTestContext::new(cx).await;
1940        cx.set_shared_state("ˇ one \n two \nthree").await;
1941        cx.simulate_shared_keystrokes(["g", "_"]).await;
1942        cx.assert_shared_state(" onˇe \n two \nthree").await;
1943        cx.simulate_shared_keystrokes(["2", "g", "_"]).await;
1944        cx.assert_shared_state(" one \n twˇo \nthree").await;
1945    }
1946
1947    #[gpui::test]
1948    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1949        let mut cx = NeovimBackedTestContext::new(cx).await;
1950        let initial_state = indoc! {r"abc
1951          def
1952          paragraph
1953          the second
1954          third ˇand
1955          final"};
1956
1957        cx.set_shared_state(initial_state).await;
1958        cx.simulate_shared_keystrokes(["shift-h"]).await;
1959        cx.assert_shared_state(indoc! {r"abˇc
1960          def
1961          paragraph
1962          the second
1963          third and
1964          final"})
1965            .await;
1966
1967        // clip point
1968        cx.set_shared_state(indoc! {r"
1969          1 2 3
1970          4 5 6
1971          7 8 ˇ9
1972          "})
1973            .await;
1974        cx.simulate_shared_keystrokes(["shift-h"]).await;
1975        cx.assert_shared_state(indoc! {r"
1976          1 2 ˇ3
1977          4 5 6
1978          7 8 9
1979          "})
1980            .await;
1981
1982        cx.set_shared_state(indoc! {r"
1983          1 2 3
1984          4 5 6
1985          ˇ7 8 9
1986          "})
1987            .await;
1988        cx.simulate_shared_keystrokes(["shift-h"]).await;
1989        cx.assert_shared_state(indoc! {r"
1990          ˇ1 2 3
1991          4 5 6
1992          7 8 9
1993          "})
1994            .await;
1995
1996        cx.set_shared_state(indoc! {r"
1997          1 2 3
1998          4 5 ˇ6
1999          7 8 9"})
2000            .await;
2001        cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
2002        cx.assert_shared_state(indoc! {r"
2003          1 2 3
2004          4 5 6
2005          7 8 ˇ9"})
2006            .await;
2007    }
2008
2009    #[gpui::test]
2010    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2011        let mut cx = NeovimBackedTestContext::new(cx).await;
2012        let initial_state = indoc! {r"abˇc
2013          def
2014          paragraph
2015          the second
2016          third and
2017          final"};
2018
2019        cx.set_shared_state(initial_state).await;
2020        cx.simulate_shared_keystrokes(["shift-m"]).await;
2021        cx.assert_shared_state(indoc! {r"abc
2022          def
2023          paˇragraph
2024          the second
2025          third and
2026          final"})
2027            .await;
2028
2029        cx.set_shared_state(indoc! {r"
2030          1 2 3
2031          4 5 6
2032          7 8 ˇ9
2033          "})
2034            .await;
2035        cx.simulate_shared_keystrokes(["shift-m"]).await;
2036        cx.assert_shared_state(indoc! {r"
2037          1 2 3
2038          4 5 ˇ6
2039          7 8 9
2040          "})
2041            .await;
2042        cx.set_shared_state(indoc! {r"
2043          1 2 3
2044          4 5 6
2045          ˇ7 8 9
2046          "})
2047            .await;
2048        cx.simulate_shared_keystrokes(["shift-m"]).await;
2049        cx.assert_shared_state(indoc! {r"
2050          1 2 3
2051          ˇ4 5 6
2052          7 8 9
2053          "})
2054            .await;
2055        cx.set_shared_state(indoc! {r"
2056          ˇ1 2 3
2057          4 5 6
2058          7 8 9
2059          "})
2060            .await;
2061        cx.simulate_shared_keystrokes(["shift-m"]).await;
2062        cx.assert_shared_state(indoc! {r"
2063          1 2 3
2064          ˇ4 5 6
2065          7 8 9
2066          "})
2067            .await;
2068        cx.set_shared_state(indoc! {r"
2069          1 2 3
2070          ˇ4 5 6
2071          7 8 9
2072          "})
2073            .await;
2074        cx.simulate_shared_keystrokes(["shift-m"]).await;
2075        cx.assert_shared_state(indoc! {r"
2076          1 2 3
2077          ˇ4 5 6
2078          7 8 9
2079          "})
2080            .await;
2081        cx.set_shared_state(indoc! {r"
2082          1 2 3
2083          4 5 ˇ6
2084          7 8 9
2085          "})
2086            .await;
2087        cx.simulate_shared_keystrokes(["shift-m"]).await;
2088        cx.assert_shared_state(indoc! {r"
2089          1 2 3
2090          4 5 ˇ6
2091          7 8 9
2092          "})
2093            .await;
2094    }
2095
2096    #[gpui::test]
2097    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2098        let mut cx = NeovimBackedTestContext::new(cx).await;
2099        let initial_state = indoc! {r"abc
2100          deˇf
2101          paragraph
2102          the second
2103          third and
2104          final"};
2105
2106        cx.set_shared_state(initial_state).await;
2107        cx.simulate_shared_keystrokes(["shift-l"]).await;
2108        cx.assert_shared_state(indoc! {r"abc
2109          def
2110          paragraph
2111          the second
2112          third and
2113          fiˇnal"})
2114            .await;
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.assert_shared_state(indoc! {r"
2124          1 2 3
2125          4 5 6
2126          7 8 9
2127          ˇ"})
2128            .await;
2129
2130        cx.set_shared_state(indoc! {r"
2131          1 2 3
2132          ˇ4 5 6
2133          7 8 9
2134          "})
2135            .await;
2136        cx.simulate_shared_keystrokes(["shift-l"]).await;
2137        cx.assert_shared_state(indoc! {r"
2138          1 2 3
2139          4 5 6
2140          7 8 9
2141          ˇ"})
2142            .await;
2143
2144        cx.set_shared_state(indoc! {r"
2145          1 2 ˇ3
2146          4 5 6
2147          7 8 9
2148          "})
2149            .await;
2150        cx.simulate_shared_keystrokes(["shift-l"]).await;
2151        cx.assert_shared_state(indoc! {r"
2152          1 2 3
2153          4 5 6
2154          7 8 9
2155          ˇ"})
2156            .await;
2157
2158        cx.set_shared_state(indoc! {r"
2159          ˇ1 2 3
2160          4 5 6
2161          7 8 9
2162          "})
2163            .await;
2164        cx.simulate_shared_keystrokes(["shift-l"]).await;
2165        cx.assert_shared_state(indoc! {r"
2166          1 2 3
2167          4 5 6
2168          7 8 9
2169          ˇ"})
2170            .await;
2171
2172        cx.set_shared_state(indoc! {r"
2173          1 2 3
2174          4 5 ˇ6
2175          7 8 9
2176          "})
2177            .await;
2178        cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
2179        cx.assert_shared_state(indoc! {r"
2180          1 2 ˇ3
2181          4 5 6
2182          7 8 9
2183          "})
2184            .await;
2185    }
2186
2187    #[gpui::test]
2188    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2189        let mut cx = NeovimBackedTestContext::new(cx).await;
2190        cx.set_shared_state(indoc! {r"
2191        456 5ˇ67 678
2192        "})
2193            .await;
2194        cx.simulate_shared_keystrokes(["g", "e"]).await;
2195        cx.assert_shared_state(indoc! {r"
2196        45ˇ6 567 678
2197        "})
2198            .await;
2199
2200        // Test times
2201        cx.set_shared_state(indoc! {r"
2202        123 234 345
2203        456 5ˇ67 678
2204        "})
2205            .await;
2206        cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2207        cx.assert_shared_state(indoc! {r"
2208        12ˇ3 234 345
2209        456 567 678
2210        "})
2211            .await;
2212
2213        // With punctuation
2214        cx.set_shared_state(indoc! {r"
2215        123 234 345
2216        4;5.6 5ˇ67 678
2217        789 890 901
2218        "})
2219            .await;
2220        cx.simulate_shared_keystrokes(["g", "e"]).await;
2221        cx.assert_shared_state(indoc! {r"
2222          123 234 345
2223          4;5.ˇ6 567 678
2224          789 890 901
2225        "})
2226            .await;
2227
2228        // With punctuation and count
2229        cx.set_shared_state(indoc! {r"
2230        123 234 345
2231        4;5.6 5ˇ67 678
2232        789 890 901
2233        "})
2234            .await;
2235        cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2236        cx.assert_shared_state(indoc! {r"
2237          123 234 345
2238          ˇ4;5.6 567 678
2239          789 890 901
2240        "})
2241            .await;
2242
2243        // newlines
2244        cx.set_shared_state(indoc! {r"
2245        123 234 345
2246
2247        78ˇ9 890 901
2248        "})
2249            .await;
2250        cx.simulate_shared_keystrokes(["g", "e"]).await;
2251        cx.assert_shared_state(indoc! {r"
2252          123 234 345
2253          ˇ
2254          789 890 901
2255        "})
2256            .await;
2257        cx.simulate_shared_keystrokes(["g", "e"]).await;
2258        cx.assert_shared_state(indoc! {r"
2259          123 234 34ˇ5
2260
2261          789 890 901
2262        "})
2263            .await;
2264
2265        // With punctuation
2266        cx.set_shared_state(indoc! {r"
2267        123 234 345
2268        4;5.ˇ6 567 678
2269        789 890 901
2270        "})
2271            .await;
2272        cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2273        cx.assert_shared_state(indoc! {r"
2274          123 234 34ˇ5
2275          4;5.6 567 678
2276          789 890 901
2277        "})
2278            .await;
2279    }
2280
2281    #[gpui::test]
2282    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2283        let mut cx = NeovimBackedTestContext::new(cx).await;
2284
2285        cx.set_shared_state(indoc! {"
2286            fn aˇ() {
2287              return
2288            }
2289        "})
2290            .await;
2291        cx.simulate_shared_keystrokes(["v", "$", "%"]).await;
2292        cx.assert_shared_state(indoc! {"
2293            fn a«() {
2294              return
2295            }ˇ»
2296        "})
2297            .await;
2298    }
2299}