motion.rs

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