motion.rs

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