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 = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1695            let left_kind = classifier.kind(left);
1696            let right_kind = classifier.kind(right);
1697            let at_newline = right == '\n';
1698
1699            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1700                || at_newline && crossed_newline
1701                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1702
1703            crossed_newline |= at_newline;
1704            found
1705        });
1706        if point == new_point {
1707            break;
1708        }
1709        point = new_point;
1710    }
1711    point
1712}
1713
1714fn next_end_impl(
1715    map: &DisplaySnapshot,
1716    mut point: DisplayPoint,
1717    times: usize,
1718    allow_cross_newline: bool,
1719    always_advance: bool,
1720    mut is_boundary: impl FnMut(char, char) -> bool,
1721) -> DisplayPoint {
1722    for _ in 0..times {
1723        let mut need_next_char = false;
1724        let new_point = if always_advance {
1725            next_char(map, point, allow_cross_newline)
1726        } else {
1727            point
1728        };
1729        let new_point = movement::find_boundary_exclusive(
1730            map,
1731            new_point,
1732            FindRange::MultiLine,
1733            |left, right| {
1734                let at_newline = right == '\n';
1735
1736                if !allow_cross_newline && at_newline {
1737                    need_next_char = true;
1738                    return true;
1739                }
1740
1741                is_boundary(left, right)
1742            },
1743        );
1744        let new_point = if need_next_char {
1745            next_char(map, new_point, true)
1746        } else {
1747            new_point
1748        };
1749        let new_point = map.clip_point(new_point, Bias::Left);
1750        if point == new_point {
1751            break;
1752        }
1753        point = new_point;
1754    }
1755    point
1756}
1757
1758pub(crate) fn next_word_end(
1759    map: &DisplaySnapshot,
1760    point: DisplayPoint,
1761    ignore_punctuation: bool,
1762    times: usize,
1763    allow_cross_newline: bool,
1764    always_advance: bool,
1765) -> DisplayPoint {
1766    let classifier = map
1767        .buffer_snapshot()
1768        .char_classifier_at(point.to_point(map))
1769        .ignore_punctuation(ignore_punctuation);
1770
1771    next_end_impl(
1772        map,
1773        point,
1774        times,
1775        allow_cross_newline,
1776        always_advance,
1777        |left, right| {
1778            let left_kind = classifier.kind(left);
1779            let right_kind = classifier.kind(right);
1780            left_kind != right_kind && left_kind != CharKind::Whitespace
1781        },
1782    )
1783}
1784
1785pub(crate) fn next_subword_end(
1786    map: &DisplaySnapshot,
1787    point: DisplayPoint,
1788    ignore_punctuation: bool,
1789    times: usize,
1790    allow_cross_newline: bool,
1791) -> DisplayPoint {
1792    let classifier = map
1793        .buffer_snapshot()
1794        .char_classifier_at(point.to_point(map))
1795        .ignore_punctuation(ignore_punctuation);
1796
1797    next_end_impl(
1798        map,
1799        point,
1800        times,
1801        allow_cross_newline,
1802        true,
1803        |left, right| {
1804            let left_kind = classifier.kind(left);
1805            let right_kind = classifier.kind(right);
1806            let is_stopping_punct = |c: char| ".$=\"'{}[]()<>".contains(c);
1807            let found_subword_end = is_subword_end(left, right, "$_-");
1808            let is_word_end = (left_kind != right_kind)
1809                && (!left.is_ascii_punctuation() || is_stopping_punct(left));
1810
1811            !left.is_whitespace() && (is_word_end || found_subword_end)
1812        },
1813    )
1814}
1815
1816fn previous_word_start(
1817    map: &DisplaySnapshot,
1818    mut point: DisplayPoint,
1819    ignore_punctuation: bool,
1820    times: usize,
1821) -> DisplayPoint {
1822    let classifier = map
1823        .buffer_snapshot()
1824        .char_classifier_at(point.to_point(map))
1825        .ignore_punctuation(ignore_punctuation);
1826    for _ in 0..times {
1827        // This works even though find_preceding_boundary is called for every character in the line containing
1828        // cursor because the newline is checked only once.
1829        let new_point = movement::find_preceding_boundary_display_point(
1830            map,
1831            point,
1832            FindRange::MultiLine,
1833            |left, right| {
1834                let left_kind = classifier.kind(left);
1835                let right_kind = classifier.kind(right);
1836
1837                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1838            },
1839        );
1840        if point == new_point {
1841            break;
1842        }
1843        point = new_point;
1844    }
1845    point
1846}
1847
1848fn previous_word_end(
1849    map: &DisplaySnapshot,
1850    point: DisplayPoint,
1851    ignore_punctuation: bool,
1852    times: usize,
1853) -> DisplayPoint {
1854    let classifier = map
1855        .buffer_snapshot()
1856        .char_classifier_at(point.to_point(map))
1857        .ignore_punctuation(ignore_punctuation);
1858    let mut point = point.to_point(map);
1859
1860    if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row))
1861        && let Some(ch) = map.buffer_snapshot().chars_at(point).next()
1862    {
1863        point.column += ch.len_utf8() as u32;
1864    }
1865    for _ in 0..times {
1866        let new_point = movement::find_preceding_boundary_point(
1867            &map.buffer_snapshot(),
1868            point,
1869            FindRange::MultiLine,
1870            |left, right| {
1871                let left_kind = classifier.kind(left);
1872                let right_kind = classifier.kind(right);
1873                match (left_kind, right_kind) {
1874                    (CharKind::Punctuation, CharKind::Whitespace)
1875                    | (CharKind::Punctuation, CharKind::Word)
1876                    | (CharKind::Word, CharKind::Whitespace)
1877                    | (CharKind::Word, CharKind::Punctuation) => true,
1878                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1879                    _ => false,
1880                }
1881            },
1882        );
1883        if new_point == point {
1884            break;
1885        }
1886        point = new_point;
1887    }
1888    movement::saturating_left(map, point.to_display_point(map))
1889}
1890
1891/// Checks if there's a subword boundary start between `left` and `right` characters.
1892/// This detects transitions like `_b` (separator to non-separator) or `aB` (lowercase to uppercase).
1893pub(crate) fn is_subword_start(left: char, right: char, separators: &str) -> bool {
1894    let is_separator = |c: char| separators.contains(c);
1895    (is_separator(left) && !is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
1896}
1897
1898/// Checks if there's a subword boundary end between `left` and `right` characters.
1899/// This detects transitions like `a_` (non-separator to separator) or `aB` (lowercase to uppercase).
1900pub(crate) fn is_subword_end(left: char, right: char, separators: &str) -> bool {
1901    let is_separator = |c: char| separators.contains(c);
1902    (!is_separator(left) && is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
1903}
1904
1905fn next_subword_start(
1906    map: &DisplaySnapshot,
1907    mut point: DisplayPoint,
1908    ignore_punctuation: bool,
1909    times: usize,
1910) -> DisplayPoint {
1911    let classifier = map
1912        .buffer_snapshot()
1913        .char_classifier_at(point.to_point(map))
1914        .ignore_punctuation(ignore_punctuation);
1915    for _ in 0..times {
1916        let mut crossed_newline = false;
1917        let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1918            let left_kind = classifier.kind(left);
1919            let right_kind = classifier.kind(right);
1920            let at_newline = right == '\n';
1921            let is_stopping_punct = |c: char| "$=\"'{}[]()<>".contains(c);
1922            let found_subword_start = is_subword_start(left, right, ".$_-");
1923            let is_word_start = (left_kind != right_kind)
1924                && (!right.is_ascii_punctuation() || is_stopping_punct(right));
1925            let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
1926                || at_newline && crossed_newline
1927                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1928
1929            crossed_newline |= at_newline;
1930            found
1931        });
1932        if point == new_point {
1933            break;
1934        }
1935        point = new_point;
1936    }
1937    point
1938}
1939
1940fn previous_subword_start(
1941    map: &DisplaySnapshot,
1942    mut point: DisplayPoint,
1943    ignore_punctuation: bool,
1944    times: usize,
1945) -> DisplayPoint {
1946    let classifier = map
1947        .buffer_snapshot()
1948        .char_classifier_at(point.to_point(map))
1949        .ignore_punctuation(ignore_punctuation);
1950    for _ in 0..times {
1951        let mut crossed_newline = false;
1952        // This works even though find_preceding_boundary is called for every character in the line containing
1953        // cursor because the newline is checked only once.
1954        let new_point = movement::find_preceding_boundary_display_point(
1955            map,
1956            point,
1957            FindRange::MultiLine,
1958            |left, right| {
1959                let left_kind = classifier.kind(left);
1960                let right_kind = classifier.kind(right);
1961                let at_newline = right == '\n';
1962
1963                let is_stopping_punct = |c: char| ".$=\"'{}[]()<>".contains(c);
1964                let is_word_start = (left_kind != right_kind)
1965                    && (is_stopping_punct(right) || !right.is_ascii_punctuation());
1966                let found_subword_start = is_subword_start(left, right, ".$_-");
1967
1968                let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
1969                    || at_newline && crossed_newline
1970                    || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1971
1972                crossed_newline |= at_newline;
1973
1974                found
1975            },
1976        );
1977        if point == new_point {
1978            break;
1979        }
1980        point = new_point;
1981    }
1982    point
1983}
1984
1985fn previous_subword_end(
1986    map: &DisplaySnapshot,
1987    point: DisplayPoint,
1988    ignore_punctuation: bool,
1989    times: usize,
1990) -> DisplayPoint {
1991    let classifier = map
1992        .buffer_snapshot()
1993        .char_classifier_at(point.to_point(map))
1994        .ignore_punctuation(ignore_punctuation);
1995    let mut point = point.to_point(map);
1996
1997    if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row))
1998        && let Some(ch) = map.buffer_snapshot().chars_at(point).next()
1999    {
2000        point.column += ch.len_utf8() as u32;
2001    }
2002    for _ in 0..times {
2003        let new_point = movement::find_preceding_boundary_point(
2004            &map.buffer_snapshot(),
2005            point,
2006            FindRange::MultiLine,
2007            |left, right| {
2008                let left_kind = classifier.kind(left);
2009                let right_kind = classifier.kind(right);
2010
2011                let is_stopping_punct = |c: char| ".$;=\"'{}[]()<>".contains(c);
2012                let found_subword_end = is_subword_end(left, right, "$_-");
2013
2014                if found_subword_end {
2015                    return true;
2016                }
2017
2018                match (left_kind, right_kind) {
2019                    (CharKind::Word, CharKind::Whitespace)
2020                    | (CharKind::Word, CharKind::Punctuation) => true,
2021                    (CharKind::Punctuation, _) if is_stopping_punct(left) => true,
2022                    (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2023                    _ => false,
2024                }
2025            },
2026        );
2027        if new_point == point {
2028            break;
2029        }
2030        point = new_point;
2031    }
2032    movement::saturating_left(map, point.to_display_point(map))
2033}
2034
2035pub(crate) fn first_non_whitespace(
2036    map: &DisplaySnapshot,
2037    display_lines: bool,
2038    from: DisplayPoint,
2039) -> DisplayPoint {
2040    let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2041    let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map));
2042    for (ch, offset) in map.buffer_chars_at(start_offset) {
2043        if ch == '\n' {
2044            return from;
2045        }
2046
2047        start_offset = offset;
2048
2049        if classifier.kind(ch) != CharKind::Whitespace {
2050            break;
2051        }
2052    }
2053
2054    start_offset.to_display_point(map)
2055}
2056
2057pub(crate) fn last_non_whitespace(
2058    map: &DisplaySnapshot,
2059    from: DisplayPoint,
2060    count: usize,
2061) -> DisplayPoint {
2062    let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2063    let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map));
2064
2065    // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2066    if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2067        && classifier.kind(ch) != CharKind::Whitespace
2068    {
2069        return end_of_line.to_display_point(map);
2070    }
2071
2072    for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2073        if ch == '\n' {
2074            break;
2075        }
2076        end_of_line = offset;
2077        if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2078            break;
2079        }
2080    }
2081
2082    end_of_line.to_display_point(map)
2083}
2084
2085pub(crate) fn start_of_line(
2086    map: &DisplaySnapshot,
2087    display_lines: bool,
2088    point: DisplayPoint,
2089) -> DisplayPoint {
2090    if display_lines {
2091        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2092    } else {
2093        map.prev_line_boundary(point.to_point(map)).1
2094    }
2095}
2096
2097pub(crate) fn middle_of_line(
2098    map: &DisplaySnapshot,
2099    display_lines: bool,
2100    point: DisplayPoint,
2101    times: Option<usize>,
2102) -> DisplayPoint {
2103    let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2104        times as f64 / 100.
2105    } else {
2106        0.5
2107    };
2108    if display_lines {
2109        map.clip_point(
2110            DisplayPoint::new(
2111                point.row(),
2112                (map.line_len(point.row()) as f64 * percent) as u32,
2113            ),
2114            Bias::Left,
2115        )
2116    } else {
2117        let mut buffer_point = point.to_point(map);
2118        buffer_point.column = (map
2119            .buffer_snapshot()
2120            .line_len(MultiBufferRow(buffer_point.row)) as f64
2121            * percent) as u32;
2122
2123        map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2124    }
2125}
2126
2127pub(crate) fn end_of_line(
2128    map: &DisplaySnapshot,
2129    display_lines: bool,
2130    mut point: DisplayPoint,
2131    times: usize,
2132) -> DisplayPoint {
2133    if times > 1 {
2134        point = map.start_of_relative_buffer_row(point, times as isize - 1);
2135    }
2136    if display_lines {
2137        map.clip_point(
2138            DisplayPoint::new(point.row(), map.line_len(point.row())),
2139            Bias::Left,
2140        )
2141    } else {
2142        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2143    }
2144}
2145
2146pub(crate) fn sentence_backwards(
2147    map: &DisplaySnapshot,
2148    point: DisplayPoint,
2149    mut times: usize,
2150) -> DisplayPoint {
2151    let mut start = point.to_point(map).to_offset(&map.buffer_snapshot());
2152    let mut chars = map.reverse_buffer_chars_at(start).peekable();
2153
2154    let mut was_newline = map
2155        .buffer_chars_at(start)
2156        .next()
2157        .is_some_and(|(c, _)| c == '\n');
2158
2159    while let Some((ch, offset)) = chars.next() {
2160        let start_of_next_sentence = if was_newline && ch == '\n' {
2161            Some(offset + ch.len_utf8())
2162        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2163            Some(next_non_blank(map, offset + ch.len_utf8()))
2164        } else if ch == '.' || ch == '?' || ch == '!' {
2165            start_of_next_sentence(map, offset + ch.len_utf8())
2166        } else {
2167            None
2168        };
2169
2170        if let Some(start_of_next_sentence) = start_of_next_sentence {
2171            if start_of_next_sentence < start {
2172                times = times.saturating_sub(1);
2173            }
2174            if times == 0 || offset.0 == 0 {
2175                return map.clip_point(
2176                    start_of_next_sentence
2177                        .to_offset(&map.buffer_snapshot())
2178                        .to_display_point(map),
2179                    Bias::Left,
2180                );
2181            }
2182        }
2183        if was_newline {
2184            start = offset;
2185        }
2186        was_newline = ch == '\n';
2187    }
2188
2189    DisplayPoint::zero()
2190}
2191
2192pub(crate) fn sentence_forwards(
2193    map: &DisplaySnapshot,
2194    point: DisplayPoint,
2195    mut times: usize,
2196) -> DisplayPoint {
2197    let start = point.to_point(map).to_offset(&map.buffer_snapshot());
2198    let mut chars = map.buffer_chars_at(start).peekable();
2199
2200    let mut was_newline = map
2201        .reverse_buffer_chars_at(start)
2202        .next()
2203        .is_some_and(|(c, _)| c == '\n')
2204        && chars.peek().is_some_and(|(c, _)| *c == '\n');
2205
2206    while let Some((ch, offset)) = chars.next() {
2207        if was_newline && ch == '\n' {
2208            continue;
2209        }
2210        let start_of_next_sentence = if was_newline {
2211            Some(next_non_blank(map, offset))
2212        } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2213            Some(next_non_blank(map, offset + ch.len_utf8()))
2214        } else if ch == '.' || ch == '?' || ch == '!' {
2215            start_of_next_sentence(map, offset + ch.len_utf8())
2216        } else {
2217            None
2218        };
2219
2220        if let Some(start_of_next_sentence) = start_of_next_sentence {
2221            times = times.saturating_sub(1);
2222            if times == 0 {
2223                return map.clip_point(
2224                    start_of_next_sentence
2225                        .to_offset(&map.buffer_snapshot())
2226                        .to_display_point(map),
2227                    Bias::Right,
2228                );
2229            }
2230        }
2231
2232        was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2233    }
2234
2235    map.max_point()
2236}
2237
2238/// Returns a position of the start of the current paragraph for vim motions,
2239/// where a paragraph is defined as a run of non-empty lines. Lines containing
2240/// only whitespace are not considered empty and do not act as paragraph
2241/// boundaries.
2242pub(crate) fn start_of_paragraph(
2243    map: &DisplaySnapshot,
2244    display_point: DisplayPoint,
2245    mut count: usize,
2246) -> DisplayPoint {
2247    let point = display_point.to_point(map);
2248    if point.row == 0 {
2249        return DisplayPoint::zero();
2250    }
2251
2252    let mut found_non_empty_line = false;
2253    for row in (0..point.row + 1).rev() {
2254        let empty = map.buffer_snapshot().line_len(MultiBufferRow(row)) == 0;
2255        if found_non_empty_line && empty {
2256            if count <= 1 {
2257                return Point::new(row, 0).to_display_point(map);
2258            }
2259            count -= 1;
2260            found_non_empty_line = false;
2261        }
2262
2263        found_non_empty_line |= !empty;
2264    }
2265
2266    DisplayPoint::zero()
2267}
2268
2269/// Returns a position of the end of the current paragraph for vim motions,
2270/// where a paragraph is defined as a run of non-empty lines. Lines containing
2271/// only whitespace are not considered empty and do not act as paragraph
2272/// boundaries.
2273pub(crate) fn end_of_paragraph(
2274    map: &DisplaySnapshot,
2275    display_point: DisplayPoint,
2276    mut count: usize,
2277) -> DisplayPoint {
2278    let point = display_point.to_point(map);
2279    if point.row == map.buffer_snapshot().max_row().0 {
2280        return map.max_point();
2281    }
2282
2283    let mut found_non_empty_line = false;
2284    for row in point.row..=map.buffer_snapshot().max_row().0 {
2285        let empty = map.buffer_snapshot().line_len(MultiBufferRow(row)) == 0;
2286        if found_non_empty_line && empty {
2287            if count <= 1 {
2288                return Point::new(row, 0).to_display_point(map);
2289            }
2290            count -= 1;
2291            found_non_empty_line = false;
2292        }
2293
2294        found_non_empty_line |= !empty;
2295    }
2296
2297    map.max_point()
2298}
2299
2300fn next_non_blank(map: &DisplaySnapshot, start: MultiBufferOffset) -> MultiBufferOffset {
2301    for (c, o) in map.buffer_chars_at(start) {
2302        if c == '\n' || !c.is_whitespace() {
2303            return o;
2304        }
2305    }
2306
2307    map.buffer_snapshot().len()
2308}
2309
2310// given the offset after a ., !, or ? find the start of the next sentence.
2311// if this is not a sentence boundary, returns None.
2312fn start_of_next_sentence(
2313    map: &DisplaySnapshot,
2314    end_of_sentence: MultiBufferOffset,
2315) -> Option<MultiBufferOffset> {
2316    let chars = map.buffer_chars_at(end_of_sentence);
2317    let mut seen_space = false;
2318
2319    for (char, offset) in chars {
2320        if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2321            continue;
2322        }
2323
2324        if char == '\n' && seen_space {
2325            return Some(offset);
2326        } else if char.is_whitespace() {
2327            seen_space = true;
2328        } else if seen_space {
2329            return Some(offset);
2330        } else {
2331            return None;
2332        }
2333    }
2334
2335    Some(map.buffer_snapshot().len())
2336}
2337
2338fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2339    let point = map.display_point_to_point(display_point, Bias::Left);
2340    let Some(mut excerpt) = map.buffer_snapshot().excerpt_containing(point..point) else {
2341        return display_point;
2342    };
2343    let offset = excerpt.buffer().point_to_offset(
2344        excerpt
2345            .buffer()
2346            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2347    );
2348    let buffer_range = excerpt.buffer_range();
2349    if offset >= buffer_range.start.0 && offset <= buffer_range.end.0 {
2350        let point = map
2351            .buffer_snapshot()
2352            .offset_to_point(excerpt.map_offset_from_buffer(BufferOffset(offset)));
2353        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2354    }
2355    for (excerpt, buffer, range) in map.buffer_snapshot().excerpts() {
2356        let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
2357            ..language::ToOffset::to_offset(&range.context.end, buffer);
2358        if offset >= excerpt_range.start && offset <= excerpt_range.end {
2359            let text_anchor = buffer.anchor_after(offset);
2360            let anchor = Anchor::in_buffer(excerpt, text_anchor);
2361            return anchor.to_display_point(map);
2362        } else if offset <= excerpt_range.start {
2363            let anchor = Anchor::in_buffer(excerpt, range.context.start);
2364            return anchor.to_display_point(map);
2365        }
2366    }
2367
2368    map.clip_point(
2369        map.point_to_display_point(
2370            map.buffer_snapshot().clip_point(point, Bias::Left),
2371            Bias::Left,
2372        ),
2373        Bias::Left,
2374    )
2375}
2376
2377fn start_of_document(
2378    map: &DisplaySnapshot,
2379    display_point: DisplayPoint,
2380    maybe_times: Option<usize>,
2381) -> DisplayPoint {
2382    if let Some(times) = maybe_times {
2383        return go_to_line(map, display_point, times);
2384    }
2385
2386    let point = map.display_point_to_point(display_point, Bias::Left);
2387    let mut first_point = Point::zero();
2388    first_point.column = point.column;
2389
2390    map.clip_point(
2391        map.point_to_display_point(
2392            map.buffer_snapshot().clip_point(first_point, Bias::Left),
2393            Bias::Left,
2394        ),
2395        Bias::Left,
2396    )
2397}
2398
2399fn end_of_document(
2400    map: &DisplaySnapshot,
2401    display_point: DisplayPoint,
2402    maybe_times: Option<usize>,
2403) -> DisplayPoint {
2404    if let Some(times) = maybe_times {
2405        return go_to_line(map, display_point, times);
2406    };
2407    let point = map.display_point_to_point(display_point, Bias::Left);
2408    let mut last_point = map.buffer_snapshot().max_point();
2409    last_point.column = point.column;
2410
2411    map.clip_point(
2412        map.point_to_display_point(
2413            map.buffer_snapshot().clip_point(last_point, Bias::Left),
2414            Bias::Left,
2415        ),
2416        Bias::Left,
2417    )
2418}
2419
2420fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2421    let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2422    let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2423
2424    if head > outer.start && head < inner.start {
2425        let mut offset = inner.end.to_offset(map, Bias::Left);
2426        for c in map.buffer_snapshot().chars_at(offset) {
2427            if c == '/' || c == '\n' || c == '>' {
2428                return Some(offset.to_display_point(map));
2429            }
2430            offset += c.len_utf8();
2431        }
2432    } else {
2433        let mut offset = outer.start.to_offset(map, Bias::Left);
2434        for c in map.buffer_snapshot().chars_at(offset) {
2435            offset += c.len_utf8();
2436            if c == '<' || c == '\n' {
2437                return Some(offset.to_display_point(map));
2438            }
2439        }
2440    }
2441
2442    None
2443}
2444
2445const BRACKET_PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
2446
2447fn get_bracket_pair(ch: char) -> Option<(char, char, bool)> {
2448    for (open, close) in BRACKET_PAIRS {
2449        if ch == open {
2450            return Some((open, close, true));
2451        }
2452        if ch == close {
2453            return Some((open, close, false));
2454        }
2455    }
2456    None
2457}
2458
2459fn find_matching_bracket_text_based(
2460    map: &DisplaySnapshot,
2461    offset: MultiBufferOffset,
2462    line_range: Range<MultiBufferOffset>,
2463) -> Option<MultiBufferOffset> {
2464    let bracket_info = map
2465        .buffer_chars_at(offset)
2466        .take_while(|(_, char_offset)| *char_offset < line_range.end)
2467        .find_map(|(ch, char_offset)| get_bracket_pair(ch).map(|info| (info, char_offset)));
2468
2469    let (open, close, is_opening) = bracket_info?.0;
2470    let bracket_offset = bracket_info?.1;
2471
2472    let mut depth = 0i32;
2473    if is_opening {
2474        for (ch, char_offset) in map.buffer_chars_at(bracket_offset) {
2475            if ch == open {
2476                depth += 1;
2477            } else if ch == close {
2478                depth -= 1;
2479                if depth == 0 {
2480                    return Some(char_offset);
2481                }
2482            }
2483        }
2484    } else {
2485        for (ch, char_offset) in map.reverse_buffer_chars_at(bracket_offset + close.len_utf8()) {
2486            if ch == close {
2487                depth += 1;
2488            } else if ch == open {
2489                depth -= 1;
2490                if depth == 0 {
2491                    return Some(char_offset);
2492                }
2493            }
2494        }
2495    }
2496
2497    None
2498}
2499
2500fn matching(
2501    map: &DisplaySnapshot,
2502    display_point: DisplayPoint,
2503    match_quotes: bool,
2504) -> DisplayPoint {
2505    if !map.is_singleton() {
2506        return display_point;
2507    }
2508    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2509    let display_point = map.clip_at_line_end(display_point);
2510    let point = display_point.to_point(map);
2511    let offset = point.to_offset(&map.buffer_snapshot());
2512    let snapshot = map.buffer_snapshot();
2513
2514    // Ensure the range is contained by the current line.
2515    let mut line_end = map.next_line_boundary(point).0;
2516    if line_end == point {
2517        line_end = map.max_point().to_point(map);
2518    }
2519
2520    let is_quote_char = |ch: char| matches!(ch, '\'' | '"' | '`');
2521
2522    let make_range_filter = |require_on_bracket: bool| {
2523        move |buffer: &language::BufferSnapshot,
2524              opening_range: Range<BufferOffset>,
2525              closing_range: Range<BufferOffset>| {
2526            if !match_quotes
2527                && buffer
2528                    .chars_at(opening_range.start)
2529                    .next()
2530                    .is_some_and(is_quote_char)
2531            {
2532                return false;
2533            }
2534
2535            if require_on_bracket {
2536                // Attempt to find the smallest enclosing bracket range that also contains
2537                // the offset, which only happens if the cursor is currently in a bracket.
2538                opening_range.contains(&BufferOffset(offset.0))
2539                    || closing_range.contains(&BufferOffset(offset.0))
2540            } else {
2541                true
2542            }
2543        }
2544    };
2545
2546    let bracket_ranges = snapshot
2547        .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(true)))
2548        .or_else(|| {
2549            snapshot
2550                .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(false)))
2551        });
2552
2553    if let Some((opening_range, closing_range)) = bracket_ranges {
2554        let mut chars = map.buffer_snapshot().chars_at(offset);
2555        match chars.next() {
2556            Some('/') => {}
2557            _ => {
2558                if opening_range.contains(&offset) {
2559                    return closing_range.start.to_display_point(map);
2560                } else if closing_range.contains(&offset) {
2561                    return opening_range.start.to_display_point(map);
2562                }
2563            }
2564        }
2565    }
2566
2567    let line_range = map.prev_line_boundary(point).0..line_end;
2568    let visible_line_range =
2569        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2570    let line_range = line_range.start.to_offset(&map.buffer_snapshot())
2571        ..line_range.end.to_offset(&map.buffer_snapshot());
2572    let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range);
2573    if let Some(ranges) = ranges {
2574        let mut closest_pair_destination = None;
2575        let mut closest_distance = usize::MAX;
2576
2577        for (open_range, close_range) in ranges {
2578            if !match_quotes
2579                && map
2580                    .buffer_snapshot()
2581                    .chars_at(open_range.start)
2582                    .next()
2583                    .is_some_and(is_quote_char)
2584            {
2585                continue;
2586            }
2587
2588            if map.buffer_snapshot().chars_at(open_range.start).next() == Some('<') {
2589                if offset > open_range.start && offset < close_range.start {
2590                    let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2591                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2592                        return display_point;
2593                    }
2594                    if let Some(tag) = matching_tag(map, display_point) {
2595                        return tag;
2596                    }
2597                } else if close_range.contains(&offset) {
2598                    return open_range.start.to_display_point(map);
2599                } else if open_range.contains(&offset) {
2600                    return (close_range.end - 1).to_display_point(map);
2601                }
2602            }
2603
2604            if (open_range.contains(&offset) || open_range.start >= offset)
2605                && line_range.contains(&open_range.start)
2606            {
2607                let distance = open_range.start.saturating_sub(offset);
2608                if distance < closest_distance {
2609                    closest_pair_destination = Some(close_range.start);
2610                    closest_distance = distance;
2611                }
2612            }
2613
2614            if (close_range.contains(&offset) || close_range.start >= offset)
2615                && line_range.contains(&close_range.start)
2616            {
2617                let distance = close_range.start.saturating_sub(offset);
2618                if distance < closest_distance {
2619                    closest_pair_destination = Some(open_range.start);
2620                    closest_distance = distance;
2621                }
2622            }
2623
2624            continue;
2625        }
2626
2627        closest_pair_destination
2628            .map(|destination| destination.to_display_point(map))
2629            .unwrap_or_else(|| {
2630                find_matching_bracket_text_based(map, offset, line_range.clone())
2631                    .map(|o| o.to_display_point(map))
2632                    .unwrap_or(display_point)
2633            })
2634    } else {
2635        find_matching_bracket_text_based(map, offset, line_range)
2636            .map(|o| o.to_display_point(map))
2637            .unwrap_or(display_point)
2638    }
2639}
2640
2641// Go to {count} percentage in the file, on the first
2642// non-blank in the line linewise.  To compute the new
2643// line number this formula is used:
2644// ({count} * number-of-lines + 99) / 100
2645//
2646// https://neovim.io/doc/user/motion.html#N%25
2647fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2648    let total_lines = map.buffer_snapshot().max_point().row + 1;
2649    let target_line = (count * total_lines as usize).div_ceil(100);
2650    let target_point = DisplayPoint::new(
2651        DisplayRow(target_line.saturating_sub(1) as u32),
2652        point.column(),
2653    );
2654    map.clip_point(target_point, Bias::Left)
2655}
2656
2657fn unmatched_forward(
2658    map: &DisplaySnapshot,
2659    mut display_point: DisplayPoint,
2660    char: char,
2661    times: usize,
2662) -> DisplayPoint {
2663    for _ in 0..times {
2664        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2665        let point = display_point.to_point(map);
2666        let offset = point.to_offset(&map.buffer_snapshot());
2667
2668        let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2669        let Some(ranges) = ranges else { break };
2670        let mut closest_closing_destination = None;
2671        let mut closest_distance = usize::MAX;
2672
2673        for (_, close_range) in ranges {
2674            if close_range.start > offset {
2675                let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2676                if Some(char) == chars.next() {
2677                    let distance = close_range.start - offset;
2678                    if distance < closest_distance {
2679                        closest_closing_destination = Some(close_range.start);
2680                        closest_distance = distance;
2681                        continue;
2682                    }
2683                }
2684            }
2685        }
2686
2687        let new_point = closest_closing_destination
2688            .map(|destination| destination.to_display_point(map))
2689            .unwrap_or(display_point);
2690        if new_point == display_point {
2691            break;
2692        }
2693        display_point = new_point;
2694    }
2695    display_point
2696}
2697
2698fn unmatched_backward(
2699    map: &DisplaySnapshot,
2700    mut display_point: DisplayPoint,
2701    char: char,
2702    times: usize,
2703) -> DisplayPoint {
2704    for _ in 0..times {
2705        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2706        let point = display_point.to_point(map);
2707        let offset = point.to_offset(&map.buffer_snapshot());
2708
2709        let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2710        let Some(ranges) = ranges else {
2711            break;
2712        };
2713
2714        let mut closest_starting_destination = None;
2715        let mut closest_distance = usize::MAX;
2716
2717        for (start_range, _) in ranges {
2718            if start_range.start < offset {
2719                let mut chars = map.buffer_snapshot().chars_at(start_range.start);
2720                if Some(char) == chars.next() {
2721                    let distance = offset - start_range.start;
2722                    if distance < closest_distance {
2723                        closest_starting_destination = Some(start_range.start);
2724                        closest_distance = distance;
2725                        continue;
2726                    }
2727                }
2728            }
2729        }
2730
2731        let new_point = closest_starting_destination
2732            .map(|destination| destination.to_display_point(map))
2733            .unwrap_or(display_point);
2734        if new_point == display_point {
2735            break;
2736        } else {
2737            display_point = new_point;
2738        }
2739    }
2740    display_point
2741}
2742
2743fn find_forward(
2744    map: &DisplaySnapshot,
2745    from: DisplayPoint,
2746    before: bool,
2747    target: char,
2748    times: usize,
2749    mode: FindRange,
2750    smartcase: bool,
2751) -> Option<DisplayPoint> {
2752    let mut to = from;
2753    let mut found = false;
2754
2755    for _ in 0..times {
2756        found = false;
2757        let new_to = find_boundary(map, to, mode, |_, right| {
2758            found = is_character_match(target, right, smartcase);
2759            found
2760        });
2761        if to == new_to {
2762            break;
2763        }
2764        to = new_to;
2765    }
2766
2767    if found {
2768        if before && to.column() > 0 {
2769            *to.column_mut() -= 1;
2770            Some(map.clip_point(to, Bias::Left))
2771        } else if before && to.row().0 > 0 {
2772            *to.row_mut() -= 1;
2773            *to.column_mut() = map.line(to.row()).len() as u32;
2774            Some(map.clip_point(to, Bias::Left))
2775        } else {
2776            Some(to)
2777        }
2778    } else {
2779        None
2780    }
2781}
2782
2783fn find_backward(
2784    map: &DisplaySnapshot,
2785    from: DisplayPoint,
2786    after: bool,
2787    target: char,
2788    times: usize,
2789    mode: FindRange,
2790    smartcase: bool,
2791) -> DisplayPoint {
2792    let mut to = from;
2793
2794    for _ in 0..times {
2795        let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2796            is_character_match(target, right, smartcase)
2797        });
2798        if to == new_to {
2799            break;
2800        }
2801        to = new_to;
2802    }
2803
2804    let next = map.buffer_snapshot().chars_at(to.to_point(map)).next();
2805    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2806        if after {
2807            *to.column_mut() += 1;
2808            map.clip_point(to, Bias::Right)
2809        } else {
2810            to
2811        }
2812    } else {
2813        from
2814    }
2815}
2816
2817/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2818pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2819    if smartcase {
2820        if target.is_uppercase() {
2821            target == other
2822        } else {
2823            target == other.to_ascii_lowercase()
2824        }
2825    } else {
2826        target == other
2827    }
2828}
2829
2830fn sneak(
2831    map: &DisplaySnapshot,
2832    from: DisplayPoint,
2833    first_target: char,
2834    second_target: char,
2835    times: usize,
2836    smartcase: bool,
2837) -> Option<DisplayPoint> {
2838    let mut to = from;
2839    let mut found = false;
2840
2841    for _ in 0..times {
2842        found = false;
2843        let new_to = find_boundary(
2844            map,
2845            movement::right(map, to),
2846            FindRange::MultiLine,
2847            |left, right| {
2848                found = is_character_match(first_target, left, smartcase)
2849                    && is_character_match(second_target, right, smartcase);
2850                found
2851            },
2852        );
2853        if to == new_to {
2854            break;
2855        }
2856        to = new_to;
2857    }
2858
2859    if found {
2860        Some(movement::left(map, to))
2861    } else {
2862        None
2863    }
2864}
2865
2866fn sneak_backward(
2867    map: &DisplaySnapshot,
2868    from: DisplayPoint,
2869    first_target: char,
2870    second_target: char,
2871    times: usize,
2872    smartcase: bool,
2873) -> Option<DisplayPoint> {
2874    let mut to = from;
2875    let mut found = false;
2876
2877    for _ in 0..times {
2878        found = false;
2879        let new_to =
2880            find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2881                found = is_character_match(first_target, left, smartcase)
2882                    && is_character_match(second_target, right, smartcase);
2883                found
2884            });
2885        if to == new_to {
2886            break;
2887        }
2888        to = new_to;
2889    }
2890
2891    if found {
2892        Some(movement::left(map, to))
2893    } else {
2894        None
2895    }
2896}
2897
2898fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2899    let correct_line = map.start_of_relative_buffer_row(point, times as isize);
2900    first_non_whitespace(map, false, correct_line)
2901}
2902
2903fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2904    let correct_line = map.start_of_relative_buffer_row(point, -(times as isize));
2905    first_non_whitespace(map, false, correct_line)
2906}
2907
2908fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2909    let correct_line = map.start_of_relative_buffer_row(point, 0);
2910    right(map, correct_line, times.saturating_sub(1))
2911}
2912
2913pub(crate) fn next_line_end(
2914    map: &DisplaySnapshot,
2915    mut point: DisplayPoint,
2916    times: usize,
2917) -> DisplayPoint {
2918    if times > 1 {
2919        point = map.start_of_relative_buffer_row(point, times as isize - 1);
2920    }
2921    end_of_line(map, false, point, 1)
2922}
2923
2924fn window_top(
2925    map: &DisplaySnapshot,
2926    point: DisplayPoint,
2927    text_layout_details: &TextLayoutDetails,
2928    mut times: usize,
2929) -> (DisplayPoint, SelectionGoal) {
2930    let first_visible_line = text_layout_details
2931        .scroll_anchor
2932        .scroll_top_display_point(map);
2933
2934    if first_visible_line.row() != DisplayRow(0)
2935        && text_layout_details.vertical_scroll_margin as usize > times
2936    {
2937        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2938    }
2939
2940    if let Some(visible_rows) = text_layout_details.visible_rows {
2941        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2942        let new_row = (first_visible_line.row().0 + (times as u32))
2943            .min(bottom_row)
2944            .min(map.max_point().row().0);
2945        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2946
2947        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2948        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2949    } else {
2950        let new_row =
2951            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2952        let new_col = point.column().min(map.line_len(first_visible_line.row()));
2953
2954        let new_point = DisplayPoint::new(new_row, new_col);
2955        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2956    }
2957}
2958
2959fn window_middle(
2960    map: &DisplaySnapshot,
2961    point: DisplayPoint,
2962    text_layout_details: &TextLayoutDetails,
2963) -> (DisplayPoint, SelectionGoal) {
2964    if let Some(visible_rows) = text_layout_details.visible_rows {
2965        let first_visible_line = text_layout_details
2966            .scroll_anchor
2967            .scroll_top_display_point(map);
2968
2969        let max_visible_rows =
2970            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2971
2972        let new_row =
2973            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2974        let new_row = DisplayRow(new_row);
2975        let new_col = point.column().min(map.line_len(new_row));
2976        let new_point = DisplayPoint::new(new_row, new_col);
2977        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2978    } else {
2979        (point, SelectionGoal::None)
2980    }
2981}
2982
2983fn window_bottom(
2984    map: &DisplaySnapshot,
2985    point: DisplayPoint,
2986    text_layout_details: &TextLayoutDetails,
2987    mut times: usize,
2988) -> (DisplayPoint, SelectionGoal) {
2989    if let Some(visible_rows) = text_layout_details.visible_rows {
2990        let first_visible_line = text_layout_details
2991            .scroll_anchor
2992            .scroll_top_display_point(map);
2993        let bottom_row = first_visible_line.row().0
2994            + (visible_rows + text_layout_details.scroll_anchor.scroll_anchor.offset.y - 1.).floor()
2995                as u32;
2996        if bottom_row < map.max_point().row().0
2997            && text_layout_details.vertical_scroll_margin as usize > times
2998        {
2999            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
3000        }
3001        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
3002        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
3003        {
3004            first_visible_line.row()
3005        } else {
3006            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
3007        };
3008        let new_col = point.column().min(map.line_len(new_row));
3009        let new_point = DisplayPoint::new(new_row, new_col);
3010        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3011    } else {
3012        (point, SelectionGoal::None)
3013    }
3014}
3015
3016fn method_motion(
3017    map: &DisplaySnapshot,
3018    mut display_point: DisplayPoint,
3019    times: usize,
3020    direction: Direction,
3021    is_start: bool,
3022) -> DisplayPoint {
3023    let snapshot = map.buffer_snapshot();
3024    if snapshot.as_singleton().is_none() {
3025        return display_point;
3026    }
3027
3028    for _ in 0..times {
3029        let offset = map
3030            .display_point_to_point(display_point, Bias::Left)
3031            .to_offset(&snapshot);
3032        let range = if direction == Direction::Prev {
3033            MultiBufferOffset(0)..offset
3034        } else {
3035            offset..snapshot.len()
3036        };
3037
3038        let possibilities = snapshot
3039            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
3040            .filter_map(|(range, object)| {
3041                if !matches!(object, language::TextObject::AroundFunction) {
3042                    return None;
3043                }
3044
3045                let relevant = if is_start { range.start } else { range.end };
3046                if direction == Direction::Prev && relevant < offset {
3047                    Some(relevant)
3048                } else if direction == Direction::Next && relevant > offset + 1usize {
3049                    Some(relevant)
3050                } else {
3051                    None
3052                }
3053            });
3054
3055        let dest = if direction == Direction::Prev {
3056            possibilities.max().unwrap_or(offset)
3057        } else {
3058            possibilities.min().unwrap_or(offset)
3059        };
3060        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
3061        if new_point == display_point {
3062            break;
3063        }
3064        display_point = new_point;
3065    }
3066    display_point
3067}
3068
3069fn comment_motion(
3070    map: &DisplaySnapshot,
3071    mut display_point: DisplayPoint,
3072    times: usize,
3073    direction: Direction,
3074) -> DisplayPoint {
3075    let snapshot = map.buffer_snapshot();
3076    if snapshot.as_singleton().is_none() {
3077        return display_point;
3078    }
3079
3080    for _ in 0..times {
3081        let offset = map
3082            .display_point_to_point(display_point, Bias::Left)
3083            .to_offset(&snapshot);
3084        let range = if direction == Direction::Prev {
3085            MultiBufferOffset(0)..offset
3086        } else {
3087            offset..snapshot.len()
3088        };
3089
3090        let possibilities = snapshot
3091            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
3092            .filter_map(|(range, object)| {
3093                if !matches!(object, language::TextObject::AroundComment) {
3094                    return None;
3095                }
3096
3097                let relevant = if direction == Direction::Prev {
3098                    range.start
3099                } else {
3100                    range.end
3101                };
3102                if direction == Direction::Prev && relevant < offset {
3103                    Some(relevant)
3104                } else if direction == Direction::Next && relevant > offset + 1usize {
3105                    Some(relevant)
3106                } else {
3107                    None
3108                }
3109            });
3110
3111        let dest = if direction == Direction::Prev {
3112            possibilities.max().unwrap_or(offset)
3113        } else {
3114            possibilities.min().unwrap_or(offset)
3115        };
3116        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
3117        if new_point == display_point {
3118            break;
3119        }
3120        display_point = new_point;
3121    }
3122
3123    display_point
3124}
3125
3126fn section_motion(
3127    map: &DisplaySnapshot,
3128    mut display_point: DisplayPoint,
3129    times: usize,
3130    direction: Direction,
3131    is_start: bool,
3132) -> DisplayPoint {
3133    if map.buffer_snapshot().as_singleton().is_some() {
3134        for _ in 0..times {
3135            let offset = map
3136                .display_point_to_point(display_point, Bias::Left)
3137                .to_offset(&map.buffer_snapshot());
3138            let range = if direction == Direction::Prev {
3139                MultiBufferOffset(0)..offset
3140            } else {
3141                offset..map.buffer_snapshot().len()
3142            };
3143
3144            // we set a max start depth here because we want a section to only be "top level"
3145            // similar to vim's default of '{' in the first column.
3146            // (and without it, ]] at the start of editor.rs is -very- slow)
3147            let mut possibilities = map
3148                .buffer_snapshot()
3149                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
3150                .filter(|(_, object)| {
3151                    matches!(
3152                        object,
3153                        language::TextObject::AroundClass | language::TextObject::AroundFunction
3154                    )
3155                })
3156                .collect::<Vec<_>>();
3157            possibilities.sort_by_key(|(range_a, _)| range_a.start);
3158            let mut prev_end = None;
3159            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
3160                if t == language::TextObject::AroundFunction
3161                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
3162                {
3163                    return None;
3164                }
3165                prev_end = Some(range.end);
3166
3167                let relevant = if is_start { range.start } else { range.end };
3168                if direction == Direction::Prev && relevant < offset {
3169                    Some(relevant)
3170                } else if direction == Direction::Next && relevant > offset + 1usize {
3171                    Some(relevant)
3172                } else {
3173                    None
3174                }
3175            });
3176
3177            let offset = if direction == Direction::Prev {
3178                possibilities.max().unwrap_or(MultiBufferOffset(0))
3179            } else {
3180                possibilities.min().unwrap_or(map.buffer_snapshot().len())
3181            };
3182
3183            let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3184            if new_point == display_point {
3185                break;
3186            }
3187            display_point = new_point;
3188        }
3189        return display_point;
3190    };
3191
3192    for _ in 0..times {
3193        let next_point = if is_start {
3194            movement::start_of_excerpt(map, display_point, direction)
3195        } else {
3196            movement::end_of_excerpt(map, display_point, direction)
3197        };
3198        if next_point == display_point {
3199            break;
3200        }
3201        display_point = next_point;
3202    }
3203
3204    display_point
3205}
3206
3207fn matches_indent_type(
3208    target_indent: &text::LineIndent,
3209    current_indent: &text::LineIndent,
3210    indent_type: IndentType,
3211) -> bool {
3212    match indent_type {
3213        IndentType::Lesser => {
3214            target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3215        }
3216        IndentType::Greater => {
3217            target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3218        }
3219        IndentType::Same => {
3220            target_indent.spaces == current_indent.spaces
3221                && target_indent.tabs == current_indent.tabs
3222        }
3223    }
3224}
3225
3226fn indent_motion(
3227    map: &DisplaySnapshot,
3228    mut display_point: DisplayPoint,
3229    times: usize,
3230    direction: Direction,
3231    indent_type: IndentType,
3232) -> DisplayPoint {
3233    let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3234    let current_row = MultiBufferRow(buffer_point.row);
3235    let current_indent = map.line_indent_for_buffer_row(current_row);
3236    if current_indent.is_line_empty() {
3237        return display_point;
3238    }
3239    let max_row = map.max_point().to_point(map).row;
3240
3241    for _ in 0..times {
3242        let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3243
3244        let target_row = match direction {
3245            Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3246                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3247                !indent.is_line_empty()
3248                    && matches_indent_type(&indent, &current_indent, indent_type)
3249            }),
3250            Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3251                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3252                !indent.is_line_empty()
3253                    && matches_indent_type(&indent, &current_indent, indent_type)
3254            }),
3255        }
3256        .unwrap_or(current_buffer_row);
3257
3258        let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3259        let new_point = first_non_whitespace(map, false, new_point);
3260        if new_point == display_point {
3261            break;
3262        }
3263        display_point = new_point;
3264    }
3265    display_point
3266}
3267
3268#[cfg(test)]
3269mod test {
3270
3271    use crate::{
3272        motion::Matching,
3273        state::Mode,
3274        test::{NeovimBackedTestContext, VimTestContext},
3275    };
3276    use editor::Inlay;
3277    use gpui::KeyBinding;
3278    use indoc::indoc;
3279    use language::Point;
3280    use multi_buffer::MultiBufferRow;
3281
3282    #[gpui::test]
3283    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3284        let mut cx = NeovimBackedTestContext::new(cx).await;
3285
3286        let initial_state = indoc! {r"ˇabc
3287            def
3288
3289            paragraph
3290            the second
3291
3292
3293
3294            third and
3295            final"};
3296
3297        // goes down once
3298        cx.set_shared_state(initial_state).await;
3299        cx.simulate_shared_keystrokes("}").await;
3300        cx.shared_state().await.assert_eq(indoc! {r"abc
3301            def
3302            ˇ
3303            paragraph
3304            the second
3305
3306
3307
3308            third and
3309            final"});
3310
3311        // goes up once
3312        cx.simulate_shared_keystrokes("{").await;
3313        cx.shared_state().await.assert_eq(initial_state);
3314
3315        // goes down twice
3316        cx.simulate_shared_keystrokes("2 }").await;
3317        cx.shared_state().await.assert_eq(indoc! {r"abc
3318            def
3319
3320            paragraph
3321            the second
3322            ˇ
3323
3324
3325            third and
3326            final"});
3327
3328        // goes down over multiple blanks
3329        cx.simulate_shared_keystrokes("}").await;
3330        cx.shared_state().await.assert_eq(indoc! {r"abc
3331                def
3332
3333                paragraph
3334                the second
3335
3336
3337
3338                third and
3339                finaˇl"});
3340
3341        // goes up twice
3342        cx.simulate_shared_keystrokes("2 {").await;
3343        cx.shared_state().await.assert_eq(indoc! {r"abc
3344                def
3345                ˇ
3346                paragraph
3347                the second
3348
3349
3350
3351                third and
3352                final"});
3353    }
3354
3355    #[gpui::test]
3356    async fn test_paragraph_motion_with_whitespace_lines(cx: &mut gpui::TestAppContext) {
3357        let mut cx = NeovimBackedTestContext::new(cx).await;
3358
3359        // Test that whitespace-only lines are NOT treated as paragraph boundaries
3360        // Per vim's :help paragraph - only truly empty lines are boundaries
3361        // Line 2 has 4 spaces (whitespace-only), line 4 is truly empty
3362        cx.set_shared_state("ˇfirst\n    \nstill first\n\nsecond")
3363            .await;
3364        cx.simulate_shared_keystrokes("}").await;
3365
3366        // Should skip whitespace-only line and stop at truly empty line
3367        let mut shared_state = cx.shared_state().await;
3368        shared_state.assert_eq("first\n    \nstill first\nˇ\nsecond");
3369        shared_state.assert_matches();
3370
3371        // Should go back to original position
3372        cx.simulate_shared_keystrokes("{").await;
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
3378    #[gpui::test]
3379    async fn test_matching(cx: &mut gpui::TestAppContext) {
3380        let mut cx = NeovimBackedTestContext::new(cx).await;
3381
3382        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3383                do(something(with<Types>.and_arrays[0, 2]))
3384            }"})
3385            .await;
3386        cx.simulate_shared_keystrokes("%").await;
3387        cx.shared_state()
3388            .await
3389            .assert_eq(indoc! {r"func (a stringˇ) {
3390                do(something(with<Types>.and_arrays[0, 2]))
3391            }"});
3392
3393        // test it works on the last character of the line
3394        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3395            do(something(with<Types>.and_arrays[0, 2]))
3396            }"})
3397            .await;
3398        cx.simulate_shared_keystrokes("%").await;
3399        cx.shared_state()
3400            .await
3401            .assert_eq(indoc! {r"func (a string) {
3402            do(something(with<Types>.and_arrays[0, 2]))
3403            ˇ}"});
3404
3405        // test it works on immediate nesting
3406        cx.set_shared_state("ˇ{()}").await;
3407        cx.simulate_shared_keystrokes("%").await;
3408        cx.shared_state().await.assert_eq("{()ˇ}");
3409        cx.simulate_shared_keystrokes("%").await;
3410        cx.shared_state().await.assert_eq("ˇ{()}");
3411
3412        // test it works on immediate nesting inside braces
3413        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3414        cx.simulate_shared_keystrokes("%").await;
3415        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3416
3417        // test it jumps to the next paren on a line
3418        cx.set_shared_state("func ˇboop() {\n}").await;
3419        cx.simulate_shared_keystrokes("%").await;
3420        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3421    }
3422
3423    #[gpui::test]
3424    async fn test_matching_quotes_disabled(cx: &mut gpui::TestAppContext) {
3425        let mut cx = NeovimBackedTestContext::new(cx).await;
3426
3427        // Bind % to Matching with match_quotes: false to match Neovim's behavior
3428        // (Neovim's % doesn't match quotes by default)
3429        cx.update(|_, cx| {
3430            cx.bind_keys([KeyBinding::new(
3431                "%",
3432                Matching {
3433                    match_quotes: false,
3434                },
3435                None,
3436            )]);
3437        });
3438
3439        cx.set_shared_state("one {two 'thˇree' four}").await;
3440        cx.simulate_shared_keystrokes("%").await;
3441        cx.shared_state().await.assert_eq("one ˇ{two 'three' four}");
3442
3443        cx.set_shared_state("'hello wˇorld'").await;
3444        cx.simulate_shared_keystrokes("%").await;
3445        cx.shared_state().await.assert_eq("'hello wˇorld'");
3446
3447        cx.set_shared_state(r#"func ("teˇst") {}"#).await;
3448        cx.simulate_shared_keystrokes("%").await;
3449        cx.shared_state().await.assert_eq(r#"func ˇ("test") {}"#);
3450
3451        cx.set_shared_state("ˇ'hello'").await;
3452        cx.simulate_shared_keystrokes("%").await;
3453        cx.shared_state().await.assert_eq("ˇ'hello'");
3454
3455        cx.set_shared_state("'helloˇ'").await;
3456        cx.simulate_shared_keystrokes("%").await;
3457        cx.shared_state().await.assert_eq("'helloˇ'");
3458
3459        cx.set_shared_state(indoc! {r"func (a string) {
3460                do('somethiˇng'))
3461            }"})
3462            .await;
3463        cx.simulate_shared_keystrokes("%").await;
3464        cx.shared_state()
3465            .await
3466            .assert_eq(indoc! {r"func (a string) {
3467                doˇ('something'))
3468            }"});
3469    }
3470
3471    #[gpui::test]
3472    async fn test_matching_quotes_enabled(cx: &mut gpui::TestAppContext) {
3473        let mut cx = VimTestContext::new_markdown_with_rust(cx).await;
3474
3475        // Test default behavior (match_quotes: true as configured in keymap/vim.json)
3476        cx.set_state("one {two 'thˇree' four}", Mode::Normal);
3477        cx.simulate_keystrokes("%");
3478        cx.assert_state("one {two ˇ'three' four}", Mode::Normal);
3479
3480        cx.set_state("'hello wˇorld'", Mode::Normal);
3481        cx.simulate_keystrokes("%");
3482        cx.assert_state("ˇ'hello world'", Mode::Normal);
3483
3484        cx.set_state(r#"func ('teˇst') {}"#, Mode::Normal);
3485        cx.simulate_keystrokes("%");
3486        cx.assert_state(r#"func (ˇ'test') {}"#, Mode::Normal);
3487
3488        cx.set_state("ˇ'hello'", Mode::Normal);
3489        cx.simulate_keystrokes("%");
3490        cx.assert_state("'helloˇ'", Mode::Normal);
3491
3492        cx.set_state("'helloˇ'", Mode::Normal);
3493        cx.simulate_keystrokes("%");
3494        cx.assert_state("ˇ'hello'", Mode::Normal);
3495
3496        cx.set_state(
3497            indoc! {r"func (a string) {
3498                do('somethiˇng'))
3499            }"},
3500            Mode::Normal,
3501        );
3502        cx.simulate_keystrokes("%");
3503        cx.assert_state(
3504            indoc! {r"func (a string) {
3505                do(ˇ'something'))
3506            }"},
3507            Mode::Normal,
3508        );
3509    }
3510
3511    #[gpui::test]
3512    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3513        let mut cx = NeovimBackedTestContext::new(cx).await;
3514
3515        // test it works with curly braces
3516        cx.set_shared_state(indoc! {r"func (a string) {
3517                do(something(with<Types>.anˇd_arrays[0, 2]))
3518            }"})
3519            .await;
3520        cx.simulate_shared_keystrokes("] }").await;
3521        cx.shared_state()
3522            .await
3523            .assert_eq(indoc! {r"func (a string) {
3524                do(something(with<Types>.and_arrays[0, 2]))
3525            ˇ}"});
3526
3527        // test it works with brackets
3528        cx.set_shared_state(indoc! {r"func (a string) {
3529                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3530            }"})
3531            .await;
3532        cx.simulate_shared_keystrokes("] )").await;
3533        cx.shared_state()
3534            .await
3535            .assert_eq(indoc! {r"func (a string) {
3536                do(something(with<Types>.and_arrays[0, 2])ˇ)
3537            }"});
3538
3539        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3540            .await;
3541        cx.simulate_shared_keystrokes("] )").await;
3542        cx.shared_state()
3543            .await
3544            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3545
3546        // test it works on immediate nesting
3547        cx.set_shared_state("{ˇ {}{}}").await;
3548        cx.simulate_shared_keystrokes("] }").await;
3549        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3550        cx.set_shared_state("(ˇ ()())").await;
3551        cx.simulate_shared_keystrokes("] )").await;
3552        cx.shared_state().await.assert_eq("( ()()ˇ)");
3553
3554        // test it works on immediate nesting inside braces
3555        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3556        cx.simulate_shared_keystrokes("] }").await;
3557        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3558        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3559        cx.simulate_shared_keystrokes("] )").await;
3560        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3561    }
3562
3563    #[gpui::test]
3564    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3565        let mut cx = NeovimBackedTestContext::new(cx).await;
3566
3567        // test it works with curly braces
3568        cx.set_shared_state(indoc! {r"func (a string) {
3569                do(something(with<Types>.anˇd_arrays[0, 2]))
3570            }"})
3571            .await;
3572        cx.simulate_shared_keystrokes("[ {").await;
3573        cx.shared_state()
3574            .await
3575            .assert_eq(indoc! {r"func (a string) ˇ{
3576                do(something(with<Types>.and_arrays[0, 2]))
3577            }"});
3578
3579        // test it works with brackets
3580        cx.set_shared_state(indoc! {r"func (a string) {
3581                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3582            }"})
3583            .await;
3584        cx.simulate_shared_keystrokes("[ (").await;
3585        cx.shared_state()
3586            .await
3587            .assert_eq(indoc! {r"func (a string) {
3588                doˇ(something(with<Types>.and_arrays[0, 2]))
3589            }"});
3590
3591        // test it works on immediate nesting
3592        cx.set_shared_state("{{}{} ˇ }").await;
3593        cx.simulate_shared_keystrokes("[ {").await;
3594        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3595        cx.set_shared_state("(()() ˇ )").await;
3596        cx.simulate_shared_keystrokes("[ (").await;
3597        cx.shared_state().await.assert_eq("ˇ(()()  )");
3598
3599        // test it works on immediate nesting inside braces
3600        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3601        cx.simulate_shared_keystrokes("[ {").await;
3602        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3603        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3604        cx.simulate_shared_keystrokes("[ (").await;
3605        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3606    }
3607
3608    #[gpui::test]
3609    async fn test_unmatched_forward_markdown(cx: &mut gpui::TestAppContext) {
3610        let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3611
3612        cx.neovim.exec("set filetype=markdown").await;
3613
3614        cx.set_shared_state(indoc! {r"
3615            ```rs
3616            impl Worktree {
3617                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3618            ˇ    }
3619            }
3620            ```
3621        "})
3622            .await;
3623        cx.simulate_shared_keystrokes("] }").await;
3624        cx.shared_state().await.assert_eq(indoc! {r"
3625            ```rs
3626            impl Worktree {
3627                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3628                ˇ}
3629            }
3630            ```
3631        "});
3632
3633        cx.set_shared_state(indoc! {r"
3634            ```rs
3635            impl Worktree {
3636                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3637                }   ˇ
3638            }
3639            ```
3640        "})
3641            .await;
3642        cx.simulate_shared_keystrokes("] }").await;
3643        cx.shared_state().await.assert_eq(indoc! {r"
3644            ```rs
3645            impl Worktree {
3646                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3647                }  •
3648            ˇ}
3649            ```
3650        "});
3651    }
3652
3653    #[gpui::test]
3654    async fn test_unmatched_backward_markdown(cx: &mut gpui::TestAppContext) {
3655        let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3656
3657        cx.neovim.exec("set filetype=markdown").await;
3658
3659        cx.set_shared_state(indoc! {r"
3660            ```rs
3661            impl Worktree {
3662                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3663            ˇ    }
3664            }
3665            ```
3666        "})
3667            .await;
3668        cx.simulate_shared_keystrokes("[ {").await;
3669        cx.shared_state().await.assert_eq(indoc! {r"
3670            ```rs
3671            impl Worktree {
3672                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
3673                }
3674            }
3675            ```
3676        "});
3677
3678        cx.set_shared_state(indoc! {r"
3679            ```rs
3680            impl Worktree {
3681                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3682                }   ˇ
3683            }
3684            ```
3685        "})
3686            .await;
3687        cx.simulate_shared_keystrokes("[ {").await;
3688        cx.shared_state().await.assert_eq(indoc! {r"
3689            ```rs
3690            impl Worktree ˇ{
3691                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3692                }  •
3693            }
3694            ```
3695        "});
3696    }
3697
3698    #[gpui::test]
3699    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3700        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3701
3702        cx.neovim.exec("set filetype=html").await;
3703
3704        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3705        cx.simulate_shared_keystrokes("%").await;
3706        cx.shared_state()
3707            .await
3708            .assert_eq(indoc! {r"<body><ˇ/body>"});
3709        cx.simulate_shared_keystrokes("%").await;
3710
3711        // test jumping backwards
3712        cx.shared_state()
3713            .await
3714            .assert_eq(indoc! {r"<ˇbody></body>"});
3715
3716        // test self-closing tags
3717        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3718        cx.simulate_shared_keystrokes("%").await;
3719        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3720
3721        // test tag with attributes
3722        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3723            </div>
3724            "})
3725            .await;
3726        cx.simulate_shared_keystrokes("%").await;
3727        cx.shared_state()
3728            .await
3729            .assert_eq(indoc! {r"<div class='test' id='main'>
3730            <ˇ/div>
3731            "});
3732
3733        // test multi-line self-closing tag
3734        cx.set_shared_state(indoc! {r#"<a>
3735            <br
3736                test = "test"
3737            /ˇ>
3738        </a>"#})
3739            .await;
3740        cx.simulate_shared_keystrokes("%").await;
3741        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3742            ˇ<br
3743                test = "test"
3744            />
3745        </a>"#});
3746
3747        // test nested closing tag
3748        cx.set_shared_state(indoc! {r#"<html>
3749            <bˇody>
3750            </body>
3751        </html>"#})
3752            .await;
3753        cx.simulate_shared_keystrokes("%").await;
3754        cx.shared_state().await.assert_eq(indoc! {r#"<html>
3755            <body>
3756            <ˇ/body>
3757        </html>"#});
3758        cx.simulate_shared_keystrokes("%").await;
3759        cx.shared_state().await.assert_eq(indoc! {r#"<html>
3760            <ˇbody>
3761            </body>
3762        </html>"#});
3763    }
3764
3765    #[gpui::test]
3766    async fn test_matching_tag_with_quotes(cx: &mut gpui::TestAppContext) {
3767        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3768        cx.update(|_, cx| {
3769            cx.bind_keys([KeyBinding::new(
3770                "%",
3771                Matching {
3772                    match_quotes: false,
3773                },
3774                None,
3775            )]);
3776        });
3777
3778        cx.neovim.exec("set filetype=html").await;
3779        cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
3780            </div>
3781            "})
3782            .await;
3783        cx.simulate_shared_keystrokes("%").await;
3784        cx.shared_state()
3785            .await
3786            .assert_eq(indoc! {r"<div class='test' id='main'>
3787            <ˇ/div>
3788            "});
3789
3790        cx.update(|_, cx| {
3791            cx.bind_keys([KeyBinding::new("%", Matching { match_quotes: true }, None)]);
3792        });
3793
3794        cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
3795            </div>
3796            "})
3797            .await;
3798        cx.simulate_shared_keystrokes("%").await;
3799        cx.shared_state()
3800            .await
3801            .assert_eq(indoc! {r"<div class='test' id='main'>
3802            <ˇ/div>
3803            "});
3804    }
3805    #[gpui::test]
3806    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3807        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3808
3809        // test brackets within tags
3810        cx.set_shared_state(indoc! {r"function f() {
3811            return (
3812                <div rules={ˇ[{ a: 1 }]}>
3813                    <h1>test</h1>
3814                </div>
3815            );
3816        }"})
3817            .await;
3818        cx.simulate_shared_keystrokes("%").await;
3819        cx.shared_state().await.assert_eq(indoc! {r"function f() {
3820            return (
3821                <div rules={[{ a: 1 }ˇ]}>
3822                    <h1>test</h1>
3823                </div>
3824            );
3825        }"});
3826    }
3827
3828    #[gpui::test]
3829    async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
3830        let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
3831
3832        cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
3833            .await;
3834        cx.simulate_shared_keystrokes("%").await;
3835        cx.shared_state()
3836            .await
3837            .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
3838        cx.simulate_shared_keystrokes("%").await;
3839        cx.shared_state()
3840            .await
3841            .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
3842    }
3843
3844    #[gpui::test]
3845    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3846        let mut cx = NeovimBackedTestContext::new(cx).await;
3847
3848        // f and F
3849        cx.set_shared_state("ˇone two three four").await;
3850        cx.simulate_shared_keystrokes("f o").await;
3851        cx.shared_state().await.assert_eq("one twˇo three four");
3852        cx.simulate_shared_keystrokes(",").await;
3853        cx.shared_state().await.assert_eq("ˇone two three four");
3854        cx.simulate_shared_keystrokes("2 ;").await;
3855        cx.shared_state().await.assert_eq("one two three fˇour");
3856        cx.simulate_shared_keystrokes("shift-f e").await;
3857        cx.shared_state().await.assert_eq("one two threˇe four");
3858        cx.simulate_shared_keystrokes("2 ;").await;
3859        cx.shared_state().await.assert_eq("onˇe two three four");
3860        cx.simulate_shared_keystrokes(",").await;
3861        cx.shared_state().await.assert_eq("one two thrˇee four");
3862
3863        // t and T
3864        cx.set_shared_state("ˇone two three four").await;
3865        cx.simulate_shared_keystrokes("t o").await;
3866        cx.shared_state().await.assert_eq("one tˇwo three four");
3867        cx.simulate_shared_keystrokes(",").await;
3868        cx.shared_state().await.assert_eq("oˇne two three four");
3869        cx.simulate_shared_keystrokes("2 ;").await;
3870        cx.shared_state().await.assert_eq("one two three ˇfour");
3871        cx.simulate_shared_keystrokes("shift-t e").await;
3872        cx.shared_state().await.assert_eq("one two threeˇ four");
3873        cx.simulate_shared_keystrokes("3 ;").await;
3874        cx.shared_state().await.assert_eq("oneˇ two three four");
3875        cx.simulate_shared_keystrokes(",").await;
3876        cx.shared_state().await.assert_eq("one two thˇree four");
3877    }
3878
3879    #[gpui::test]
3880    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3881        let mut cx = NeovimBackedTestContext::new(cx).await;
3882        let initial_state = indoc! {r"something(ˇfoo)"};
3883        cx.set_shared_state(initial_state).await;
3884        cx.simulate_shared_keystrokes("}").await;
3885        cx.shared_state().await.assert_eq("something(fooˇ)");
3886    }
3887
3888    #[gpui::test]
3889    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3890        let mut cx = NeovimBackedTestContext::new(cx).await;
3891        cx.set_shared_state("ˇone\n  two\nthree").await;
3892        cx.simulate_shared_keystrokes("enter").await;
3893        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3894    }
3895
3896    #[gpui::test]
3897    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3898        let mut cx = NeovimBackedTestContext::new(cx).await;
3899        cx.set_shared_state("ˇ one\n two \nthree").await;
3900        cx.simulate_shared_keystrokes("g _").await;
3901        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3902
3903        cx.set_shared_state("ˇ one \n two \nthree").await;
3904        cx.simulate_shared_keystrokes("g _").await;
3905        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3906        cx.simulate_shared_keystrokes("2 g _").await;
3907        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3908    }
3909
3910    #[gpui::test]
3911    async fn test_end_of_line_with_vertical_motion(cx: &mut gpui::TestAppContext) {
3912        let mut cx = NeovimBackedTestContext::new(cx).await;
3913
3914        // test $ followed by k maintains end-of-line position
3915        cx.set_shared_state(indoc! {"
3916            The quick brown
3917            fˇox
3918            jumps over the
3919            lazy dog
3920            "})
3921            .await;
3922        cx.simulate_shared_keystrokes("$ k").await;
3923        cx.shared_state().await.assert_eq(indoc! {"
3924            The quick browˇn
3925            fox
3926            jumps over the
3927            lazy dog
3928            "});
3929        cx.simulate_shared_keystrokes("j j").await;
3930        cx.shared_state().await.assert_eq(indoc! {"
3931            The quick brown
3932            fox
3933            jumps over thˇe
3934            lazy dog
3935            "});
3936
3937        // test horizontal movement resets the end-of-line behavior
3938        cx.set_shared_state(indoc! {"
3939            The quick brown fox
3940            jumps over the
3941            lazy ˇdog
3942            "})
3943            .await;
3944        cx.simulate_shared_keystrokes("$ k").await;
3945        cx.shared_state().await.assert_eq(indoc! {"
3946            The quick brown fox
3947            jumps over thˇe
3948            lazy dog
3949            "});
3950        cx.simulate_shared_keystrokes("b b").await;
3951        cx.shared_state().await.assert_eq(indoc! {"
3952            The quick brown fox
3953            jumps ˇover the
3954            lazy dog
3955            "});
3956        cx.simulate_shared_keystrokes("k").await;
3957        cx.shared_state().await.assert_eq(indoc! {"
3958            The quˇick brown fox
3959            jumps over the
3960            lazy dog
3961            "});
3962
3963        // Test that, when the cursor is moved to the end of the line using `l`,
3964        // if `$` is used, the cursor stays at the end of the line when moving
3965        // to a longer line, ensuring that the selection goal was correctly
3966        // updated.
3967        cx.set_shared_state(indoc! {"
3968            The quick brown fox
3969            jumps over the
3970            lazy dˇog
3971            "})
3972            .await;
3973        cx.simulate_shared_keystrokes("l").await;
3974        cx.shared_state().await.assert_eq(indoc! {"
3975            The quick brown fox
3976            jumps over the
3977            lazy doˇg
3978            "});
3979        cx.simulate_shared_keystrokes("$ k").await;
3980        cx.shared_state().await.assert_eq(indoc! {"
3981            The quick brown fox
3982            jumps over thˇe
3983            lazy dog
3984            "});
3985    }
3986
3987    #[gpui::test]
3988    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3989        let mut cx = NeovimBackedTestContext::new(cx).await;
3990        let initial_state = indoc! {r"abc
3991          def
3992          paragraph
3993          the second
3994          third ˇand
3995          final"};
3996
3997        cx.set_shared_state(initial_state).await;
3998        cx.simulate_shared_keystrokes("shift-h").await;
3999        cx.shared_state().await.assert_eq(indoc! {r"abˇc
4000          def
4001          paragraph
4002          the second
4003          third and
4004          final"});
4005
4006        // clip point
4007        cx.set_shared_state(indoc! {r"
4008          1 2 3
4009          4 5 6
4010          7 8 ˇ9
4011          "})
4012            .await;
4013        cx.simulate_shared_keystrokes("shift-h").await;
4014        cx.shared_state().await.assert_eq(indoc! {"
4015          1 2 ˇ3
4016          4 5 6
4017          7 8 9
4018          "});
4019
4020        cx.set_shared_state(indoc! {r"
4021          1 2 3
4022          4 5 6
4023          ˇ7 8 9
4024          "})
4025            .await;
4026        cx.simulate_shared_keystrokes("shift-h").await;
4027        cx.shared_state().await.assert_eq(indoc! {"
4028          ˇ1 2 3
4029          4 5 6
4030          7 8 9
4031          "});
4032
4033        cx.set_shared_state(indoc! {r"
4034          1 2 3
4035          4 5 ˇ6
4036          7 8 9"})
4037            .await;
4038        cx.simulate_shared_keystrokes("9 shift-h").await;
4039        cx.shared_state().await.assert_eq(indoc! {"
4040          1 2 3
4041          4 5 6
4042          7 8 ˇ9"});
4043    }
4044
4045    #[gpui::test]
4046    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
4047        let mut cx = NeovimBackedTestContext::new(cx).await;
4048        let initial_state = indoc! {r"abˇc
4049          def
4050          paragraph
4051          the second
4052          third and
4053          final"};
4054
4055        cx.set_shared_state(initial_state).await;
4056        cx.simulate_shared_keystrokes("shift-m").await;
4057        cx.shared_state().await.assert_eq(indoc! {r"abc
4058          def
4059          paˇragraph
4060          the second
4061          third and
4062          final"});
4063
4064        cx.set_shared_state(indoc! {r"
4065          1 2 3
4066          4 5 6
4067          7 8 ˇ9
4068          "})
4069            .await;
4070        cx.simulate_shared_keystrokes("shift-m").await;
4071        cx.shared_state().await.assert_eq(indoc! {"
4072          1 2 3
4073          4 5 ˇ6
4074          7 8 9
4075          "});
4076        cx.set_shared_state(indoc! {r"
4077          1 2 3
4078          4 5 6
4079          ˇ7 8 9
4080          "})
4081            .await;
4082        cx.simulate_shared_keystrokes("shift-m").await;
4083        cx.shared_state().await.assert_eq(indoc! {"
4084          1 2 3
4085          ˇ4 5 6
4086          7 8 9
4087          "});
4088        cx.set_shared_state(indoc! {r"
4089          ˇ1 2 3
4090          4 5 6
4091          7 8 9
4092          "})
4093            .await;
4094        cx.simulate_shared_keystrokes("shift-m").await;
4095        cx.shared_state().await.assert_eq(indoc! {"
4096          1 2 3
4097          ˇ4 5 6
4098          7 8 9
4099          "});
4100        cx.set_shared_state(indoc! {r"
4101          1 2 3
4102          ˇ4 5 6
4103          7 8 9
4104          "})
4105            .await;
4106        cx.simulate_shared_keystrokes("shift-m").await;
4107        cx.shared_state().await.assert_eq(indoc! {"
4108          1 2 3
4109          ˇ4 5 6
4110          7 8 9
4111          "});
4112        cx.set_shared_state(indoc! {r"
4113          1 2 3
4114          4 5 ˇ6
4115          7 8 9
4116          "})
4117            .await;
4118        cx.simulate_shared_keystrokes("shift-m").await;
4119        cx.shared_state().await.assert_eq(indoc! {"
4120          1 2 3
4121          4 5 ˇ6
4122          7 8 9
4123          "});
4124    }
4125
4126    #[gpui::test]
4127    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
4128        let mut cx = NeovimBackedTestContext::new(cx).await;
4129        let initial_state = indoc! {r"abc
4130          deˇf
4131          paragraph
4132          the second
4133          third and
4134          final"};
4135
4136        cx.set_shared_state(initial_state).await;
4137        cx.simulate_shared_keystrokes("shift-l").await;
4138        cx.shared_state().await.assert_eq(indoc! {r"abc
4139          def
4140          paragraph
4141          the second
4142          third and
4143          fiˇnal"});
4144
4145        cx.set_shared_state(indoc! {r"
4146          1 2 3
4147          4 5 ˇ6
4148          7 8 9
4149          "})
4150            .await;
4151        cx.simulate_shared_keystrokes("shift-l").await;
4152        cx.shared_state().await.assert_eq(indoc! {"
4153          1 2 3
4154          4 5 6
4155          7 8 9
4156          ˇ"});
4157
4158        cx.set_shared_state(indoc! {r"
4159          1 2 3
4160          ˇ4 5 6
4161          7 8 9
4162          "})
4163            .await;
4164        cx.simulate_shared_keystrokes("shift-l").await;
4165        cx.shared_state().await.assert_eq(indoc! {"
4166          1 2 3
4167          4 5 6
4168          7 8 9
4169          ˇ"});
4170
4171        cx.set_shared_state(indoc! {r"
4172          1 2 ˇ3
4173          4 5 6
4174          7 8 9
4175          "})
4176            .await;
4177        cx.simulate_shared_keystrokes("shift-l").await;
4178        cx.shared_state().await.assert_eq(indoc! {"
4179          1 2 3
4180          4 5 6
4181          7 8 9
4182          ˇ"});
4183
4184        cx.set_shared_state(indoc! {r"
4185          ˇ1 2 3
4186          4 5 6
4187          7 8 9
4188          "})
4189            .await;
4190        cx.simulate_shared_keystrokes("shift-l").await;
4191        cx.shared_state().await.assert_eq(indoc! {"
4192          1 2 3
4193          4 5 6
4194          7 8 9
4195          ˇ"});
4196
4197        cx.set_shared_state(indoc! {r"
4198          1 2 3
4199          4 5 ˇ6
4200          7 8 9
4201          "})
4202            .await;
4203        cx.simulate_shared_keystrokes("9 shift-l").await;
4204        cx.shared_state().await.assert_eq(indoc! {"
4205          1 2 ˇ3
4206          4 5 6
4207          7 8 9
4208          "});
4209    }
4210
4211    #[gpui::test]
4212    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
4213        let mut cx = NeovimBackedTestContext::new(cx).await;
4214        cx.set_shared_state(indoc! {r"
4215        456 5ˇ67 678
4216        "})
4217            .await;
4218        cx.simulate_shared_keystrokes("g e").await;
4219        cx.shared_state().await.assert_eq(indoc! {"
4220        45ˇ6 567 678
4221        "});
4222
4223        // Test times
4224        cx.set_shared_state(indoc! {r"
4225        123 234 345
4226        456 5ˇ67 678
4227        "})
4228            .await;
4229        cx.simulate_shared_keystrokes("4 g e").await;
4230        cx.shared_state().await.assert_eq(indoc! {"
4231        12ˇ3 234 345
4232        456 567 678
4233        "});
4234
4235        // With punctuation
4236        cx.set_shared_state(indoc! {r"
4237        123 234 345
4238        4;5.6 5ˇ67 678
4239        789 890 901
4240        "})
4241            .await;
4242        cx.simulate_shared_keystrokes("g e").await;
4243        cx.shared_state().await.assert_eq(indoc! {"
4244          123 234 345
4245          4;5.ˇ6 567 678
4246          789 890 901
4247        "});
4248
4249        // With punctuation and count
4250        cx.set_shared_state(indoc! {r"
4251        123 234 345
4252        4;5.6 5ˇ67 678
4253        789 890 901
4254        "})
4255            .await;
4256        cx.simulate_shared_keystrokes("5 g e").await;
4257        cx.shared_state().await.assert_eq(indoc! {"
4258          123 234 345
4259          ˇ4;5.6 567 678
4260          789 890 901
4261        "});
4262
4263        // newlines
4264        cx.set_shared_state(indoc! {r"
4265        123 234 345
4266
4267        78ˇ9 890 901
4268        "})
4269            .await;
4270        cx.simulate_shared_keystrokes("g e").await;
4271        cx.shared_state().await.assert_eq(indoc! {"
4272          123 234 345
4273          ˇ
4274          789 890 901
4275        "});
4276        cx.simulate_shared_keystrokes("g e").await;
4277        cx.shared_state().await.assert_eq(indoc! {"
4278          123 234 34ˇ5
4279
4280          789 890 901
4281        "});
4282
4283        // With punctuation
4284        cx.set_shared_state(indoc! {r"
4285        123 234 345
4286        4;5.ˇ6 567 678
4287        789 890 901
4288        "})
4289            .await;
4290        cx.simulate_shared_keystrokes("g shift-e").await;
4291        cx.shared_state().await.assert_eq(indoc! {"
4292          123 234 34ˇ5
4293          4;5.6 567 678
4294          789 890 901
4295        "});
4296
4297        // With multi byte char
4298        cx.set_shared_state(indoc! {r"
4299        bar ˇó
4300        "})
4301            .await;
4302        cx.simulate_shared_keystrokes("g e").await;
4303        cx.shared_state().await.assert_eq(indoc! {"
4304        baˇr ó
4305        "});
4306    }
4307
4308    #[gpui::test]
4309    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
4310        let mut cx = NeovimBackedTestContext::new(cx).await;
4311
4312        cx.set_shared_state(indoc! {"
4313            fn aˇ() {
4314              return
4315            }
4316        "})
4317            .await;
4318        cx.simulate_shared_keystrokes("v $ %").await;
4319        cx.shared_state().await.assert_eq(indoc! {"
4320            fn a«() {
4321              return
4322            }ˇ»
4323        "});
4324    }
4325
4326    #[gpui::test]
4327    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
4328        let mut cx = VimTestContext::new(cx, true).await;
4329
4330        cx.set_state(
4331            indoc! {"
4332                struct Foo {
4333                ˇ
4334                }
4335            "},
4336            Mode::Normal,
4337        );
4338
4339        cx.update_editor(|editor, _window, cx| {
4340            let range = editor.selections.newest_anchor().range();
4341            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
4342            let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
4343            editor.splice_inlays(&[], vec![inlay], cx);
4344        });
4345
4346        cx.simulate_keystrokes("j");
4347        cx.assert_state(
4348            indoc! {"
4349                struct Foo {
4350
4351                ˇ}
4352            "},
4353            Mode::Normal,
4354        );
4355    }
4356
4357    #[gpui::test]
4358    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
4359        let mut cx = VimTestContext::new(cx, true).await;
4360
4361        cx.set_state(
4362            indoc! {"
4363            ˇstruct Foo {
4364
4365            }
4366        "},
4367            Mode::Normal,
4368        );
4369        cx.update_editor(|editor, _window, cx| {
4370            let snapshot = editor.buffer().read(cx).snapshot(cx);
4371            let end_of_line =
4372                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
4373            let inlay_text = " hint";
4374            let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
4375            editor.splice_inlays(&[], vec![inlay], cx);
4376        });
4377        cx.simulate_keystrokes("$");
4378        cx.assert_state(
4379            indoc! {"
4380            struct Foo ˇ{
4381
4382            }
4383        "},
4384            Mode::Normal,
4385        );
4386    }
4387
4388    #[gpui::test]
4389    async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
4390        let mut cx = VimTestContext::new(cx, true).await;
4391
4392        // Test the exact scenario from issue #29134
4393        cx.set_state(
4394            indoc! {"
4395                fn main() {
4396                    let this_is_a_long_name = Vec::<u32>::new();
4397                    let new_oneˇ = this_is_a_long_name
4398                        .iter()
4399                        .map(|i| i + 1)
4400                        .map(|i| i * 2)
4401                        .collect::<Vec<_>>();
4402                }
4403            "},
4404            Mode::Normal,
4405        );
4406
4407        // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
4408        cx.update_editor(|editor, _window, cx| {
4409            let snapshot = editor.buffer().read(cx).snapshot(cx);
4410            // The empty line is at line 3 (0-indexed)
4411            let line_start = snapshot.anchor_after(Point::new(3, 0));
4412            let inlay_text = ": Vec<u32>";
4413            let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
4414            editor.splice_inlays(&[], vec![inlay], cx);
4415        });
4416
4417        // Enter visual mode
4418        cx.simulate_keystrokes("v");
4419        cx.assert_state(
4420            indoc! {"
4421                fn main() {
4422                    let this_is_a_long_name = Vec::<u32>::new();
4423                    let new_one« ˇ»= this_is_a_long_name
4424                        .iter()
4425                        .map(|i| i + 1)
4426                        .map(|i| i * 2)
4427                        .collect::<Vec<_>>();
4428                }
4429            "},
4430            Mode::Visual,
4431        );
4432
4433        // Move down - should go to the beginning of line 4, not skip to line 5
4434        cx.simulate_keystrokes("j");
4435        cx.assert_state(
4436            indoc! {"
4437                fn main() {
4438                    let this_is_a_long_name = Vec::<u32>::new();
4439                    let new_one« = this_is_a_long_name
4440                      ˇ»  .iter()
4441                        .map(|i| i + 1)
4442                        .map(|i| i * 2)
4443                        .collect::<Vec<_>>();
4444                }
4445            "},
4446            Mode::Visual,
4447        );
4448
4449        // Test with multiple movements
4450        cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
4451
4452        // Add type hint on the empty line
4453        cx.update_editor(|editor, _window, cx| {
4454            let snapshot = editor.buffer().read(cx).snapshot(cx);
4455            let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
4456            let inlay_text = ": i32";
4457            let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
4458            editor.splice_inlays(&[], vec![inlay], cx);
4459        });
4460
4461        // Enter visual mode and move down twice
4462        cx.simulate_keystrokes("v j j");
4463        cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
4464    }
4465
4466    #[gpui::test]
4467    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
4468        let mut cx = NeovimBackedTestContext::new(cx).await;
4469        // Normal mode
4470        cx.set_shared_state(indoc! {"
4471            The ˇquick brown
4472            fox jumps over
4473            the lazy dog
4474            The quick brown
4475            fox jumps over
4476            the lazy dog
4477            The quick brown
4478            fox jumps over
4479            the lazy dog"})
4480            .await;
4481        cx.simulate_shared_keystrokes("2 0 %").await;
4482        cx.shared_state().await.assert_eq(indoc! {"
4483            The quick brown
4484            fox ˇjumps over
4485            the lazy dog
4486            The quick brown
4487            fox jumps over
4488            the lazy dog
4489            The quick brown
4490            fox jumps over
4491            the lazy dog"});
4492
4493        cx.simulate_shared_keystrokes("2 5 %").await;
4494        cx.shared_state().await.assert_eq(indoc! {"
4495            The quick brown
4496            fox jumps over
4497            the ˇlazy dog
4498            The quick brown
4499            fox jumps over
4500            the lazy dog
4501            The quick brown
4502            fox jumps over
4503            the lazy dog"});
4504
4505        cx.simulate_shared_keystrokes("7 5 %").await;
4506        cx.shared_state().await.assert_eq(indoc! {"
4507            The quick brown
4508            fox jumps over
4509            the lazy dog
4510            The quick brown
4511            fox jumps over
4512            the lazy dog
4513            The ˇquick brown
4514            fox jumps over
4515            the lazy dog"});
4516
4517        // Visual mode
4518        cx.set_shared_state(indoc! {"
4519            The ˇquick brown
4520            fox jumps over
4521            the lazy dog
4522            The quick brown
4523            fox jumps over
4524            the lazy dog
4525            The quick brown
4526            fox jumps over
4527            the lazy dog"})
4528            .await;
4529        cx.simulate_shared_keystrokes("v 5 0 %").await;
4530        cx.shared_state().await.assert_eq(indoc! {"
4531            The «quick brown
4532            fox jumps over
4533            the lazy dog
4534            The quick brown
4535            fox jˇ»umps over
4536            the lazy dog
4537            The quick brown
4538            fox jumps over
4539            the lazy dog"});
4540
4541        cx.set_shared_state(indoc! {"
4542            The ˇquick brown
4543            fox jumps over
4544            the lazy dog
4545            The quick brown
4546            fox jumps over
4547            the lazy dog
4548            The quick brown
4549            fox jumps over
4550            the lazy dog"})
4551            .await;
4552        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4553        cx.shared_state().await.assert_eq(indoc! {"
4554            The «quick brown
4555            fox jumps over
4556            the lazy dog
4557            The quick brown
4558            fox jumps over
4559            the lazy dog
4560            The quick brown
4561            fox jumps over
4562            the lˇ»azy dog"});
4563    }
4564
4565    #[gpui::test]
4566    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4567        let mut cx = NeovimBackedTestContext::new(cx).await;
4568
4569        cx.set_shared_state("ˇπππππ").await;
4570        cx.simulate_shared_keystrokes("3 space").await;
4571        cx.shared_state().await.assert_eq("πππˇππ");
4572    }
4573
4574    #[gpui::test]
4575    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4576        let mut cx = NeovimBackedTestContext::new(cx).await;
4577
4578        cx.set_shared_state(indoc! {"
4579            ππππˇπ
4580            πanotherline"})
4581            .await;
4582        cx.simulate_shared_keystrokes("4 space").await;
4583        cx.shared_state().await.assert_eq(indoc! {"
4584            πππππ
4585            πanˇotherline"});
4586    }
4587
4588    #[gpui::test]
4589    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4590        let mut cx = NeovimBackedTestContext::new(cx).await;
4591
4592        cx.set_shared_state(indoc! {"
4593                        ππππ
4594                        πanˇotherline"})
4595            .await;
4596        cx.simulate_shared_keystrokes("4 backspace").await;
4597        cx.shared_state().await.assert_eq(indoc! {"
4598                        πππˇπ
4599                        πanotherline"});
4600    }
4601
4602    #[gpui::test]
4603    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4604        let mut cx = VimTestContext::new(cx, true).await;
4605        cx.set_state(
4606            indoc! {
4607                "func empty(a string) bool {
4608                     ˇif a == \"\" {
4609                         return true
4610                     }
4611                     return false
4612                }"
4613            },
4614            Mode::Normal,
4615        );
4616        cx.simulate_keystrokes("[ -");
4617        cx.assert_state(
4618            indoc! {
4619                "ˇfunc empty(a string) bool {
4620                     if a == \"\" {
4621                         return true
4622                     }
4623                     return false
4624                }"
4625            },
4626            Mode::Normal,
4627        );
4628        cx.simulate_keystrokes("] =");
4629        cx.assert_state(
4630            indoc! {
4631                "func empty(a string) bool {
4632                     if a == \"\" {
4633                         return true
4634                     }
4635                     return false
4636                ˇ}"
4637            },
4638            Mode::Normal,
4639        );
4640        cx.simulate_keystrokes("[ +");
4641        cx.assert_state(
4642            indoc! {
4643                "func empty(a string) bool {
4644                     if a == \"\" {
4645                         return true
4646                     }
4647                     ˇreturn false
4648                }"
4649            },
4650            Mode::Normal,
4651        );
4652        cx.simulate_keystrokes("2 [ =");
4653        cx.assert_state(
4654            indoc! {
4655                "func empty(a string) bool {
4656                     ˇif a == \"\" {
4657                         return true
4658                     }
4659                     return false
4660                }"
4661            },
4662            Mode::Normal,
4663        );
4664        cx.simulate_keystrokes("] +");
4665        cx.assert_state(
4666            indoc! {
4667                "func empty(a string) bool {
4668                     if a == \"\" {
4669                         ˇreturn true
4670                     }
4671                     return false
4672                }"
4673            },
4674            Mode::Normal,
4675        );
4676        cx.simulate_keystrokes("] -");
4677        cx.assert_state(
4678            indoc! {
4679                "func empty(a string) bool {
4680                     if a == \"\" {
4681                         return true
4682                     ˇ}
4683                     return false
4684                }"
4685            },
4686            Mode::Normal,
4687        );
4688    }
4689
4690    #[gpui::test]
4691    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4692        let mut cx = NeovimBackedTestContext::new(cx).await;
4693        cx.set_shared_state("abˇc").await;
4694        cx.simulate_shared_keystrokes("delete").await;
4695        cx.shared_state().await.assert_eq("aˇb");
4696    }
4697
4698    #[gpui::test]
4699    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4700        let mut cx = NeovimBackedTestContext::new(cx).await;
4701
4702        cx.set_shared_state(indoc! {"
4703             ˇthe quick brown fox
4704             jumped over the lazy dog"})
4705            .await;
4706        cx.simulate_shared_keystrokes("d v 0").await;
4707        cx.shared_state().await.assert_eq(indoc! {"
4708             ˇhe quick brown fox
4709             jumped over the lazy dog"});
4710        assert!(!cx.cx.forced_motion());
4711
4712        cx.set_shared_state(indoc! {"
4713            the quick bˇrown fox
4714            jumped over the lazy dog"})
4715            .await;
4716        cx.simulate_shared_keystrokes("d v 0").await;
4717        cx.shared_state().await.assert_eq(indoc! {"
4718            ˇown fox
4719            jumped over the lazy dog"});
4720        assert!(!cx.cx.forced_motion());
4721
4722        cx.set_shared_state(indoc! {"
4723            the quick brown foˇx
4724            jumped over the lazy dog"})
4725            .await;
4726        cx.simulate_shared_keystrokes("d v 0").await;
4727        cx.shared_state().await.assert_eq(indoc! {"
4728            ˇ
4729            jumped over the lazy dog"});
4730        assert!(!cx.cx.forced_motion());
4731    }
4732
4733    #[gpui::test]
4734    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4735        let mut cx = NeovimBackedTestContext::new(cx).await;
4736
4737        cx.set_shared_state(indoc! {"
4738             ˇthe quick brown fox
4739             jumped over the lazy dog"})
4740            .await;
4741        cx.simulate_shared_keystrokes("d v g shift-m").await;
4742        cx.shared_state().await.assert_eq(indoc! {"
4743             ˇbrown fox
4744             jumped over the lazy dog"});
4745        assert!(!cx.cx.forced_motion());
4746
4747        cx.set_shared_state(indoc! {"
4748            the quick bˇrown fox
4749            jumped over the lazy dog"})
4750            .await;
4751        cx.simulate_shared_keystrokes("d v g shift-m").await;
4752        cx.shared_state().await.assert_eq(indoc! {"
4753            the quickˇown fox
4754            jumped over the lazy dog"});
4755        assert!(!cx.cx.forced_motion());
4756
4757        cx.set_shared_state(indoc! {"
4758            the quick brown foˇx
4759            jumped over the lazy dog"})
4760            .await;
4761        cx.simulate_shared_keystrokes("d v g shift-m").await;
4762        cx.shared_state().await.assert_eq(indoc! {"
4763            the quicˇk
4764            jumped over the lazy dog"});
4765        assert!(!cx.cx.forced_motion());
4766
4767        cx.set_shared_state(indoc! {"
4768            ˇthe quick brown fox
4769            jumped over the lazy dog"})
4770            .await;
4771        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4772        cx.shared_state().await.assert_eq(indoc! {"
4773            ˇ fox
4774            jumped over the lazy dog"});
4775        assert!(!cx.cx.forced_motion());
4776
4777        cx.set_shared_state(indoc! {"
4778            ˇthe quick brown fox
4779            jumped over the lazy dog"})
4780            .await;
4781        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4782        cx.shared_state().await.assert_eq(indoc! {"
4783            ˇuick brown fox
4784            jumped over the lazy dog"});
4785        assert!(!cx.cx.forced_motion());
4786    }
4787
4788    #[gpui::test]
4789    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4790        let mut cx = NeovimBackedTestContext::new(cx).await;
4791
4792        cx.set_shared_state(indoc! {"
4793             the quick brown foˇx
4794             jumped over the lazy dog"})
4795            .await;
4796        cx.simulate_shared_keystrokes("d v $").await;
4797        cx.shared_state().await.assert_eq(indoc! {"
4798             the quick brown foˇx
4799             jumped over the lazy dog"});
4800        assert!(!cx.cx.forced_motion());
4801
4802        cx.set_shared_state(indoc! {"
4803             ˇthe quick brown fox
4804             jumped over the lazy dog"})
4805            .await;
4806        cx.simulate_shared_keystrokes("d v $").await;
4807        cx.shared_state().await.assert_eq(indoc! {"
4808             ˇx
4809             jumped over the lazy dog"});
4810        assert!(!cx.cx.forced_motion());
4811    }
4812
4813    #[gpui::test]
4814    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4815        let mut cx = NeovimBackedTestContext::new(cx).await;
4816
4817        cx.set_shared_state(indoc! {"
4818               ˇthe quick brown fox
4819               jumped over the lazy dog"})
4820            .await;
4821        cx.simulate_shared_keystrokes("y v j p").await;
4822        cx.shared_state().await.assert_eq(indoc! {"
4823               the quick brown fox
4824               ˇthe quick brown fox
4825               jumped over the lazy dog"});
4826        assert!(!cx.cx.forced_motion());
4827
4828        cx.set_shared_state(indoc! {"
4829              the quick bˇrown fox
4830              jumped over the lazy dog"})
4831            .await;
4832        cx.simulate_shared_keystrokes("y v j p").await;
4833        cx.shared_state().await.assert_eq(indoc! {"
4834              the quick brˇrown fox
4835              jumped overown fox
4836              jumped over the lazy dog"});
4837        assert!(!cx.cx.forced_motion());
4838
4839        cx.set_shared_state(indoc! {"
4840             the quick brown foˇx
4841             jumped over the lazy dog"})
4842            .await;
4843        cx.simulate_shared_keystrokes("y v j p").await;
4844        cx.shared_state().await.assert_eq(indoc! {"
4845             the quick brown foxˇx
4846             jumped over the la
4847             jumped over the lazy dog"});
4848        assert!(!cx.cx.forced_motion());
4849
4850        cx.set_shared_state(indoc! {"
4851             the quick brown fox
4852             jˇumped over the lazy dog"})
4853            .await;
4854        cx.simulate_shared_keystrokes("y v k p").await;
4855        cx.shared_state().await.assert_eq(indoc! {"
4856            thˇhe quick brown fox
4857            je quick brown fox
4858            jumped over the lazy dog"});
4859        assert!(!cx.cx.forced_motion());
4860    }
4861
4862    #[gpui::test]
4863    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4864        let mut cx = NeovimBackedTestContext::new(cx).await;
4865
4866        cx.set_shared_state(indoc! {"
4867              ˇthe quick brown fox
4868              jumped over the lazy dog"})
4869            .await;
4870        cx.simulate_shared_keystrokes("d v e").await;
4871        cx.shared_state().await.assert_eq(indoc! {"
4872              ˇe quick brown fox
4873              jumped over the lazy dog"});
4874        assert!(!cx.cx.forced_motion());
4875
4876        cx.set_shared_state(indoc! {"
4877              the quick bˇrown fox
4878              jumped over the lazy dog"})
4879            .await;
4880        cx.simulate_shared_keystrokes("d v e").await;
4881        cx.shared_state().await.assert_eq(indoc! {"
4882              the quick bˇn fox
4883              jumped over the lazy dog"});
4884        assert!(!cx.cx.forced_motion());
4885
4886        cx.set_shared_state(indoc! {"
4887             the quick brown foˇx
4888             jumped over the lazy dog"})
4889            .await;
4890        cx.simulate_shared_keystrokes("d v e").await;
4891        cx.shared_state().await.assert_eq(indoc! {"
4892        the quick brown foˇd over the lazy dog"});
4893        assert!(!cx.cx.forced_motion());
4894    }
4895
4896    #[gpui::test]
4897    async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
4898        let mut cx = VimTestContext::new(cx, true).await;
4899
4900        // Setup custom keybindings for subword motions so we can use the bindings
4901        // in `simulate_keystrokes`.
4902        cx.update(|_window, cx| {
4903            cx.bind_keys([KeyBinding::new(
4904                "w",
4905                super::NextSubwordStart {
4906                    ignore_punctuation: false,
4907                },
4908                None,
4909            )]);
4910        });
4911
4912        cx.set_state("ˇfoo.bar", Mode::Normal);
4913        cx.simulate_keystrokes("w");
4914        cx.assert_state("foo.ˇbar", Mode::Normal);
4915
4916        cx.set_state("ˇfoo(bar)", Mode::Normal);
4917        cx.simulate_keystrokes("w");
4918        cx.assert_state("fooˇ(bar)", Mode::Normal);
4919        cx.simulate_keystrokes("w");
4920        cx.assert_state("foo(ˇbar)", Mode::Normal);
4921        cx.simulate_keystrokes("w");
4922        cx.assert_state("foo(barˇ)", Mode::Normal);
4923
4924        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
4925        cx.simulate_keystrokes("w");
4926        cx.assert_state("foo_ˇbar_baz", Mode::Normal);
4927        cx.simulate_keystrokes("w");
4928        cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
4929
4930        cx.set_state("ˇfooBarBaz", Mode::Normal);
4931        cx.simulate_keystrokes("w");
4932        cx.assert_state("fooˇBarBaz", Mode::Normal);
4933        cx.simulate_keystrokes("w");
4934        cx.assert_state("fooBarˇBaz", Mode::Normal);
4935
4936        cx.set_state("ˇfoo;bar", Mode::Normal);
4937        cx.simulate_keystrokes("w");
4938        cx.assert_state("foo;ˇbar", Mode::Normal);
4939
4940        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::Normal);
4941        cx.simulate_keystrokes("w");
4942        cx.assert_state("<?ˇphp\n\n$someVariable = 2;", Mode::Normal);
4943        cx.simulate_keystrokes("w");
4944        cx.assert_state("<?php\nˇ\n$someVariable = 2;", Mode::Normal);
4945        cx.simulate_keystrokes("w");
4946        cx.assert_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$someˇVariable = 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    }
4956
4957    #[gpui::test]
4958    async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
4959        let mut cx = VimTestContext::new(cx, true).await;
4960
4961        // Setup custom keybindings for subword motions so we can use the bindings
4962        // in `simulate_keystrokes`.
4963        cx.update(|_window, cx| {
4964            cx.bind_keys([KeyBinding::new(
4965                "e",
4966                super::NextSubwordEnd {
4967                    ignore_punctuation: false,
4968                },
4969                None,
4970            )]);
4971        });
4972
4973        cx.set_state("ˇfoo.bar", Mode::Normal);
4974        cx.simulate_keystrokes("e");
4975        cx.assert_state("foˇo.bar", Mode::Normal);
4976        cx.simulate_keystrokes("e");
4977        cx.assert_state("fooˇ.bar", Mode::Normal);
4978        cx.simulate_keystrokes("e");
4979        cx.assert_state("foo.baˇr", Mode::Normal);
4980
4981        cx.set_state("ˇfoo(bar)", Mode::Normal);
4982        cx.simulate_keystrokes("e");
4983        cx.assert_state("foˇo(bar)", Mode::Normal);
4984        cx.simulate_keystrokes("e");
4985        cx.assert_state("fooˇ(bar)", Mode::Normal);
4986        cx.simulate_keystrokes("e");
4987        cx.assert_state("foo(baˇr)", Mode::Normal);
4988        cx.simulate_keystrokes("e");
4989        cx.assert_state("foo(barˇ)", Mode::Normal);
4990
4991        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
4992        cx.simulate_keystrokes("e");
4993        cx.assert_state("foˇo_bar_baz", Mode::Normal);
4994        cx.simulate_keystrokes("e");
4995        cx.assert_state("foo_baˇr_baz", Mode::Normal);
4996        cx.simulate_keystrokes("e");
4997        cx.assert_state("foo_bar_baˇz", Mode::Normal);
4998
4999        cx.set_state("ˇfooBarBaz", Mode::Normal);
5000        cx.simulate_keystrokes("e");
5001        cx.set_state("foˇoBarBaz", Mode::Normal);
5002        cx.simulate_keystrokes("e");
5003        cx.set_state("fooBaˇrBaz", Mode::Normal);
5004        cx.simulate_keystrokes("e");
5005        cx.set_state("fooBarBaˇz", Mode::Normal);
5006
5007        cx.set_state("ˇfoo;bar", Mode::Normal);
5008        cx.simulate_keystrokes("e");
5009        cx.set_state("foˇo;bar", Mode::Normal);
5010        cx.simulate_keystrokes("e");
5011        cx.set_state("fooˇ;bar", Mode::Normal);
5012        cx.simulate_keystrokes("e");
5013        cx.set_state("foo;baˇr", Mode::Normal);
5014    }
5015
5016    #[gpui::test]
5017    async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
5018        let mut cx = VimTestContext::new(cx, true).await;
5019
5020        // Setup custom keybindings for subword motions so we can use the bindings
5021        // in `simulate_keystrokes`.
5022        cx.update(|_window, cx| {
5023            cx.bind_keys([KeyBinding::new(
5024                "b",
5025                super::PreviousSubwordStart {
5026                    ignore_punctuation: false,
5027                },
5028                None,
5029            )]);
5030        });
5031
5032        cx.set_state("foo.barˇ", Mode::Normal);
5033        cx.simulate_keystrokes("b");
5034        cx.assert_state("foo.ˇbar", Mode::Normal);
5035        cx.simulate_keystrokes("b");
5036        cx.assert_state("fooˇ.bar", Mode::Normal);
5037        cx.simulate_keystrokes("b");
5038        cx.assert_state("ˇfoo.bar", Mode::Normal);
5039
5040        cx.set_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        cx.simulate_keystrokes("b");
5046        cx.assert_state("ˇfoo(bar)", Mode::Normal);
5047
5048        cx.set_state("foo_bar_bazˇ", Mode::Normal);
5049        cx.simulate_keystrokes("b");
5050        cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
5051        cx.simulate_keystrokes("b");
5052        cx.assert_state("foo_ˇbar_baz", Mode::Normal);
5053        cx.simulate_keystrokes("b");
5054        cx.assert_state("ˇfoo_bar_baz", Mode::Normal);
5055
5056        cx.set_state("fooBarBazˇ", Mode::Normal);
5057        cx.simulate_keystrokes("b");
5058        cx.assert_state("fooBarˇBaz", Mode::Normal);
5059        cx.simulate_keystrokes("b");
5060        cx.assert_state("fooˇBarBaz", Mode::Normal);
5061        cx.simulate_keystrokes("b");
5062        cx.assert_state("ˇfooBarBaz", Mode::Normal);
5063
5064        cx.set_state("foo;barˇ", Mode::Normal);
5065        cx.simulate_keystrokes("b");
5066        cx.assert_state("foo;ˇbar", Mode::Normal);
5067        cx.simulate_keystrokes("b");
5068        cx.assert_state("ˇfoo;bar", Mode::Normal);
5069
5070        cx.set_state("<?php\n\n$someVariable = 2ˇ;", Mode::Normal);
5071        cx.simulate_keystrokes("b");
5072        cx.assert_state("<?php\n\n$someVariable = ˇ2;", Mode::Normal);
5073        cx.simulate_keystrokes("b");
5074        cx.assert_state("<?php\n\n$someVariable ˇ= 2;", Mode::Normal);
5075        cx.simulate_keystrokes("b");
5076        cx.assert_state("<?php\n\n$someˇVariable = 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$someVariable = 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    }
5088
5089    #[gpui::test]
5090    async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
5091        let mut cx = VimTestContext::new(cx, true).await;
5092
5093        // Setup custom keybindings for subword motions so we can use the bindings
5094        // in `simulate_keystrokes`.
5095        cx.update(|_window, cx| {
5096            cx.bind_keys([KeyBinding::new(
5097                "g e",
5098                super::PreviousSubwordEnd {
5099                    ignore_punctuation: false,
5100                },
5101                None,
5102            )]);
5103        });
5104
5105        cx.set_state("foo.baˇr", Mode::Normal);
5106        cx.simulate_keystrokes("g e");
5107        cx.assert_state("fooˇ.bar", Mode::Normal);
5108        cx.simulate_keystrokes("g e");
5109        cx.assert_state("foˇo.bar", Mode::Normal);
5110
5111        cx.set_state("foo(barˇ)", Mode::Normal);
5112        cx.simulate_keystrokes("g e");
5113        cx.assert_state("foo(baˇr)", Mode::Normal);
5114        cx.simulate_keystrokes("g e");
5115        cx.assert_state("fooˇ(bar)", Mode::Normal);
5116        cx.simulate_keystrokes("g e");
5117        cx.assert_state("foˇo(bar)", Mode::Normal);
5118
5119        cx.set_state("foo_bar_baˇz", Mode::Normal);
5120        cx.simulate_keystrokes("g e");
5121        cx.assert_state("foo_baˇr_baz", Mode::Normal);
5122        cx.simulate_keystrokes("g e");
5123        cx.assert_state("foˇo_bar_baz", Mode::Normal);
5124
5125        cx.set_state("fooBarBaˇz", Mode::Normal);
5126        cx.simulate_keystrokes("g e");
5127        cx.assert_state("fooBaˇrBaz", Mode::Normal);
5128        cx.simulate_keystrokes("g e");
5129        cx.assert_state("foˇoBarBaz", Mode::Normal);
5130
5131        cx.set_state("foo;baˇr", Mode::Normal);
5132        cx.simulate_keystrokes("g e");
5133        cx.assert_state("fooˇ;bar", Mode::Normal);
5134        cx.simulate_keystrokes("g e");
5135        cx.assert_state("foˇo;bar", Mode::Normal);
5136    }
5137
5138    #[gpui::test]
5139    async fn test_method_motion_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
5140        let mut cx = VimTestContext::new(cx, true).await;
5141
5142        let diff_base = indoc! {r#"
5143            fn first() {
5144                println!("first");
5145                println!("removed line");
5146            }
5147
5148            fn second() {
5149                println!("second");
5150            }
5151
5152            fn third() {
5153                println!("third");
5154            }
5155        "#};
5156
5157        let current_text = indoc! {r#"
5158            fn first() {
5159                println!("first");
5160            }
5161
5162            fn second() {
5163                println!("second");
5164            }
5165
5166            fn third() {
5167                println!("third");
5168            }
5169        "#};
5170
5171        cx.set_state(&format!("ˇ{}", current_text), Mode::Normal);
5172        cx.set_head_text(diff_base);
5173        cx.update_editor(|editor, window, cx| {
5174            editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
5175        });
5176
5177        // When diff hunks are expanded, the deleted line from the diff base
5178        // appears in the MultiBuffer. The method motion should correctly
5179        // navigate to the second function even with this extra content.
5180        cx.simulate_keystrokes("] m");
5181        cx.assert_editor_state(indoc! {r#"
5182            fn first() {
5183                println!("first");
5184                println!("removed line");
5185            }
5186
5187            ˇfn second() {
5188                println!("second");
5189            }
5190
5191            fn third() {
5192                println!("third");
5193            }
5194        "#});
5195
5196        cx.simulate_keystrokes("] m");
5197        cx.assert_editor_state(indoc! {r#"
5198            fn first() {
5199                println!("first");
5200                println!("removed line");
5201            }
5202
5203            fn second() {
5204                println!("second");
5205            }
5206
5207            ˇfn third() {
5208                println!("third");
5209            }
5210        "#});
5211
5212        cx.simulate_keystrokes("[ m");
5213        cx.assert_editor_state(indoc! {r#"
5214            fn first() {
5215                println!("first");
5216                println!("removed line");
5217            }
5218
5219            ˇfn second() {
5220                println!("second");
5221            }
5222
5223            fn third() {
5224                println!("third");
5225            }
5226        "#});
5227
5228        cx.simulate_keystrokes("[ m");
5229        cx.assert_editor_state(indoc! {r#"
5230            ˇfn first() {
5231                println!("first");
5232                println!("removed line");
5233            }
5234
5235            fn second() {
5236                println!("second");
5237            }
5238
5239            fn third() {
5240                println!("third");
5241            }
5242        "#});
5243    }
5244
5245    #[gpui::test]
5246    async fn test_comment_motion_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
5247        let mut cx = VimTestContext::new(cx, true).await;
5248
5249        let diff_base = indoc! {r#"
5250            // first comment
5251            fn first() {
5252                // removed comment
5253                println!("first");
5254            }
5255
5256            // second comment
5257            fn second() { println!("second"); }
5258        "#};
5259
5260        let current_text = indoc! {r#"
5261            // first comment
5262            fn first() {
5263                println!("first");
5264            }
5265
5266            // second comment
5267            fn second() { println!("second"); }
5268        "#};
5269
5270        cx.set_state(&format!("ˇ{}", current_text), Mode::Normal);
5271        cx.set_head_text(diff_base);
5272        cx.update_editor(|editor, window, cx| {
5273            editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
5274        });
5275
5276        // The first `] /` (vim::NextComment) should go to the end of the first
5277        // comment.
5278        cx.simulate_keystrokes("] /");
5279        cx.assert_editor_state(indoc! {r#"
5280            // first commenˇt
5281            fn first() {
5282                // removed comment
5283                println!("first");
5284            }
5285
5286            // second comment
5287            fn second() { println!("second"); }
5288        "#});
5289
5290        // The next `] /` (vim::NextComment) should go to the end of the second
5291        // comment, skipping over the removed comment, since it's not in the
5292        // actual buffer.
5293        cx.simulate_keystrokes("] /");
5294        cx.assert_editor_state(indoc! {r#"
5295            // first comment
5296            fn first() {
5297                // removed comment
5298                println!("first");
5299            }
5300
5301            // second commenˇt
5302            fn second() { println!("second"); }
5303        "#});
5304
5305        // Going back to previous comment with `[ /` (vim::PreviousComment)
5306        // should go back to the start of the second comment.
5307        cx.simulate_keystrokes("[ /");
5308        cx.assert_editor_state(indoc! {r#"
5309            // first comment
5310            fn first() {
5311                // removed comment
5312                println!("first");
5313            }
5314
5315            ˇ// second comment
5316            fn second() { println!("second"); }
5317        "#});
5318
5319        // Going back again with `[ /` (vim::PreviousComment) should finally put
5320        // the cursor at the start of the first comment.
5321        cx.simulate_keystrokes("[ /");
5322        cx.assert_editor_state(indoc! {r#"
5323            ˇ// first comment
5324            fn first() {
5325                // removed comment
5326                println!("first");
5327            }
5328
5329            // second comment
5330            fn second() { println!("second"); }
5331        "#});
5332    }
5333}