motion.rs

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