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        }
 451    });
 452}
 453
 454// Motion handling is specified here:
 455// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 456impl Motion {
 457    pub fn linewise(&self) -> bool {
 458        use Motion::*;
 459        match self {
 460            Down { .. }
 461            | Up { .. }
 462            | StartOfDocument
 463            | EndOfDocument
 464            | CurrentLine
 465            | NextLineStart
 466            | StartOfLineDownward
 467            | StartOfParagraph
 468            | WindowTop
 469            | WindowMiddle
 470            | WindowBottom
 471            | EndOfParagraph => true,
 472            EndOfLine { .. }
 473            | Matching
 474            | FindForward { .. }
 475            | Left
 476            | Backspace
 477            | Right
 478            | Space
 479            | StartOfLine { .. }
 480            | EndOfLineDownward
 481            | GoToColumn
 482            | NextWordStart { .. }
 483            | NextWordEnd { .. }
 484            | PreviousWordStart { .. }
 485            | PreviousWordEnd { .. }
 486            | NextSubwordStart { .. }
 487            | NextSubwordEnd { .. }
 488            | PreviousSubwordStart { .. }
 489            | PreviousSubwordEnd { .. }
 490            | FirstNonWhitespace { .. }
 491            | FindBackward { .. }
 492            | RepeatFind { .. }
 493            | RepeatFindReversed { .. }
 494            | ZedSearchResult { .. } => false,
 495        }
 496    }
 497
 498    pub fn infallible(&self) -> bool {
 499        use Motion::*;
 500        match self {
 501            StartOfDocument | EndOfDocument | CurrentLine => true,
 502            Down { .. }
 503            | Up { .. }
 504            | EndOfLine { .. }
 505            | Matching
 506            | FindForward { .. }
 507            | RepeatFind { .. }
 508            | Left
 509            | Backspace
 510            | Right
 511            | Space
 512            | StartOfLine { .. }
 513            | StartOfParagraph
 514            | EndOfParagraph
 515            | StartOfLineDownward
 516            | EndOfLineDownward
 517            | GoToColumn
 518            | NextWordStart { .. }
 519            | NextWordEnd { .. }
 520            | PreviousWordStart { .. }
 521            | PreviousWordEnd { .. }
 522            | NextSubwordStart { .. }
 523            | NextSubwordEnd { .. }
 524            | PreviousSubwordStart { .. }
 525            | PreviousSubwordEnd { .. }
 526            | FirstNonWhitespace { .. }
 527            | FindBackward { .. }
 528            | RepeatFindReversed { .. }
 529            | WindowTop
 530            | WindowMiddle
 531            | WindowBottom
 532            | NextLineStart
 533            | ZedSearchResult { .. } => false,
 534        }
 535    }
 536
 537    pub fn inclusive(&self) -> bool {
 538        use Motion::*;
 539        match self {
 540            Down { .. }
 541            | Up { .. }
 542            | StartOfDocument
 543            | EndOfDocument
 544            | CurrentLine
 545            | EndOfLine { .. }
 546            | EndOfLineDownward
 547            | Matching
 548            | FindForward { .. }
 549            | WindowTop
 550            | WindowMiddle
 551            | WindowBottom
 552            | NextWordEnd { .. }
 553            | PreviousWordEnd { .. }
 554            | NextSubwordEnd { .. }
 555            | PreviousSubwordEnd { .. }
 556            | NextLineStart => true,
 557            Left
 558            | Backspace
 559            | Right
 560            | Space
 561            | StartOfLine { .. }
 562            | StartOfLineDownward
 563            | StartOfParagraph
 564            | EndOfParagraph
 565            | GoToColumn
 566            | NextWordStart { .. }
 567            | PreviousWordStart { .. }
 568            | NextSubwordStart { .. }
 569            | PreviousSubwordStart { .. }
 570            | FirstNonWhitespace { .. }
 571            | FindBackward { .. }
 572            | ZedSearchResult { .. } => false,
 573            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 574                motion.inclusive()
 575            }
 576        }
 577    }
 578
 579    pub fn move_point(
 580        &self,
 581        map: &DisplaySnapshot,
 582        point: DisplayPoint,
 583        goal: SelectionGoal,
 584        maybe_times: Option<usize>,
 585        text_layout_details: &TextLayoutDetails,
 586    ) -> Option<(DisplayPoint, SelectionGoal)> {
 587        let times = maybe_times.unwrap_or(1);
 588        use Motion::*;
 589        let infallible = self.infallible();
 590        let (new_point, goal) = match self {
 591            Left => (left(map, point, times), SelectionGoal::None),
 592            Backspace => (backspace(map, point, times), SelectionGoal::None),
 593            Down {
 594                display_lines: false,
 595            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
 596            Down {
 597                display_lines: true,
 598            } => down_display(map, point, goal, times, &text_layout_details),
 599            Up {
 600                display_lines: false,
 601            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
 602            Up {
 603                display_lines: true,
 604            } => up_display(map, point, goal, times, &text_layout_details),
 605            Right => (right(map, point, times), SelectionGoal::None),
 606            Space => (space(map, point, times), SelectionGoal::None),
 607            NextWordStart { ignore_punctuation } => (
 608                next_word_start(map, point, *ignore_punctuation, times),
 609                SelectionGoal::None,
 610            ),
 611            NextWordEnd { ignore_punctuation } => (
 612                next_word_end(map, point, *ignore_punctuation, times, true),
 613                SelectionGoal::None,
 614            ),
 615            PreviousWordStart { ignore_punctuation } => (
 616                previous_word_start(map, point, *ignore_punctuation, times),
 617                SelectionGoal::None,
 618            ),
 619            PreviousWordEnd { ignore_punctuation } => (
 620                previous_word_end(map, point, *ignore_punctuation, times),
 621                SelectionGoal::None,
 622            ),
 623            NextSubwordStart { ignore_punctuation } => (
 624                next_subword_start(map, point, *ignore_punctuation, times),
 625                SelectionGoal::None,
 626            ),
 627            NextSubwordEnd { ignore_punctuation } => (
 628                next_subword_end(map, point, *ignore_punctuation, times, true),
 629                SelectionGoal::None,
 630            ),
 631            PreviousSubwordStart { ignore_punctuation } => (
 632                previous_subword_start(map, point, *ignore_punctuation, times),
 633                SelectionGoal::None,
 634            ),
 635            PreviousSubwordEnd { ignore_punctuation } => (
 636                previous_subword_end(map, point, *ignore_punctuation, times),
 637                SelectionGoal::None,
 638            ),
 639            FirstNonWhitespace { display_lines } => (
 640                first_non_whitespace(map, *display_lines, point),
 641                SelectionGoal::None,
 642            ),
 643            StartOfLine { display_lines } => (
 644                start_of_line(map, *display_lines, point),
 645                SelectionGoal::None,
 646            ),
 647            EndOfLine { display_lines } => (
 648                end_of_line(map, *display_lines, point, times),
 649                SelectionGoal::None,
 650            ),
 651            StartOfParagraph => (
 652                movement::start_of_paragraph(map, point, times),
 653                SelectionGoal::None,
 654            ),
 655            EndOfParagraph => (
 656                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
 657                SelectionGoal::None,
 658            ),
 659            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
 660            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
 661            EndOfDocument => (
 662                end_of_document(map, point, maybe_times),
 663                SelectionGoal::None,
 664            ),
 665            Matching => (matching(map, point), SelectionGoal::None),
 666            // t f
 667            FindForward {
 668                before,
 669                char,
 670                mode,
 671                smartcase,
 672            } => {
 673                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
 674                    .map(|new_point| (new_point, SelectionGoal::None))
 675            }
 676            // T F
 677            FindBackward {
 678                after,
 679                char,
 680                mode,
 681                smartcase,
 682            } => (
 683                find_backward(map, point, *after, *char, times, *mode, *smartcase),
 684                SelectionGoal::None,
 685            ),
 686            // ; -- repeat the last find done with t, f, T, F
 687            RepeatFind { last_find } => match **last_find {
 688                Motion::FindForward {
 689                    before,
 690                    char,
 691                    mode,
 692                    smartcase,
 693                } => {
 694                    let mut new_point =
 695                        find_forward(map, point, before, char, times, mode, smartcase);
 696                    if new_point == Some(point) {
 697                        new_point =
 698                            find_forward(map, point, before, char, times + 1, mode, smartcase);
 699                    }
 700
 701                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 702                }
 703
 704                Motion::FindBackward {
 705                    after,
 706                    char,
 707                    mode,
 708                    smartcase,
 709                } => {
 710                    let mut new_point =
 711                        find_backward(map, point, after, char, times, mode, smartcase);
 712                    if new_point == point {
 713                        new_point =
 714                            find_backward(map, point, after, char, times + 1, mode, smartcase);
 715                    }
 716
 717                    (new_point, SelectionGoal::None)
 718                }
 719                _ => return None,
 720            },
 721            // , -- repeat the last find done with t, f, T, F, in opposite direction
 722            RepeatFindReversed { last_find } => match **last_find {
 723                Motion::FindForward {
 724                    before,
 725                    char,
 726                    mode,
 727                    smartcase,
 728                } => {
 729                    let mut new_point =
 730                        find_backward(map, point, before, char, times, mode, smartcase);
 731                    if new_point == point {
 732                        new_point =
 733                            find_backward(map, point, before, char, times + 1, mode, smartcase);
 734                    }
 735
 736                    (new_point, SelectionGoal::None)
 737                }
 738
 739                Motion::FindBackward {
 740                    after,
 741                    char,
 742                    mode,
 743                    smartcase,
 744                } => {
 745                    let mut new_point =
 746                        find_forward(map, point, after, char, times, mode, smartcase);
 747                    if new_point == Some(point) {
 748                        new_point =
 749                            find_forward(map, point, after, char, times + 1, mode, smartcase);
 750                    }
 751
 752                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
 753                }
 754                _ => return None,
 755            },
 756            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
 757            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
 758            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
 759            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
 760            WindowTop => window_top(map, point, &text_layout_details, times - 1),
 761            WindowMiddle => window_middle(map, point, &text_layout_details),
 762            WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
 763            ZedSearchResult { new_selections, .. } => {
 764                // There will be only one selection, as
 765                // Search::SelectNextMatch selects a single match.
 766                if let Some(new_selection) = new_selections.first() {
 767                    (
 768                        new_selection.start.to_display_point(map),
 769                        SelectionGoal::None,
 770                    )
 771                } else {
 772                    return None;
 773                }
 774            }
 775        };
 776
 777        (new_point != point || infallible).then_some((new_point, goal))
 778    }
 779
 780    // Get the range value after self is applied to the specified selection.
 781    pub fn range(
 782        &self,
 783        map: &DisplaySnapshot,
 784        selection: Selection<DisplayPoint>,
 785        times: Option<usize>,
 786        expand_to_surrounding_newline: bool,
 787        text_layout_details: &TextLayoutDetails,
 788    ) -> Option<Range<DisplayPoint>> {
 789        if let Motion::ZedSearchResult {
 790            prior_selections,
 791            new_selections,
 792        } = self
 793        {
 794            if let Some((prior_selection, new_selection)) =
 795                prior_selections.first().zip(new_selections.first())
 796            {
 797                let start = prior_selection
 798                    .start
 799                    .to_display_point(map)
 800                    .min(new_selection.start.to_display_point(map));
 801                let end = new_selection
 802                    .end
 803                    .to_display_point(map)
 804                    .max(prior_selection.end.to_display_point(map));
 805
 806                if start < end {
 807                    return Some(start..end);
 808                } else {
 809                    return Some(end..start);
 810                }
 811            } else {
 812                return None;
 813            }
 814        }
 815
 816        if let Some((new_head, goal)) = self.move_point(
 817            map,
 818            selection.head(),
 819            selection.goal,
 820            times,
 821            &text_layout_details,
 822        ) {
 823            let mut selection = selection.clone();
 824            selection.set_head(new_head, goal);
 825
 826            if self.linewise() {
 827                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 828
 829                if expand_to_surrounding_newline {
 830                    if selection.end.row() < map.max_point().row() {
 831                        *selection.end.row_mut() += 1;
 832                        *selection.end.column_mut() = 0;
 833                        selection.end = map.clip_point(selection.end, Bias::Right);
 834                        // Don't reset the end here
 835                        return Some(selection.start..selection.end);
 836                    } else if selection.start.row() > 0 {
 837                        *selection.start.row_mut() -= 1;
 838                        *selection.start.column_mut() = map.line_len(selection.start.row());
 839                        selection.start = map.clip_point(selection.start, Bias::Left);
 840                    }
 841                }
 842
 843                selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
 844            } else {
 845                // Another special case: When using the "w" motion in combination with an
 846                // operator and the last word moved over is at the end of a line, the end of
 847                // that word becomes the end of the operated text, not the first word in the
 848                // next line.
 849                if let Motion::NextWordStart {
 850                    ignore_punctuation: _,
 851                } = self
 852                {
 853                    let start_row = selection.start.to_point(&map).row;
 854                    if selection.end.to_point(&map).row > start_row {
 855                        selection.end =
 856                            Point::new(start_row, map.buffer_snapshot.line_len(start_row))
 857                                .to_display_point(&map)
 858                    }
 859                }
 860
 861                // If the motion is exclusive and the end of the motion is in column 1, the
 862                // end of the motion is moved to the end of the previous line and the motion
 863                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
 864                // but "d}" will not include that line.
 865                let mut inclusive = self.inclusive();
 866                if !inclusive
 867                    && self != &Motion::Backspace
 868                    && selection.end.row() > selection.start.row()
 869                    && selection.end.column() == 0
 870                {
 871                    inclusive = true;
 872                    *selection.end.row_mut() -= 1;
 873                    *selection.end.column_mut() = 0;
 874                    selection.end = map.clip_point(
 875                        map.next_line_boundary(selection.end.to_point(map)).1,
 876                        Bias::Left,
 877                    );
 878                }
 879
 880                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
 881                    *selection.end.column_mut() += 1;
 882                }
 883            }
 884            Some(selection.start..selection.end)
 885        } else {
 886            None
 887        }
 888    }
 889
 890    // Expands a selection using self for an operator
 891    pub fn expand_selection(
 892        &self,
 893        map: &DisplaySnapshot,
 894        selection: &mut Selection<DisplayPoint>,
 895        times: Option<usize>,
 896        expand_to_surrounding_newline: bool,
 897        text_layout_details: &TextLayoutDetails,
 898    ) -> bool {
 899        if let Some(range) = self.range(
 900            map,
 901            selection.clone(),
 902            times,
 903            expand_to_surrounding_newline,
 904            text_layout_details,
 905        ) {
 906            selection.start = range.start;
 907            selection.end = range.end;
 908            true
 909        } else {
 910            false
 911        }
 912    }
 913}
 914
 915fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 916    for _ in 0..times {
 917        point = movement::saturating_left(map, point);
 918        if point.column() == 0 {
 919            break;
 920        }
 921    }
 922    point
 923}
 924
 925pub(crate) fn backspace(
 926    map: &DisplaySnapshot,
 927    mut point: DisplayPoint,
 928    times: usize,
 929) -> DisplayPoint {
 930    for _ in 0..times {
 931        point = movement::left(map, point);
 932        if point.is_zero() {
 933            break;
 934        }
 935    }
 936    point
 937}
 938
 939fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
 940    for _ in 0..times {
 941        point = wrapping_right(map, point);
 942        if point == map.max_point() {
 943            break;
 944        }
 945    }
 946    point
 947}
 948
 949fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 950    let max_column = map.line_len(point.row()).saturating_sub(1);
 951    if point.column() < max_column {
 952        *point.column_mut() += 1;
 953    } else if point.row() < map.max_point().row() {
 954        *point.row_mut() += 1;
 955        *point.column_mut() = 0;
 956    }
 957    point
 958}
 959
 960pub(crate) fn start_of_relative_buffer_row(
 961    map: &DisplaySnapshot,
 962    point: DisplayPoint,
 963    times: isize,
 964) -> DisplayPoint {
 965    let start = map.display_point_to_fold_point(point, Bias::Left);
 966    let target = start.row() as isize + times;
 967    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
 968
 969    map.clip_point(
 970        map.fold_point_to_display_point(
 971            map.fold_snapshot
 972                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
 973        ),
 974        Bias::Right,
 975    )
 976}
 977
 978fn up_down_buffer_rows(
 979    map: &DisplaySnapshot,
 980    point: DisplayPoint,
 981    mut goal: SelectionGoal,
 982    times: isize,
 983    text_layout_details: &TextLayoutDetails,
 984) -> (DisplayPoint, SelectionGoal) {
 985    let start = map.display_point_to_fold_point(point, Bias::Left);
 986    let begin_folded_line = map.fold_point_to_display_point(
 987        map.fold_snapshot
 988            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
 989    );
 990    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 991
 992    let (goal_wrap, goal_x) = match goal {
 993        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
 994        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
 995        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
 996        _ => {
 997            let x = map.x_for_display_point(point, text_layout_details);
 998            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
 999            (select_nth_wrapped_row, x.0)
1000        }
1001    };
1002
1003    let target = start.row() as isize + times;
1004    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1005
1006    let mut begin_folded_line = map.fold_point_to_display_point(
1007        map.fold_snapshot
1008            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
1009    );
1010
1011    let mut i = 0;
1012    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1013        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
1014        if map
1015            .display_point_to_fold_point(next_folded_line, Bias::Right)
1016            .row()
1017            == new_row
1018        {
1019            i += 1;
1020            begin_folded_line = next_folded_line;
1021        } else {
1022            break;
1023        }
1024    }
1025
1026    let new_col = if i == goal_wrap {
1027        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1028    } else {
1029        map.line_len(begin_folded_line.row())
1030    };
1031
1032    (
1033        map.clip_point(
1034            DisplayPoint::new(begin_folded_line.row(), new_col),
1035            Bias::Left,
1036        ),
1037        goal,
1038    )
1039}
1040
1041fn down_display(
1042    map: &DisplaySnapshot,
1043    mut point: DisplayPoint,
1044    mut goal: SelectionGoal,
1045    times: usize,
1046    text_layout_details: &TextLayoutDetails,
1047) -> (DisplayPoint, SelectionGoal) {
1048    for _ in 0..times {
1049        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1050    }
1051
1052    (point, goal)
1053}
1054
1055fn up_display(
1056    map: &DisplaySnapshot,
1057    mut point: DisplayPoint,
1058    mut goal: SelectionGoal,
1059    times: usize,
1060    text_layout_details: &TextLayoutDetails,
1061) -> (DisplayPoint, SelectionGoal) {
1062    for _ in 0..times {
1063        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
1064    }
1065
1066    (point, goal)
1067}
1068
1069pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1070    for _ in 0..times {
1071        let new_point = movement::saturating_right(map, point);
1072        if point == new_point {
1073            break;
1074        }
1075        point = new_point;
1076    }
1077    point
1078}
1079
1080pub(crate) fn next_char(
1081    map: &DisplaySnapshot,
1082    point: DisplayPoint,
1083    allow_cross_newline: bool,
1084) -> DisplayPoint {
1085    let mut new_point = point;
1086    let mut max_column = map.line_len(new_point.row());
1087    if !allow_cross_newline {
1088        max_column -= 1;
1089    }
1090    if new_point.column() < max_column {
1091        *new_point.column_mut() += 1;
1092    } else if new_point < map.max_point() && allow_cross_newline {
1093        *new_point.row_mut() += 1;
1094        *new_point.column_mut() = 0;
1095    }
1096    new_point
1097}
1098
1099pub(crate) fn next_word_start(
1100    map: &DisplaySnapshot,
1101    mut point: DisplayPoint,
1102    ignore_punctuation: bool,
1103    times: usize,
1104) -> DisplayPoint {
1105    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1106    for _ in 0..times {
1107        let mut crossed_newline = false;
1108        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1109            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1110            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1111            let at_newline = right == '\n';
1112
1113            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1114                || at_newline && crossed_newline
1115                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1116
1117            crossed_newline |= at_newline;
1118            found
1119        });
1120        if point == new_point {
1121            break;
1122        }
1123        point = new_point;
1124    }
1125    point
1126}
1127
1128pub(crate) fn next_word_end(
1129    map: &DisplaySnapshot,
1130    mut point: DisplayPoint,
1131    ignore_punctuation: bool,
1132    times: usize,
1133    allow_cross_newline: bool,
1134) -> DisplayPoint {
1135    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1136    for _ in 0..times {
1137        let new_point = next_char(map, point, allow_cross_newline);
1138        let mut need_next_char = false;
1139        let new_point = movement::find_boundary_exclusive(
1140            map,
1141            new_point,
1142            FindRange::MultiLine,
1143            |left, right| {
1144                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1145                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1146                let at_newline = right == '\n';
1147
1148                if !allow_cross_newline && at_newline {
1149                    need_next_char = true;
1150                    return true;
1151                }
1152
1153                left_kind != right_kind && left_kind != CharKind::Whitespace
1154            },
1155        );
1156        let new_point = if need_next_char {
1157            next_char(map, new_point, true)
1158        } else {
1159            new_point
1160        };
1161        let new_point = map.clip_point(new_point, Bias::Left);
1162        if point == new_point {
1163            break;
1164        }
1165        point = new_point;
1166    }
1167    point
1168}
1169
1170fn previous_word_start(
1171    map: &DisplaySnapshot,
1172    mut point: DisplayPoint,
1173    ignore_punctuation: bool,
1174    times: usize,
1175) -> DisplayPoint {
1176    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1177    for _ in 0..times {
1178        // This works even though find_preceding_boundary is called for every character in the line containing
1179        // cursor because the newline is checked only once.
1180        let new_point = movement::find_preceding_boundary_display_point(
1181            map,
1182            point,
1183            FindRange::MultiLine,
1184            |left, right| {
1185                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1186                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1187
1188                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1189            },
1190        );
1191        if point == new_point {
1192            break;
1193        }
1194        point = new_point;
1195    }
1196    point
1197}
1198
1199fn previous_word_end(
1200    map: &DisplaySnapshot,
1201    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    let mut point = point.to_point(map);
1207
1208    if point.column < map.buffer_snapshot.line_len(point.row) {
1209        point.column += 1;
1210    }
1211    for _ in 0..times {
1212        let new_point = movement::find_preceding_boundary_point(
1213            &map.buffer_snapshot,
1214            point,
1215            FindRange::MultiLine,
1216            |left, right| {
1217                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1218                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1219                match (left_kind, right_kind) {
1220                    (CharKind::Punctuation, CharKind::Whitespace)
1221                    | (CharKind::Punctuation, CharKind::Word)
1222                    | (CharKind::Word, CharKind::Whitespace)
1223                    | (CharKind::Word, CharKind::Punctuation) => true,
1224                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1225                    _ => false,
1226                }
1227            },
1228        );
1229        if new_point == point {
1230            break;
1231        }
1232        point = new_point;
1233    }
1234    movement::saturating_left(map, point.to_display_point(map))
1235}
1236
1237fn next_subword_start(
1238    map: &DisplaySnapshot,
1239    mut point: DisplayPoint,
1240    ignore_punctuation: bool,
1241    times: usize,
1242) -> DisplayPoint {
1243    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1244    for _ in 0..times {
1245        let mut crossed_newline = false;
1246        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1247            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1248            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1249            let at_newline = right == '\n';
1250
1251            let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1252            let is_subword_start =
1253                left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1254
1255            let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1256                || at_newline && crossed_newline
1257                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1258
1259            crossed_newline |= at_newline;
1260            found
1261        });
1262        if point == new_point {
1263            break;
1264        }
1265        point = new_point;
1266    }
1267    point
1268}
1269
1270pub(crate) fn next_subword_end(
1271    map: &DisplaySnapshot,
1272    mut point: DisplayPoint,
1273    ignore_punctuation: bool,
1274    times: usize,
1275    allow_cross_newline: bool,
1276) -> DisplayPoint {
1277    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1278    for _ in 0..times {
1279        let new_point = next_char(map, point, allow_cross_newline);
1280
1281        let mut crossed_newline = false;
1282        let mut need_backtrack = false;
1283        let new_point =
1284            movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1285                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1286                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1287                let at_newline = right == '\n';
1288
1289                if !allow_cross_newline && at_newline {
1290                    return true;
1291                }
1292
1293                let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1294                let is_subword_end =
1295                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1296
1297                let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1298
1299                if found && (is_word_end || is_subword_end) {
1300                    need_backtrack = true;
1301                }
1302
1303                crossed_newline |= at_newline;
1304                found
1305            });
1306        let mut new_point = map.clip_point(new_point, Bias::Left);
1307        if need_backtrack {
1308            *new_point.column_mut() -= 1;
1309        }
1310        if point == new_point {
1311            break;
1312        }
1313        point = new_point;
1314    }
1315    point
1316}
1317
1318fn previous_subword_start(
1319    map: &DisplaySnapshot,
1320    mut point: DisplayPoint,
1321    ignore_punctuation: bool,
1322    times: usize,
1323) -> DisplayPoint {
1324    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1325    for _ in 0..times {
1326        let mut crossed_newline = false;
1327        // This works even though find_preceding_boundary is called for every character in the line containing
1328        // cursor because the newline is checked only once.
1329        let new_point = movement::find_preceding_boundary_display_point(
1330            map,
1331            point,
1332            FindRange::MultiLine,
1333            |left, right| {
1334                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1335                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1336                let at_newline = right == '\n';
1337
1338                let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1339                let is_subword_start =
1340                    left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1341
1342                let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1343                    || at_newline && crossed_newline
1344                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1345
1346                crossed_newline |= at_newline;
1347
1348                found
1349            },
1350        );
1351        if point == new_point {
1352            break;
1353        }
1354        point = new_point;
1355    }
1356    point
1357}
1358
1359fn previous_subword_end(
1360    map: &DisplaySnapshot,
1361    point: DisplayPoint,
1362    ignore_punctuation: bool,
1363    times: usize,
1364) -> DisplayPoint {
1365    let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1366    let mut point = point.to_point(map);
1367
1368    if point.column < map.buffer_snapshot.line_len(point.row) {
1369        point.column += 1;
1370    }
1371    for _ in 0..times {
1372        let new_point = movement::find_preceding_boundary_point(
1373            &map.buffer_snapshot,
1374            point,
1375            FindRange::MultiLine,
1376            |left, right| {
1377                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1378                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1379
1380                let is_subword_end =
1381                    left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1382
1383                if is_subword_end {
1384                    return true;
1385                }
1386
1387                match (left_kind, right_kind) {
1388                    (CharKind::Word, CharKind::Whitespace)
1389                    | (CharKind::Word, CharKind::Punctuation) => true,
1390                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1391                    _ => false,
1392                }
1393            },
1394        );
1395        if new_point == point {
1396            break;
1397        }
1398        point = new_point;
1399    }
1400    movement::saturating_left(map, point.to_display_point(map))
1401}
1402
1403pub(crate) fn first_non_whitespace(
1404    map: &DisplaySnapshot,
1405    display_lines: bool,
1406    from: DisplayPoint,
1407) -> DisplayPoint {
1408    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1409    let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1410    for (ch, offset) in map.buffer_chars_at(start_offset) {
1411        if ch == '\n' {
1412            return from;
1413        }
1414
1415        start_offset = offset;
1416
1417        if char_kind(&scope, ch) != CharKind::Whitespace {
1418            break;
1419        }
1420    }
1421
1422    start_offset.to_display_point(map)
1423}
1424
1425pub(crate) fn start_of_line(
1426    map: &DisplaySnapshot,
1427    display_lines: bool,
1428    point: DisplayPoint,
1429) -> DisplayPoint {
1430    if display_lines {
1431        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1432    } else {
1433        map.prev_line_boundary(point.to_point(map)).1
1434    }
1435}
1436
1437pub(crate) fn end_of_line(
1438    map: &DisplaySnapshot,
1439    display_lines: bool,
1440    mut point: DisplayPoint,
1441    times: usize,
1442) -> DisplayPoint {
1443    if times > 1 {
1444        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1445    }
1446    if display_lines {
1447        map.clip_point(
1448            DisplayPoint::new(point.row(), map.line_len(point.row())),
1449            Bias::Left,
1450        )
1451    } else {
1452        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1453    }
1454}
1455
1456fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1457    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1458    *new_point.column_mut() = point.column();
1459    map.clip_point(new_point, Bias::Left)
1460}
1461
1462fn end_of_document(
1463    map: &DisplaySnapshot,
1464    point: DisplayPoint,
1465    line: Option<usize>,
1466) -> DisplayPoint {
1467    let new_row = if let Some(line) = line {
1468        (line - 1) as u32
1469    } else {
1470        map.max_buffer_row()
1471    };
1472
1473    let new_point = Point::new(new_row, point.column());
1474    map.clip_point(new_point.to_display_point(map), Bias::Left)
1475}
1476
1477fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1478    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1479    let display_point = map.clip_at_line_end(display_point);
1480    let point = display_point.to_point(map);
1481    let offset = point.to_offset(&map.buffer_snapshot);
1482
1483    // Ensure the range is contained by the current line.
1484    let mut line_end = map.next_line_boundary(point).0;
1485    if line_end == point {
1486        line_end = map.max_point().to_point(map);
1487    }
1488
1489    let line_range = map.prev_line_boundary(point).0..line_end;
1490    let visible_line_range =
1491        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1492    let ranges = map
1493        .buffer_snapshot
1494        .bracket_ranges(visible_line_range.clone());
1495    if let Some(ranges) = ranges {
1496        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1497            ..line_range.end.to_offset(&map.buffer_snapshot);
1498        let mut closest_pair_destination = None;
1499        let mut closest_distance = usize::MAX;
1500
1501        for (open_range, close_range) in ranges {
1502            if open_range.start >= offset && line_range.contains(&open_range.start) {
1503                let distance = open_range.start - offset;
1504                if distance < closest_distance {
1505                    closest_pair_destination = Some(close_range.start);
1506                    closest_distance = distance;
1507                    continue;
1508                }
1509            }
1510
1511            if close_range.start >= offset && line_range.contains(&close_range.start) {
1512                let distance = close_range.start - offset;
1513                if distance < closest_distance {
1514                    closest_pair_destination = Some(open_range.start);
1515                    closest_distance = distance;
1516                    continue;
1517                }
1518            }
1519
1520            continue;
1521        }
1522
1523        closest_pair_destination
1524            .map(|destination| destination.to_display_point(map))
1525            .unwrap_or(display_point)
1526    } else {
1527        display_point
1528    }
1529}
1530
1531fn find_forward(
1532    map: &DisplaySnapshot,
1533    from: DisplayPoint,
1534    before: bool,
1535    target: char,
1536    times: usize,
1537    mode: FindRange,
1538    smartcase: bool,
1539) -> Option<DisplayPoint> {
1540    let mut to = from;
1541    let mut found = false;
1542
1543    for _ in 0..times {
1544        found = false;
1545        let new_to = find_boundary(map, to, mode, |_, right| {
1546            found = is_character_match(target, right, smartcase);
1547            found
1548        });
1549        if to == new_to {
1550            break;
1551        }
1552        to = new_to;
1553    }
1554
1555    if found {
1556        if before && to.column() > 0 {
1557            *to.column_mut() -= 1;
1558            Some(map.clip_point(to, Bias::Left))
1559        } else {
1560            Some(to)
1561        }
1562    } else {
1563        None
1564    }
1565}
1566
1567fn find_backward(
1568    map: &DisplaySnapshot,
1569    from: DisplayPoint,
1570    after: bool,
1571    target: char,
1572    times: usize,
1573    mode: FindRange,
1574    smartcase: bool,
1575) -> DisplayPoint {
1576    let mut to = from;
1577
1578    for _ in 0..times {
1579        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1580            is_character_match(target, right, smartcase)
1581        });
1582        if to == new_to {
1583            break;
1584        }
1585        to = new_to;
1586    }
1587
1588    let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1589    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1590        if after {
1591            *to.column_mut() += 1;
1592            map.clip_point(to, Bias::Right)
1593        } else {
1594            to
1595        }
1596    } else {
1597        from
1598    }
1599}
1600
1601fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1602    if smartcase {
1603        if target.is_uppercase() {
1604            target == other
1605        } else {
1606            target == other.to_ascii_lowercase()
1607        }
1608    } else {
1609        target == other
1610    }
1611}
1612
1613fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1614    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1615    first_non_whitespace(map, false, correct_line)
1616}
1617
1618fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1619    let correct_line = start_of_relative_buffer_row(map, point, 0);
1620    right(map, correct_line, times.saturating_sub(1))
1621}
1622
1623pub(crate) fn next_line_end(
1624    map: &DisplaySnapshot,
1625    mut point: DisplayPoint,
1626    times: usize,
1627) -> DisplayPoint {
1628    if times > 1 {
1629        point = start_of_relative_buffer_row(map, point, times as isize - 1);
1630    }
1631    end_of_line(map, false, point, 1)
1632}
1633
1634fn window_top(
1635    map: &DisplaySnapshot,
1636    point: DisplayPoint,
1637    text_layout_details: &TextLayoutDetails,
1638    mut times: usize,
1639) -> (DisplayPoint, SelectionGoal) {
1640    let first_visible_line = text_layout_details
1641        .scroll_anchor
1642        .anchor
1643        .to_display_point(map);
1644
1645    if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
1646    {
1647        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1648    }
1649
1650    if let Some(visible_rows) = text_layout_details.visible_rows {
1651        let bottom_row = first_visible_line.row() + visible_rows as u32;
1652        let new_row = (first_visible_line.row() + (times as u32))
1653            .min(bottom_row)
1654            .min(map.max_point().row());
1655        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1656
1657        let new_point = DisplayPoint::new(new_row, new_col);
1658        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1659    } else {
1660        let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
1661        let new_col = point.column().min(map.line_len(first_visible_line.row()));
1662
1663        let new_point = DisplayPoint::new(new_row, new_col);
1664        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1665    }
1666}
1667
1668fn window_middle(
1669    map: &DisplaySnapshot,
1670    point: DisplayPoint,
1671    text_layout_details: &TextLayoutDetails,
1672) -> (DisplayPoint, SelectionGoal) {
1673    if let Some(visible_rows) = text_layout_details.visible_rows {
1674        let first_visible_line = text_layout_details
1675            .scroll_anchor
1676            .anchor
1677            .to_display_point(map);
1678
1679        let max_visible_rows =
1680            (visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
1681
1682        let new_row =
1683            (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row());
1684        let new_col = point.column().min(map.line_len(new_row));
1685        let new_point = DisplayPoint::new(new_row, new_col);
1686        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1687    } else {
1688        (point, SelectionGoal::None)
1689    }
1690}
1691
1692fn window_bottom(
1693    map: &DisplaySnapshot,
1694    point: DisplayPoint,
1695    text_layout_details: &TextLayoutDetails,
1696    mut times: usize,
1697) -> (DisplayPoint, SelectionGoal) {
1698    if let Some(visible_rows) = text_layout_details.visible_rows {
1699        let first_visible_line = text_layout_details
1700            .scroll_anchor
1701            .anchor
1702            .to_display_point(map);
1703        let bottom_row = first_visible_line.row()
1704            + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1705        if bottom_row < map.max_point().row()
1706            && text_layout_details.vertical_scroll_margin as usize > times
1707        {
1708            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1709        }
1710        let bottom_row_capped = bottom_row.min(map.max_point().row());
1711        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
1712            first_visible_line.row()
1713        } else {
1714            bottom_row_capped.saturating_sub(times as u32)
1715        };
1716        let new_col = point.column().min(map.line_len(new_row));
1717        let new_point = DisplayPoint::new(new_row, new_col);
1718        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1719    } else {
1720        (point, SelectionGoal::None)
1721    }
1722}
1723
1724#[cfg(test)]
1725mod test {
1726
1727    use crate::test::NeovimBackedTestContext;
1728    use indoc::indoc;
1729
1730    #[gpui::test]
1731    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1732        let mut cx = NeovimBackedTestContext::new(cx).await;
1733
1734        let initial_state = indoc! {r"ˇabc
1735            def
1736
1737            paragraph
1738            the second
1739
1740
1741
1742            third and
1743            final"};
1744
1745        // goes down once
1746        cx.set_shared_state(initial_state).await;
1747        cx.simulate_shared_keystrokes(["}"]).await;
1748        cx.assert_shared_state(indoc! {r"abc
1749            def
1750            ˇ
1751            paragraph
1752            the second
1753
1754
1755
1756            third and
1757            final"})
1758            .await;
1759
1760        // goes up once
1761        cx.simulate_shared_keystrokes(["{"]).await;
1762        cx.assert_shared_state(initial_state).await;
1763
1764        // goes down twice
1765        cx.simulate_shared_keystrokes(["2", "}"]).await;
1766        cx.assert_shared_state(indoc! {r"abc
1767            def
1768
1769            paragraph
1770            the second
1771            ˇ
1772
1773
1774            third and
1775            final"})
1776            .await;
1777
1778        // goes down over multiple blanks
1779        cx.simulate_shared_keystrokes(["}"]).await;
1780        cx.assert_shared_state(indoc! {r"abc
1781                def
1782
1783                paragraph
1784                the second
1785
1786
1787
1788                third and
1789                finaˇl"})
1790            .await;
1791
1792        // goes up twice
1793        cx.simulate_shared_keystrokes(["2", "{"]).await;
1794        cx.assert_shared_state(indoc! {r"abc
1795                def
1796                ˇ
1797                paragraph
1798                the second
1799
1800
1801
1802                third and
1803                final"})
1804            .await
1805    }
1806
1807    #[gpui::test]
1808    async fn test_matching(cx: &mut gpui::TestAppContext) {
1809        let mut cx = NeovimBackedTestContext::new(cx).await;
1810
1811        cx.set_shared_state(indoc! {r"func ˇ(a string) {
1812                do(something(with<Types>.and_arrays[0, 2]))
1813            }"})
1814            .await;
1815        cx.simulate_shared_keystrokes(["%"]).await;
1816        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1817                do(something(with<Types>.and_arrays[0, 2]))
1818            }"})
1819            .await;
1820
1821        // test it works on the last character of the line
1822        cx.set_shared_state(indoc! {r"func (a string) ˇ{
1823            do(something(with<Types>.and_arrays[0, 2]))
1824            }"})
1825            .await;
1826        cx.simulate_shared_keystrokes(["%"]).await;
1827        cx.assert_shared_state(indoc! {r"func (a string) {
1828            do(something(with<Types>.and_arrays[0, 2]))
1829            ˇ}"})
1830            .await;
1831
1832        // test it works on immediate nesting
1833        cx.set_shared_state("ˇ{()}").await;
1834        cx.simulate_shared_keystrokes(["%"]).await;
1835        cx.assert_shared_state("{()ˇ}").await;
1836        cx.simulate_shared_keystrokes(["%"]).await;
1837        cx.assert_shared_state("ˇ{()}").await;
1838
1839        // test it works on immediate nesting inside braces
1840        cx.set_shared_state("{\n    ˇ{()}\n}").await;
1841        cx.simulate_shared_keystrokes(["%"]).await;
1842        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
1843
1844        // test it jumps to the next paren on a line
1845        cx.set_shared_state("func ˇboop() {\n}").await;
1846        cx.simulate_shared_keystrokes(["%"]).await;
1847        cx.assert_shared_state("func boop(ˇ) {\n}").await;
1848    }
1849
1850    #[gpui::test]
1851    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1852        let mut cx = NeovimBackedTestContext::new(cx).await;
1853
1854        // f and F
1855        cx.set_shared_state("ˇone two three four").await;
1856        cx.simulate_shared_keystrokes(["f", "o"]).await;
1857        cx.assert_shared_state("one twˇo three four").await;
1858        cx.simulate_shared_keystrokes([","]).await;
1859        cx.assert_shared_state("ˇone two three four").await;
1860        cx.simulate_shared_keystrokes(["2", ";"]).await;
1861        cx.assert_shared_state("one two three fˇour").await;
1862        cx.simulate_shared_keystrokes(["shift-f", "e"]).await;
1863        cx.assert_shared_state("one two threˇe four").await;
1864        cx.simulate_shared_keystrokes(["2", ";"]).await;
1865        cx.assert_shared_state("onˇe two three four").await;
1866        cx.simulate_shared_keystrokes([","]).await;
1867        cx.assert_shared_state("one two thrˇee four").await;
1868
1869        // t and T
1870        cx.set_shared_state("ˇone two three four").await;
1871        cx.simulate_shared_keystrokes(["t", "o"]).await;
1872        cx.assert_shared_state("one tˇwo three four").await;
1873        cx.simulate_shared_keystrokes([","]).await;
1874        cx.assert_shared_state("oˇne two three four").await;
1875        cx.simulate_shared_keystrokes(["2", ";"]).await;
1876        cx.assert_shared_state("one two three ˇfour").await;
1877        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1878        cx.assert_shared_state("one two threeˇ four").await;
1879        cx.simulate_shared_keystrokes(["3", ";"]).await;
1880        cx.assert_shared_state("oneˇ two three four").await;
1881        cx.simulate_shared_keystrokes([","]).await;
1882        cx.assert_shared_state("one two thˇree four").await;
1883    }
1884
1885    #[gpui::test]
1886    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1887        let mut cx = NeovimBackedTestContext::new(cx).await;
1888        let initial_state = indoc! {r"something(ˇfoo)"};
1889        cx.set_shared_state(initial_state).await;
1890        cx.simulate_shared_keystrokes(["}"]).await;
1891        cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
1892    }
1893
1894    #[gpui::test]
1895    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1896        let mut cx = NeovimBackedTestContext::new(cx).await;
1897        cx.set_shared_state("ˇone\n  two\nthree").await;
1898        cx.simulate_shared_keystrokes(["enter"]).await;
1899        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
1900    }
1901
1902    #[gpui::test]
1903    async fn test_window_top(cx: &mut gpui::TestAppContext) {
1904        let mut cx = NeovimBackedTestContext::new(cx).await;
1905        let initial_state = indoc! {r"abc
1906          def
1907          paragraph
1908          the second
1909          third ˇand
1910          final"};
1911
1912        cx.set_shared_state(initial_state).await;
1913        cx.simulate_shared_keystrokes(["shift-h"]).await;
1914        cx.assert_shared_state(indoc! {r"abˇc
1915          def
1916          paragraph
1917          the second
1918          third and
1919          final"})
1920            .await;
1921
1922        // clip point
1923        cx.set_shared_state(indoc! {r"
1924          1 2 3
1925          4 5 6
1926          7 8 ˇ9
1927          "})
1928            .await;
1929        cx.simulate_shared_keystrokes(["shift-h"]).await;
1930        cx.assert_shared_state(indoc! {r"
1931          1 2 ˇ3
1932          4 5 6
1933          7 8 9
1934          "})
1935            .await;
1936
1937        cx.set_shared_state(indoc! {r"
1938          1 2 3
1939          4 5 6
1940          ˇ7 8 9
1941          "})
1942            .await;
1943        cx.simulate_shared_keystrokes(["shift-h"]).await;
1944        cx.assert_shared_state(indoc! {r"
1945          ˇ1 2 3
1946          4 5 6
1947          7 8 9
1948          "})
1949            .await;
1950
1951        cx.set_shared_state(indoc! {r"
1952          1 2 3
1953          4 5 ˇ6
1954          7 8 9"})
1955            .await;
1956        cx.simulate_shared_keystrokes(["9", "shift-h"]).await;
1957        cx.assert_shared_state(indoc! {r"
1958          1 2 3
1959          4 5 6
1960          7 8 ˇ9"})
1961            .await;
1962    }
1963
1964    #[gpui::test]
1965    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1966        let mut cx = NeovimBackedTestContext::new(cx).await;
1967        let initial_state = indoc! {r"abˇc
1968          def
1969          paragraph
1970          the second
1971          third and
1972          final"};
1973
1974        cx.set_shared_state(initial_state).await;
1975        cx.simulate_shared_keystrokes(["shift-m"]).await;
1976        cx.assert_shared_state(indoc! {r"abc
1977          def
1978          paˇragraph
1979          the second
1980          third and
1981          final"})
1982            .await;
1983
1984        cx.set_shared_state(indoc! {r"
1985          1 2 3
1986          4 5 6
1987          7 8 ˇ9
1988          "})
1989            .await;
1990        cx.simulate_shared_keystrokes(["shift-m"]).await;
1991        cx.assert_shared_state(indoc! {r"
1992          1 2 3
1993          4 5 ˇ6
1994          7 8 9
1995          "})
1996            .await;
1997        cx.set_shared_state(indoc! {r"
1998          1 2 3
1999          4 5 6
2000          ˇ7 8 9
2001          "})
2002            .await;
2003        cx.simulate_shared_keystrokes(["shift-m"]).await;
2004        cx.assert_shared_state(indoc! {r"
2005          1 2 3
2006          ˇ4 5 6
2007          7 8 9
2008          "})
2009            .await;
2010        cx.set_shared_state(indoc! {r"
2011          ˇ1 2 3
2012          4 5 6
2013          7 8 9
2014          "})
2015            .await;
2016        cx.simulate_shared_keystrokes(["shift-m"]).await;
2017        cx.assert_shared_state(indoc! {r"
2018          1 2 3
2019          ˇ4 5 6
2020          7 8 9
2021          "})
2022            .await;
2023        cx.set_shared_state(indoc! {r"
2024          1 2 3
2025          ˇ4 5 6
2026          7 8 9
2027          "})
2028            .await;
2029        cx.simulate_shared_keystrokes(["shift-m"]).await;
2030        cx.assert_shared_state(indoc! {r"
2031          1 2 3
2032          ˇ4 5 6
2033          7 8 9
2034          "})
2035            .await;
2036        cx.set_shared_state(indoc! {r"
2037          1 2 3
2038          4 5 ˇ6
2039          7 8 9
2040          "})
2041            .await;
2042        cx.simulate_shared_keystrokes(["shift-m"]).await;
2043        cx.assert_shared_state(indoc! {r"
2044          1 2 3
2045          4 5 ˇ6
2046          7 8 9
2047          "})
2048            .await;
2049    }
2050
2051    #[gpui::test]
2052    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2053        let mut cx = NeovimBackedTestContext::new(cx).await;
2054        let initial_state = indoc! {r"abc
2055          deˇf
2056          paragraph
2057          the second
2058          third and
2059          final"};
2060
2061        cx.set_shared_state(initial_state).await;
2062        cx.simulate_shared_keystrokes(["shift-l"]).await;
2063        cx.assert_shared_state(indoc! {r"abc
2064          def
2065          paragraph
2066          the second
2067          third and
2068          fiˇnal"})
2069            .await;
2070
2071        cx.set_shared_state(indoc! {r"
2072          1 2 3
2073          4 5 ˇ6
2074          7 8 9
2075          "})
2076            .await;
2077        cx.simulate_shared_keystrokes(["shift-l"]).await;
2078        cx.assert_shared_state(indoc! {r"
2079          1 2 3
2080          4 5 6
2081          7 8 9
2082          ˇ"})
2083            .await;
2084
2085        cx.set_shared_state(indoc! {r"
2086          1 2 3
2087          ˇ4 5 6
2088          7 8 9
2089          "})
2090            .await;
2091        cx.simulate_shared_keystrokes(["shift-l"]).await;
2092        cx.assert_shared_state(indoc! {r"
2093          1 2 3
2094          4 5 6
2095          7 8 9
2096          ˇ"})
2097            .await;
2098
2099        cx.set_shared_state(indoc! {r"
2100          1 2 ˇ3
2101          4 5 6
2102          7 8 9
2103          "})
2104            .await;
2105        cx.simulate_shared_keystrokes(["shift-l"]).await;
2106        cx.assert_shared_state(indoc! {r"
2107          1 2 3
2108          4 5 6
2109          7 8 9
2110          ˇ"})
2111            .await;
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-l"]).await;
2120        cx.assert_shared_state(indoc! {r"
2121          1 2 3
2122          4 5 6
2123          7 8 9
2124          ˇ"})
2125            .await;
2126
2127        cx.set_shared_state(indoc! {r"
2128          1 2 3
2129          4 5 ˇ6
2130          7 8 9
2131          "})
2132            .await;
2133        cx.simulate_shared_keystrokes(["9", "shift-l"]).await;
2134        cx.assert_shared_state(indoc! {r"
2135          1 2 ˇ3
2136          4 5 6
2137          7 8 9
2138          "})
2139            .await;
2140    }
2141
2142    #[gpui::test]
2143    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2144        let mut cx = NeovimBackedTestContext::new(cx).await;
2145        cx.set_shared_state(indoc! {r"
2146        456 5ˇ67 678
2147        "})
2148            .await;
2149        cx.simulate_shared_keystrokes(["g", "e"]).await;
2150        cx.assert_shared_state(indoc! {r"
2151        45ˇ6 567 678
2152        "})
2153            .await;
2154
2155        // Test times
2156        cx.set_shared_state(indoc! {r"
2157        123 234 345
2158        456 5ˇ67 678
2159        "})
2160            .await;
2161        cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
2162        cx.assert_shared_state(indoc! {r"
2163        12ˇ3 234 345
2164        456 567 678
2165        "})
2166            .await;
2167
2168        // With punctuation
2169        cx.set_shared_state(indoc! {r"
2170        123 234 345
2171        4;5.6 5ˇ67 678
2172        789 890 901
2173        "})
2174            .await;
2175        cx.simulate_shared_keystrokes(["g", "e"]).await;
2176        cx.assert_shared_state(indoc! {r"
2177          123 234 345
2178          4;5.ˇ6 567 678
2179          789 890 901
2180        "})
2181            .await;
2182
2183        // With punctuation and count
2184        cx.set_shared_state(indoc! {r"
2185        123 234 345
2186        4;5.6 5ˇ67 678
2187        789 890 901
2188        "})
2189            .await;
2190        cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
2191        cx.assert_shared_state(indoc! {r"
2192          123 234 345
2193          ˇ4;5.6 567 678
2194          789 890 901
2195        "})
2196            .await;
2197
2198        // newlines
2199        cx.set_shared_state(indoc! {r"
2200        123 234 345
2201
2202        78ˇ9 890 901
2203        "})
2204            .await;
2205        cx.simulate_shared_keystrokes(["g", "e"]).await;
2206        cx.assert_shared_state(indoc! {r"
2207          123 234 345
2208          ˇ
2209          789 890 901
2210        "})
2211            .await;
2212        cx.simulate_shared_keystrokes(["g", "e"]).await;
2213        cx.assert_shared_state(indoc! {r"
2214          123 234 34ˇ5
2215
2216          789 890 901
2217        "})
2218            .await;
2219
2220        // With punctuation
2221        cx.set_shared_state(indoc! {r"
2222        123 234 345
2223        4;5.ˇ6 567 678
2224        789 890 901
2225        "})
2226            .await;
2227        cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
2228        cx.assert_shared_state(indoc! {r"
2229          123 234 34ˇ5
2230          4;5.6 567 678
2231          789 890 901
2232        "})
2233            .await;
2234    }
2235
2236    #[gpui::test]
2237    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2238        let mut cx = NeovimBackedTestContext::new(cx).await;
2239
2240        cx.set_shared_state(indoc! {"
2241            fn aˇ() {
2242              return
2243            }
2244        "})
2245            .await;
2246        cx.simulate_shared_keystrokes(["v", "$", "%"]).await;
2247        cx.assert_shared_state(indoc! {"
2248            fn a«() {
2249              return
2250            }ˇ»
2251        "})
2252            .await;
2253    }
2254}