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