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