motion.rs

   1use editor::{
   2    Anchor, Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, RowExt, ToOffset,
   3    display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
   4    movement::{
   5        self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
   6    },
   7};
   8use gpui::{Action, Context, Window, actions, px};
   9use language::{CharKind, Point, Selection, SelectionGoal};
  10use multi_buffer::MultiBufferRow;
  11use schemars::JsonSchema;
  12use serde::Deserialize;
  13use std::{f64, ops::Range};
  14use workspace::searchable::Direction;
  15
  16use crate::{
  17    Vim,
  18    normal::mark,
  19    state::{Mode, Operator},
  20    surrounds::SurroundsType,
  21};
  22
  23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
  24pub(crate) enum MotionKind {
  25    Linewise,
  26    Exclusive,
  27    Inclusive,
  28}
  29
  30impl MotionKind {
  31    pub(crate) fn for_mode(mode: Mode) -> Self {
  32        match mode {
  33            Mode::VisualLine => MotionKind::Linewise,
  34            _ => MotionKind::Exclusive,
  35        }
  36    }
  37
  38    pub(crate) fn linewise(&self) -> bool {
  39        matches!(self, MotionKind::Linewise)
  40    }
  41}
  42
  43#[derive(Clone, Debug, PartialEq, Eq)]
  44pub enum Motion {
  45    Left,
  46    WrappingLeft,
  47    Down {
  48        display_lines: bool,
  49    },
  50    Up {
  51        display_lines: bool,
  52    },
  53    Right,
  54    WrappingRight,
  55    NextWordStart {
  56        ignore_punctuation: bool,
  57    },
  58    NextWordEnd {
  59        ignore_punctuation: bool,
  60    },
  61    PreviousWordStart {
  62        ignore_punctuation: bool,
  63    },
  64    PreviousWordEnd {
  65        ignore_punctuation: bool,
  66    },
  67    NextSubwordStart {
  68        ignore_punctuation: bool,
  69    },
  70    NextSubwordEnd {
  71        ignore_punctuation: bool,
  72    },
  73    PreviousSubwordStart {
  74        ignore_punctuation: bool,
  75    },
  76    PreviousSubwordEnd {
  77        ignore_punctuation: bool,
  78    },
  79    FirstNonWhitespace {
  80        display_lines: bool,
  81    },
  82    CurrentLine,
  83    StartOfLine {
  84        display_lines: bool,
  85    },
  86    MiddleOfLine {
  87        display_lines: bool,
  88    },
  89    EndOfLine {
  90        display_lines: bool,
  91    },
  92    SentenceBackward,
  93    SentenceForward,
  94    StartOfParagraph,
  95    EndOfParagraph,
  96    StartOfDocument,
  97    EndOfDocument,
  98    Matching {
  99        match_quotes: bool,
 100    },
 101    GoToPercentage,
 102    UnmatchedForward {
 103        char: char,
 104    },
 105    UnmatchedBackward {
 106        char: char,
 107    },
 108    FindForward {
 109        before: bool,
 110        char: char,
 111        mode: FindRange,
 112        smartcase: bool,
 113    },
 114    FindBackward {
 115        after: bool,
 116        char: char,
 117        mode: FindRange,
 118        smartcase: bool,
 119    },
 120    Sneak {
 121        first_char: char,
 122        second_char: char,
 123        smartcase: bool,
 124    },
 125    SneakBackward {
 126        first_char: char,
 127        second_char: char,
 128        smartcase: bool,
 129    },
 130    RepeatFind {
 131        last_find: Box<Motion>,
 132    },
 133    RepeatFindReversed {
 134        last_find: Box<Motion>,
 135    },
 136    NextLineStart,
 137    PreviousLineStart,
 138    StartOfLineDownward,
 139    EndOfLineDownward,
 140    GoToColumn,
 141    WindowTop,
 142    WindowMiddle,
 143    WindowBottom,
 144    NextSectionStart,
 145    NextSectionEnd,
 146    PreviousSectionStart,
 147    PreviousSectionEnd,
 148    NextMethodStart,
 149    NextMethodEnd,
 150    PreviousMethodStart,
 151    PreviousMethodEnd,
 152    NextComment,
 153    PreviousComment,
 154    PreviousLesserIndent,
 155    PreviousGreaterIndent,
 156    PreviousSameIndent,
 157    NextLesserIndent,
 158    NextGreaterIndent,
 159    NextSameIndent,
 160
 161    // we don't have a good way to run a search synchronously, so
 162    // we handle search motions by running the search async and then
 163    // calling back into motion with this
 164    ZedSearchResult {
 165        prior_selections: Vec<Range<Anchor>>,
 166        new_selections: Vec<Range<Anchor>>,
 167    },
 168    Jump {
 169        anchor: Anchor,
 170        line: bool,
 171    },
 172}
 173
 174#[derive(Clone, Copy)]
 175enum IndentType {
 176    Lesser,
 177    Greater,
 178    Same,
 179}
 180
 181/// Moves to the start of the next word.
 182#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 183#[action(namespace = vim)]
 184#[serde(deny_unknown_fields)]
 185struct NextWordStart {
 186    #[serde(default)]
 187    ignore_punctuation: bool,
 188}
 189
 190/// Moves to the end of the next word.
 191#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 192#[action(namespace = vim)]
 193#[serde(deny_unknown_fields)]
 194struct NextWordEnd {
 195    #[serde(default)]
 196    ignore_punctuation: bool,
 197}
 198
 199/// Moves to the start of the previous word.
 200#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 201#[action(namespace = vim)]
 202#[serde(deny_unknown_fields)]
 203struct PreviousWordStart {
 204    #[serde(default)]
 205    ignore_punctuation: bool,
 206}
 207
 208/// Moves to the end of the previous word.
 209#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 210#[action(namespace = vim)]
 211#[serde(deny_unknown_fields)]
 212struct PreviousWordEnd {
 213    #[serde(default)]
 214    ignore_punctuation: bool,
 215}
 216
 217/// Moves to the start of the next subword.
 218#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 219#[action(namespace = vim)]
 220#[serde(deny_unknown_fields)]
 221pub(crate) struct NextSubwordStart {
 222    #[serde(default)]
 223    pub(crate) ignore_punctuation: bool,
 224}
 225
 226/// Moves to the end of the next subword.
 227#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 228#[action(namespace = vim)]
 229#[serde(deny_unknown_fields)]
 230pub(crate) struct NextSubwordEnd {
 231    #[serde(default)]
 232    pub(crate) ignore_punctuation: bool,
 233}
 234
 235/// Moves to the start of the previous subword.
 236#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 237#[action(namespace = vim)]
 238#[serde(deny_unknown_fields)]
 239pub(crate) struct PreviousSubwordStart {
 240    #[serde(default)]
 241    pub(crate) ignore_punctuation: bool,
 242}
 243
 244/// Moves to the end of the previous subword.
 245#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 246#[action(namespace = vim)]
 247#[serde(deny_unknown_fields)]
 248pub(crate) struct PreviousSubwordEnd {
 249    #[serde(default)]
 250    pub(crate) ignore_punctuation: bool,
 251}
 252
 253/// Moves cursor up by the specified number of lines.
 254#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 255#[action(namespace = vim)]
 256#[serde(deny_unknown_fields)]
 257pub(crate) struct Up {
 258    #[serde(default)]
 259    pub(crate) display_lines: bool,
 260}
 261
 262/// Moves cursor down by the specified number of lines.
 263#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 264#[action(namespace = vim)]
 265#[serde(deny_unknown_fields)]
 266pub(crate) struct Down {
 267    #[serde(default)]
 268    pub(crate) display_lines: bool,
 269}
 270
 271/// Moves to the first non-whitespace character on the current line.
 272#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 273#[action(namespace = vim)]
 274#[serde(deny_unknown_fields)]
 275struct FirstNonWhitespace {
 276    #[serde(default)]
 277    display_lines: bool,
 278}
 279
 280/// Moves to the matching bracket or delimiter.
 281#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 282#[action(namespace = vim)]
 283#[serde(deny_unknown_fields)]
 284struct Matching {
 285    #[serde(default)]
 286    /// Whether to include quote characters (`'`, `"`, `` ` ``) when searching
 287    /// for matching pairs. When `false`, only brackets and parentheses are
 288    /// matched, which aligns with Neovim's default `%` behavior.
 289    match_quotes: bool,
 290}
 291
 292/// Moves to the end of the current line.
 293#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 294#[action(namespace = vim)]
 295#[serde(deny_unknown_fields)]
 296struct EndOfLine {
 297    #[serde(default)]
 298    display_lines: bool,
 299}
 300
 301/// Moves to the start of the current line.
 302#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 303#[action(namespace = vim)]
 304#[serde(deny_unknown_fields)]
 305pub struct StartOfLine {
 306    #[serde(default)]
 307    pub(crate) display_lines: bool,
 308}
 309
 310/// Moves to the middle of the current line.
 311#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 312#[action(namespace = vim)]
 313#[serde(deny_unknown_fields)]
 314struct MiddleOfLine {
 315    #[serde(default)]
 316    display_lines: bool,
 317}
 318
 319/// Finds the next unmatched bracket or delimiter.
 320#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 321#[action(namespace = vim)]
 322#[serde(deny_unknown_fields)]
 323struct UnmatchedForward {
 324    #[serde(default)]
 325    char: char,
 326}
 327
 328/// Finds the previous unmatched bracket or delimiter.
 329#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
 330#[action(namespace = vim)]
 331#[serde(deny_unknown_fields)]
 332struct UnmatchedBackward {
 333    #[serde(default)]
 334    char: char,
 335}
 336
 337actions!(
 338    vim,
 339    [
 340        /// Moves cursor left one character.
 341        Left,
 342        /// Moves cursor left one character, wrapping to previous line.
 343        #[action(deprecated_aliases = ["vim::Backspace"])]
 344        WrappingLeft,
 345        /// Moves cursor right one character.
 346        Right,
 347        /// Moves cursor right one character, wrapping to next line.
 348        #[action(deprecated_aliases = ["vim::Space"])]
 349        WrappingRight,
 350        /// Selects the current line.
 351        CurrentLine,
 352        /// Moves to the start of the next sentence.
 353        SentenceForward,
 354        /// Moves to the start of the previous sentence.
 355        SentenceBackward,
 356        /// Moves to the start of the paragraph.
 357        StartOfParagraph,
 358        /// Moves to the end of the paragraph.
 359        EndOfParagraph,
 360        /// Moves to the start of the document.
 361        StartOfDocument,
 362        /// Moves to the end of the document.
 363        EndOfDocument,
 364        /// Goes to a percentage position in the file.
 365        GoToPercentage,
 366        /// Moves to the start of the next line.
 367        NextLineStart,
 368        /// Moves to the start of the previous line.
 369        PreviousLineStart,
 370        /// Moves to the start of a line downward.
 371        StartOfLineDownward,
 372        /// Moves to the end of a line downward.
 373        EndOfLineDownward,
 374        /// Goes to a specific column number.
 375        GoToColumn,
 376        /// Repeats the last character find.
 377        RepeatFind,
 378        /// Repeats the last character find in reverse.
 379        RepeatFindReversed,
 380        /// Moves to the top of the window.
 381        WindowTop,
 382        /// Moves to the middle of the window.
 383        WindowMiddle,
 384        /// Moves to the bottom of the window.
 385        WindowBottom,
 386        /// Moves to the start of the next section.
 387        NextSectionStart,
 388        /// Moves to the end of the next section.
 389        NextSectionEnd,
 390        /// Moves to the start of the previous section.
 391        PreviousSectionStart,
 392        /// Moves to the end of the previous section.
 393        PreviousSectionEnd,
 394        /// Moves to the start of the next method.
 395        NextMethodStart,
 396        /// Moves to the end of the next method.
 397        NextMethodEnd,
 398        /// Moves to the start of the previous method.
 399        PreviousMethodStart,
 400        /// Moves to the end of the previous method.
 401        PreviousMethodEnd,
 402        /// Moves to the next comment.
 403        NextComment,
 404        /// Moves to the previous comment.
 405        PreviousComment,
 406        /// Moves to the previous line with lesser indentation.
 407        PreviousLesserIndent,
 408        /// Moves to the previous line with greater indentation.
 409        PreviousGreaterIndent,
 410        /// Moves to the previous line with the same indentation.
 411        PreviousSameIndent,
 412        /// Moves to the next line with lesser indentation.
 413        NextLesserIndent,
 414        /// Moves to the next line with greater indentation.
 415        NextGreaterIndent,
 416        /// Moves to the next line with the same indentation.
 417        NextSameIndent,
 418    ]
 419);
 420
 421pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 422    Vim::action(editor, cx, |vim, _: &Left, window, cx| {
 423        vim.motion(Motion::Left, window, cx)
 424    });
 425    Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
 426        vim.motion(Motion::WrappingLeft, window, cx)
 427    });
 428    Vim::action(editor, cx, |vim, action: &Down, window, cx| {
 429        vim.motion(
 430            Motion::Down {
 431                display_lines: action.display_lines,
 432            },
 433            window,
 434            cx,
 435        )
 436    });
 437    Vim::action(editor, cx, |vim, action: &Up, window, cx| {
 438        vim.motion(
 439            Motion::Up {
 440                display_lines: action.display_lines,
 441            },
 442            window,
 443            cx,
 444        )
 445    });
 446    Vim::action(editor, cx, |vim, _: &Right, window, cx| {
 447        vim.motion(Motion::Right, window, cx)
 448    });
 449    Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
 450        vim.motion(Motion::WrappingRight, window, cx)
 451    });
 452    Vim::action(
 453        editor,
 454        cx,
 455        |vim, action: &FirstNonWhitespace, window, cx| {
 456            vim.motion(
 457                Motion::FirstNonWhitespace {
 458                    display_lines: action.display_lines,
 459                },
 460                window,
 461                cx,
 462            )
 463        },
 464    );
 465    Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
 466        vim.motion(
 467            Motion::StartOfLine {
 468                display_lines: action.display_lines,
 469            },
 470            window,
 471            cx,
 472        )
 473    });
 474    Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| {
 475        vim.motion(
 476            Motion::MiddleOfLine {
 477                display_lines: action.display_lines,
 478            },
 479            window,
 480            cx,
 481        )
 482    });
 483    Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
 484        vim.motion(
 485            Motion::EndOfLine {
 486                display_lines: action.display_lines,
 487            },
 488            window,
 489            cx,
 490        )
 491    });
 492    Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
 493        vim.motion(Motion::CurrentLine, window, cx)
 494    });
 495    Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
 496        vim.motion(Motion::StartOfParagraph, window, cx)
 497    });
 498    Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
 499        vim.motion(Motion::EndOfParagraph, window, cx)
 500    });
 501
 502    Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
 503        vim.motion(Motion::SentenceForward, window, cx)
 504    });
 505    Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
 506        vim.motion(Motion::SentenceBackward, window, cx)
 507    });
 508    Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
 509        vim.motion(Motion::StartOfDocument, window, cx)
 510    });
 511    Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
 512        vim.motion(Motion::EndOfDocument, window, cx)
 513    });
 514    Vim::action(
 515        editor,
 516        cx,
 517        |vim, &Matching { match_quotes }: &Matching, window, cx| {
 518            vim.motion(Motion::Matching { match_quotes }, window, cx)
 519        },
 520    );
 521
 522    Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
 523        vim.motion(Motion::GoToPercentage, window, cx)
 524    });
 525    Vim::action(
 526        editor,
 527        cx,
 528        |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
 529            vim.motion(Motion::UnmatchedForward { char }, window, cx)
 530        },
 531    );
 532    Vim::action(
 533        editor,
 534        cx,
 535        |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
 536            vim.motion(Motion::UnmatchedBackward { char }, window, cx)
 537        },
 538    );
 539    Vim::action(
 540        editor,
 541        cx,
 542        |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
 543            vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
 544        },
 545    );
 546    Vim::action(
 547        editor,
 548        cx,
 549        |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
 550            vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
 551        },
 552    );
 553    Vim::action(
 554        editor,
 555        cx,
 556        |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
 557            vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
 558        },
 559    );
 560    Vim::action(
 561        editor,
 562        cx,
 563        |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
 564            vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
 565        },
 566    );
 567    Vim::action(
 568        editor,
 569        cx,
 570        |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
 571            vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
 572        },
 573    );
 574    Vim::action(
 575        editor,
 576        cx,
 577        |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
 578            vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
 579        },
 580    );
 581    Vim::action(
 582        editor,
 583        cx,
 584        |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
 585            vim.motion(
 586                Motion::PreviousSubwordStart { ignore_punctuation },
 587                window,
 588                cx,
 589            )
 590        },
 591    );
 592    Vim::action(
 593        editor,
 594        cx,
 595        |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
 596            vim.motion(
 597                Motion::PreviousSubwordEnd { ignore_punctuation },
 598                window,
 599                cx,
 600            )
 601        },
 602    );
 603    Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
 604        vim.motion(Motion::NextLineStart, window, cx)
 605    });
 606    Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
 607        vim.motion(Motion::PreviousLineStart, window, cx)
 608    });
 609    Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
 610        vim.motion(Motion::StartOfLineDownward, window, cx)
 611    });
 612    Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
 613        vim.motion(Motion::EndOfLineDownward, window, cx)
 614    });
 615    Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
 616        vim.motion(Motion::GoToColumn, window, cx)
 617    });
 618
 619    Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
 620        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 621            vim.motion(Motion::RepeatFind { last_find }, window, cx);
 622        }
 623    });
 624
 625    Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
 626        if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
 627            vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
 628        }
 629    });
 630    Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
 631        vim.motion(Motion::WindowTop, window, cx)
 632    });
 633    Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
 634        vim.motion(Motion::WindowMiddle, window, cx)
 635    });
 636    Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
 637        vim.motion(Motion::WindowBottom, window, cx)
 638    });
 639
 640    Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
 641        vim.motion(Motion::PreviousSectionStart, window, cx)
 642    });
 643    Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
 644        vim.motion(Motion::NextSectionStart, window, cx)
 645    });
 646    Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
 647        vim.motion(Motion::PreviousSectionEnd, window, cx)
 648    });
 649    Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
 650        vim.motion(Motion::NextSectionEnd, window, cx)
 651    });
 652    Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
 653        vim.motion(Motion::PreviousMethodStart, window, cx)
 654    });
 655    Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
 656        vim.motion(Motion::NextMethodStart, window, cx)
 657    });
 658    Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
 659        vim.motion(Motion::PreviousMethodEnd, window, cx)
 660    });
 661    Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
 662        vim.motion(Motion::NextMethodEnd, window, cx)
 663    });
 664    Vim::action(editor, cx, |vim, &NextComment, window, cx| {
 665        vim.motion(Motion::NextComment, window, cx)
 666    });
 667    Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
 668        vim.motion(Motion::PreviousComment, window, cx)
 669    });
 670    Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| {
 671        vim.motion(Motion::PreviousLesserIndent, window, cx)
 672    });
 673    Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| {
 674        vim.motion(Motion::PreviousGreaterIndent, window, cx)
 675    });
 676    Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| {
 677        vim.motion(Motion::PreviousSameIndent, window, cx)
 678    });
 679    Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| {
 680        vim.motion(Motion::NextLesserIndent, window, cx)
 681    });
 682    Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| {
 683        vim.motion(Motion::NextGreaterIndent, window, cx)
 684    });
 685    Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| {
 686        vim.motion(Motion::NextSameIndent, window, cx)
 687    });
 688}
 689
 690impl Vim {
 691    pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
 692        if let Motion::ZedSearchResult {
 693            prior_selections, ..
 694        } = &m
 695        {
 696            match self.mode {
 697                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 698                    if !prior_selections.is_empty() {
 699                        self.update_editor(cx, |_, editor, cx| {
 700                            editor.change_selections(Default::default(), window, cx, |s| {
 701                                s.select_ranges(prior_selections.iter().cloned())
 702                            })
 703                        });
 704                    }
 705                }
 706                Mode::Normal | Mode::Replace | Mode::Insert => {
 707                    if self.active_operator().is_none() {
 708                        return;
 709                    }
 710                }
 711                Mode::HelixNormal | Mode::HelixSelect => {}
 712            }
 713        }
 714
 715        self.motion(m, window, cx)
 716    }
 717
 718    pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context<Self>) {
 719        if let Some(Operator::FindForward { .. })
 720        | Some(Operator::Sneak { .. })
 721        | Some(Operator::SneakBackward { .. })
 722        | Some(Operator::FindBackward { .. }) = self.active_operator()
 723        {
 724            self.pop_operator(window, cx);
 725        }
 726
 727        let count = Vim::take_count(cx);
 728        let forced_motion = Vim::take_forced_motion(cx);
 729        let active_operator = self.active_operator();
 730        let mut waiting_operator: Option<Operator> = None;
 731        match self.mode {
 732            Mode::Normal | Mode::Replace | Mode::Insert => {
 733                if active_operator == Some(Operator::AddSurrounds { target: None }) {
 734                    waiting_operator = Some(Operator::AddSurrounds {
 735                        target: Some(SurroundsType::Motion(motion)),
 736                    });
 737                } else {
 738                    self.normal_motion(motion, active_operator, count, forced_motion, window, cx)
 739                }
 740            }
 741            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
 742                self.visual_motion(motion, count, window, cx)
 743            }
 744
 745            Mode::HelixNormal => self.helix_normal_motion(motion, count, window, cx),
 746            Mode::HelixSelect => self.helix_select_motion(motion, count, window, cx),
 747        }
 748        self.clear_operator(window, cx);
 749        if let Some(operator) = waiting_operator {
 750            self.push_operator(operator, window, cx);
 751            Vim::globals(cx).pre_count = count
 752        }
 753    }
 754}
 755
 756// Motion handling is specified here:
 757// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 758impl Motion {
 759    fn default_kind(&self) -> MotionKind {
 760        use Motion::*;
 761        match self {
 762            Down { .. }
 763            | Up { .. }
 764            | StartOfDocument
 765            | EndOfDocument
 766            | CurrentLine
 767            | NextLineStart
 768            | PreviousLineStart
 769            | StartOfLineDownward
 770            | WindowTop
 771            | WindowMiddle
 772            | WindowBottom
 773            | NextSectionStart
 774            | NextSectionEnd
 775            | PreviousSectionStart
 776            | PreviousSectionEnd
 777            | NextMethodStart
 778            | NextMethodEnd
 779            | PreviousMethodStart
 780            | PreviousMethodEnd
 781            | NextComment
 782            | PreviousComment
 783            | PreviousLesserIndent
 784            | PreviousGreaterIndent
 785            | PreviousSameIndent
 786            | NextLesserIndent
 787            | NextGreaterIndent
 788            | NextSameIndent
 789            | GoToPercentage
 790            | Jump { line: true, .. } => MotionKind::Linewise,
 791            EndOfLine { .. }
 792            | EndOfLineDownward
 793            | Matching { .. }
 794            | FindForward { .. }
 795            | NextWordEnd { .. }
 796            | PreviousWordEnd { .. }
 797            | NextSubwordEnd { .. }
 798            | PreviousSubwordEnd { .. } => MotionKind::Inclusive,
 799            Left
 800            | WrappingLeft
 801            | Right
 802            | WrappingRight
 803            | StartOfLine { .. }
 804            | StartOfParagraph
 805            | EndOfParagraph
 806            | SentenceBackward
 807            | SentenceForward
 808            | GoToColumn
 809            | MiddleOfLine { .. }
 810            | UnmatchedForward { .. }
 811            | UnmatchedBackward { .. }
 812            | NextWordStart { .. }
 813            | PreviousWordStart { .. }
 814            | NextSubwordStart { .. }
 815            | PreviousSubwordStart { .. }
 816            | FirstNonWhitespace { .. }
 817            | FindBackward { .. }
 818            | Sneak { .. }
 819            | SneakBackward { .. }
 820            | Jump { .. }
 821            | ZedSearchResult { .. } => MotionKind::Exclusive,
 822            RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
 823                motion.default_kind()
 824            }
 825        }
 826    }
 827
 828    fn skip_exclusive_special_case(&self) -> bool {
 829        matches!(self, Motion::WrappingLeft | Motion::WrappingRight)
 830    }
 831
 832    pub(crate) fn push_to_jump_list(&self) -> bool {
 833        use Motion::*;
 834        match self {
 835            CurrentLine
 836            | Down { .. }
 837            | EndOfLine { .. }
 838            | EndOfLineDownward
 839            | FindBackward { .. }
 840            | FindForward { .. }
 841            | FirstNonWhitespace { .. }
 842            | GoToColumn
 843            | Left
 844            | MiddleOfLine { .. }
 845            | NextLineStart
 846            | NextSubwordEnd { .. }
 847            | NextSubwordStart { .. }
 848            | NextWordEnd { .. }
 849            | NextWordStart { .. }
 850            | PreviousLineStart
 851            | PreviousSubwordEnd { .. }
 852            | PreviousSubwordStart { .. }
 853            | PreviousWordEnd { .. }
 854            | PreviousWordStart { .. }
 855            | RepeatFind { .. }
 856            | RepeatFindReversed { .. }
 857            | Right
 858            | StartOfLine { .. }
 859            | StartOfLineDownward
 860            | Up { .. }
 861            | WrappingLeft
 862            | WrappingRight => false,
 863            EndOfDocument
 864            | EndOfParagraph
 865            | GoToPercentage
 866            | Jump { .. }
 867            | Matching { .. }
 868            | NextComment
 869            | NextGreaterIndent
 870            | NextLesserIndent
 871            | NextMethodEnd
 872            | NextMethodStart
 873            | NextSameIndent
 874            | NextSectionEnd
 875            | NextSectionStart
 876            | PreviousComment
 877            | PreviousGreaterIndent
 878            | PreviousLesserIndent
 879            | PreviousMethodEnd
 880            | PreviousMethodStart
 881            | PreviousSameIndent
 882            | PreviousSectionEnd
 883            | PreviousSectionStart
 884            | SentenceBackward
 885            | SentenceForward
 886            | Sneak { .. }
 887            | SneakBackward { .. }
 888            | StartOfDocument
 889            | StartOfParagraph
 890            | UnmatchedBackward { .. }
 891            | UnmatchedForward { .. }
 892            | WindowBottom
 893            | WindowMiddle
 894            | WindowTop
 895            | ZedSearchResult { .. } => true,
 896        }
 897    }
 898
 899    pub fn infallible(&self) -> bool {
 900        use Motion::*;
 901        match self {
 902            StartOfDocument | EndOfDocument | CurrentLine | EndOfLine { .. } => true,
 903            Down { .. }
 904            | Up { .. }
 905            | MiddleOfLine { .. }
 906            | Matching { .. }
 907            | UnmatchedForward { .. }
 908            | UnmatchedBackward { .. }
 909            | FindForward { .. }
 910            | RepeatFind { .. }
 911            | Left
 912            | WrappingLeft
 913            | Right
 914            | WrappingRight
 915            | StartOfLine { .. }
 916            | StartOfParagraph
 917            | EndOfParagraph
 918            | SentenceBackward
 919            | SentenceForward
 920            | StartOfLineDownward
 921            | EndOfLineDownward
 922            | GoToColumn
 923            | GoToPercentage
 924            | NextWordStart { .. }
 925            | NextWordEnd { .. }
 926            | PreviousWordStart { .. }
 927            | PreviousWordEnd { .. }
 928            | NextSubwordStart { .. }
 929            | NextSubwordEnd { .. }
 930            | PreviousSubwordStart { .. }
 931            | PreviousSubwordEnd { .. }
 932            | FirstNonWhitespace { .. }
 933            | FindBackward { .. }
 934            | Sneak { .. }
 935            | SneakBackward { .. }
 936            | RepeatFindReversed { .. }
 937            | WindowTop
 938            | WindowMiddle
 939            | WindowBottom
 940            | NextLineStart
 941            | PreviousLineStart
 942            | ZedSearchResult { .. }
 943            | NextSectionStart
 944            | NextSectionEnd
 945            | PreviousSectionStart
 946            | PreviousSectionEnd
 947            | NextMethodStart
 948            | NextMethodEnd
 949            | PreviousMethodStart
 950            | PreviousMethodEnd
 951            | NextComment
 952            | PreviousComment
 953            | PreviousLesserIndent
 954            | PreviousGreaterIndent
 955            | PreviousSameIndent
 956            | NextLesserIndent
 957            | NextGreaterIndent
 958            | NextSameIndent
 959            | Jump { .. } => false,
 960        }
 961    }
 962
 963    pub fn move_point(
 964        &self,
 965        map: &DisplaySnapshot,
 966        point: DisplayPoint,
 967        goal: SelectionGoal,
 968        maybe_times: Option<usize>,
 969        text_layout_details: &TextLayoutDetails,
 970    ) -> Option<(DisplayPoint, SelectionGoal)> {
 971        let times = maybe_times.unwrap_or(1);
 972        use Motion::*;
 973        let infallible = self.infallible();
 974        let (new_point, goal) = match self {
 975            Left => (left(map, point, times), SelectionGoal::None),
 976            WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None),
 977            Down {
 978                display_lines: false,
 979            } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
 980            Down {
 981                display_lines: true,
 982            } => down_display(map, point, goal, times, text_layout_details),
 983            Up {
 984                display_lines: false,
 985            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
 986            Up {
 987                display_lines: true,
 988            } => up_display(map, point, goal, times, text_layout_details),
 989            Right => (right(map, point, times), SelectionGoal::None),
 990            WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None),
 991            NextWordStart { ignore_punctuation } => (
 992                next_word_start(map, point, *ignore_punctuation, times),
 993                SelectionGoal::None,
 994            ),
 995            NextWordEnd { ignore_punctuation } => (
 996                next_word_end(map, point, *ignore_punctuation, times, true, true),
 997                SelectionGoal::None,
 998            ),
 999            PreviousWordStart { ignore_punctuation } => (
1000                previous_word_start(map, point, *ignore_punctuation, times),
1001                SelectionGoal::None,
1002            ),
1003            PreviousWordEnd { ignore_punctuation } => (
1004                previous_word_end(map, point, *ignore_punctuation, times),
1005                SelectionGoal::None,
1006            ),
1007            NextSubwordStart { ignore_punctuation } => (
1008                next_subword_start(map, point, *ignore_punctuation, times),
1009                SelectionGoal::None,
1010            ),
1011            NextSubwordEnd { ignore_punctuation } => (
1012                next_subword_end(map, point, *ignore_punctuation, times, true),
1013                SelectionGoal::None,
1014            ),
1015            PreviousSubwordStart { ignore_punctuation } => (
1016                previous_subword_start(map, point, *ignore_punctuation, times),
1017                SelectionGoal::None,
1018            ),
1019            PreviousSubwordEnd { ignore_punctuation } => (
1020                previous_subword_end(map, point, *ignore_punctuation, times),
1021                SelectionGoal::None,
1022            ),
1023            FirstNonWhitespace { display_lines } => (
1024                first_non_whitespace(map, *display_lines, point),
1025                SelectionGoal::None,
1026            ),
1027            StartOfLine { display_lines } => (
1028                start_of_line(map, *display_lines, point),
1029                SelectionGoal::None,
1030            ),
1031            MiddleOfLine { display_lines } => (
1032                middle_of_line(map, *display_lines, point, maybe_times),
1033                SelectionGoal::None,
1034            ),
1035            EndOfLine { display_lines } => (
1036                end_of_line(map, *display_lines, point, times),
1037                SelectionGoal::HorizontalPosition(f64::INFINITY),
1038            ),
1039            SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
1040            SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
1041            StartOfParagraph => (
1042                movement::start_of_paragraph(map, point, times),
1043                SelectionGoal::None,
1044            ),
1045            EndOfParagraph => (
1046                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
1047                SelectionGoal::None,
1048            ),
1049            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
1050            StartOfDocument => (
1051                start_of_document(map, point, maybe_times),
1052                SelectionGoal::None,
1053            ),
1054            EndOfDocument => (
1055                end_of_document(map, point, maybe_times),
1056                SelectionGoal::None,
1057            ),
1058            Matching { match_quotes } => (matching(map, point, *match_quotes), SelectionGoal::None),
1059            GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None),
1060            UnmatchedForward { char } => (
1061                unmatched_forward(map, point, *char, times),
1062                SelectionGoal::None,
1063            ),
1064            UnmatchedBackward { char } => (
1065                unmatched_backward(map, point, *char, times),
1066                SelectionGoal::None,
1067            ),
1068            // t f
1069            FindForward {
1070                before,
1071                char,
1072                mode,
1073                smartcase,
1074            } => {
1075                return find_forward(map, point, *before, *char, times, *mode, *smartcase)
1076                    .map(|new_point| (new_point, SelectionGoal::None));
1077            }
1078            // T F
1079            FindBackward {
1080                after,
1081                char,
1082                mode,
1083                smartcase,
1084            } => (
1085                find_backward(map, point, *after, *char, times, *mode, *smartcase),
1086                SelectionGoal::None,
1087            ),
1088            Sneak {
1089                first_char,
1090                second_char,
1091                smartcase,
1092            } => {
1093                return sneak(map, point, *first_char, *second_char, times, *smartcase)
1094                    .map(|new_point| (new_point, SelectionGoal::None));
1095            }
1096            SneakBackward {
1097                first_char,
1098                second_char,
1099                smartcase,
1100            } => {
1101                return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
1102                    .map(|new_point| (new_point, SelectionGoal::None));
1103            }
1104            // ; -- repeat the last find done with t, f, T, F
1105            RepeatFind { last_find } => match **last_find {
1106                Motion::FindForward {
1107                    before,
1108                    char,
1109                    mode,
1110                    smartcase,
1111                } => {
1112                    let mut new_point =
1113                        find_forward(map, point, before, char, times, mode, smartcase);
1114                    if new_point == Some(point) {
1115                        new_point =
1116                            find_forward(map, point, before, char, times + 1, mode, smartcase);
1117                    }
1118
1119                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1120                }
1121
1122                Motion::FindBackward {
1123                    after,
1124                    char,
1125                    mode,
1126                    smartcase,
1127                } => {
1128                    let mut new_point =
1129                        find_backward(map, point, after, char, times, mode, smartcase);
1130                    if new_point == point {
1131                        new_point =
1132                            find_backward(map, point, after, char, times + 1, mode, smartcase);
1133                    }
1134
1135                    (new_point, SelectionGoal::None)
1136                }
1137                Motion::Sneak {
1138                    first_char,
1139                    second_char,
1140                    smartcase,
1141                } => {
1142                    let mut new_point =
1143                        sneak(map, point, first_char, second_char, times, smartcase);
1144                    if new_point == Some(point) {
1145                        new_point =
1146                            sneak(map, point, first_char, second_char, times + 1, smartcase);
1147                    }
1148
1149                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1150                }
1151
1152                Motion::SneakBackward {
1153                    first_char,
1154                    second_char,
1155                    smartcase,
1156                } => {
1157                    let mut new_point =
1158                        sneak_backward(map, point, first_char, second_char, times, smartcase);
1159                    if new_point == Some(point) {
1160                        new_point = sneak_backward(
1161                            map,
1162                            point,
1163                            first_char,
1164                            second_char,
1165                            times + 1,
1166                            smartcase,
1167                        );
1168                    }
1169
1170                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1171                }
1172                _ => return None,
1173            },
1174            // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
1175            RepeatFindReversed { last_find } => match **last_find {
1176                Motion::FindForward {
1177                    before,
1178                    char,
1179                    mode,
1180                    smartcase,
1181                } => {
1182                    let mut new_point =
1183                        find_backward(map, point, before, char, times, mode, smartcase);
1184                    if new_point == point {
1185                        new_point =
1186                            find_backward(map, point, before, char, times + 1, mode, smartcase);
1187                    }
1188
1189                    (new_point, SelectionGoal::None)
1190                }
1191
1192                Motion::FindBackward {
1193                    after,
1194                    char,
1195                    mode,
1196                    smartcase,
1197                } => {
1198                    let mut new_point =
1199                        find_forward(map, point, after, char, times, mode, smartcase);
1200                    if new_point == Some(point) {
1201                        new_point =
1202                            find_forward(map, point, after, char, times + 1, mode, smartcase);
1203                    }
1204
1205                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1206                }
1207
1208                Motion::Sneak {
1209                    first_char,
1210                    second_char,
1211                    smartcase,
1212                } => {
1213                    let mut new_point =
1214                        sneak_backward(map, point, first_char, second_char, times, smartcase);
1215                    if new_point == Some(point) {
1216                        new_point = sneak_backward(
1217                            map,
1218                            point,
1219                            first_char,
1220                            second_char,
1221                            times + 1,
1222                            smartcase,
1223                        );
1224                    }
1225
1226                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1227                }
1228
1229                Motion::SneakBackward {
1230                    first_char,
1231                    second_char,
1232                    smartcase,
1233                } => {
1234                    let mut new_point =
1235                        sneak(map, point, first_char, second_char, times, smartcase);
1236                    if new_point == Some(point) {
1237                        new_point =
1238                            sneak(map, point, first_char, second_char, times + 1, smartcase);
1239                    }
1240
1241                    return new_point.map(|new_point| (new_point, SelectionGoal::None));
1242                }
1243                _ => return None,
1244            },
1245            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
1246            PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
1247            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
1248            EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
1249            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
1250            WindowTop => window_top(map, point, text_layout_details, times - 1),
1251            WindowMiddle => window_middle(map, point, text_layout_details),
1252            WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
1253            Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
1254            ZedSearchResult { new_selections, .. } => {
1255                // There will be only one selection, as
1256                // Search::SelectNextMatch selects a single match.
1257                if let Some(new_selection) = new_selections.first() {
1258                    (
1259                        new_selection.start.to_display_point(map),
1260                        SelectionGoal::None,
1261                    )
1262                } else {
1263                    return None;
1264                }
1265            }
1266            NextSectionStart => (
1267                section_motion(map, point, times, Direction::Next, true),
1268                SelectionGoal::None,
1269            ),
1270            NextSectionEnd => (
1271                section_motion(map, point, times, Direction::Next, false),
1272                SelectionGoal::None,
1273            ),
1274            PreviousSectionStart => (
1275                section_motion(map, point, times, Direction::Prev, true),
1276                SelectionGoal::None,
1277            ),
1278            PreviousSectionEnd => (
1279                section_motion(map, point, times, Direction::Prev, false),
1280                SelectionGoal::None,
1281            ),
1282
1283            NextMethodStart => (
1284                method_motion(map, point, times, Direction::Next, true),
1285                SelectionGoal::None,
1286            ),
1287            NextMethodEnd => (
1288                method_motion(map, point, times, Direction::Next, false),
1289                SelectionGoal::None,
1290            ),
1291            PreviousMethodStart => (
1292                method_motion(map, point, times, Direction::Prev, true),
1293                SelectionGoal::None,
1294            ),
1295            PreviousMethodEnd => (
1296                method_motion(map, point, times, Direction::Prev, false),
1297                SelectionGoal::None,
1298            ),
1299            NextComment => (
1300                comment_motion(map, point, times, Direction::Next),
1301                SelectionGoal::None,
1302            ),
1303            PreviousComment => (
1304                comment_motion(map, point, times, Direction::Prev),
1305                SelectionGoal::None,
1306            ),
1307            PreviousLesserIndent => (
1308                indent_motion(map, point, times, Direction::Prev, IndentType::Lesser),
1309                SelectionGoal::None,
1310            ),
1311            PreviousGreaterIndent => (
1312                indent_motion(map, point, times, Direction::Prev, IndentType::Greater),
1313                SelectionGoal::None,
1314            ),
1315            PreviousSameIndent => (
1316                indent_motion(map, point, times, Direction::Prev, IndentType::Same),
1317                SelectionGoal::None,
1318            ),
1319            NextLesserIndent => (
1320                indent_motion(map, point, times, Direction::Next, IndentType::Lesser),
1321                SelectionGoal::None,
1322            ),
1323            NextGreaterIndent => (
1324                indent_motion(map, point, times, Direction::Next, IndentType::Greater),
1325                SelectionGoal::None,
1326            ),
1327            NextSameIndent => (
1328                indent_motion(map, point, times, Direction::Next, IndentType::Same),
1329                SelectionGoal::None,
1330            ),
1331        };
1332        (new_point != point || infallible).then_some((new_point, goal))
1333    }
1334
1335    // Get the range value after self is applied to the specified selection.
1336    pub fn range(
1337        &self,
1338        map: &DisplaySnapshot,
1339        mut selection: Selection<DisplayPoint>,
1340        times: Option<usize>,
1341        text_layout_details: &TextLayoutDetails,
1342        forced_motion: bool,
1343    ) -> Option<(Range<DisplayPoint>, MotionKind)> {
1344        if let Motion::ZedSearchResult {
1345            prior_selections,
1346            new_selections,
1347        } = self
1348        {
1349            if let Some((prior_selection, new_selection)) =
1350                prior_selections.first().zip(new_selections.first())
1351            {
1352                let start = prior_selection
1353                    .start
1354                    .to_display_point(map)
1355                    .min(new_selection.start.to_display_point(map));
1356                let end = new_selection
1357                    .end
1358                    .to_display_point(map)
1359                    .max(prior_selection.end.to_display_point(map));
1360
1361                if start < end {
1362                    return Some((start..end, MotionKind::Exclusive));
1363                } else {
1364                    return Some((end..start, MotionKind::Exclusive));
1365                }
1366            } else {
1367                return None;
1368            }
1369        }
1370        let maybe_new_point = self.move_point(
1371            map,
1372            selection.head(),
1373            selection.goal,
1374            times,
1375            text_layout_details,
1376        );
1377
1378        let (new_head, goal) = match (maybe_new_point, forced_motion) {
1379            (Some((p, g)), _) => Some((p, g)),
1380            (None, false) => None,
1381            (None, true) => Some((selection.head(), selection.goal)),
1382        }?;
1383
1384        selection.set_head(new_head, goal);
1385
1386        let mut kind = match (self.default_kind(), forced_motion) {
1387            (MotionKind::Linewise, true) => MotionKind::Exclusive,
1388            (MotionKind::Exclusive, true) => MotionKind::Inclusive,
1389            (MotionKind::Inclusive, true) => MotionKind::Exclusive,
1390            (kind, false) => kind,
1391        };
1392
1393        if let Motion::NextWordStart {
1394            ignore_punctuation: _,
1395        } = self
1396        {
1397            // Another special case: When using the "w" motion in combination with an
1398            // operator and the last word moved over is at the end of a line, the end of
1399            // that word becomes the end of the operated text, not the first word in the
1400            // next line.
1401            let start = selection.start.to_point(map);
1402            let end = selection.end.to_point(map);
1403            let start_row = MultiBufferRow(selection.start.to_point(map).row);
1404            if end.row > start.row {
1405                selection.end = Point::new(start_row.0, map.buffer_snapshot().line_len(start_row))
1406                    .to_display_point(map);
1407
1408                // a bit of a hack, we need `cw` on a blank line to not delete the newline,
1409                // but dw on a blank line should. The `Linewise` returned from this method
1410                // causes the `d` operator to include the trailing newline.
1411                if selection.start == selection.end {
1412                    return Some((selection.start..selection.end, MotionKind::Linewise));
1413                }
1414            }
1415        } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() {
1416            let start_point = selection.start.to_point(map);
1417            let mut end_point = selection.end.to_point(map);
1418            let mut next_point = selection.end;
1419            *next_point.column_mut() += 1;
1420            next_point = map.clip_point(next_point, Bias::Right);
1421            if next_point.to_point(map) == end_point && forced_motion {
1422                selection.end = movement::saturating_left(map, selection.end);
1423            }
1424
1425            if end_point.row > start_point.row {
1426                let first_non_blank_of_start_row = map
1427                    .line_indent_for_buffer_row(MultiBufferRow(start_point.row))
1428                    .raw_len();
1429                // https://github.com/neovim/neovim/blob/ee143aaf65a0e662c42c636aa4a959682858b3e7/src/nvim/ops.c#L6178-L6203
1430                if end_point.column == 0 {
1431                    // If the motion is exclusive and the end of the motion is in column 1, the
1432                    // end of the motion is moved to the end of the previous line and the motion
1433                    // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1434                    // but "d}" will not include that line.
1435                    //
1436                    // If the motion is exclusive, the end of the motion is in column 1 and the
1437                    // start of the motion was at or before the first non-blank in the line, the
1438                    // motion becomes linewise.  Example: If a paragraph begins with some blanks
1439                    // and you do "d}" while standing on the first non-blank, all the lines of
1440                    // the paragraph are deleted, including the blanks.
1441                    if start_point.column <= first_non_blank_of_start_row {
1442                        kind = MotionKind::Linewise;
1443                    } else {
1444                        kind = MotionKind::Inclusive;
1445                    }
1446                    end_point.row -= 1;
1447                    end_point.column = 0;
1448                    selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1449                } else if let Motion::EndOfParagraph = self {
1450                    // Special case: When using the "}" motion, it's possible
1451                    // that there's no blank lines after the paragraph the
1452                    // cursor is currently on.
1453                    // In this situation the `end_point.column` value will be
1454                    // greater than 0, so the selection doesn't actually end on
1455                    // the first character of a blank line. In that case, we'll
1456                    // want to move one column to the right, to actually include
1457                    // all characters of the last non-blank line.
1458                    selection.end = movement::saturating_right(map, selection.end)
1459                }
1460            }
1461        } else if kind == MotionKind::Inclusive {
1462            selection.end = movement::saturating_right(map, selection.end)
1463        }
1464
1465        if kind == MotionKind::Linewise {
1466            selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1467            selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1468        }
1469        Some((selection.start..selection.end, kind))
1470    }
1471
1472    // Expands a selection using self for an operator
1473    pub fn expand_selection(
1474        &self,
1475        map: &DisplaySnapshot,
1476        selection: &mut Selection<DisplayPoint>,
1477        times: Option<usize>,
1478        text_layout_details: &TextLayoutDetails,
1479        forced_motion: bool,
1480    ) -> Option<MotionKind> {
1481        let (range, kind) = self.range(
1482            map,
1483            selection.clone(),
1484            times,
1485            text_layout_details,
1486            forced_motion,
1487        )?;
1488        selection.start = range.start;
1489        selection.end = range.end;
1490        Some(kind)
1491    }
1492}
1493
1494fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1495    for _ in 0..times {
1496        point = movement::saturating_left(map, point);
1497        if point.column() == 0 {
1498            break;
1499        }
1500    }
1501    point
1502}
1503
1504pub(crate) fn wrapping_left(
1505    map: &DisplaySnapshot,
1506    mut point: DisplayPoint,
1507    times: usize,
1508) -> DisplayPoint {
1509    for _ in 0..times {
1510        point = movement::left(map, point);
1511        if point.is_zero() {
1512            break;
1513        }
1514    }
1515    point
1516}
1517
1518fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1519    for _ in 0..times {
1520        point = wrapping_right_single(map, point);
1521        if point == map.max_point() {
1522            break;
1523        }
1524    }
1525    point
1526}
1527
1528fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
1529    let mut next_point = point;
1530    *next_point.column_mut() += 1;
1531    next_point = map.clip_point(next_point, Bias::Right);
1532    if next_point == point {
1533        if next_point.row() == map.max_point().row() {
1534            next_point
1535        } else {
1536            DisplayPoint::new(next_point.row().next_row(), 0)
1537        }
1538    } else {
1539        next_point
1540    }
1541}
1542
1543fn up_down_buffer_rows(
1544    map: &DisplaySnapshot,
1545    mut point: DisplayPoint,
1546    mut goal: SelectionGoal,
1547    mut times: isize,
1548    text_layout_details: &TextLayoutDetails,
1549) -> (DisplayPoint, SelectionGoal) {
1550    let bias = if times < 0 { Bias::Left } else { Bias::Right };
1551
1552    while map.is_folded_buffer_header(point.row()) {
1553        if times < 0 {
1554            (point, _) = movement::up(map, point, goal, true, text_layout_details);
1555            times += 1;
1556        } else if times > 0 {
1557            (point, _) = movement::down(map, point, goal, true, text_layout_details);
1558            times -= 1;
1559        } else {
1560            break;
1561        }
1562    }
1563
1564    let start = map.display_point_to_fold_point(point, Bias::Left);
1565    let begin_folded_line = map.fold_point_to_display_point(
1566        map.fold_snapshot()
1567            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1568    );
1569    let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1570
1571    let (goal_wrap, goal_x) = match goal {
1572        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1573        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end as f32),
1574        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x as f32),
1575        _ => {
1576            let x = map.x_for_display_point(point, text_layout_details);
1577            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.into()));
1578            (select_nth_wrapped_row, x.into())
1579        }
1580    };
1581
1582    let target = start.row() as isize + times;
1583    let new_row = (target.max(0) as u32).min(map.fold_snapshot().max_point().row());
1584
1585    let mut begin_folded_line = map.fold_point_to_display_point(
1586        map.fold_snapshot()
1587            .clip_point(FoldPoint::new(new_row, 0), bias),
1588    );
1589
1590    let mut i = 0;
1591    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1592        let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1593        if map
1594            .display_point_to_fold_point(next_folded_line, bias)
1595            .row()
1596            == new_row
1597        {
1598            i += 1;
1599            begin_folded_line = next_folded_line;
1600        } else {
1601            break;
1602        }
1603    }
1604
1605    let new_col = if i == goal_wrap {
1606        map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1607    } else {
1608        map.line_len(begin_folded_line.row())
1609    };
1610
1611    let point = DisplayPoint::new(begin_folded_line.row(), new_col);
1612    let mut clipped_point = map.clip_point(point, bias);
1613
1614    // When navigating vertically in vim mode with inlay hints present,
1615    // we need to handle the case where clipping moves us to a different row.
1616    // This can happen when moving down (Bias::Right) and hitting an inlay hint.
1617    // Re-clip with opposite bias to stay on the intended line.
1618    //
1619    // See: https://github.com/zed-industries/zed/issues/29134
1620    if clipped_point.row() > point.row() {
1621        clipped_point = map.clip_point(point, Bias::Left);
1622    }
1623
1624    (clipped_point, goal)
1625}
1626
1627fn down_display(
1628    map: &DisplaySnapshot,
1629    mut point: DisplayPoint,
1630    mut goal: SelectionGoal,
1631    times: usize,
1632    text_layout_details: &TextLayoutDetails,
1633) -> (DisplayPoint, SelectionGoal) {
1634    for _ in 0..times {
1635        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1636    }
1637
1638    (point, goal)
1639}
1640
1641fn up_display(
1642    map: &DisplaySnapshot,
1643    mut point: DisplayPoint,
1644    mut goal: SelectionGoal,
1645    times: usize,
1646    text_layout_details: &TextLayoutDetails,
1647) -> (DisplayPoint, SelectionGoal) {
1648    for _ in 0..times {
1649        (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1650    }
1651
1652    (point, goal)
1653}
1654
1655pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1656    for _ in 0..times {
1657        let new_point = movement::saturating_right(map, point);
1658        if point == new_point {
1659            break;
1660        }
1661        point = new_point;
1662    }
1663    point
1664}
1665
1666pub(crate) fn next_char(
1667    map: &DisplaySnapshot,
1668    point: DisplayPoint,
1669    allow_cross_newline: bool,
1670) -> DisplayPoint {
1671    let mut new_point = point;
1672    let mut max_column = map.line_len(new_point.row());
1673    if !allow_cross_newline {
1674        max_column -= 1;
1675    }
1676    if new_point.column() < max_column {
1677        *new_point.column_mut() += 1;
1678    } else if new_point < map.max_point() && allow_cross_newline {
1679        *new_point.row_mut() += 1;
1680        *new_point.column_mut() = 0;
1681    }
1682    map.clip_ignoring_line_ends(new_point, Bias::Right)
1683}
1684
1685pub(crate) fn next_word_start(
1686    map: &DisplaySnapshot,
1687    mut point: DisplayPoint,
1688    ignore_punctuation: bool,
1689    times: usize,
1690) -> DisplayPoint {
1691    let classifier = map
1692        .buffer_snapshot()
1693        .char_classifier_at(point.to_point(map))
1694        .ignore_punctuation(ignore_punctuation);
1695    for _ in 0..times {
1696        let mut crossed_newline = false;
1697        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1698            let left_kind = classifier.kind(left);
1699            let right_kind = classifier.kind(right);
1700            let at_newline = right == '\n';
1701
1702            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1703                || at_newline && crossed_newline
1704                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1705
1706            crossed_newline |= at_newline;
1707            found
1708        });
1709        if point == new_point {
1710            break;
1711        }
1712        point = new_point;
1713    }
1714    point
1715}
1716
1717fn next_end_impl(
1718    map: &DisplaySnapshot,
1719    mut point: DisplayPoint,
1720    times: usize,
1721    allow_cross_newline: bool,
1722    always_advance: bool,
1723    mut is_boundary: impl FnMut(char, char) -> bool,
1724) -> DisplayPoint {
1725    for _ in 0..times {
1726        let mut need_next_char = false;
1727        let new_point = if always_advance {
1728            next_char(map, point, allow_cross_newline)
1729        } else {
1730            point
1731        };
1732        let new_point = movement::find_boundary_exclusive(
1733            map,
1734            new_point,
1735            FindRange::MultiLine,
1736            |left, right| {
1737                let at_newline = right == '\n';
1738
1739                if !allow_cross_newline && at_newline {
1740                    need_next_char = true;
1741                    return true;
1742                }
1743
1744                is_boundary(left, right)
1745            },
1746        );
1747        let new_point = if need_next_char {
1748            next_char(map, new_point, true)
1749        } else {
1750            new_point
1751        };
1752        let new_point = map.clip_point(new_point, Bias::Left);
1753        if point == new_point {
1754            break;
1755        }
1756        point = new_point;
1757    }
1758    point
1759}
1760
1761pub(crate) fn next_word_end(
1762    map: &DisplaySnapshot,
1763    point: DisplayPoint,
1764    ignore_punctuation: bool,
1765    times: usize,
1766    allow_cross_newline: bool,
1767    always_advance: bool,
1768) -> DisplayPoint {
1769    let classifier = map
1770        .buffer_snapshot()
1771        .char_classifier_at(point.to_point(map))
1772        .ignore_punctuation(ignore_punctuation);
1773
1774    next_end_impl(
1775        map,
1776        point,
1777        times,
1778        allow_cross_newline,
1779        always_advance,
1780        |left, right| {
1781            let left_kind = classifier.kind(left);
1782            let right_kind = classifier.kind(right);
1783            left_kind != right_kind && left_kind != CharKind::Whitespace
1784        },
1785    )
1786}
1787
1788pub(crate) fn next_subword_end(
1789    map: &DisplaySnapshot,
1790    point: DisplayPoint,
1791    ignore_punctuation: bool,
1792    times: usize,
1793    allow_cross_newline: bool,
1794) -> DisplayPoint {
1795    let classifier = map
1796        .buffer_snapshot()
1797        .char_classifier_at(point.to_point(map))
1798        .ignore_punctuation(ignore_punctuation);
1799
1800    next_end_impl(
1801        map,
1802        point,
1803        times,
1804        allow_cross_newline,
1805        true,
1806        |left, right| {
1807            let left_kind = classifier.kind(left);
1808            let right_kind = classifier.kind(right);
1809            let is_stopping_punct = |c: char| ".$=\"'{}[]()<>".contains(c);
1810            let found_subword_end = is_subword_end(left, right, "$_-");
1811            let is_word_end = (left_kind != right_kind)
1812                && (!left.is_ascii_punctuation() || is_stopping_punct(left));
1813
1814            !left.is_whitespace() && (is_word_end || found_subword_end)
1815        },
1816    )
1817}
1818
1819fn previous_word_start(
1820    map: &DisplaySnapshot,
1821    mut point: DisplayPoint,
1822    ignore_punctuation: bool,
1823    times: usize,
1824) -> DisplayPoint {
1825    let classifier = map
1826        .buffer_snapshot()
1827        .char_classifier_at(point.to_point(map))
1828        .ignore_punctuation(ignore_punctuation);
1829    for _ in 0..times {
1830        // This works even though find_preceding_boundary is called for every character in the line containing
1831        // cursor because the newline is checked only once.
1832        let new_point = movement::find_preceding_boundary_display_point(
1833            map,
1834            point,
1835            FindRange::MultiLine,
1836            |left, right| {
1837                let left_kind = classifier.kind(left);
1838                let right_kind = classifier.kind(right);
1839
1840                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1841            },
1842        );
1843        if point == new_point {
1844            break;
1845        }
1846        point = new_point;
1847    }
1848    point
1849}
1850
1851fn previous_word_end(
1852    map: &DisplaySnapshot,
1853    point: DisplayPoint,
1854    ignore_punctuation: bool,
1855    times: usize,
1856) -> DisplayPoint {
1857    let classifier = map
1858        .buffer_snapshot()
1859        .char_classifier_at(point.to_point(map))
1860        .ignore_punctuation(ignore_punctuation);
1861    let mut point = point.to_point(map);
1862
1863    if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row))
1864        && let Some(ch) = map.buffer_snapshot().chars_at(point).next()
1865    {
1866        point.column += ch.len_utf8() as u32;
1867    }
1868    for _ in 0..times {
1869        let new_point = movement::find_preceding_boundary_point(
1870            &map.buffer_snapshot(),
1871            point,
1872            FindRange::MultiLine,
1873            |left, right| {
1874                let left_kind = classifier.kind(left);
1875                let right_kind = classifier.kind(right);
1876                match (left_kind, right_kind) {
1877                    (CharKind::Punctuation, CharKind::Whitespace)
1878                    | (CharKind::Punctuation, CharKind::Word)
1879                    | (CharKind::Word, CharKind::Whitespace)
1880                    | (CharKind::Word, CharKind::Punctuation) => true,
1881                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1882                    _ => false,
1883                }
1884            },
1885        );
1886        if new_point == point {
1887            break;
1888        }
1889        point = new_point;
1890    }
1891    movement::saturating_left(map, point.to_display_point(map))
1892}
1893
1894/// Checks if there's a subword boundary start between `left` and `right` characters.
1895/// This detects transitions like `_b` (separator to non-separator) or `aB` (lowercase to uppercase).
1896pub(crate) fn is_subword_start(left: char, right: char, separators: &str) -> bool {
1897    let is_separator = |c: char| separators.contains(c);
1898    (is_separator(left) && !is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
1899}
1900
1901/// Checks if there's a subword boundary end between `left` and `right` characters.
1902/// This detects transitions like `a_` (non-separator to separator) or `aB` (lowercase to uppercase).
1903pub(crate) fn is_subword_end(left: char, right: char, separators: &str) -> bool {
1904    let is_separator = |c: char| separators.contains(c);
1905    (!is_separator(left) && is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
1906}
1907
1908fn next_subword_start(
1909    map: &DisplaySnapshot,
1910    mut point: DisplayPoint,
1911    ignore_punctuation: bool,
1912    times: usize,
1913) -> DisplayPoint {
1914    let classifier = map
1915        .buffer_snapshot()
1916        .char_classifier_at(point.to_point(map))
1917        .ignore_punctuation(ignore_punctuation);
1918    for _ in 0..times {
1919        let mut crossed_newline = false;
1920        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1921            let left_kind = classifier.kind(left);
1922            let right_kind = classifier.kind(right);
1923            let at_newline = right == '\n';
1924            let is_stopping_punct = |c: char| "$=\"'{}[]()<>".contains(c);
1925            let found_subword_start = is_subword_start(left, right, ".$_-");
1926            let is_word_start = (left_kind != right_kind)
1927                && (!right.is_ascii_punctuation() || is_stopping_punct(right));
1928            let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
1929                || at_newline && crossed_newline
1930                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1931
1932            crossed_newline |= at_newline;
1933            found
1934        });
1935        if point == new_point {
1936            break;
1937        }
1938        point = new_point;
1939    }
1940    point
1941}
1942
1943fn previous_subword_start(
1944    map: &DisplaySnapshot,
1945    mut point: DisplayPoint,
1946    ignore_punctuation: bool,
1947    times: usize,
1948) -> DisplayPoint {
1949    let classifier = map
1950        .buffer_snapshot()
1951        .char_classifier_at(point.to_point(map))
1952        .ignore_punctuation(ignore_punctuation);
1953    for _ in 0..times {
1954        let mut crossed_newline = false;
1955        // This works even though find_preceding_boundary is called for every character in the line containing
1956        // cursor because the newline is checked only once.
1957        let new_point = movement::find_preceding_boundary_display_point(
1958            map,
1959            point,
1960            FindRange::MultiLine,
1961            |left, right| {
1962                let left_kind = classifier.kind(left);
1963                let right_kind = classifier.kind(right);
1964                let at_newline = right == '\n';
1965
1966                let is_stopping_punct = |c: char| ".$=\"'{}[]()<>".contains(c);
1967                let is_word_start = (left_kind != right_kind)
1968                    && (is_stopping_punct(right) || !right.is_ascii_punctuation());
1969                let found_subword_start = is_subword_start(left, right, ".$_-");
1970
1971                let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
1972                    || at_newline && crossed_newline
1973                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1974
1975                crossed_newline |= at_newline;
1976
1977                found
1978            },
1979        );
1980        if point == new_point {
1981            break;
1982        }
1983        point = new_point;
1984    }
1985    point
1986}
1987
1988fn previous_subword_end(
1989    map: &DisplaySnapshot,
1990    point: DisplayPoint,
1991    ignore_punctuation: bool,
1992    times: usize,
1993) -> DisplayPoint {
1994    let classifier = map
1995        .buffer_snapshot()
1996        .char_classifier_at(point.to_point(map))
1997        .ignore_punctuation(ignore_punctuation);
1998    let mut point = point.to_point(map);
1999
2000    if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row))
2001        && let Some(ch) = map.buffer_snapshot().chars_at(point).next()
2002    {
2003        point.column += ch.len_utf8() as u32;
2004    }
2005    for _ in 0..times {
2006        let new_point = movement::find_preceding_boundary_point(
2007            &map.buffer_snapshot(),
2008            point,
2009            FindRange::MultiLine,
2010            |left, right| {
2011                let left_kind = classifier.kind(left);
2012                let right_kind = classifier.kind(right);
2013
2014                let is_stopping_punct = |c: char| ".$;=\"'{}[]()<>".contains(c);
2015                let found_subword_end = is_subword_end(left, right, "$_-");
2016
2017                if found_subword_end {
2018                    return true;
2019                }
2020
2021                match (left_kind, right_kind) {
2022                    (CharKind::Word, CharKind::Whitespace)
2023                    | (CharKind::Word, CharKind::Punctuation) => true,
2024                    (CharKind::Punctuation, _) if is_stopping_punct(left) => true,
2025                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2026                    _ => false,
2027                }
2028            },
2029        );
2030        if new_point == point {
2031            break;
2032        }
2033        point = new_point;
2034    }
2035    movement::saturating_left(map, point.to_display_point(map))
2036}
2037
2038pub(crate) fn first_non_whitespace(
2039    map: &DisplaySnapshot,
2040    display_lines: bool,
2041    from: DisplayPoint,
2042) -> DisplayPoint {
2043    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2044    let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map));
2045    for (ch, offset) in map.buffer_chars_at(start_offset) {
2046        if ch == '\n' {
2047            return from;
2048        }
2049
2050        start_offset = offset;
2051
2052        if classifier.kind(ch) != CharKind::Whitespace {
2053            break;
2054        }
2055    }
2056
2057    start_offset.to_display_point(map)
2058}
2059
2060pub(crate) fn last_non_whitespace(
2061    map: &DisplaySnapshot,
2062    from: DisplayPoint,
2063    count: usize,
2064) -> DisplayPoint {
2065    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2066    let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map));
2067
2068    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2069    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2070        && classifier.kind(ch) != CharKind::Whitespace
2071    {
2072        return end_of_line.to_display_point(map);
2073    }
2074
2075    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2076        if ch == '\n' {
2077            break;
2078        }
2079        end_of_line = offset;
2080        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2081            break;
2082        }
2083    }
2084
2085    end_of_line.to_display_point(map)
2086}
2087
2088pub(crate) fn start_of_line(
2089    map: &DisplaySnapshot,
2090    display_lines: bool,
2091    point: DisplayPoint,
2092) -> DisplayPoint {
2093    if display_lines {
2094        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2095    } else {
2096        map.prev_line_boundary(point.to_point(map)).1
2097    }
2098}
2099
2100pub(crate) fn middle_of_line(
2101    map: &DisplaySnapshot,
2102    display_lines: bool,
2103    point: DisplayPoint,
2104    times: Option<usize>,
2105) -> DisplayPoint {
2106    let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2107        times as f64 / 100.
2108    } else {
2109        0.5
2110    };
2111    if display_lines {
2112        map.clip_point(
2113            DisplayPoint::new(
2114                point.row(),
2115                (map.line_len(point.row()) as f64 * percent) as u32,
2116            ),
2117            Bias::Left,
2118        )
2119    } else {
2120        let mut buffer_point = point.to_point(map);
2121        buffer_point.column = (map
2122            .buffer_snapshot()
2123            .line_len(MultiBufferRow(buffer_point.row)) as f64
2124            * percent) as u32;
2125
2126        map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2127    }
2128}
2129
2130pub(crate) fn end_of_line(
2131    map: &DisplaySnapshot,
2132    display_lines: bool,
2133    mut point: DisplayPoint,
2134    times: usize,
2135) -> DisplayPoint {
2136    if times > 1 {
2137        point = map.start_of_relative_buffer_row(point, times as isize - 1);
2138    }
2139    if display_lines {
2140        map.clip_point(
2141            DisplayPoint::new(point.row(), map.line_len(point.row())),
2142            Bias::Left,
2143        )
2144    } else {
2145        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2146    }
2147}
2148
2149pub(crate) fn sentence_backwards(
2150    map: &DisplaySnapshot,
2151    point: DisplayPoint,
2152    mut times: usize,
2153) -> DisplayPoint {
2154    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot());
2155    let mut chars = map.reverse_buffer_chars_at(start).peekable();
2156
2157    let mut was_newline = map
2158        .buffer_chars_at(start)
2159        .next()
2160        .is_some_and(|(c, _)| c == '\n');
2161
2162    while let Some((ch, offset)) = chars.next() {
2163        let start_of_next_sentence = if was_newline && ch == '\n' {
2164            Some(offset + ch.len_utf8())
2165        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2166            Some(next_non_blank(map, offset + ch.len_utf8()))
2167        } else if ch == '.' || ch == '?' || ch == '!' {
2168            start_of_next_sentence(map, offset + ch.len_utf8())
2169        } else {
2170            None
2171        };
2172
2173        if let Some(start_of_next_sentence) = start_of_next_sentence {
2174            if start_of_next_sentence < start {
2175                times = times.saturating_sub(1);
2176            }
2177            if times == 0 || offset.0 == 0 {
2178                return map.clip_point(
2179                    start_of_next_sentence
2180                        .to_offset(&map.buffer_snapshot())
2181                        .to_display_point(map),
2182                    Bias::Left,
2183                );
2184            }
2185        }
2186        if was_newline {
2187            start = offset;
2188        }
2189        was_newline = ch == '\n';
2190    }
2191
2192    DisplayPoint::zero()
2193}
2194
2195pub(crate) fn sentence_forwards(
2196    map: &DisplaySnapshot,
2197    point: DisplayPoint,
2198    mut times: usize,
2199) -> DisplayPoint {
2200    let start = point.to_point(map).to_offset(&map.buffer_snapshot());
2201    let mut chars = map.buffer_chars_at(start).peekable();
2202
2203    let mut was_newline = map
2204        .reverse_buffer_chars_at(start)
2205        .next()
2206        .is_some_and(|(c, _)| c == '\n')
2207        && chars.peek().is_some_and(|(c, _)| *c == '\n');
2208
2209    while let Some((ch, offset)) = chars.next() {
2210        if was_newline && ch == '\n' {
2211            continue;
2212        }
2213        let start_of_next_sentence = if was_newline {
2214            Some(next_non_blank(map, offset))
2215        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2216            Some(next_non_blank(map, offset + ch.len_utf8()))
2217        } else if ch == '.' || ch == '?' || ch == '!' {
2218            start_of_next_sentence(map, offset + ch.len_utf8())
2219        } else {
2220            None
2221        };
2222
2223        if let Some(start_of_next_sentence) = start_of_next_sentence {
2224            times = times.saturating_sub(1);
2225            if times == 0 {
2226                return map.clip_point(
2227                    start_of_next_sentence
2228                        .to_offset(&map.buffer_snapshot())
2229                        .to_display_point(map),
2230                    Bias::Right,
2231                );
2232            }
2233        }
2234
2235        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2236    }
2237
2238    map.max_point()
2239}
2240
2241fn next_non_blank(map: &DisplaySnapshot, start: MultiBufferOffset) -> MultiBufferOffset {
2242    for (c, o) in map.buffer_chars_at(start) {
2243        if c == '\n' || !c.is_whitespace() {
2244            return o;
2245        }
2246    }
2247
2248    map.buffer_snapshot().len()
2249}
2250
2251// given the offset after a ., !, or ? find the start of the next sentence.
2252// if this is not a sentence boundary, returns None.
2253fn start_of_next_sentence(
2254    map: &DisplaySnapshot,
2255    end_of_sentence: MultiBufferOffset,
2256) -> Option<MultiBufferOffset> {
2257    let chars = map.buffer_chars_at(end_of_sentence);
2258    let mut seen_space = false;
2259
2260    for (char, offset) in chars {
2261        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2262            continue;
2263        }
2264
2265        if char == '\n' && seen_space {
2266            return Some(offset);
2267        } else if char.is_whitespace() {
2268            seen_space = true;
2269        } else if seen_space {
2270            return Some(offset);
2271        } else {
2272            return None;
2273        }
2274    }
2275
2276    Some(map.buffer_snapshot().len())
2277}
2278
2279fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2280    let point = map.display_point_to_point(display_point, Bias::Left);
2281    let Some(mut excerpt) = map.buffer_snapshot().excerpt_containing(point..point) else {
2282        return display_point;
2283    };
2284    let offset = excerpt.buffer().point_to_offset(
2285        excerpt
2286            .buffer()
2287            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2288    );
2289    let buffer_range = excerpt.buffer_range();
2290    if offset >= buffer_range.start.0 && offset <= buffer_range.end.0 {
2291        let point = map
2292            .buffer_snapshot()
2293            .offset_to_point(excerpt.map_offset_from_buffer(BufferOffset(offset)));
2294        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2295    }
2296    for (excerpt, buffer, range) in map.buffer_snapshot().excerpts() {
2297        let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
2298            ..language::ToOffset::to_offset(&range.context.end, buffer);
2299        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2300            let text_anchor = buffer.anchor_after(offset);
2301            let anchor = Anchor::in_buffer(excerpt, text_anchor);
2302            return anchor.to_display_point(map);
2303        } else if offset <= excerpt_range.start {
2304            let anchor = Anchor::in_buffer(excerpt, range.context.start);
2305            return anchor.to_display_point(map);
2306        }
2307    }
2308
2309    map.clip_point(
2310        map.point_to_display_point(
2311            map.buffer_snapshot().clip_point(point, Bias::Left),
2312            Bias::Left,
2313        ),
2314        Bias::Left,
2315    )
2316}
2317
2318fn start_of_document(
2319    map: &DisplaySnapshot,
2320    display_point: DisplayPoint,
2321    maybe_times: Option<usize>,
2322) -> DisplayPoint {
2323    if let Some(times) = maybe_times {
2324        return go_to_line(map, display_point, times);
2325    }
2326
2327    let point = map.display_point_to_point(display_point, Bias::Left);
2328    let mut first_point = Point::zero();
2329    first_point.column = point.column;
2330
2331    map.clip_point(
2332        map.point_to_display_point(
2333            map.buffer_snapshot().clip_point(first_point, Bias::Left),
2334            Bias::Left,
2335        ),
2336        Bias::Left,
2337    )
2338}
2339
2340fn end_of_document(
2341    map: &DisplaySnapshot,
2342    display_point: DisplayPoint,
2343    maybe_times: Option<usize>,
2344) -> DisplayPoint {
2345    if let Some(times) = maybe_times {
2346        return go_to_line(map, display_point, times);
2347    };
2348    let point = map.display_point_to_point(display_point, Bias::Left);
2349    let mut last_point = map.buffer_snapshot().max_point();
2350    last_point.column = point.column;
2351
2352    map.clip_point(
2353        map.point_to_display_point(
2354            map.buffer_snapshot().clip_point(last_point, Bias::Left),
2355            Bias::Left,
2356        ),
2357        Bias::Left,
2358    )
2359}
2360
2361fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2362    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2363    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2364
2365    if head > outer.start && head < inner.start {
2366        let mut offset = inner.end.to_offset(map, Bias::Left);
2367        for c in map.buffer_snapshot().chars_at(offset) {
2368            if c == '/' || c == '\n' || c == '>' {
2369                return Some(offset.to_display_point(map));
2370            }
2371            offset += c.len_utf8();
2372        }
2373    } else {
2374        let mut offset = outer.start.to_offset(map, Bias::Left);
2375        for c in map.buffer_snapshot().chars_at(offset) {
2376            offset += c.len_utf8();
2377            if c == '<' || c == '\n' {
2378                return Some(offset.to_display_point(map));
2379            }
2380        }
2381    }
2382
2383    None
2384}
2385
2386const BRACKET_PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
2387
2388fn get_bracket_pair(ch: char) -> Option<(char, char, bool)> {
2389    for (open, close) in BRACKET_PAIRS {
2390        if ch == open {
2391            return Some((open, close, true));
2392        }
2393        if ch == close {
2394            return Some((open, close, false));
2395        }
2396    }
2397    None
2398}
2399
2400fn find_matching_bracket_text_based(
2401    map: &DisplaySnapshot,
2402    offset: MultiBufferOffset,
2403    line_range: Range<MultiBufferOffset>,
2404) -> Option<MultiBufferOffset> {
2405    let bracket_info = map
2406        .buffer_chars_at(offset)
2407        .take_while(|(_, char_offset)| *char_offset < line_range.end)
2408        .find_map(|(ch, char_offset)| get_bracket_pair(ch).map(|info| (info, char_offset)));
2409
2410    let (open, close, is_opening) = bracket_info?.0;
2411    let bracket_offset = bracket_info?.1;
2412
2413    let mut depth = 0i32;
2414    if is_opening {
2415        for (ch, char_offset) in map.buffer_chars_at(bracket_offset) {
2416            if ch == open {
2417                depth += 1;
2418            } else if ch == close {
2419                depth -= 1;
2420                if depth == 0 {
2421                    return Some(char_offset);
2422                }
2423            }
2424        }
2425    } else {
2426        for (ch, char_offset) in map.reverse_buffer_chars_at(bracket_offset + close.len_utf8()) {
2427            if ch == close {
2428                depth += 1;
2429            } else if ch == open {
2430                depth -= 1;
2431                if depth == 0 {
2432                    return Some(char_offset);
2433                }
2434            }
2435        }
2436    }
2437
2438    None
2439}
2440
2441fn matching(
2442    map: &DisplaySnapshot,
2443    display_point: DisplayPoint,
2444    match_quotes: bool,
2445) -> DisplayPoint {
2446    if !map.is_singleton() {
2447        return display_point;
2448    }
2449    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2450    let display_point = map.clip_at_line_end(display_point);
2451    let point = display_point.to_point(map);
2452    let offset = point.to_offset(&map.buffer_snapshot());
2453    let snapshot = map.buffer_snapshot();
2454
2455    // Ensure the range is contained by the current line.
2456    let mut line_end = map.next_line_boundary(point).0;
2457    if line_end == point {
2458        line_end = map.max_point().to_point(map);
2459    }
2460
2461    let is_quote_char = |ch: char| matches!(ch, '\'' | '"' | '`');
2462
2463    let make_range_filter = |require_on_bracket: bool| {
2464        move |buffer: &language::BufferSnapshot,
2465              opening_range: Range<BufferOffset>,
2466              closing_range: Range<BufferOffset>| {
2467            if !match_quotes
2468                && buffer
2469                    .chars_at(opening_range.start)
2470                    .next()
2471                    .is_some_and(is_quote_char)
2472            {
2473                return false;
2474            }
2475
2476            if require_on_bracket {
2477                // Attempt to find the smallest enclosing bracket range that also contains
2478                // the offset, which only happens if the cursor is currently in a bracket.
2479                opening_range.contains(&BufferOffset(offset.0))
2480                    || closing_range.contains(&BufferOffset(offset.0))
2481            } else {
2482                true
2483            }
2484        }
2485    };
2486
2487    let bracket_ranges = snapshot
2488        .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(true)))
2489        .or_else(|| {
2490            snapshot
2491                .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(false)))
2492        });
2493
2494    if let Some((opening_range, closing_range)) = bracket_ranges {
2495        let mut chars = map.buffer_snapshot().chars_at(offset);
2496        match chars.next() {
2497            Some('/') => {}
2498            _ => {
2499                if opening_range.contains(&offset) {
2500                    return closing_range.start.to_display_point(map);
2501                } else if closing_range.contains(&offset) {
2502                    return opening_range.start.to_display_point(map);
2503                }
2504            }
2505        }
2506    }
2507
2508    let line_range = map.prev_line_boundary(point).0..line_end;
2509    let visible_line_range =
2510        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2511    let line_range = line_range.start.to_offset(&map.buffer_snapshot())
2512        ..line_range.end.to_offset(&map.buffer_snapshot());
2513    let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range);
2514    if let Some(ranges) = ranges {
2515        let mut closest_pair_destination = None;
2516        let mut closest_distance = usize::MAX;
2517
2518        for (open_range, close_range) in ranges {
2519            if !match_quotes
2520                && map
2521                    .buffer_snapshot()
2522                    .chars_at(open_range.start)
2523                    .next()
2524                    .is_some_and(is_quote_char)
2525            {
2526                continue;
2527            }
2528
2529            if map.buffer_snapshot().chars_at(open_range.start).next() == Some('<') {
2530                if offset > open_range.start && offset < close_range.start {
2531                    let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2532                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2533                        return display_point;
2534                    }
2535                    if let Some(tag) = matching_tag(map, display_point) {
2536                        return tag;
2537                    }
2538                } else if close_range.contains(&offset) {
2539                    return open_range.start.to_display_point(map);
2540                } else if open_range.contains(&offset) {
2541                    return (close_range.end - 1).to_display_point(map);
2542                }
2543            }
2544
2545            if (open_range.contains(&offset) || open_range.start >= offset)
2546                && line_range.contains(&open_range.start)
2547            {
2548                let distance = open_range.start.saturating_sub(offset);
2549                if distance < closest_distance {
2550                    closest_pair_destination = Some(close_range.start);
2551                    closest_distance = distance;
2552                }
2553            }
2554
2555            if (close_range.contains(&offset) || close_range.start >= offset)
2556                && line_range.contains(&close_range.start)
2557            {
2558                let distance = close_range.start.saturating_sub(offset);
2559                if distance < closest_distance {
2560                    closest_pair_destination = Some(open_range.start);
2561                    closest_distance = distance;
2562                }
2563            }
2564
2565            continue;
2566        }
2567
2568        closest_pair_destination
2569            .map(|destination| destination.to_display_point(map))
2570            .unwrap_or_else(|| {
2571                find_matching_bracket_text_based(map, offset, line_range.clone())
2572                    .map(|o| o.to_display_point(map))
2573                    .unwrap_or(display_point)
2574            })
2575    } else {
2576        find_matching_bracket_text_based(map, offset, line_range)
2577            .map(|o| o.to_display_point(map))
2578            .unwrap_or(display_point)
2579    }
2580}
2581
2582// Go to {count} percentage in the file, on the first
2583// non-blank in the line linewise.  To compute the new
2584// line number this formula is used:
2585// ({count} * number-of-lines + 99) / 100
2586//
2587// https://neovim.io/doc/user/motion.html#N%25
2588fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2589    let total_lines = map.buffer_snapshot().max_point().row + 1;
2590    let target_line = (count * total_lines as usize).div_ceil(100);
2591    let target_point = DisplayPoint::new(
2592        DisplayRow(target_line.saturating_sub(1) as u32),
2593        point.column(),
2594    );
2595    map.clip_point(target_point, Bias::Left)
2596}
2597
2598fn unmatched_forward(
2599    map: &DisplaySnapshot,
2600    mut display_point: DisplayPoint,
2601    char: char,
2602    times: usize,
2603) -> DisplayPoint {
2604    for _ in 0..times {
2605        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2606        let point = display_point.to_point(map);
2607        let offset = point.to_offset(&map.buffer_snapshot());
2608
2609        let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2610        let Some(ranges) = ranges else { break };
2611        let mut closest_closing_destination = None;
2612        let mut closest_distance = usize::MAX;
2613
2614        for (_, close_range) in ranges {
2615            if close_range.start > offset {
2616                let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2617                if Some(char) == chars.next() {
2618                    let distance = close_range.start - offset;
2619                    if distance < closest_distance {
2620                        closest_closing_destination = Some(close_range.start);
2621                        closest_distance = distance;
2622                        continue;
2623                    }
2624                }
2625            }
2626        }
2627
2628        let new_point = closest_closing_destination
2629            .map(|destination| destination.to_display_point(map))
2630            .unwrap_or(display_point);
2631        if new_point == display_point {
2632            break;
2633        }
2634        display_point = new_point;
2635    }
2636    display_point
2637}
2638
2639fn unmatched_backward(
2640    map: &DisplaySnapshot,
2641    mut display_point: DisplayPoint,
2642    char: char,
2643    times: usize,
2644) -> DisplayPoint {
2645    for _ in 0..times {
2646        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2647        let point = display_point.to_point(map);
2648        let offset = point.to_offset(&map.buffer_snapshot());
2649
2650        let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2651        let Some(ranges) = ranges else {
2652            break;
2653        };
2654
2655        let mut closest_starting_destination = None;
2656        let mut closest_distance = usize::MAX;
2657
2658        for (start_range, _) in ranges {
2659            if start_range.start < offset {
2660                let mut chars = map.buffer_snapshot().chars_at(start_range.start);
2661                if Some(char) == chars.next() {
2662                    let distance = offset - start_range.start;
2663                    if distance < closest_distance {
2664                        closest_starting_destination = Some(start_range.start);
2665                        closest_distance = distance;
2666                        continue;
2667                    }
2668                }
2669            }
2670        }
2671
2672        let new_point = closest_starting_destination
2673            .map(|destination| destination.to_display_point(map))
2674            .unwrap_or(display_point);
2675        if new_point == display_point {
2676            break;
2677        } else {
2678            display_point = new_point;
2679        }
2680    }
2681    display_point
2682}
2683
2684fn find_forward(
2685    map: &DisplaySnapshot,
2686    from: DisplayPoint,
2687    before: bool,
2688    target: char,
2689    times: usize,
2690    mode: FindRange,
2691    smartcase: bool,
2692) -> Option<DisplayPoint> {
2693    let mut to = from;
2694    let mut found = false;
2695
2696    for _ in 0..times {
2697        found = false;
2698        let new_to = find_boundary(map, to, mode, |_, right| {
2699            found = is_character_match(target, right, smartcase);
2700            found
2701        });
2702        if to == new_to {
2703            break;
2704        }
2705        to = new_to;
2706    }
2707
2708    if found {
2709        if before && to.column() > 0 {
2710            *to.column_mut() -= 1;
2711            Some(map.clip_point(to, Bias::Left))
2712        } else if before && to.row().0 > 0 {
2713            *to.row_mut() -= 1;
2714            *to.column_mut() = map.line(to.row()).len() as u32;
2715            Some(map.clip_point(to, Bias::Left))
2716        } else {
2717            Some(to)
2718        }
2719    } else {
2720        None
2721    }
2722}
2723
2724fn find_backward(
2725    map: &DisplaySnapshot,
2726    from: DisplayPoint,
2727    after: bool,
2728    target: char,
2729    times: usize,
2730    mode: FindRange,
2731    smartcase: bool,
2732) -> DisplayPoint {
2733    let mut to = from;
2734
2735    for _ in 0..times {
2736        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2737            is_character_match(target, right, smartcase)
2738        });
2739        if to == new_to {
2740            break;
2741        }
2742        to = new_to;
2743    }
2744
2745    let next = map.buffer_snapshot().chars_at(to.to_point(map)).next();
2746    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2747        if after {
2748            *to.column_mut() += 1;
2749            map.clip_point(to, Bias::Right)
2750        } else {
2751            to
2752        }
2753    } else {
2754        from
2755    }
2756}
2757
2758/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2759pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2760    if smartcase {
2761        if target.is_uppercase() {
2762            target == other
2763        } else {
2764            target == other.to_ascii_lowercase()
2765        }
2766    } else {
2767        target == other
2768    }
2769}
2770
2771fn sneak(
2772    map: &DisplaySnapshot,
2773    from: DisplayPoint,
2774    first_target: char,
2775    second_target: char,
2776    times: usize,
2777    smartcase: bool,
2778) -> Option<DisplayPoint> {
2779    let mut to = from;
2780    let mut found = false;
2781
2782    for _ in 0..times {
2783        found = false;
2784        let new_to = find_boundary(
2785            map,
2786            movement::right(map, to),
2787            FindRange::MultiLine,
2788            |left, right| {
2789                found = is_character_match(first_target, left, smartcase)
2790                    && is_character_match(second_target, right, smartcase);
2791                found
2792            },
2793        );
2794        if to == new_to {
2795            break;
2796        }
2797        to = new_to;
2798    }
2799
2800    if found {
2801        Some(movement::left(map, to))
2802    } else {
2803        None
2804    }
2805}
2806
2807fn sneak_backward(
2808    map: &DisplaySnapshot,
2809    from: DisplayPoint,
2810    first_target: char,
2811    second_target: char,
2812    times: usize,
2813    smartcase: bool,
2814) -> Option<DisplayPoint> {
2815    let mut to = from;
2816    let mut found = false;
2817
2818    for _ in 0..times {
2819        found = false;
2820        let new_to =
2821            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2822                found = is_character_match(first_target, left, smartcase)
2823                    && is_character_match(second_target, right, smartcase);
2824                found
2825            });
2826        if to == new_to {
2827            break;
2828        }
2829        to = new_to;
2830    }
2831
2832    if found {
2833        Some(movement::left(map, to))
2834    } else {
2835        None
2836    }
2837}
2838
2839fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2840    let correct_line = map.start_of_relative_buffer_row(point, times as isize);
2841    first_non_whitespace(map, false, correct_line)
2842}
2843
2844fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2845    let correct_line = map.start_of_relative_buffer_row(point, -(times as isize));
2846    first_non_whitespace(map, false, correct_line)
2847}
2848
2849fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2850    let correct_line = map.start_of_relative_buffer_row(point, 0);
2851    right(map, correct_line, times.saturating_sub(1))
2852}
2853
2854pub(crate) fn next_line_end(
2855    map: &DisplaySnapshot,
2856    mut point: DisplayPoint,
2857    times: usize,
2858) -> DisplayPoint {
2859    if times > 1 {
2860        point = map.start_of_relative_buffer_row(point, times as isize - 1);
2861    }
2862    end_of_line(map, false, point, 1)
2863}
2864
2865fn window_top(
2866    map: &DisplaySnapshot,
2867    point: DisplayPoint,
2868    text_layout_details: &TextLayoutDetails,
2869    mut times: usize,
2870) -> (DisplayPoint, SelectionGoal) {
2871    let first_visible_line = text_layout_details
2872        .scroll_anchor
2873        .scroll_top_display_point(map);
2874
2875    if first_visible_line.row() != DisplayRow(0)
2876        && text_layout_details.vertical_scroll_margin as usize > times
2877    {
2878        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2879    }
2880
2881    if let Some(visible_rows) = text_layout_details.visible_rows {
2882        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2883        let new_row = (first_visible_line.row().0 + (times as u32))
2884            .min(bottom_row)
2885            .min(map.max_point().row().0);
2886        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2887
2888        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2889        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2890    } else {
2891        let new_row =
2892            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2893        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2894
2895        let new_point = DisplayPoint::new(new_row, new_col);
2896        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2897    }
2898}
2899
2900fn window_middle(
2901    map: &DisplaySnapshot,
2902    point: DisplayPoint,
2903    text_layout_details: &TextLayoutDetails,
2904) -> (DisplayPoint, SelectionGoal) {
2905    if let Some(visible_rows) = text_layout_details.visible_rows {
2906        let first_visible_line = text_layout_details
2907            .scroll_anchor
2908            .scroll_top_display_point(map);
2909
2910        let max_visible_rows =
2911            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2912
2913        let new_row =
2914            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2915        let new_row = DisplayRow(new_row);
2916        let new_col = point.column().min(map.line_len(new_row));
2917        let new_point = DisplayPoint::new(new_row, new_col);
2918        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2919    } else {
2920        (point, SelectionGoal::None)
2921    }
2922}
2923
2924fn window_bottom(
2925    map: &DisplaySnapshot,
2926    point: DisplayPoint,
2927    text_layout_details: &TextLayoutDetails,
2928    mut times: usize,
2929) -> (DisplayPoint, SelectionGoal) {
2930    if let Some(visible_rows) = text_layout_details.visible_rows {
2931        let first_visible_line = text_layout_details
2932            .scroll_anchor
2933            .scroll_top_display_point(map);
2934        let bottom_row = first_visible_line.row().0
2935            + (visible_rows + text_layout_details.scroll_anchor.scroll_anchor.offset.y - 1.).floor()
2936                as u32;
2937        if bottom_row < map.max_point().row().0
2938            && text_layout_details.vertical_scroll_margin as usize > times
2939        {
2940            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2941        }
2942        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2943        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2944        {
2945            first_visible_line.row()
2946        } else {
2947            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2948        };
2949        let new_col = point.column().min(map.line_len(new_row));
2950        let new_point = DisplayPoint::new(new_row, new_col);
2951        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2952    } else {
2953        (point, SelectionGoal::None)
2954    }
2955}
2956
2957fn method_motion(
2958    map: &DisplaySnapshot,
2959    mut display_point: DisplayPoint,
2960    times: usize,
2961    direction: Direction,
2962    is_start: bool,
2963) -> DisplayPoint {
2964    let snapshot = map.buffer_snapshot();
2965    if snapshot.as_singleton().is_none() {
2966        return display_point;
2967    }
2968
2969    for _ in 0..times {
2970        let offset = map
2971            .display_point_to_point(display_point, Bias::Left)
2972            .to_offset(&snapshot);
2973        let range = if direction == Direction::Prev {
2974            MultiBufferOffset(0)..offset
2975        } else {
2976            offset..snapshot.len()
2977        };
2978
2979        let possibilities = snapshot
2980            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2981            .filter_map(|(range, object)| {
2982                if !matches!(object, language::TextObject::AroundFunction) {
2983                    return None;
2984                }
2985
2986                let relevant = if is_start { range.start } else { range.end };
2987                if direction == Direction::Prev && relevant < offset {
2988                    Some(relevant)
2989                } else if direction == Direction::Next && relevant > offset + 1usize {
2990                    Some(relevant)
2991                } else {
2992                    None
2993                }
2994            });
2995
2996        let dest = if direction == Direction::Prev {
2997            possibilities.max().unwrap_or(offset)
2998        } else {
2999            possibilities.min().unwrap_or(offset)
3000        };
3001        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
3002        if new_point == display_point {
3003            break;
3004        }
3005        display_point = new_point;
3006    }
3007    display_point
3008}
3009
3010fn comment_motion(
3011    map: &DisplaySnapshot,
3012    mut display_point: DisplayPoint,
3013    times: usize,
3014    direction: Direction,
3015) -> DisplayPoint {
3016    let snapshot = map.buffer_snapshot();
3017    if snapshot.as_singleton().is_none() {
3018        return display_point;
3019    }
3020
3021    for _ in 0..times {
3022        let offset = map
3023            .display_point_to_point(display_point, Bias::Left)
3024            .to_offset(&snapshot);
3025        let range = if direction == Direction::Prev {
3026            MultiBufferOffset(0)..offset
3027        } else {
3028            offset..snapshot.len()
3029        };
3030
3031        let possibilities = snapshot
3032            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
3033            .filter_map(|(range, object)| {
3034                if !matches!(object, language::TextObject::AroundComment) {
3035                    return None;
3036                }
3037
3038                let relevant = if direction == Direction::Prev {
3039                    range.start
3040                } else {
3041                    range.end
3042                };
3043                if direction == Direction::Prev && relevant < offset {
3044                    Some(relevant)
3045                } else if direction == Direction::Next && relevant > offset + 1usize {
3046                    Some(relevant)
3047                } else {
3048                    None
3049                }
3050            });
3051
3052        let dest = if direction == Direction::Prev {
3053            possibilities.max().unwrap_or(offset)
3054        } else {
3055            possibilities.min().unwrap_or(offset)
3056        };
3057        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
3058        if new_point == display_point {
3059            break;
3060        }
3061        display_point = new_point;
3062    }
3063
3064    display_point
3065}
3066
3067fn section_motion(
3068    map: &DisplaySnapshot,
3069    mut display_point: DisplayPoint,
3070    times: usize,
3071    direction: Direction,
3072    is_start: bool,
3073) -> DisplayPoint {
3074    if map.buffer_snapshot().as_singleton().is_some() {
3075        for _ in 0..times {
3076            let offset = map
3077                .display_point_to_point(display_point, Bias::Left)
3078                .to_offset(&map.buffer_snapshot());
3079            let range = if direction == Direction::Prev {
3080                MultiBufferOffset(0)..offset
3081            } else {
3082                offset..map.buffer_snapshot().len()
3083            };
3084
3085            // we set a max start depth here because we want a section to only be "top level"
3086            // similar to vim's default of '{' in the first column.
3087            // (and without it, ]] at the start of editor.rs is -very- slow)
3088            let mut possibilities = map
3089                .buffer_snapshot()
3090                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
3091                .filter(|(_, object)| {
3092                    matches!(
3093                        object,
3094                        language::TextObject::AroundClass | language::TextObject::AroundFunction
3095                    )
3096                })
3097                .collect::<Vec<_>>();
3098            possibilities.sort_by_key(|(range_a, _)| range_a.start);
3099            let mut prev_end = None;
3100            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
3101                if t == language::TextObject::AroundFunction
3102                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
3103                {
3104                    return None;
3105                }
3106                prev_end = Some(range.end);
3107
3108                let relevant = if is_start { range.start } else { range.end };
3109                if direction == Direction::Prev && relevant < offset {
3110                    Some(relevant)
3111                } else if direction == Direction::Next && relevant > offset + 1usize {
3112                    Some(relevant)
3113                } else {
3114                    None
3115                }
3116            });
3117
3118            let offset = if direction == Direction::Prev {
3119                possibilities.max().unwrap_or(MultiBufferOffset(0))
3120            } else {
3121                possibilities.min().unwrap_or(map.buffer_snapshot().len())
3122            };
3123
3124            let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3125            if new_point == display_point {
3126                break;
3127            }
3128            display_point = new_point;
3129        }
3130        return display_point;
3131    };
3132
3133    for _ in 0..times {
3134        let next_point = if is_start {
3135            movement::start_of_excerpt(map, display_point, direction)
3136        } else {
3137            movement::end_of_excerpt(map, display_point, direction)
3138        };
3139        if next_point == display_point {
3140            break;
3141        }
3142        display_point = next_point;
3143    }
3144
3145    display_point
3146}
3147
3148fn matches_indent_type(
3149    target_indent: &text::LineIndent,
3150    current_indent: &text::LineIndent,
3151    indent_type: IndentType,
3152) -> bool {
3153    match indent_type {
3154        IndentType::Lesser => {
3155            target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3156        }
3157        IndentType::Greater => {
3158            target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3159        }
3160        IndentType::Same => {
3161            target_indent.spaces == current_indent.spaces
3162                && target_indent.tabs == current_indent.tabs
3163        }
3164    }
3165}
3166
3167fn indent_motion(
3168    map: &DisplaySnapshot,
3169    mut display_point: DisplayPoint,
3170    times: usize,
3171    direction: Direction,
3172    indent_type: IndentType,
3173) -> DisplayPoint {
3174    let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3175    let current_row = MultiBufferRow(buffer_point.row);
3176    let current_indent = map.line_indent_for_buffer_row(current_row);
3177    if current_indent.is_line_empty() {
3178        return display_point;
3179    }
3180    let max_row = map.max_point().to_point(map).row;
3181
3182    for _ in 0..times {
3183        let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3184
3185        let target_row = match direction {
3186            Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3187                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3188                !indent.is_line_empty()
3189                    && matches_indent_type(&indent, &current_indent, indent_type)
3190            }),
3191            Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3192                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3193                !indent.is_line_empty()
3194                    && matches_indent_type(&indent, &current_indent, indent_type)
3195            }),
3196        }
3197        .unwrap_or(current_buffer_row);
3198
3199        let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3200        let new_point = first_non_whitespace(map, false, new_point);
3201        if new_point == display_point {
3202            break;
3203        }
3204        display_point = new_point;
3205    }
3206    display_point
3207}
3208
3209#[cfg(test)]
3210mod test {
3211
3212    use crate::{
3213        motion::Matching,
3214        state::Mode,
3215        test::{NeovimBackedTestContext, VimTestContext},
3216    };
3217    use editor::Inlay;
3218    use gpui::KeyBinding;
3219    use indoc::indoc;
3220    use language::Point;
3221    use multi_buffer::MultiBufferRow;
3222
3223    #[gpui::test]
3224    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3225        let mut cx = NeovimBackedTestContext::new(cx).await;
3226
3227        let initial_state = indoc! {r"ˇabc
3228            def
3229
3230            paragraph
3231            the second
3232
3233
3234
3235            third and
3236            final"};
3237
3238        // goes down once
3239        cx.set_shared_state(initial_state).await;
3240        cx.simulate_shared_keystrokes("}").await;
3241        cx.shared_state().await.assert_eq(indoc! {r"abc
3242            def
3243            ˇ
3244            paragraph
3245            the second
3246
3247
3248
3249            third and
3250            final"});
3251
3252        // goes up once
3253        cx.simulate_shared_keystrokes("{").await;
3254        cx.shared_state().await.assert_eq(initial_state);
3255
3256        // goes down twice
3257        cx.simulate_shared_keystrokes("2 }").await;
3258        cx.shared_state().await.assert_eq(indoc! {r"abc
3259            def
3260
3261            paragraph
3262            the second
3263            ˇ
3264
3265
3266            third and
3267            final"});
3268
3269        // goes down over multiple blanks
3270        cx.simulate_shared_keystrokes("}").await;
3271        cx.shared_state().await.assert_eq(indoc! {r"abc
3272                def
3273
3274                paragraph
3275                the second
3276
3277
3278
3279                third and
3280                finaˇl"});
3281
3282        // goes up twice
3283        cx.simulate_shared_keystrokes("2 {").await;
3284        cx.shared_state().await.assert_eq(indoc! {r"abc
3285                def
3286                ˇ
3287                paragraph
3288                the second
3289
3290
3291
3292                third and
3293                final"});
3294    }
3295
3296    #[gpui::test]
3297    async fn test_paragraph_motion_with_whitespace_lines(cx: &mut gpui::TestAppContext) {
3298        let mut cx = NeovimBackedTestContext::new(cx).await;
3299
3300        // Test that whitespace-only lines are NOT treated as paragraph boundaries
3301        // Per vim's :help paragraph - only truly empty lines are boundaries
3302        // Line 2 has 4 spaces (whitespace-only), line 4 is truly empty
3303        cx.set_shared_state("ˇfirst\n    \nstill first\n\nsecond")
3304            .await;
3305        cx.simulate_shared_keystrokes("}").await;
3306
3307        // Should skip whitespace-only line and stop at truly empty line
3308        let mut shared_state = cx.shared_state().await;
3309        shared_state.assert_eq("first\n    \nstill first\nˇ\nsecond");
3310        shared_state.assert_matches();
3311
3312        // Should go back to original position
3313        cx.simulate_shared_keystrokes("{").await;
3314        let mut shared_state = cx.shared_state().await;
3315        shared_state.assert_eq("ˇfirst\n    \nstill first\n\nsecond");
3316        shared_state.assert_matches();
3317    }
3318
3319    #[gpui::test]
3320    async fn test_matching(cx: &mut gpui::TestAppContext) {
3321        let mut cx = NeovimBackedTestContext::new(cx).await;
3322
3323        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3324                do(something(with<Types>.and_arrays[0, 2]))
3325            }"})
3326            .await;
3327        cx.simulate_shared_keystrokes("%").await;
3328        cx.shared_state()
3329            .await
3330            .assert_eq(indoc! {r"func (a stringˇ) {
3331                do(something(with<Types>.and_arrays[0, 2]))
3332            }"});
3333
3334        // test it works on the last character of the line
3335        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3336            do(something(with<Types>.and_arrays[0, 2]))
3337            }"})
3338            .await;
3339        cx.simulate_shared_keystrokes("%").await;
3340        cx.shared_state()
3341            .await
3342            .assert_eq(indoc! {r"func (a string) {
3343            do(something(with<Types>.and_arrays[0, 2]))
3344            ˇ}"});
3345
3346        // test it works on immediate nesting
3347        cx.set_shared_state("ˇ{()}").await;
3348        cx.simulate_shared_keystrokes("%").await;
3349        cx.shared_state().await.assert_eq("{()ˇ}");
3350        cx.simulate_shared_keystrokes("%").await;
3351        cx.shared_state().await.assert_eq("ˇ{()}");
3352
3353        // test it works on immediate nesting inside braces
3354        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3355        cx.simulate_shared_keystrokes("%").await;
3356        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3357
3358        // test it jumps to the next paren on a line
3359        cx.set_shared_state("func ˇboop() {\n}").await;
3360        cx.simulate_shared_keystrokes("%").await;
3361        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3362    }
3363
3364    #[gpui::test]
3365    async fn test_matching_quotes_disabled(cx: &mut gpui::TestAppContext) {
3366        let mut cx = NeovimBackedTestContext::new(cx).await;
3367
3368        // Bind % to Matching with match_quotes: false to match Neovim's behavior
3369        // (Neovim's % doesn't match quotes by default)
3370        cx.update(|_, cx| {
3371            cx.bind_keys([KeyBinding::new(
3372                "%",
3373                Matching {
3374                    match_quotes: false,
3375                },
3376                None,
3377            )]);
3378        });
3379
3380        cx.set_shared_state("one {two 'thˇree' four}").await;
3381        cx.simulate_shared_keystrokes("%").await;
3382        cx.shared_state().await.assert_eq("one ˇ{two 'three' four}");
3383
3384        cx.set_shared_state("'hello wˇorld'").await;
3385        cx.simulate_shared_keystrokes("%").await;
3386        cx.shared_state().await.assert_eq("'hello wˇorld'");
3387
3388        cx.set_shared_state(r#"func ("teˇst") {}"#).await;
3389        cx.simulate_shared_keystrokes("%").await;
3390        cx.shared_state().await.assert_eq(r#"func ˇ("test") {}"#);
3391
3392        cx.set_shared_state("ˇ'hello'").await;
3393        cx.simulate_shared_keystrokes("%").await;
3394        cx.shared_state().await.assert_eq("ˇ'hello'");
3395
3396        cx.set_shared_state("'helloˇ'").await;
3397        cx.simulate_shared_keystrokes("%").await;
3398        cx.shared_state().await.assert_eq("'helloˇ'");
3399
3400        cx.set_shared_state(indoc! {r"func (a string) {
3401                do('somethiˇng'))
3402            }"})
3403            .await;
3404        cx.simulate_shared_keystrokes("%").await;
3405        cx.shared_state()
3406            .await
3407            .assert_eq(indoc! {r"func (a string) {
3408                doˇ('something'))
3409            }"});
3410    }
3411
3412    #[gpui::test]
3413    async fn test_matching_quotes_enabled(cx: &mut gpui::TestAppContext) {
3414        let mut cx = VimTestContext::new_markdown_with_rust(cx).await;
3415
3416        // Test default behavior (match_quotes: true as configured in keymap/vim.json)
3417        cx.set_state("one {two 'thˇree' four}", Mode::Normal);
3418        cx.simulate_keystrokes("%");
3419        cx.assert_state("one {two ˇ'three' four}", Mode::Normal);
3420
3421        cx.set_state("'hello wˇorld'", Mode::Normal);
3422        cx.simulate_keystrokes("%");
3423        cx.assert_state("ˇ'hello world'", Mode::Normal);
3424
3425        cx.set_state(r#"func ('teˇst') {}"#, Mode::Normal);
3426        cx.simulate_keystrokes("%");
3427        cx.assert_state(r#"func (ˇ'test') {}"#, Mode::Normal);
3428
3429        cx.set_state("ˇ'hello'", Mode::Normal);
3430        cx.simulate_keystrokes("%");
3431        cx.assert_state("'helloˇ'", Mode::Normal);
3432
3433        cx.set_state("'helloˇ'", Mode::Normal);
3434        cx.simulate_keystrokes("%");
3435        cx.assert_state("ˇ'hello'", Mode::Normal);
3436
3437        cx.set_state(
3438            indoc! {r"func (a string) {
3439                do('somethiˇng'))
3440            }"},
3441            Mode::Normal,
3442        );
3443        cx.simulate_keystrokes("%");
3444        cx.assert_state(
3445            indoc! {r"func (a string) {
3446                do(ˇ'something'))
3447            }"},
3448            Mode::Normal,
3449        );
3450    }
3451
3452    #[gpui::test]
3453    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3454        let mut cx = NeovimBackedTestContext::new(cx).await;
3455
3456        // test it works with curly braces
3457        cx.set_shared_state(indoc! {r"func (a string) {
3458                do(something(with<Types>.anˇd_arrays[0, 2]))
3459            }"})
3460            .await;
3461        cx.simulate_shared_keystrokes("] }").await;
3462        cx.shared_state()
3463            .await
3464            .assert_eq(indoc! {r"func (a string) {
3465                do(something(with<Types>.and_arrays[0, 2]))
3466            ˇ}"});
3467
3468        // test it works with brackets
3469        cx.set_shared_state(indoc! {r"func (a string) {
3470                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3471            }"})
3472            .await;
3473        cx.simulate_shared_keystrokes("] )").await;
3474        cx.shared_state()
3475            .await
3476            .assert_eq(indoc! {r"func (a string) {
3477                do(something(with<Types>.and_arrays[0, 2])ˇ)
3478            }"});
3479
3480        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3481            .await;
3482        cx.simulate_shared_keystrokes("] )").await;
3483        cx.shared_state()
3484            .await
3485            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3486
3487        // test it works on immediate nesting
3488        cx.set_shared_state("{ˇ {}{}}").await;
3489        cx.simulate_shared_keystrokes("] }").await;
3490        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3491        cx.set_shared_state("(ˇ ()())").await;
3492        cx.simulate_shared_keystrokes("] )").await;
3493        cx.shared_state().await.assert_eq("( ()()ˇ)");
3494
3495        // test it works on immediate nesting inside braces
3496        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3497        cx.simulate_shared_keystrokes("] }").await;
3498        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3499        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3500        cx.simulate_shared_keystrokes("] )").await;
3501        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3502    }
3503
3504    #[gpui::test]
3505    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3506        let mut cx = NeovimBackedTestContext::new(cx).await;
3507
3508        // test it works with curly braces
3509        cx.set_shared_state(indoc! {r"func (a string) {
3510                do(something(with<Types>.anˇd_arrays[0, 2]))
3511            }"})
3512            .await;
3513        cx.simulate_shared_keystrokes("[ {").await;
3514        cx.shared_state()
3515            .await
3516            .assert_eq(indoc! {r"func (a string) ˇ{
3517                do(something(with<Types>.and_arrays[0, 2]))
3518            }"});
3519
3520        // test it works with brackets
3521        cx.set_shared_state(indoc! {r"func (a string) {
3522                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3523            }"})
3524            .await;
3525        cx.simulate_shared_keystrokes("[ (").await;
3526        cx.shared_state()
3527            .await
3528            .assert_eq(indoc! {r"func (a string) {
3529                doˇ(something(with<Types>.and_arrays[0, 2]))
3530            }"});
3531
3532        // test it works on immediate nesting
3533        cx.set_shared_state("{{}{} ˇ }").await;
3534        cx.simulate_shared_keystrokes("[ {").await;
3535        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3536        cx.set_shared_state("(()() ˇ )").await;
3537        cx.simulate_shared_keystrokes("[ (").await;
3538        cx.shared_state().await.assert_eq("ˇ(()()  )");
3539
3540        // test it works on immediate nesting inside braces
3541        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3542        cx.simulate_shared_keystrokes("[ {").await;
3543        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3544        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3545        cx.simulate_shared_keystrokes("[ (").await;
3546        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3547    }
3548
3549    #[gpui::test]
3550    async fn test_unmatched_forward_markdown(cx: &mut gpui::TestAppContext) {
3551        let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3552
3553        cx.neovim.exec("set filetype=markdown").await;
3554
3555        cx.set_shared_state(indoc! {r"
3556            ```rs
3557            impl Worktree {
3558                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3559            ˇ    }
3560            }
3561            ```
3562        "})
3563            .await;
3564        cx.simulate_shared_keystrokes("] }").await;
3565        cx.shared_state().await.assert_eq(indoc! {r"
3566            ```rs
3567            impl Worktree {
3568                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3569                ˇ}
3570            }
3571            ```
3572        "});
3573
3574        cx.set_shared_state(indoc! {r"
3575            ```rs
3576            impl Worktree {
3577                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3578                }   ˇ
3579            }
3580            ```
3581        "})
3582            .await;
3583        cx.simulate_shared_keystrokes("] }").await;
3584        cx.shared_state().await.assert_eq(indoc! {r"
3585            ```rs
3586            impl Worktree {
3587                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3588                }  •
3589            ˇ}
3590            ```
3591        "});
3592    }
3593
3594    #[gpui::test]
3595    async fn test_unmatched_backward_markdown(cx: &mut gpui::TestAppContext) {
3596        let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3597
3598        cx.neovim.exec("set filetype=markdown").await;
3599
3600        cx.set_shared_state(indoc! {r"
3601            ```rs
3602            impl Worktree {
3603                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3604            ˇ    }
3605            }
3606            ```
3607        "})
3608            .await;
3609        cx.simulate_shared_keystrokes("[ {").await;
3610        cx.shared_state().await.assert_eq(indoc! {r"
3611            ```rs
3612            impl Worktree {
3613                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
3614                }
3615            }
3616            ```
3617        "});
3618
3619        cx.set_shared_state(indoc! {r"
3620            ```rs
3621            impl Worktree {
3622                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3623                }   ˇ
3624            }
3625            ```
3626        "})
3627            .await;
3628        cx.simulate_shared_keystrokes("[ {").await;
3629        cx.shared_state().await.assert_eq(indoc! {r"
3630            ```rs
3631            impl Worktree ˇ{
3632                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3633                }  •
3634            }
3635            ```
3636        "});
3637    }
3638
3639    #[gpui::test]
3640    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3641        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3642
3643        cx.neovim.exec("set filetype=html").await;
3644
3645        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3646        cx.simulate_shared_keystrokes("%").await;
3647        cx.shared_state()
3648            .await
3649            .assert_eq(indoc! {r"<body><ˇ/body>"});
3650        cx.simulate_shared_keystrokes("%").await;
3651
3652        // test jumping backwards
3653        cx.shared_state()
3654            .await
3655            .assert_eq(indoc! {r"<ˇbody></body>"});
3656
3657        // test self-closing tags
3658        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3659        cx.simulate_shared_keystrokes("%").await;
3660        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3661
3662        // test tag with attributes
3663        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3664            </div>
3665            "})
3666            .await;
3667        cx.simulate_shared_keystrokes("%").await;
3668        cx.shared_state()
3669            .await
3670            .assert_eq(indoc! {r"<div class='test' id='main'>
3671            <ˇ/div>
3672            "});
3673
3674        // test multi-line self-closing tag
3675        cx.set_shared_state(indoc! {r#"<a>
3676            <br
3677                test = "test"
3678            /ˇ>
3679        </a>"#})
3680            .await;
3681        cx.simulate_shared_keystrokes("%").await;
3682        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3683            ˇ<br
3684                test = "test"
3685            />
3686        </a>"#});
3687
3688        // test nested closing tag
3689        cx.set_shared_state(indoc! {r#"<html>
3690            <bˇody>
3691            </body>
3692        </html>"#})
3693            .await;
3694        cx.simulate_shared_keystrokes("%").await;
3695        cx.shared_state().await.assert_eq(indoc! {r#"<html>
3696            <body>
3697            <ˇ/body>
3698        </html>"#});
3699        cx.simulate_shared_keystrokes("%").await;
3700        cx.shared_state().await.assert_eq(indoc! {r#"<html>
3701            <ˇbody>
3702            </body>
3703        </html>"#});
3704    }
3705
3706    #[gpui::test]
3707    async fn test_matching_tag_with_quotes(cx: &mut gpui::TestAppContext) {
3708        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3709        cx.update(|_, cx| {
3710            cx.bind_keys([KeyBinding::new(
3711                "%",
3712                Matching {
3713                    match_quotes: false,
3714                },
3715                None,
3716            )]);
3717        });
3718
3719        cx.neovim.exec("set filetype=html").await;
3720        cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
3721            </div>
3722            "})
3723            .await;
3724        cx.simulate_shared_keystrokes("%").await;
3725        cx.shared_state()
3726            .await
3727            .assert_eq(indoc! {r"<div class='test' id='main'>
3728            <ˇ/div>
3729            "});
3730
3731        cx.update(|_, cx| {
3732            cx.bind_keys([KeyBinding::new("%", Matching { match_quotes: true }, None)]);
3733        });
3734
3735        cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
3736            </div>
3737            "})
3738            .await;
3739        cx.simulate_shared_keystrokes("%").await;
3740        cx.shared_state()
3741            .await
3742            .assert_eq(indoc! {r"<div class='test' id='main'>
3743            <ˇ/div>
3744            "});
3745    }
3746    #[gpui::test]
3747    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3748        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3749
3750        // test brackets within tags
3751        cx.set_shared_state(indoc! {r"function f() {
3752            return (
3753                <div rules={ˇ[{ a: 1 }]}>
3754                    <h1>test</h1>
3755                </div>
3756            );
3757        }"})
3758            .await;
3759        cx.simulate_shared_keystrokes("%").await;
3760        cx.shared_state().await.assert_eq(indoc! {r"function f() {
3761            return (
3762                <div rules={[{ a: 1 }ˇ]}>
3763                    <h1>test</h1>
3764                </div>
3765            );
3766        }"});
3767    }
3768
3769    #[gpui::test]
3770    async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
3771        let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
3772
3773        cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
3774            .await;
3775        cx.simulate_shared_keystrokes("%").await;
3776        cx.shared_state()
3777            .await
3778            .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
3779        cx.simulate_shared_keystrokes("%").await;
3780        cx.shared_state()
3781            .await
3782            .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
3783    }
3784
3785    #[gpui::test]
3786    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3787        let mut cx = NeovimBackedTestContext::new(cx).await;
3788
3789        // f and F
3790        cx.set_shared_state("ˇone two three four").await;
3791        cx.simulate_shared_keystrokes("f o").await;
3792        cx.shared_state().await.assert_eq("one twˇo three four");
3793        cx.simulate_shared_keystrokes(",").await;
3794        cx.shared_state().await.assert_eq("ˇone two three four");
3795        cx.simulate_shared_keystrokes("2 ;").await;
3796        cx.shared_state().await.assert_eq("one two three fˇour");
3797        cx.simulate_shared_keystrokes("shift-f e").await;
3798        cx.shared_state().await.assert_eq("one two threˇe four");
3799        cx.simulate_shared_keystrokes("2 ;").await;
3800        cx.shared_state().await.assert_eq("onˇe two three four");
3801        cx.simulate_shared_keystrokes(",").await;
3802        cx.shared_state().await.assert_eq("one two thrˇee four");
3803
3804        // t and T
3805        cx.set_shared_state("ˇone two three four").await;
3806        cx.simulate_shared_keystrokes("t o").await;
3807        cx.shared_state().await.assert_eq("one tˇwo three four");
3808        cx.simulate_shared_keystrokes(",").await;
3809        cx.shared_state().await.assert_eq("oˇne two three four");
3810        cx.simulate_shared_keystrokes("2 ;").await;
3811        cx.shared_state().await.assert_eq("one two three ˇfour");
3812        cx.simulate_shared_keystrokes("shift-t e").await;
3813        cx.shared_state().await.assert_eq("one two threeˇ four");
3814        cx.simulate_shared_keystrokes("3 ;").await;
3815        cx.shared_state().await.assert_eq("oneˇ two three four");
3816        cx.simulate_shared_keystrokes(",").await;
3817        cx.shared_state().await.assert_eq("one two thˇree four");
3818    }
3819
3820    #[gpui::test]
3821    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3822        let mut cx = NeovimBackedTestContext::new(cx).await;
3823        let initial_state = indoc! {r"something(ˇfoo)"};
3824        cx.set_shared_state(initial_state).await;
3825        cx.simulate_shared_keystrokes("}").await;
3826        cx.shared_state().await.assert_eq("something(fooˇ)");
3827    }
3828
3829    #[gpui::test]
3830    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3831        let mut cx = NeovimBackedTestContext::new(cx).await;
3832        cx.set_shared_state("ˇone\n  two\nthree").await;
3833        cx.simulate_shared_keystrokes("enter").await;
3834        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3835    }
3836
3837    #[gpui::test]
3838    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3839        let mut cx = NeovimBackedTestContext::new(cx).await;
3840        cx.set_shared_state("ˇ one\n two \nthree").await;
3841        cx.simulate_shared_keystrokes("g _").await;
3842        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3843
3844        cx.set_shared_state("ˇ one \n two \nthree").await;
3845        cx.simulate_shared_keystrokes("g _").await;
3846        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3847        cx.simulate_shared_keystrokes("2 g _").await;
3848        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3849    }
3850
3851    #[gpui::test]
3852    async fn test_end_of_line_with_vertical_motion(cx: &mut gpui::TestAppContext) {
3853        let mut cx = NeovimBackedTestContext::new(cx).await;
3854
3855        // test $ followed by k maintains end-of-line position
3856        cx.set_shared_state(indoc! {"
3857            The quick brown
3858            fˇox
3859            jumps over the
3860            lazy dog
3861            "})
3862            .await;
3863        cx.simulate_shared_keystrokes("$ k").await;
3864        cx.shared_state().await.assert_eq(indoc! {"
3865            The quick browˇn
3866            fox
3867            jumps over the
3868            lazy dog
3869            "});
3870        cx.simulate_shared_keystrokes("j j").await;
3871        cx.shared_state().await.assert_eq(indoc! {"
3872            The quick brown
3873            fox
3874            jumps over thˇe
3875            lazy dog
3876            "});
3877
3878        // test horizontal movement resets the end-of-line behavior
3879        cx.set_shared_state(indoc! {"
3880            The quick brown fox
3881            jumps over the
3882            lazy ˇdog
3883            "})
3884            .await;
3885        cx.simulate_shared_keystrokes("$ k").await;
3886        cx.shared_state().await.assert_eq(indoc! {"
3887            The quick brown fox
3888            jumps over thˇe
3889            lazy dog
3890            "});
3891        cx.simulate_shared_keystrokes("b b").await;
3892        cx.shared_state().await.assert_eq(indoc! {"
3893            The quick brown fox
3894            jumps ˇover the
3895            lazy dog
3896            "});
3897        cx.simulate_shared_keystrokes("k").await;
3898        cx.shared_state().await.assert_eq(indoc! {"
3899            The quˇick brown fox
3900            jumps over the
3901            lazy dog
3902            "});
3903
3904        // Test that, when the cursor is moved to the end of the line using `l`,
3905        // if `$` is used, the cursor stays at the end of the line when moving
3906        // to a longer line, ensuring that the selection goal was correctly
3907        // updated.
3908        cx.set_shared_state(indoc! {"
3909            The quick brown fox
3910            jumps over the
3911            lazy dˇog
3912            "})
3913            .await;
3914        cx.simulate_shared_keystrokes("l").await;
3915        cx.shared_state().await.assert_eq(indoc! {"
3916            The quick brown fox
3917            jumps over the
3918            lazy doˇg
3919            "});
3920        cx.simulate_shared_keystrokes("$ k").await;
3921        cx.shared_state().await.assert_eq(indoc! {"
3922            The quick brown fox
3923            jumps over thˇe
3924            lazy dog
3925            "});
3926    }
3927
3928    #[gpui::test]
3929    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3930        let mut cx = NeovimBackedTestContext::new(cx).await;
3931        let initial_state = indoc! {r"abc
3932          def
3933          paragraph
3934          the second
3935          third ˇand
3936          final"};
3937
3938        cx.set_shared_state(initial_state).await;
3939        cx.simulate_shared_keystrokes("shift-h").await;
3940        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3941          def
3942          paragraph
3943          the second
3944          third and
3945          final"});
3946
3947        // clip point
3948        cx.set_shared_state(indoc! {r"
3949          1 2 3
3950          4 5 6
3951          7 8 ˇ9
3952          "})
3953            .await;
3954        cx.simulate_shared_keystrokes("shift-h").await;
3955        cx.shared_state().await.assert_eq(indoc! {"
3956          1 2 ˇ3
3957          4 5 6
3958          7 8 9
3959          "});
3960
3961        cx.set_shared_state(indoc! {r"
3962          1 2 3
3963          4 5 6
3964          ˇ7 8 9
3965          "})
3966            .await;
3967        cx.simulate_shared_keystrokes("shift-h").await;
3968        cx.shared_state().await.assert_eq(indoc! {"
3969          ˇ1 2 3
3970          4 5 6
3971          7 8 9
3972          "});
3973
3974        cx.set_shared_state(indoc! {r"
3975          1 2 3
3976          4 5 ˇ6
3977          7 8 9"})
3978            .await;
3979        cx.simulate_shared_keystrokes("9 shift-h").await;
3980        cx.shared_state().await.assert_eq(indoc! {"
3981          1 2 3
3982          4 5 6
3983          7 8 ˇ9"});
3984    }
3985
3986    #[gpui::test]
3987    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3988        let mut cx = NeovimBackedTestContext::new(cx).await;
3989        let initial_state = indoc! {r"abˇc
3990          def
3991          paragraph
3992          the second
3993          third and
3994          final"};
3995
3996        cx.set_shared_state(initial_state).await;
3997        cx.simulate_shared_keystrokes("shift-m").await;
3998        cx.shared_state().await.assert_eq(indoc! {r"abc
3999          def
4000          paˇragraph
4001          the second
4002          third and
4003          final"});
4004
4005        cx.set_shared_state(indoc! {r"
4006          1 2 3
4007          4 5 6
4008          7 8 ˇ9
4009          "})
4010            .await;
4011        cx.simulate_shared_keystrokes("shift-m").await;
4012        cx.shared_state().await.assert_eq(indoc! {"
4013          1 2 3
4014          4 5 ˇ6
4015          7 8 9
4016          "});
4017        cx.set_shared_state(indoc! {r"
4018          1 2 3
4019          4 5 6
4020          ˇ7 8 9
4021          "})
4022            .await;
4023        cx.simulate_shared_keystrokes("shift-m").await;
4024        cx.shared_state().await.assert_eq(indoc! {"
4025          1 2 3
4026          ˇ4 5 6
4027          7 8 9
4028          "});
4029        cx.set_shared_state(indoc! {r"
4030          ˇ1 2 3
4031          4 5 6
4032          7 8 9
4033          "})
4034            .await;
4035        cx.simulate_shared_keystrokes("shift-m").await;
4036        cx.shared_state().await.assert_eq(indoc! {"
4037          1 2 3
4038          ˇ4 5 6
4039          7 8 9
4040          "});
4041        cx.set_shared_state(indoc! {r"
4042          1 2 3
4043          ˇ4 5 6
4044          7 8 9
4045          "})
4046            .await;
4047        cx.simulate_shared_keystrokes("shift-m").await;
4048        cx.shared_state().await.assert_eq(indoc! {"
4049          1 2 3
4050          ˇ4 5 6
4051          7 8 9
4052          "});
4053        cx.set_shared_state(indoc! {r"
4054          1 2 3
4055          4 5 ˇ6
4056          7 8 9
4057          "})
4058            .await;
4059        cx.simulate_shared_keystrokes("shift-m").await;
4060        cx.shared_state().await.assert_eq(indoc! {"
4061          1 2 3
4062          4 5 ˇ6
4063          7 8 9
4064          "});
4065    }
4066
4067    #[gpui::test]
4068    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
4069        let mut cx = NeovimBackedTestContext::new(cx).await;
4070        let initial_state = indoc! {r"abc
4071          deˇf
4072          paragraph
4073          the second
4074          third and
4075          final"};
4076
4077        cx.set_shared_state(initial_state).await;
4078        cx.simulate_shared_keystrokes("shift-l").await;
4079        cx.shared_state().await.assert_eq(indoc! {r"abc
4080          def
4081          paragraph
4082          the second
4083          third and
4084          fiˇnal"});
4085
4086        cx.set_shared_state(indoc! {r"
4087          1 2 3
4088          4 5 ˇ6
4089          7 8 9
4090          "})
4091            .await;
4092        cx.simulate_shared_keystrokes("shift-l").await;
4093        cx.shared_state().await.assert_eq(indoc! {"
4094          1 2 3
4095          4 5 6
4096          7 8 9
4097          ˇ"});
4098
4099        cx.set_shared_state(indoc! {r"
4100          1 2 3
4101          ˇ4 5 6
4102          7 8 9
4103          "})
4104            .await;
4105        cx.simulate_shared_keystrokes("shift-l").await;
4106        cx.shared_state().await.assert_eq(indoc! {"
4107          1 2 3
4108          4 5 6
4109          7 8 9
4110          ˇ"});
4111
4112        cx.set_shared_state(indoc! {r"
4113          1 2 ˇ3
4114          4 5 6
4115          7 8 9
4116          "})
4117            .await;
4118        cx.simulate_shared_keystrokes("shift-l").await;
4119        cx.shared_state().await.assert_eq(indoc! {"
4120          1 2 3
4121          4 5 6
4122          7 8 9
4123          ˇ"});
4124
4125        cx.set_shared_state(indoc! {r"
4126          ˇ1 2 3
4127          4 5 6
4128          7 8 9
4129          "})
4130            .await;
4131        cx.simulate_shared_keystrokes("shift-l").await;
4132        cx.shared_state().await.assert_eq(indoc! {"
4133          1 2 3
4134          4 5 6
4135          7 8 9
4136          ˇ"});
4137
4138        cx.set_shared_state(indoc! {r"
4139          1 2 3
4140          4 5 ˇ6
4141          7 8 9
4142          "})
4143            .await;
4144        cx.simulate_shared_keystrokes("9 shift-l").await;
4145        cx.shared_state().await.assert_eq(indoc! {"
4146          1 2 ˇ3
4147          4 5 6
4148          7 8 9
4149          "});
4150    }
4151
4152    #[gpui::test]
4153    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
4154        let mut cx = NeovimBackedTestContext::new(cx).await;
4155        cx.set_shared_state(indoc! {r"
4156        456 5ˇ67 678
4157        "})
4158            .await;
4159        cx.simulate_shared_keystrokes("g e").await;
4160        cx.shared_state().await.assert_eq(indoc! {"
4161        45ˇ6 567 678
4162        "});
4163
4164        // Test times
4165        cx.set_shared_state(indoc! {r"
4166        123 234 345
4167        456 5ˇ67 678
4168        "})
4169            .await;
4170        cx.simulate_shared_keystrokes("4 g e").await;
4171        cx.shared_state().await.assert_eq(indoc! {"
4172        12ˇ3 234 345
4173        456 567 678
4174        "});
4175
4176        // With punctuation
4177        cx.set_shared_state(indoc! {r"
4178        123 234 345
4179        4;5.6 5ˇ67 678
4180        789 890 901
4181        "})
4182            .await;
4183        cx.simulate_shared_keystrokes("g e").await;
4184        cx.shared_state().await.assert_eq(indoc! {"
4185          123 234 345
4186          4;5.ˇ6 567 678
4187          789 890 901
4188        "});
4189
4190        // With punctuation and count
4191        cx.set_shared_state(indoc! {r"
4192        123 234 345
4193        4;5.6 5ˇ67 678
4194        789 890 901
4195        "})
4196            .await;
4197        cx.simulate_shared_keystrokes("5 g e").await;
4198        cx.shared_state().await.assert_eq(indoc! {"
4199          123 234 345
4200          ˇ4;5.6 567 678
4201          789 890 901
4202        "});
4203
4204        // newlines
4205        cx.set_shared_state(indoc! {r"
4206        123 234 345
4207
4208        78ˇ9 890 901
4209        "})
4210            .await;
4211        cx.simulate_shared_keystrokes("g e").await;
4212        cx.shared_state().await.assert_eq(indoc! {"
4213          123 234 345
4214          ˇ
4215          789 890 901
4216        "});
4217        cx.simulate_shared_keystrokes("g e").await;
4218        cx.shared_state().await.assert_eq(indoc! {"
4219          123 234 34ˇ5
4220
4221          789 890 901
4222        "});
4223
4224        // With punctuation
4225        cx.set_shared_state(indoc! {r"
4226        123 234 345
4227        4;5.ˇ6 567 678
4228        789 890 901
4229        "})
4230            .await;
4231        cx.simulate_shared_keystrokes("g shift-e").await;
4232        cx.shared_state().await.assert_eq(indoc! {"
4233          123 234 34ˇ5
4234          4;5.6 567 678
4235          789 890 901
4236        "});
4237
4238        // With multi byte char
4239        cx.set_shared_state(indoc! {r"
4240        bar ˇó
4241        "})
4242            .await;
4243        cx.simulate_shared_keystrokes("g e").await;
4244        cx.shared_state().await.assert_eq(indoc! {"
4245        baˇr ó
4246        "});
4247    }
4248
4249    #[gpui::test]
4250    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
4251        let mut cx = NeovimBackedTestContext::new(cx).await;
4252
4253        cx.set_shared_state(indoc! {"
4254            fn aˇ() {
4255              return
4256            }
4257        "})
4258            .await;
4259        cx.simulate_shared_keystrokes("v $ %").await;
4260        cx.shared_state().await.assert_eq(indoc! {"
4261            fn a«() {
4262              return
4263            }ˇ»
4264        "});
4265    }
4266
4267    #[gpui::test]
4268    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
4269        let mut cx = VimTestContext::new(cx, true).await;
4270
4271        cx.set_state(
4272            indoc! {"
4273                struct Foo {
4274                ˇ
4275                }
4276            "},
4277            Mode::Normal,
4278        );
4279
4280        cx.update_editor(|editor, _window, cx| {
4281            let range = editor.selections.newest_anchor().range();
4282            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
4283            let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
4284            editor.splice_inlays(&[], vec![inlay], cx);
4285        });
4286
4287        cx.simulate_keystrokes("j");
4288        cx.assert_state(
4289            indoc! {"
4290                struct Foo {
4291
4292                ˇ}
4293            "},
4294            Mode::Normal,
4295        );
4296    }
4297
4298    #[gpui::test]
4299    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
4300        let mut cx = VimTestContext::new(cx, true).await;
4301
4302        cx.set_state(
4303            indoc! {"
4304            ˇstruct Foo {
4305
4306            }
4307        "},
4308            Mode::Normal,
4309        );
4310        cx.update_editor(|editor, _window, cx| {
4311            let snapshot = editor.buffer().read(cx).snapshot(cx);
4312            let end_of_line =
4313                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
4314            let inlay_text = " hint";
4315            let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
4316            editor.splice_inlays(&[], vec![inlay], cx);
4317        });
4318        cx.simulate_keystrokes("$");
4319        cx.assert_state(
4320            indoc! {"
4321            struct Foo ˇ{
4322
4323            }
4324        "},
4325            Mode::Normal,
4326        );
4327    }
4328
4329    #[gpui::test]
4330    async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
4331        let mut cx = VimTestContext::new(cx, true).await;
4332
4333        // Test the exact scenario from issue #29134
4334        cx.set_state(
4335            indoc! {"
4336                fn main() {
4337                    let this_is_a_long_name = Vec::<u32>::new();
4338                    let new_oneˇ = this_is_a_long_name
4339                        .iter()
4340                        .map(|i| i + 1)
4341                        .map(|i| i * 2)
4342                        .collect::<Vec<_>>();
4343                }
4344            "},
4345            Mode::Normal,
4346        );
4347
4348        // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
4349        cx.update_editor(|editor, _window, cx| {
4350            let snapshot = editor.buffer().read(cx).snapshot(cx);
4351            // The empty line is at line 3 (0-indexed)
4352            let line_start = snapshot.anchor_after(Point::new(3, 0));
4353            let inlay_text = ": Vec<u32>";
4354            let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
4355            editor.splice_inlays(&[], vec![inlay], cx);
4356        });
4357
4358        // Enter visual mode
4359        cx.simulate_keystrokes("v");
4360        cx.assert_state(
4361            indoc! {"
4362                fn main() {
4363                    let this_is_a_long_name = Vec::<u32>::new();
4364                    let new_one« ˇ»= this_is_a_long_name
4365                        .iter()
4366                        .map(|i| i + 1)
4367                        .map(|i| i * 2)
4368                        .collect::<Vec<_>>();
4369                }
4370            "},
4371            Mode::Visual,
4372        );
4373
4374        // Move down - should go to the beginning of line 4, not skip to line 5
4375        cx.simulate_keystrokes("j");
4376        cx.assert_state(
4377            indoc! {"
4378                fn main() {
4379                    let this_is_a_long_name = Vec::<u32>::new();
4380                    let new_one« = this_is_a_long_name
4381                      ˇ»  .iter()
4382                        .map(|i| i + 1)
4383                        .map(|i| i * 2)
4384                        .collect::<Vec<_>>();
4385                }
4386            "},
4387            Mode::Visual,
4388        );
4389
4390        // Test with multiple movements
4391        cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
4392
4393        // Add type hint on the empty line
4394        cx.update_editor(|editor, _window, cx| {
4395            let snapshot = editor.buffer().read(cx).snapshot(cx);
4396            let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
4397            let inlay_text = ": i32";
4398            let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
4399            editor.splice_inlays(&[], vec![inlay], cx);
4400        });
4401
4402        // Enter visual mode and move down twice
4403        cx.simulate_keystrokes("v j j");
4404        cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
4405    }
4406
4407    #[gpui::test]
4408    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
4409        let mut cx = NeovimBackedTestContext::new(cx).await;
4410        // Normal mode
4411        cx.set_shared_state(indoc! {"
4412            The ˇquick brown
4413            fox jumps over
4414            the lazy dog
4415            The quick brown
4416            fox jumps over
4417            the lazy dog
4418            The quick brown
4419            fox jumps over
4420            the lazy dog"})
4421            .await;
4422        cx.simulate_shared_keystrokes("2 0 %").await;
4423        cx.shared_state().await.assert_eq(indoc! {"
4424            The quick brown
4425            fox ˇjumps over
4426            the lazy dog
4427            The quick brown
4428            fox jumps over
4429            the lazy dog
4430            The quick brown
4431            fox jumps over
4432            the lazy dog"});
4433
4434        cx.simulate_shared_keystrokes("2 5 %").await;
4435        cx.shared_state().await.assert_eq(indoc! {"
4436            The quick brown
4437            fox jumps over
4438            the ˇlazy dog
4439            The quick brown
4440            fox jumps over
4441            the lazy dog
4442            The quick brown
4443            fox jumps over
4444            the lazy dog"});
4445
4446        cx.simulate_shared_keystrokes("7 5 %").await;
4447        cx.shared_state().await.assert_eq(indoc! {"
4448            The quick brown
4449            fox jumps over
4450            the lazy dog
4451            The quick brown
4452            fox jumps over
4453            the lazy dog
4454            The ˇquick brown
4455            fox jumps over
4456            the lazy dog"});
4457
4458        // Visual mode
4459        cx.set_shared_state(indoc! {"
4460            The ˇquick brown
4461            fox jumps over
4462            the lazy dog
4463            The quick brown
4464            fox jumps over
4465            the lazy dog
4466            The quick brown
4467            fox jumps over
4468            the lazy dog"})
4469            .await;
4470        cx.simulate_shared_keystrokes("v 5 0 %").await;
4471        cx.shared_state().await.assert_eq(indoc! {"
4472            The «quick brown
4473            fox jumps over
4474            the lazy dog
4475            The quick brown
4476            fox jˇ»umps over
4477            the lazy dog
4478            The quick brown
4479            fox jumps over
4480            the lazy dog"});
4481
4482        cx.set_shared_state(indoc! {"
4483            The ˇquick brown
4484            fox jumps over
4485            the lazy dog
4486            The quick brown
4487            fox jumps over
4488            the lazy dog
4489            The quick brown
4490            fox jumps over
4491            the lazy dog"})
4492            .await;
4493        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4494        cx.shared_state().await.assert_eq(indoc! {"
4495            The «quick brown
4496            fox jumps over
4497            the lazy dog
4498            The quick brown
4499            fox jumps over
4500            the lazy dog
4501            The quick brown
4502            fox jumps over
4503            the lˇ»azy dog"});
4504    }
4505
4506    #[gpui::test]
4507    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4508        let mut cx = NeovimBackedTestContext::new(cx).await;
4509
4510        cx.set_shared_state("ˇπππππ").await;
4511        cx.simulate_shared_keystrokes("3 space").await;
4512        cx.shared_state().await.assert_eq("πππˇππ");
4513    }
4514
4515    #[gpui::test]
4516    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4517        let mut cx = NeovimBackedTestContext::new(cx).await;
4518
4519        cx.set_shared_state(indoc! {"
4520            ππππˇπ
4521            πanotherline"})
4522            .await;
4523        cx.simulate_shared_keystrokes("4 space").await;
4524        cx.shared_state().await.assert_eq(indoc! {"
4525            πππππ
4526            πanˇotherline"});
4527    }
4528
4529    #[gpui::test]
4530    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4531        let mut cx = NeovimBackedTestContext::new(cx).await;
4532
4533        cx.set_shared_state(indoc! {"
4534                        ππππ
4535                        πanˇotherline"})
4536            .await;
4537        cx.simulate_shared_keystrokes("4 backspace").await;
4538        cx.shared_state().await.assert_eq(indoc! {"
4539                        πππˇπ
4540                        πanotherline"});
4541    }
4542
4543    #[gpui::test]
4544    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4545        let mut cx = VimTestContext::new(cx, true).await;
4546        cx.set_state(
4547            indoc! {
4548                "func empty(a string) bool {
4549                     ˇif a == \"\" {
4550                         return true
4551                     }
4552                     return false
4553                }"
4554            },
4555            Mode::Normal,
4556        );
4557        cx.simulate_keystrokes("[ -");
4558        cx.assert_state(
4559            indoc! {
4560                "ˇfunc empty(a string) bool {
4561                     if a == \"\" {
4562                         return true
4563                     }
4564                     return false
4565                }"
4566            },
4567            Mode::Normal,
4568        );
4569        cx.simulate_keystrokes("] =");
4570        cx.assert_state(
4571            indoc! {
4572                "func empty(a string) bool {
4573                     if a == \"\" {
4574                         return true
4575                     }
4576                     return false
4577                ˇ}"
4578            },
4579            Mode::Normal,
4580        );
4581        cx.simulate_keystrokes("[ +");
4582        cx.assert_state(
4583            indoc! {
4584                "func empty(a string) bool {
4585                     if a == \"\" {
4586                         return true
4587                     }
4588                     ˇreturn false
4589                }"
4590            },
4591            Mode::Normal,
4592        );
4593        cx.simulate_keystrokes("2 [ =");
4594        cx.assert_state(
4595            indoc! {
4596                "func empty(a string) bool {
4597                     ˇif a == \"\" {
4598                         return true
4599                     }
4600                     return false
4601                }"
4602            },
4603            Mode::Normal,
4604        );
4605        cx.simulate_keystrokes("] +");
4606        cx.assert_state(
4607            indoc! {
4608                "func empty(a string) bool {
4609                     if a == \"\" {
4610                         ˇreturn true
4611                     }
4612                     return false
4613                }"
4614            },
4615            Mode::Normal,
4616        );
4617        cx.simulate_keystrokes("] -");
4618        cx.assert_state(
4619            indoc! {
4620                "func empty(a string) bool {
4621                     if a == \"\" {
4622                         return true
4623                     ˇ}
4624                     return false
4625                }"
4626            },
4627            Mode::Normal,
4628        );
4629    }
4630
4631    #[gpui::test]
4632    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4633        let mut cx = NeovimBackedTestContext::new(cx).await;
4634        cx.set_shared_state("abˇc").await;
4635        cx.simulate_shared_keystrokes("delete").await;
4636        cx.shared_state().await.assert_eq("aˇb");
4637    }
4638
4639    #[gpui::test]
4640    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4641        let mut cx = NeovimBackedTestContext::new(cx).await;
4642
4643        cx.set_shared_state(indoc! {"
4644             ˇthe quick brown fox
4645             jumped over the lazy dog"})
4646            .await;
4647        cx.simulate_shared_keystrokes("d v 0").await;
4648        cx.shared_state().await.assert_eq(indoc! {"
4649             ˇhe quick brown fox
4650             jumped over the lazy dog"});
4651        assert!(!cx.cx.forced_motion());
4652
4653        cx.set_shared_state(indoc! {"
4654            the quick bˇrown fox
4655            jumped over the lazy dog"})
4656            .await;
4657        cx.simulate_shared_keystrokes("d v 0").await;
4658        cx.shared_state().await.assert_eq(indoc! {"
4659            ˇown fox
4660            jumped over the lazy dog"});
4661        assert!(!cx.cx.forced_motion());
4662
4663        cx.set_shared_state(indoc! {"
4664            the quick brown foˇx
4665            jumped over the lazy dog"})
4666            .await;
4667        cx.simulate_shared_keystrokes("d v 0").await;
4668        cx.shared_state().await.assert_eq(indoc! {"
4669            ˇ
4670            jumped over the lazy dog"});
4671        assert!(!cx.cx.forced_motion());
4672    }
4673
4674    #[gpui::test]
4675    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4676        let mut cx = NeovimBackedTestContext::new(cx).await;
4677
4678        cx.set_shared_state(indoc! {"
4679             ˇthe quick brown fox
4680             jumped over the lazy dog"})
4681            .await;
4682        cx.simulate_shared_keystrokes("d v g shift-m").await;
4683        cx.shared_state().await.assert_eq(indoc! {"
4684             ˇbrown fox
4685             jumped over the lazy dog"});
4686        assert!(!cx.cx.forced_motion());
4687
4688        cx.set_shared_state(indoc! {"
4689            the quick bˇrown fox
4690            jumped over the lazy dog"})
4691            .await;
4692        cx.simulate_shared_keystrokes("d v g shift-m").await;
4693        cx.shared_state().await.assert_eq(indoc! {"
4694            the quickˇown fox
4695            jumped over the lazy dog"});
4696        assert!(!cx.cx.forced_motion());
4697
4698        cx.set_shared_state(indoc! {"
4699            the quick brown foˇx
4700            jumped over the lazy dog"})
4701            .await;
4702        cx.simulate_shared_keystrokes("d v g shift-m").await;
4703        cx.shared_state().await.assert_eq(indoc! {"
4704            the quicˇk
4705            jumped over the lazy dog"});
4706        assert!(!cx.cx.forced_motion());
4707
4708        cx.set_shared_state(indoc! {"
4709            ˇthe quick brown fox
4710            jumped over the lazy dog"})
4711            .await;
4712        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4713        cx.shared_state().await.assert_eq(indoc! {"
4714            ˇ fox
4715            jumped over the lazy dog"});
4716        assert!(!cx.cx.forced_motion());
4717
4718        cx.set_shared_state(indoc! {"
4719            ˇthe quick brown fox
4720            jumped over the lazy dog"})
4721            .await;
4722        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4723        cx.shared_state().await.assert_eq(indoc! {"
4724            ˇuick brown fox
4725            jumped over the lazy dog"});
4726        assert!(!cx.cx.forced_motion());
4727    }
4728
4729    #[gpui::test]
4730    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4731        let mut cx = NeovimBackedTestContext::new(cx).await;
4732
4733        cx.set_shared_state(indoc! {"
4734             the quick brown foˇx
4735             jumped over the lazy dog"})
4736            .await;
4737        cx.simulate_shared_keystrokes("d v $").await;
4738        cx.shared_state().await.assert_eq(indoc! {"
4739             the quick brown foˇx
4740             jumped over the lazy dog"});
4741        assert!(!cx.cx.forced_motion());
4742
4743        cx.set_shared_state(indoc! {"
4744             ˇthe quick brown fox
4745             jumped over the lazy dog"})
4746            .await;
4747        cx.simulate_shared_keystrokes("d v $").await;
4748        cx.shared_state().await.assert_eq(indoc! {"
4749             ˇx
4750             jumped over the lazy dog"});
4751        assert!(!cx.cx.forced_motion());
4752    }
4753
4754    #[gpui::test]
4755    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4756        let mut cx = NeovimBackedTestContext::new(cx).await;
4757
4758        cx.set_shared_state(indoc! {"
4759               ˇthe quick brown fox
4760               jumped over the lazy dog"})
4761            .await;
4762        cx.simulate_shared_keystrokes("y v j p").await;
4763        cx.shared_state().await.assert_eq(indoc! {"
4764               the quick brown fox
4765               ˇthe quick brown fox
4766               jumped over the lazy dog"});
4767        assert!(!cx.cx.forced_motion());
4768
4769        cx.set_shared_state(indoc! {"
4770              the quick bˇrown fox
4771              jumped over the lazy dog"})
4772            .await;
4773        cx.simulate_shared_keystrokes("y v j p").await;
4774        cx.shared_state().await.assert_eq(indoc! {"
4775              the quick brˇrown fox
4776              jumped overown fox
4777              jumped over the lazy dog"});
4778        assert!(!cx.cx.forced_motion());
4779
4780        cx.set_shared_state(indoc! {"
4781             the quick brown foˇx
4782             jumped over the lazy dog"})
4783            .await;
4784        cx.simulate_shared_keystrokes("y v j p").await;
4785        cx.shared_state().await.assert_eq(indoc! {"
4786             the quick brown foxˇx
4787             jumped over the la
4788             jumped over the lazy dog"});
4789        assert!(!cx.cx.forced_motion());
4790
4791        cx.set_shared_state(indoc! {"
4792             the quick brown fox
4793             jˇumped over the lazy dog"})
4794            .await;
4795        cx.simulate_shared_keystrokes("y v k p").await;
4796        cx.shared_state().await.assert_eq(indoc! {"
4797            thˇhe quick brown fox
4798            je quick brown fox
4799            jumped over the lazy dog"});
4800        assert!(!cx.cx.forced_motion());
4801    }
4802
4803    #[gpui::test]
4804    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4805        let mut cx = NeovimBackedTestContext::new(cx).await;
4806
4807        cx.set_shared_state(indoc! {"
4808              ˇthe quick brown fox
4809              jumped over the lazy dog"})
4810            .await;
4811        cx.simulate_shared_keystrokes("d v e").await;
4812        cx.shared_state().await.assert_eq(indoc! {"
4813              ˇe quick brown fox
4814              jumped over the lazy dog"});
4815        assert!(!cx.cx.forced_motion());
4816
4817        cx.set_shared_state(indoc! {"
4818              the quick bˇrown fox
4819              jumped over the lazy dog"})
4820            .await;
4821        cx.simulate_shared_keystrokes("d v e").await;
4822        cx.shared_state().await.assert_eq(indoc! {"
4823              the quick bˇn fox
4824              jumped over the lazy dog"});
4825        assert!(!cx.cx.forced_motion());
4826
4827        cx.set_shared_state(indoc! {"
4828             the quick brown foˇx
4829             jumped over the lazy dog"})
4830            .await;
4831        cx.simulate_shared_keystrokes("d v e").await;
4832        cx.shared_state().await.assert_eq(indoc! {"
4833        the quick brown foˇd over the lazy dog"});
4834        assert!(!cx.cx.forced_motion());
4835    }
4836
4837    #[gpui::test]
4838    async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
4839        let mut cx = VimTestContext::new(cx, true).await;
4840
4841        // Setup custom keybindings for subword motions so we can use the bindings
4842        // in `simulate_keystrokes`.
4843        cx.update(|_window, cx| {
4844            cx.bind_keys([KeyBinding::new(
4845                "w",
4846                super::NextSubwordStart {
4847                    ignore_punctuation: false,
4848                },
4849                None,
4850            )]);
4851        });
4852
4853        cx.set_state("ˇfoo.bar", Mode::Normal);
4854        cx.simulate_keystrokes("w");
4855        cx.assert_state("foo.ˇbar", Mode::Normal);
4856
4857        cx.set_state("ˇfoo(bar)", Mode::Normal);
4858        cx.simulate_keystrokes("w");
4859        cx.assert_state("fooˇ(bar)", Mode::Normal);
4860        cx.simulate_keystrokes("w");
4861        cx.assert_state("foo(ˇbar)", Mode::Normal);
4862        cx.simulate_keystrokes("w");
4863        cx.assert_state("foo(barˇ)", Mode::Normal);
4864
4865        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
4866        cx.simulate_keystrokes("w");
4867        cx.assert_state("foo_ˇbar_baz", Mode::Normal);
4868        cx.simulate_keystrokes("w");
4869        cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
4870
4871        cx.set_state("ˇfooBarBaz", Mode::Normal);
4872        cx.simulate_keystrokes("w");
4873        cx.assert_state("fooˇBarBaz", Mode::Normal);
4874        cx.simulate_keystrokes("w");
4875        cx.assert_state("fooBarˇBaz", Mode::Normal);
4876
4877        cx.set_state("ˇfoo;bar", Mode::Normal);
4878        cx.simulate_keystrokes("w");
4879        cx.assert_state("foo;ˇbar", Mode::Normal);
4880
4881        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::Normal);
4882        cx.simulate_keystrokes("w");
4883        cx.assert_state("<?ˇphp\n\n$someVariable = 2;", Mode::Normal);
4884        cx.simulate_keystrokes("w");
4885        cx.assert_state("<?php\nˇ\n$someVariable = 2;", Mode::Normal);
4886        cx.simulate_keystrokes("w");
4887        cx.assert_state("<?php\n\nˇ$someVariable = 2;", Mode::Normal);
4888        cx.simulate_keystrokes("w");
4889        cx.assert_state("<?php\n\n$ˇsomeVariable = 2;", Mode::Normal);
4890        cx.simulate_keystrokes("w");
4891        cx.assert_state("<?php\n\n$someˇVariable = 2;", Mode::Normal);
4892        cx.simulate_keystrokes("w");
4893        cx.assert_state("<?php\n\n$someVariable ˇ= 2;", Mode::Normal);
4894        cx.simulate_keystrokes("w");
4895        cx.assert_state("<?php\n\n$someVariable = ˇ2;", Mode::Normal);
4896    }
4897
4898    #[gpui::test]
4899    async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
4900        let mut cx = VimTestContext::new(cx, true).await;
4901
4902        // Setup custom keybindings for subword motions so we can use the bindings
4903        // in `simulate_keystrokes`.
4904        cx.update(|_window, cx| {
4905            cx.bind_keys([KeyBinding::new(
4906                "e",
4907                super::NextSubwordEnd {
4908                    ignore_punctuation: false,
4909                },
4910                None,
4911            )]);
4912        });
4913
4914        cx.set_state("ˇfoo.bar", Mode::Normal);
4915        cx.simulate_keystrokes("e");
4916        cx.assert_state("foˇo.bar", Mode::Normal);
4917        cx.simulate_keystrokes("e");
4918        cx.assert_state("fooˇ.bar", Mode::Normal);
4919        cx.simulate_keystrokes("e");
4920        cx.assert_state("foo.baˇr", Mode::Normal);
4921
4922        cx.set_state("ˇfoo(bar)", Mode::Normal);
4923        cx.simulate_keystrokes("e");
4924        cx.assert_state("foˇo(bar)", Mode::Normal);
4925        cx.simulate_keystrokes("e");
4926        cx.assert_state("fooˇ(bar)", Mode::Normal);
4927        cx.simulate_keystrokes("e");
4928        cx.assert_state("foo(baˇr)", Mode::Normal);
4929        cx.simulate_keystrokes("e");
4930        cx.assert_state("foo(barˇ)", Mode::Normal);
4931
4932        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
4933        cx.simulate_keystrokes("e");
4934        cx.assert_state("foˇo_bar_baz", Mode::Normal);
4935        cx.simulate_keystrokes("e");
4936        cx.assert_state("foo_baˇr_baz", Mode::Normal);
4937        cx.simulate_keystrokes("e");
4938        cx.assert_state("foo_bar_baˇz", Mode::Normal);
4939
4940        cx.set_state("ˇfooBarBaz", Mode::Normal);
4941        cx.simulate_keystrokes("e");
4942        cx.set_state("foˇoBarBaz", Mode::Normal);
4943        cx.simulate_keystrokes("e");
4944        cx.set_state("fooBaˇrBaz", Mode::Normal);
4945        cx.simulate_keystrokes("e");
4946        cx.set_state("fooBarBaˇz", Mode::Normal);
4947
4948        cx.set_state("ˇfoo;bar", Mode::Normal);
4949        cx.simulate_keystrokes("e");
4950        cx.set_state("foˇo;bar", Mode::Normal);
4951        cx.simulate_keystrokes("e");
4952        cx.set_state("fooˇ;bar", Mode::Normal);
4953        cx.simulate_keystrokes("e");
4954        cx.set_state("foo;baˇr", Mode::Normal);
4955    }
4956
4957    #[gpui::test]
4958    async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
4959        let mut cx = VimTestContext::new(cx, true).await;
4960
4961        // Setup custom keybindings for subword motions so we can use the bindings
4962        // in `simulate_keystrokes`.
4963        cx.update(|_window, cx| {
4964            cx.bind_keys([KeyBinding::new(
4965                "b",
4966                super::PreviousSubwordStart {
4967                    ignore_punctuation: false,
4968                },
4969                None,
4970            )]);
4971        });
4972
4973        cx.set_state("foo.barˇ", Mode::Normal);
4974        cx.simulate_keystrokes("b");
4975        cx.assert_state("foo.ˇbar", Mode::Normal);
4976        cx.simulate_keystrokes("b");
4977        cx.assert_state("fooˇ.bar", Mode::Normal);
4978        cx.simulate_keystrokes("b");
4979        cx.assert_state("ˇfoo.bar", Mode::Normal);
4980
4981        cx.set_state("foo(barˇ)", Mode::Normal);
4982        cx.simulate_keystrokes("b");
4983        cx.assert_state("foo(ˇbar)", Mode::Normal);
4984        cx.simulate_keystrokes("b");
4985        cx.assert_state("fooˇ(bar)", Mode::Normal);
4986        cx.simulate_keystrokes("b");
4987        cx.assert_state("ˇfoo(bar)", Mode::Normal);
4988
4989        cx.set_state("foo_bar_bazˇ", Mode::Normal);
4990        cx.simulate_keystrokes("b");
4991        cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
4992        cx.simulate_keystrokes("b");
4993        cx.assert_state("foo_ˇbar_baz", Mode::Normal);
4994        cx.simulate_keystrokes("b");
4995        cx.assert_state("ˇfoo_bar_baz", Mode::Normal);
4996
4997        cx.set_state("fooBarBazˇ", Mode::Normal);
4998        cx.simulate_keystrokes("b");
4999        cx.assert_state("fooBarˇBaz", Mode::Normal);
5000        cx.simulate_keystrokes("b");
5001        cx.assert_state("fooˇBarBaz", Mode::Normal);
5002        cx.simulate_keystrokes("b");
5003        cx.assert_state("ˇfooBarBaz", Mode::Normal);
5004
5005        cx.set_state("foo;barˇ", Mode::Normal);
5006        cx.simulate_keystrokes("b");
5007        cx.assert_state("foo;ˇbar", Mode::Normal);
5008        cx.simulate_keystrokes("b");
5009        cx.assert_state("ˇfoo;bar", Mode::Normal);
5010
5011        cx.set_state("<?php\n\n$someVariable = 2ˇ;", Mode::Normal);
5012        cx.simulate_keystrokes("b");
5013        cx.assert_state("<?php\n\n$someVariable = ˇ2;", Mode::Normal);
5014        cx.simulate_keystrokes("b");
5015        cx.assert_state("<?php\n\n$someVariable ˇ= 2;", Mode::Normal);
5016        cx.simulate_keystrokes("b");
5017        cx.assert_state("<?php\n\n$someˇVariable = 2;", Mode::Normal);
5018        cx.simulate_keystrokes("b");
5019        cx.assert_state("<?php\n\n$ˇsomeVariable = 2;", Mode::Normal);
5020        cx.simulate_keystrokes("b");
5021        cx.assert_state("<?php\n\nˇ$someVariable = 2;", Mode::Normal);
5022        cx.simulate_keystrokes("b");
5023        cx.assert_state("<?php\nˇ\n$someVariable = 2;", Mode::Normal);
5024        cx.simulate_keystrokes("b");
5025        cx.assert_state("<?ˇphp\n\n$someVariable = 2;", Mode::Normal);
5026        cx.simulate_keystrokes("b");
5027        cx.assert_state("ˇ<?php\n\n$someVariable = 2;", Mode::Normal);
5028    }
5029
5030    #[gpui::test]
5031    async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
5032        let mut cx = VimTestContext::new(cx, true).await;
5033
5034        // Setup custom keybindings for subword motions so we can use the bindings
5035        // in `simulate_keystrokes`.
5036        cx.update(|_window, cx| {
5037            cx.bind_keys([KeyBinding::new(
5038                "g e",
5039                super::PreviousSubwordEnd {
5040                    ignore_punctuation: false,
5041                },
5042                None,
5043            )]);
5044        });
5045
5046        cx.set_state("foo.baˇr", Mode::Normal);
5047        cx.simulate_keystrokes("g e");
5048        cx.assert_state("fooˇ.bar", Mode::Normal);
5049        cx.simulate_keystrokes("g e");
5050        cx.assert_state("foˇo.bar", Mode::Normal);
5051
5052        cx.set_state("foo(barˇ)", Mode::Normal);
5053        cx.simulate_keystrokes("g e");
5054        cx.assert_state("foo(baˇr)", Mode::Normal);
5055        cx.simulate_keystrokes("g e");
5056        cx.assert_state("fooˇ(bar)", Mode::Normal);
5057        cx.simulate_keystrokes("g e");
5058        cx.assert_state("foˇo(bar)", Mode::Normal);
5059
5060        cx.set_state("foo_bar_baˇz", Mode::Normal);
5061        cx.simulate_keystrokes("g e");
5062        cx.assert_state("foo_baˇr_baz", Mode::Normal);
5063        cx.simulate_keystrokes("g e");
5064        cx.assert_state("foˇo_bar_baz", Mode::Normal);
5065
5066        cx.set_state("fooBarBaˇz", Mode::Normal);
5067        cx.simulate_keystrokes("g e");
5068        cx.assert_state("fooBaˇrBaz", Mode::Normal);
5069        cx.simulate_keystrokes("g e");
5070        cx.assert_state("foˇoBarBaz", Mode::Normal);
5071
5072        cx.set_state("foo;baˇr", Mode::Normal);
5073        cx.simulate_keystrokes("g e");
5074        cx.assert_state("fooˇ;bar", Mode::Normal);
5075        cx.simulate_keystrokes("g e");
5076        cx.assert_state("foˇo;bar", Mode::Normal);
5077    }
5078
5079    #[gpui::test]
5080    async fn test_method_motion_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
5081        let mut cx = VimTestContext::new(cx, true).await;
5082
5083        let diff_base = indoc! {r#"
5084            fn first() {
5085                println!("first");
5086                println!("removed line");
5087            }
5088
5089            fn second() {
5090                println!("second");
5091            }
5092
5093            fn third() {
5094                println!("third");
5095            }
5096        "#};
5097
5098        let current_text = indoc! {r#"
5099            fn first() {
5100                println!("first");
5101            }
5102
5103            fn second() {
5104                println!("second");
5105            }
5106
5107            fn third() {
5108                println!("third");
5109            }
5110        "#};
5111
5112        cx.set_state(&format!("ˇ{}", current_text), Mode::Normal);
5113        cx.set_head_text(diff_base);
5114        cx.update_editor(|editor, window, cx| {
5115            editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
5116        });
5117
5118        // When diff hunks are expanded, the deleted line from the diff base
5119        // appears in the MultiBuffer. The method motion should correctly
5120        // navigate to the second function even with this extra content.
5121        cx.simulate_keystrokes("] m");
5122        cx.assert_editor_state(indoc! {r#"
5123            fn first() {
5124                println!("first");
5125                println!("removed line");
5126            }
5127
5128            ˇfn second() {
5129                println!("second");
5130            }
5131
5132            fn third() {
5133                println!("third");
5134            }
5135        "#});
5136
5137        cx.simulate_keystrokes("] m");
5138        cx.assert_editor_state(indoc! {r#"
5139            fn first() {
5140                println!("first");
5141                println!("removed line");
5142            }
5143
5144            fn second() {
5145                println!("second");
5146            }
5147
5148            ˇfn third() {
5149                println!("third");
5150            }
5151        "#});
5152
5153        cx.simulate_keystrokes("[ m");
5154        cx.assert_editor_state(indoc! {r#"
5155            fn first() {
5156                println!("first");
5157                println!("removed line");
5158            }
5159
5160            ˇfn second() {
5161                println!("second");
5162            }
5163
5164            fn third() {
5165                println!("third");
5166            }
5167        "#});
5168
5169        cx.simulate_keystrokes("[ m");
5170        cx.assert_editor_state(indoc! {r#"
5171            ˇfn first() {
5172                println!("first");
5173                println!("removed line");
5174            }
5175
5176            fn second() {
5177                println!("second");
5178            }
5179
5180            fn third() {
5181                println!("third");
5182            }
5183        "#});
5184    }
5185
5186    #[gpui::test]
5187    async fn test_comment_motion_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
5188        let mut cx = VimTestContext::new(cx, true).await;
5189
5190        let diff_base = indoc! {r#"
5191            // first comment
5192            fn first() {
5193                // removed comment
5194                println!("first");
5195            }
5196
5197            // second comment
5198            fn second() { println!("second"); }
5199        "#};
5200
5201        let current_text = indoc! {r#"
5202            // first comment
5203            fn first() {
5204                println!("first");
5205            }
5206
5207            // second comment
5208            fn second() { println!("second"); }
5209        "#};
5210
5211        cx.set_state(&format!("ˇ{}", current_text), Mode::Normal);
5212        cx.set_head_text(diff_base);
5213        cx.update_editor(|editor, window, cx| {
5214            editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
5215        });
5216
5217        // The first `] /` (vim::NextComment) should go to the end of the first
5218        // comment.
5219        cx.simulate_keystrokes("] /");
5220        cx.assert_editor_state(indoc! {r#"
5221            // first commenˇt
5222            fn first() {
5223                // removed comment
5224                println!("first");
5225            }
5226
5227            // second comment
5228            fn second() { println!("second"); }
5229        "#});
5230
5231        // The next `] /` (vim::NextComment) should go to the end of the second
5232        // comment, skipping over the removed comment, since it's not in the
5233        // actual buffer.
5234        cx.simulate_keystrokes("] /");
5235        cx.assert_editor_state(indoc! {r#"
5236            // first comment
5237            fn first() {
5238                // removed comment
5239                println!("first");
5240            }
5241
5242            // second commenˇt
5243            fn second() { println!("second"); }
5244        "#});
5245
5246        // Going back to previous comment with `[ /` (vim::PreviousComment)
5247        // should go back to the start of the second comment.
5248        cx.simulate_keystrokes("[ /");
5249        cx.assert_editor_state(indoc! {r#"
5250            // first comment
5251            fn first() {
5252                // removed comment
5253                println!("first");
5254            }
5255
5256            ˇ// second comment
5257            fn second() { println!("second"); }
5258        "#});
5259
5260        // Going back again with `[ /` (vim::PreviousComment) should finally put
5261        // the cursor at the start of the first comment.
5262        cx.simulate_keystrokes("[ /");
5263        cx.assert_editor_state(indoc! {r#"
5264            ˇ// first comment
5265            fn first() {
5266                // removed comment
5267                println!("first");
5268            }
5269
5270            // second comment
5271            fn second() { println!("second"); }
5272        "#});
5273    }
5274}