motion.rs

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