motion.rs

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