motion.rs

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