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