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