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