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::{char_kind, 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 scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1135    for _ in 0..times {
1136        let mut crossed_newline = false;
1137        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1138            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1139            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1140            let at_newline = right == '\n';
1141
1142            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1143                || at_newline && crossed_newline
1144                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1145
1146            crossed_newline |= at_newline;
1147            found
1148        });
1149        if point == new_point {
1150            break;
1151        }
1152        point = new_point;
1153    }
1154    point
1155}
1156
1157pub(crate) fn next_word_end(
1158    map: &DisplaySnapshot,
1159    mut point: DisplayPoint,
1160    ignore_punctuation: bool,
1161    times: usize,
1162    allow_cross_newline: bool,
1163) -> DisplayPoint {
1164    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1165    for _ in 0..times {
1166        let new_point = next_char(map, point, allow_cross_newline);
1167        let mut need_next_char = false;
1168        let new_point = movement::find_boundary_exclusive(
1169            map,
1170            new_point,
1171            FindRange::MultiLine,
1172            |left, right| {
1173                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1174                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1175                let at_newline = right == '\n';
1176
1177                if !allow_cross_newline && at_newline {
1178                    need_next_char = true;
1179                    return true;
1180                }
1181
1182                left_kind != right_kind && left_kind != CharKind::Whitespace
1183            },
1184        );
1185        let new_point = if need_next_char {
1186            next_char(map, new_point, true)
1187        } else {
1188            new_point
1189        };
1190        let new_point = map.clip_point(new_point, Bias::Left);
1191        if point == new_point {
1192            break;
1193        }
1194        point = new_point;
1195    }
1196    point
1197}
1198
1199fn previous_word_start(
1200    map: &DisplaySnapshot,
1201    mut point: DisplayPoint,
1202    ignore_punctuation: bool,
1203    times: usize,
1204) -> DisplayPoint {
1205    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1206    for _ in 0..times {
1207        // This works even though find_preceding_boundary is called for every character in the line containing
1208        // cursor because the newline is checked only once.
1209        let new_point = movement::find_preceding_boundary_display_point(
1210            map,
1211            point,
1212            FindRange::MultiLine,
1213            |left, right| {
1214                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1215                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1216
1217                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1218            },
1219        );
1220        if point == new_point {
1221            break;
1222        }
1223        point = new_point;
1224    }
1225    point
1226}
1227
1228fn previous_word_end(
1229    map: &DisplaySnapshot,
1230    point: DisplayPoint,
1231    ignore_punctuation: bool,
1232    times: usize,
1233) -> DisplayPoint {
1234    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1235    let mut point = point.to_point(map);
1236
1237    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1238        point.column += 1;
1239    }
1240    for _ in 0..times {
1241        let new_point = movement::find_preceding_boundary_point(
1242            &map.buffer_snapshot,
1243            point,
1244            FindRange::MultiLine,
1245            |left, right| {
1246                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1247                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1248                match (left_kind, right_kind) {
1249                    (CharKind::Punctuation, CharKind::Whitespace)
1250                    | (CharKind::Punctuation, CharKind::Word)
1251                    | (CharKind::Word, CharKind::Whitespace)
1252                    | (CharKind::Word, CharKind::Punctuation) => true,
1253                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1254                    _ => false,
1255                }
1256            },
1257        );
1258        if new_point == point {
1259            break;
1260        }
1261        point = new_point;
1262    }
1263    movement::saturating_left(map, point.to_display_point(map))
1264}
1265
1266fn next_subword_start(
1267    map: &DisplaySnapshot,
1268    mut point: DisplayPoint,
1269    ignore_punctuation: bool,
1270    times: usize,
1271) -> DisplayPoint {
1272    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1273    for _ in 0..times {
1274        let mut crossed_newline = false;
1275        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1276            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1277            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1278            let at_newline = right == '\n';
1279
1280            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1281            let is_subword_start =
1282                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1283
1284            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1285                || at_newline && crossed_newline
1286                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1287
1288            crossed_newline |= at_newline;
1289            found
1290        });
1291        if point == new_point {
1292            break;
1293        }
1294        point = new_point;
1295    }
1296    point
1297}
1298
1299pub(crate) fn next_subword_end(
1300    map: &DisplaySnapshot,
1301    mut point: DisplayPoint,
1302    ignore_punctuation: bool,
1303    times: usize,
1304    allow_cross_newline: bool,
1305) -> DisplayPoint {
1306    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1307    for _ in 0..times {
1308        let new_point = next_char(map, point, allow_cross_newline);
1309
1310        let mut crossed_newline = false;
1311        let mut need_backtrack = false;
1312        let new_point =
1313            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1314                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1315                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1316                let at_newline = right == '\n';
1317
1318                if !allow_cross_newline && at_newline {
1319                    return true;
1320                }
1321
1322                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1323                let is_subword_end =
1324                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1325
1326                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1327
1328                if found && (is_word_end || is_subword_end) {
1329                    need_backtrack = true;
1330                }
1331
1332                crossed_newline |= at_newline;
1333                found
1334            });
1335        let mut new_point = map.clip_point(new_point, Bias::Left);
1336        if need_backtrack {
1337            *new_point.column_mut() -= 1;
1338        }
1339        if point == new_point {
1340            break;
1341        }
1342        point = new_point;
1343    }
1344    point
1345}
1346
1347fn previous_subword_start(
1348    map: &DisplaySnapshot,
1349    mut point: DisplayPoint,
1350    ignore_punctuation: bool,
1351    times: usize,
1352) -> DisplayPoint {
1353    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1354    for _ in 0..times {
1355        let mut crossed_newline = false;
1356        // This works even though find_preceding_boundary is called for every character in the line containing
1357        // cursor because the newline is checked only once.
1358        let new_point = movement::find_preceding_boundary_display_point(
1359            map,
1360            point,
1361            FindRange::MultiLine,
1362            |left, right| {
1363                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1364                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1365                let at_newline = right == '\n';
1366
1367                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1368                let is_subword_start =
1369                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1370
1371                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1372                    || at_newline && crossed_newline
1373                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1374
1375                crossed_newline |= at_newline;
1376
1377                found
1378            },
1379        );
1380        if point == new_point {
1381            break;
1382        }
1383        point = new_point;
1384    }
1385    point
1386}
1387
1388fn previous_subword_end(
1389    map: &DisplaySnapshot,
1390    point: DisplayPoint,
1391    ignore_punctuation: bool,
1392    times: usize,
1393) -> DisplayPoint {
1394    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1395    let mut point = point.to_point(map);
1396
1397    if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1398        point.column += 1;
1399    }
1400    for _ in 0..times {
1401        let new_point = movement::find_preceding_boundary_point(
1402            &map.buffer_snapshot,
1403            point,
1404            FindRange::MultiLine,
1405            |left, right| {
1406                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1407                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1408
1409                let is_subword_end =
1410                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1411
1412                if is_subword_end {
1413                    return true;
1414                }
1415
1416                match (left_kind, right_kind) {
1417                    (CharKind::Word, CharKind::Whitespace)
1418                    | (CharKind::Word, CharKind::Punctuation) => true,
1419                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1420                    _ => false,
1421                }
1422            },
1423        );
1424        if new_point == point {
1425            break;
1426        }
1427        point = new_point;
1428    }
1429    movement::saturating_left(map, point.to_display_point(map))
1430}
1431
1432pub(crate) fn first_non_whitespace(
1433    map: &DisplaySnapshot,
1434    display_lines: bool,
1435    from: DisplayPoint,
1436) -> DisplayPoint {
1437    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1438    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1439    for (ch, offset) in map.buffer_chars_at(start_offset) {
1440        if ch == '\n' {
1441            return from;
1442        }
1443
1444        start_offset = offset;
1445
1446        if char_kind(&scope, ch) != CharKind::Whitespace {
1447            break;
1448        }
1449    }
1450
1451    start_offset.to_display_point(map)
1452}
1453
1454pub(crate) fn last_non_whitespace(
1455    map: &DisplaySnapshot,
1456    from: DisplayPoint,
1457    count: usize,
1458) -> DisplayPoint {
1459    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1460    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1461
1462    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1463    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1464        if char_kind(&scope, ch) != CharKind::Whitespace {
1465            return end_of_line.to_display_point(map);
1466        }
1467    }
1468
1469    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1470        if ch == '\n' {
1471            break;
1472        }
1473        end_of_line = offset;
1474        if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
1475            break;
1476        }
1477    }
1478
1479    end_of_line.to_display_point(map)
1480}
1481
1482pub(crate) fn start_of_line(
1483    map: &DisplaySnapshot,
1484    display_lines: bool,
1485    point: DisplayPoint,
1486) -> DisplayPoint {
1487    if display_lines {
1488        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1489    } else {
1490        map.prev_line_boundary(point.to_point(map)).1
1491    }
1492}
1493
1494pub(crate) fn end_of_line(
1495    map: &DisplaySnapshot,
1496    display_lines: bool,
1497    mut point: DisplayPoint,
1498    times: usize,
1499) -> DisplayPoint {
1500    if times > 1 {
1501        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1502    }
1503    if display_lines {
1504        map.clip_point(
1505            DisplayPoint::new(point.row(), map.line_len(point.row())),
1506            Bias::Left,
1507        )
1508    } else {
1509        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1510    }
1511}
1512
1513fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1514    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1515    *new_point.column_mut() = point.column();
1516    map.clip_point(new_point, Bias::Left)
1517}
1518
1519fn end_of_document(
1520    map: &DisplaySnapshot,
1521    point: DisplayPoint,
1522    line: Option<usize>,
1523) -> DisplayPoint {
1524    let new_row = if let Some(line) = line {
1525        (line - 1) as u32
1526    } else {
1527        map.max_buffer_row().0
1528    };
1529
1530    let new_point = Point::new(new_row, point.column());
1531    map.clip_point(new_point.to_display_point(map), Bias::Left)
1532}
1533
1534fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1535    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1536    let display_point = map.clip_at_line_end(display_point);
1537    let point = display_point.to_point(map);
1538    let offset = point.to_offset(&map.buffer_snapshot);
1539
1540    // Ensure the range is contained by the current line.
1541    let mut line_end = map.next_line_boundary(point).0;
1542    if line_end == point {
1543        line_end = map.max_point().to_point(map);
1544    }
1545
1546    let line_range = map.prev_line_boundary(point).0..line_end;
1547    let visible_line_range =
1548        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1549    let ranges = map
1550        .buffer_snapshot
1551        .bracket_ranges(visible_line_range.clone());
1552    if let Some(ranges) = ranges {
1553        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1554            ..line_range.end.to_offset(&map.buffer_snapshot);
1555        let mut closest_pair_destination = None;
1556        let mut closest_distance = usize::MAX;
1557
1558        for (open_range, close_range) in ranges {
1559            if open_range.start >= offset && line_range.contains(&open_range.start) {
1560                let distance = open_range.start - offset;
1561                if distance < closest_distance {
1562                    closest_pair_destination = Some(close_range.start);
1563                    closest_distance = distance;
1564                    continue;
1565                }
1566            }
1567
1568            if close_range.start >= offset && line_range.contains(&close_range.start) {
1569                let distance = close_range.start - offset;
1570                if distance < closest_distance {
1571                    closest_pair_destination = Some(open_range.start);
1572                    closest_distance = distance;
1573                    continue;
1574                }
1575            }
1576
1577            continue;
1578        }
1579
1580        closest_pair_destination
1581            .map(|destination| destination.to_display_point(map))
1582            .unwrap_or(display_point)
1583    } else {
1584        display_point
1585    }
1586}
1587
1588fn find_forward(
1589    map: &DisplaySnapshot,
1590    from: DisplayPoint,
1591    before: bool,
1592    target: char,
1593    times: usize,
1594    mode: FindRange,
1595    smartcase: bool,
1596) -> Option<DisplayPoint> {
1597    let mut to = from;
1598    let mut found = false;
1599
1600    for _ in 0..times {
1601        found = false;
1602        let new_to = find_boundary(map, to, mode, |_, right| {
1603            found = is_character_match(target, right, smartcase);
1604            found
1605        });
1606        if to == new_to {
1607            break;
1608        }
1609        to = new_to;
1610    }
1611
1612    if found {
1613        if before && to.column() > 0 {
1614            *to.column_mut() -= 1;
1615            Some(map.clip_point(to, Bias::Left))
1616        } else {
1617            Some(to)
1618        }
1619    } else {
1620        None
1621    }
1622}
1623
1624fn find_backward(
1625    map: &DisplaySnapshot,
1626    from: DisplayPoint,
1627    after: bool,
1628    target: char,
1629    times: usize,
1630    mode: FindRange,
1631    smartcase: bool,
1632) -> DisplayPoint {
1633    let mut to = from;
1634
1635    for _ in 0..times {
1636        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1637            is_character_match(target, right, smartcase)
1638        });
1639        if to == new_to {
1640            break;
1641        }
1642        to = new_to;
1643    }
1644
1645    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1646    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1647        if after {
1648            *to.column_mut() += 1;
1649            map.clip_point(to, Bias::Right)
1650        } else {
1651            to
1652        }
1653    } else {
1654        from
1655    }
1656}
1657
1658fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1659    if smartcase {
1660        if target.is_uppercase() {
1661            target == other
1662        } else {
1663            target == other.to_ascii_lowercase()
1664        }
1665    } else {
1666        target == other
1667    }
1668}
1669
1670fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1671    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1672    first_non_whitespace(map, false, correct_line)
1673}
1674
1675fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1676    let correct_line = start_of_relative_buffer_row(map, point, (times as isize) * -1);
1677    first_non_whitespace(map, false, correct_line)
1678}
1679
1680fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1681    let correct_line = start_of_relative_buffer_row(map, point, 0);
1682    right(map, correct_line, times.saturating_sub(1))
1683}
1684
1685pub(crate) fn next_line_end(
1686    map: &DisplaySnapshot,
1687    mut point: DisplayPoint,
1688    times: usize,
1689) -> DisplayPoint {
1690    if times > 1 {
1691        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1692    }
1693    end_of_line(map, false, point, 1)
1694}
1695
1696fn window_top(
1697    map: &DisplaySnapshot,
1698    point: DisplayPoint,
1699    text_layout_details: &TextLayoutDetails,
1700    mut times: usize,
1701) -> (DisplayPoint, SelectionGoal) {
1702    let first_visible_line = text_layout_details
1703        .scroll_anchor
1704        .anchor
1705        .to_display_point(map);
1706
1707    if first_visible_line.row() != DisplayRow(0)
1708        && text_layout_details.vertical_scroll_margin as usize > times
1709    {
1710        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1711    }
1712
1713    if let Some(visible_rows) = text_layout_details.visible_rows {
1714        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1715        let new_row = (first_visible_line.row().0 + (times as u32))
1716            .min(bottom_row)
1717            .min(map.max_point().row().0);
1718        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1719
1720        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1721        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1722    } else {
1723        let new_row =
1724            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1725        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1726
1727        let new_point = DisplayPoint::new(new_row, new_col);
1728        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1729    }
1730}
1731
1732fn window_middle(
1733    map: &DisplaySnapshot,
1734    point: DisplayPoint,
1735    text_layout_details: &TextLayoutDetails,
1736) -> (DisplayPoint, SelectionGoal) {
1737    if let Some(visible_rows) = text_layout_details.visible_rows {
1738        let first_visible_line = text_layout_details
1739            .scroll_anchor
1740            .anchor
1741            .to_display_point(map);
1742
1743        let max_visible_rows =
1744            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1745
1746        let new_row =
1747            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1748        let new_row = DisplayRow(new_row);
1749        let new_col = point.column().min(map.line_len(new_row));
1750        let new_point = DisplayPoint::new(new_row, new_col);
1751        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1752    } else {
1753        (point, SelectionGoal::None)
1754    }
1755}
1756
1757fn window_bottom(
1758    map: &DisplaySnapshot,
1759    point: DisplayPoint,
1760    text_layout_details: &TextLayoutDetails,
1761    mut times: usize,
1762) -> (DisplayPoint, SelectionGoal) {
1763    if let Some(visible_rows) = text_layout_details.visible_rows {
1764        let first_visible_line = text_layout_details
1765            .scroll_anchor
1766            .anchor
1767            .to_display_point(map);
1768        let bottom_row = first_visible_line.row().0
1769            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1770        if bottom_row < map.max_point().row().0
1771            && text_layout_details.vertical_scroll_margin as usize > times
1772        {
1773            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1774        }
1775        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1776        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1777        {
1778            first_visible_line.row()
1779        } else {
1780            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1781        };
1782        let new_col = point.column().min(map.line_len(new_row));
1783        let new_point = DisplayPoint::new(new_row, new_col);
1784        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1785    } else {
1786        (point, SelectionGoal::None)
1787    }
1788}
1789
1790pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind {
1791    if treat_punctuation_as_word && kind == CharKind::Punctuation {
1792        CharKind::Word
1793    } else {
1794        kind
1795    }
1796}
1797
1798#[cfg(test)]
1799mod test {
1800
1801    use crate::test::NeovimBackedTestContext;
1802    use indoc::indoc;
1803
1804    #[gpui::test]
1805    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1806        let mut cx = NeovimBackedTestContext::new(cx).await;
1807
1808        let initial_state = indoc! {r"ˇabc
1809            def
1810
1811            paragraph
1812            the second
1813
1814
1815
1816            third and
1817            final"};
1818
1819        // goes down once
1820        cx.set_shared_state(initial_state).await;
1821        cx.simulate_shared_keystrokes("}").await;
1822        cx.shared_state().await.assert_eq(indoc! {r"abc
1823            def
1824            ˇ
1825            paragraph
1826            the second
1827
1828
1829
1830            third and
1831            final"});
1832
1833        // goes up once
1834        cx.simulate_shared_keystrokes("{").await;
1835        cx.shared_state().await.assert_eq(initial_state);
1836
1837        // goes down twice
1838        cx.simulate_shared_keystrokes("2 }").await;
1839        cx.shared_state().await.assert_eq(indoc! {r"abc
1840            def
1841
1842            paragraph
1843            the second
1844            ˇ
1845
1846
1847            third and
1848            final"});
1849
1850        // goes down over multiple blanks
1851        cx.simulate_shared_keystrokes("}").await;
1852        cx.shared_state().await.assert_eq(indoc! {r"abc
1853                def
1854
1855                paragraph
1856                the second
1857
1858
1859
1860                third and
1861                finaˇl"});
1862
1863        // goes up twice
1864        cx.simulate_shared_keystrokes("2 {").await;
1865        cx.shared_state().await.assert_eq(indoc! {r"abc
1866                def
1867                ˇ
1868                paragraph
1869                the second
1870
1871
1872
1873                third and
1874                final"});
1875    }
1876
1877    #[gpui::test]
1878    async fn test_matching(cx: &mut gpui::TestAppContext) {
1879        let mut cx = NeovimBackedTestContext::new(cx).await;
1880
1881        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1882                do(something(with<Types>.and_arrays[0, 2]))
1883            }"})
1884            .await;
1885        cx.simulate_shared_keystrokes("%").await;
1886        cx.shared_state()
1887            .await
1888            .assert_eq(indoc! {r"func (a stringˇ) {
1889                do(something(with<Types>.and_arrays[0, 2]))
1890            }"});
1891
1892        // test it works on the last character of the line
1893        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1894            do(something(with<Types>.and_arrays[0, 2]))
1895            }"})
1896            .await;
1897        cx.simulate_shared_keystrokes("%").await;
1898        cx.shared_state()
1899            .await
1900            .assert_eq(indoc! {r"func (a string) {
1901            do(something(with<Types>.and_arrays[0, 2]))
1902            ˇ}"});
1903
1904        // test it works on immediate nesting
1905        cx.set_shared_state("ˇ{()}").await;
1906        cx.simulate_shared_keystrokes("%").await;
1907        cx.shared_state().await.assert_eq("{()ˇ}");
1908        cx.simulate_shared_keystrokes("%").await;
1909        cx.shared_state().await.assert_eq("ˇ{()}");
1910
1911        // test it works on immediate nesting inside braces
1912        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1913        cx.simulate_shared_keystrokes("%").await;
1914        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
1915
1916        // test it jumps to the next paren on a line
1917        cx.set_shared_state("func ˇboop() {\n}").await;
1918        cx.simulate_shared_keystrokes("%").await;
1919        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
1920    }
1921
1922    #[gpui::test]
1923    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1924        let mut cx = NeovimBackedTestContext::new(cx).await;
1925
1926        // f and F
1927        cx.set_shared_state("ˇone two three four").await;
1928        cx.simulate_shared_keystrokes("f o").await;
1929        cx.shared_state().await.assert_eq("one twˇo three four");
1930        cx.simulate_shared_keystrokes(",").await;
1931        cx.shared_state().await.assert_eq("ˇone two three four");
1932        cx.simulate_shared_keystrokes("2 ;").await;
1933        cx.shared_state().await.assert_eq("one two three fˇour");
1934        cx.simulate_shared_keystrokes("shift-f e").await;
1935        cx.shared_state().await.assert_eq("one two threˇe four");
1936        cx.simulate_shared_keystrokes("2 ;").await;
1937        cx.shared_state().await.assert_eq("onˇe two three four");
1938        cx.simulate_shared_keystrokes(",").await;
1939        cx.shared_state().await.assert_eq("one two thrˇee four");
1940
1941        // t and T
1942        cx.set_shared_state("ˇone two three four").await;
1943        cx.simulate_shared_keystrokes("t o").await;
1944        cx.shared_state().await.assert_eq("one tˇwo three four");
1945        cx.simulate_shared_keystrokes(",").await;
1946        cx.shared_state().await.assert_eq("oˇne two three four");
1947        cx.simulate_shared_keystrokes("2 ;").await;
1948        cx.shared_state().await.assert_eq("one two three ˇfour");
1949        cx.simulate_shared_keystrokes("shift-t e").await;
1950        cx.shared_state().await.assert_eq("one two threeˇ four");
1951        cx.simulate_shared_keystrokes("3 ;").await;
1952        cx.shared_state().await.assert_eq("oneˇ two three four");
1953        cx.simulate_shared_keystrokes(",").await;
1954        cx.shared_state().await.assert_eq("one two thˇree four");
1955    }
1956
1957    #[gpui::test]
1958    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1959        let mut cx = NeovimBackedTestContext::new(cx).await;
1960        let initial_state = indoc! {r"something(ˇfoo)"};
1961        cx.set_shared_state(initial_state).await;
1962        cx.simulate_shared_keystrokes("}").await;
1963        cx.shared_state().await.assert_eq("something(fooˇ)");
1964    }
1965
1966    #[gpui::test]
1967    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1968        let mut cx = NeovimBackedTestContext::new(cx).await;
1969        cx.set_shared_state("ˇone\n  two\nthree").await;
1970        cx.simulate_shared_keystrokes("enter").await;
1971        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
1972    }
1973
1974    #[gpui::test]
1975    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1976        let mut cx = NeovimBackedTestContext::new(cx).await;
1977        cx.set_shared_state("ˇ one\n two \nthree").await;
1978        cx.simulate_shared_keystrokes("g _").await;
1979        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
1980
1981        cx.set_shared_state("ˇ one \n two \nthree").await;
1982        cx.simulate_shared_keystrokes("g _").await;
1983        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
1984        cx.simulate_shared_keystrokes("2 g _").await;
1985        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
1986    }
1987
1988    #[gpui::test]
1989    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1990        let mut cx = NeovimBackedTestContext::new(cx).await;
1991        let initial_state = indoc! {r"abc
1992          def
1993          paragraph
1994          the second
1995          third ˇand
1996          final"};
1997
1998        cx.set_shared_state(initial_state).await;
1999        cx.simulate_shared_keystrokes("shift-h").await;
2000        cx.shared_state().await.assert_eq(indoc! {r"abˇc
2001          def
2002          paragraph
2003          the second
2004          third and
2005          final"});
2006
2007        // clip point
2008        cx.set_shared_state(indoc! {r"
2009          1 2 3
2010          4 5 6
2011          7 8 ˇ9
2012          "})
2013            .await;
2014        cx.simulate_shared_keystrokes("shift-h").await;
2015        cx.shared_state().await.assert_eq(indoc! {"
2016          1 2 ˇ3
2017          4 5 6
2018          7 8 9
2019          "});
2020
2021        cx.set_shared_state(indoc! {r"
2022          1 2 3
2023          4 5 6
2024          ˇ7 8 9
2025          "})
2026            .await;
2027        cx.simulate_shared_keystrokes("shift-h").await;
2028        cx.shared_state().await.assert_eq(indoc! {"
2029          ˇ1 2 3
2030          4 5 6
2031          7 8 9
2032          "});
2033
2034        cx.set_shared_state(indoc! {r"
2035          1 2 3
2036          4 5 ˇ6
2037          7 8 9"})
2038            .await;
2039        cx.simulate_shared_keystrokes("9 shift-h").await;
2040        cx.shared_state().await.assert_eq(indoc! {"
2041          1 2 3
2042          4 5 6
2043          7 8 ˇ9"});
2044    }
2045
2046    #[gpui::test]
2047    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2048        let mut cx = NeovimBackedTestContext::new(cx).await;
2049        let initial_state = indoc! {r"abˇc
2050          def
2051          paragraph
2052          the second
2053          third and
2054          final"};
2055
2056        cx.set_shared_state(initial_state).await;
2057        cx.simulate_shared_keystrokes("shift-m").await;
2058        cx.shared_state().await.assert_eq(indoc! {r"abc
2059          def
2060          paˇragraph
2061          the second
2062          third and
2063          final"});
2064
2065        cx.set_shared_state(indoc! {r"
2066          1 2 3
2067          4 5 6
2068          7 8 ˇ9
2069          "})
2070            .await;
2071        cx.simulate_shared_keystrokes("shift-m").await;
2072        cx.shared_state().await.assert_eq(indoc! {"
2073          1 2 3
2074          4 5 ˇ6
2075          7 8 9
2076          "});
2077        cx.set_shared_state(indoc! {r"
2078          1 2 3
2079          4 5 6
2080          ˇ7 8 9
2081          "})
2082            .await;
2083        cx.simulate_shared_keystrokes("shift-m").await;
2084        cx.shared_state().await.assert_eq(indoc! {"
2085          1 2 3
2086          ˇ4 5 6
2087          7 8 9
2088          "});
2089        cx.set_shared_state(indoc! {r"
2090          ˇ1 2 3
2091          4 5 6
2092          7 8 9
2093          "})
2094            .await;
2095        cx.simulate_shared_keystrokes("shift-m").await;
2096        cx.shared_state().await.assert_eq(indoc! {"
2097          1 2 3
2098          ˇ4 5 6
2099          7 8 9
2100          "});
2101        cx.set_shared_state(indoc! {r"
2102          1 2 3
2103          ˇ4 5 6
2104          7 8 9
2105          "})
2106            .await;
2107        cx.simulate_shared_keystrokes("shift-m").await;
2108        cx.shared_state().await.assert_eq(indoc! {"
2109          1 2 3
2110          ˇ4 5 6
2111          7 8 9
2112          "});
2113        cx.set_shared_state(indoc! {r"
2114          1 2 3
2115          4 5 ˇ6
2116          7 8 9
2117          "})
2118            .await;
2119        cx.simulate_shared_keystrokes("shift-m").await;
2120        cx.shared_state().await.assert_eq(indoc! {"
2121          1 2 3
2122          4 5 ˇ6
2123          7 8 9
2124          "});
2125    }
2126
2127    #[gpui::test]
2128    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2129        let mut cx = NeovimBackedTestContext::new(cx).await;
2130        let initial_state = indoc! {r"abc
2131          deˇf
2132          paragraph
2133          the second
2134          third and
2135          final"};
2136
2137        cx.set_shared_state(initial_state).await;
2138        cx.simulate_shared_keystrokes("shift-l").await;
2139        cx.shared_state().await.assert_eq(indoc! {r"abc
2140          def
2141          paragraph
2142          the second
2143          third and
2144          fiˇnal"});
2145
2146        cx.set_shared_state(indoc! {r"
2147          1 2 3
2148          4 5 ˇ6
2149          7 8 9
2150          "})
2151            .await;
2152        cx.simulate_shared_keystrokes("shift-l").await;
2153        cx.shared_state().await.assert_eq(indoc! {"
2154          1 2 3
2155          4 5 6
2156          7 8 9
2157          ˇ"});
2158
2159        cx.set_shared_state(indoc! {r"
2160          1 2 3
2161          ˇ4 5 6
2162          7 8 9
2163          "})
2164            .await;
2165        cx.simulate_shared_keystrokes("shift-l").await;
2166        cx.shared_state().await.assert_eq(indoc! {"
2167          1 2 3
2168          4 5 6
2169          7 8 9
2170          ˇ"});
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("shift-l").await;
2179        cx.shared_state().await.assert_eq(indoc! {"
2180          1 2 3
2181          4 5 6
2182          7 8 9
2183          ˇ"});
2184
2185        cx.set_shared_state(indoc! {r"
2186          ˇ1 2 3
2187          4 5 6
2188          7 8 9
2189          "})
2190            .await;
2191        cx.simulate_shared_keystrokes("shift-l").await;
2192        cx.shared_state().await.assert_eq(indoc! {"
2193          1 2 3
2194          4 5 6
2195          7 8 9
2196          ˇ"});
2197
2198        cx.set_shared_state(indoc! {r"
2199          1 2 3
2200          4 5 ˇ6
2201          7 8 9
2202          "})
2203            .await;
2204        cx.simulate_shared_keystrokes("9 shift-l").await;
2205        cx.shared_state().await.assert_eq(indoc! {"
2206          1 2 ˇ3
2207          4 5 6
2208          7 8 9
2209          "});
2210    }
2211
2212    #[gpui::test]
2213    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2214        let mut cx = NeovimBackedTestContext::new(cx).await;
2215        cx.set_shared_state(indoc! {r"
2216        456 5ˇ67 678
2217        "})
2218            .await;
2219        cx.simulate_shared_keystrokes("g e").await;
2220        cx.shared_state().await.assert_eq(indoc! {"
2221        45ˇ6 567 678
2222        "});
2223
2224        // Test times
2225        cx.set_shared_state(indoc! {r"
2226        123 234 345
2227        456 5ˇ67 678
2228        "})
2229            .await;
2230        cx.simulate_shared_keystrokes("4 g e").await;
2231        cx.shared_state().await.assert_eq(indoc! {"
2232        12ˇ3 234 345
2233        456 567 678
2234        "});
2235
2236        // With punctuation
2237        cx.set_shared_state(indoc! {r"
2238        123 234 345
2239        4;5.6 5ˇ67 678
2240        789 890 901
2241        "})
2242            .await;
2243        cx.simulate_shared_keystrokes("g e").await;
2244        cx.shared_state().await.assert_eq(indoc! {"
2245          123 234 345
2246          4;5.ˇ6 567 678
2247          789 890 901
2248        "});
2249
2250        // With punctuation and count
2251        cx.set_shared_state(indoc! {r"
2252        123 234 345
2253        4;5.6 5ˇ67 678
2254        789 890 901
2255        "})
2256            .await;
2257        cx.simulate_shared_keystrokes("5 g e").await;
2258        cx.shared_state().await.assert_eq(indoc! {"
2259          123 234 345
2260          ˇ4;5.6 567 678
2261          789 890 901
2262        "});
2263
2264        // newlines
2265        cx.set_shared_state(indoc! {r"
2266        123 234 345
2267
2268        78ˇ9 890 901
2269        "})
2270            .await;
2271        cx.simulate_shared_keystrokes("g e").await;
2272        cx.shared_state().await.assert_eq(indoc! {"
2273          123 234 345
2274          ˇ
2275          789 890 901
2276        "});
2277        cx.simulate_shared_keystrokes("g e").await;
2278        cx.shared_state().await.assert_eq(indoc! {"
2279          123 234 34ˇ5
2280
2281          789 890 901
2282        "});
2283
2284        // With punctuation
2285        cx.set_shared_state(indoc! {r"
2286        123 234 345
2287        4;5.ˇ6 567 678
2288        789 890 901
2289        "})
2290            .await;
2291        cx.simulate_shared_keystrokes("g shift-e").await;
2292        cx.shared_state().await.assert_eq(indoc! {"
2293          123 234 34ˇ5
2294          4;5.6 567 678
2295          789 890 901
2296        "});
2297    }
2298
2299    #[gpui::test]
2300    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2301        let mut cx = NeovimBackedTestContext::new(cx).await;
2302
2303        cx.set_shared_state(indoc! {"
2304            fn aˇ() {
2305              return
2306            }
2307        "})
2308            .await;
2309        cx.simulate_shared_keystrokes("v $ %").await;
2310        cx.shared_state().await.assert_eq(indoc! {"
2311            fn a«() {
2312              return
2313            }ˇ»
2314        "});
2315    }
2316}