motion.rs

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