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, TextObject, TreeSitterOptions};
  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    if bracket_info.is_none() {
2455        return find_matching_c_preprocessor_directive(map, line_range);
2456    }
2457
2458    let (open, close, is_opening) = bracket_info?.0;
2459    let bracket_offset = bracket_info?.1;
2460
2461    let mut depth = 0i32;
2462    if is_opening {
2463        for (ch, char_offset) in map.buffer_chars_at(bracket_offset) {
2464            if ch == open {
2465                depth += 1;
2466            } else if ch == close {
2467                depth -= 1;
2468                if depth == 0 {
2469                    return Some(char_offset);
2470                }
2471            }
2472        }
2473    } else {
2474        for (ch, char_offset) in map.reverse_buffer_chars_at(bracket_offset + close.len_utf8()) {
2475            if ch == close {
2476                depth += 1;
2477            } else if ch == open {
2478                depth -= 1;
2479                if depth == 0 {
2480                    return Some(char_offset);
2481                }
2482            }
2483        }
2484    }
2485
2486    None
2487}
2488
2489fn find_matching_c_preprocessor_directive(
2490    map: &DisplaySnapshot,
2491    line_range: Range<MultiBufferOffset>,
2492) -> Option<MultiBufferOffset> {
2493    let line_start = map
2494        .buffer_chars_at(line_range.start)
2495        .skip_while(|(c, _)| *c == ' ' || *c == '\t')
2496        .map(|(c, _)| c)
2497        .take(6)
2498        .collect::<String>();
2499
2500    if line_start.starts_with("#if")
2501        || line_start.starts_with("#else")
2502        || line_start.starts_with("#elif")
2503    {
2504        let mut depth = 0i32;
2505        for (ch, char_offset) in map.buffer_chars_at(line_range.end) {
2506            if ch != '\n' {
2507                continue;
2508            }
2509            let mut line_offset = char_offset + '\n'.len_utf8();
2510
2511            // Skip leading whitespace
2512            map.buffer_chars_at(line_offset)
2513                .take_while(|(c, _)| *c == ' ' || *c == '\t')
2514                .for_each(|(_, _)| line_offset += 1);
2515
2516            // Check what directive starts the next line
2517            let next_line_start = map
2518                .buffer_chars_at(line_offset)
2519                .map(|(c, _)| c)
2520                .take(6)
2521                .collect::<String>();
2522
2523            if next_line_start.starts_with("#if") {
2524                depth += 1;
2525            } else if next_line_start.starts_with("#endif") {
2526                if depth > 0 {
2527                    depth -= 1;
2528                } else {
2529                    return Some(line_offset);
2530                }
2531            } else if next_line_start.starts_with("#else") || next_line_start.starts_with("#elif") {
2532                if depth == 0 {
2533                    return Some(line_offset);
2534                }
2535            }
2536        }
2537    } else if line_start.starts_with("#endif") {
2538        let mut depth = 0i32;
2539        for (ch, char_offset) in
2540            map.reverse_buffer_chars_at(line_range.start.saturating_sub_usize(1))
2541        {
2542            let mut line_offset = if char_offset == MultiBufferOffset(0) {
2543                MultiBufferOffset(0)
2544            } else if ch != '\n' {
2545                continue;
2546            } else {
2547                char_offset + '\n'.len_utf8()
2548            };
2549
2550            // Skip leading whitespace
2551            map.buffer_chars_at(line_offset)
2552                .take_while(|(c, _)| *c == ' ' || *c == '\t')
2553                .for_each(|(_, _)| line_offset += 1);
2554
2555            // Check what directive starts this line
2556            let line_start = map
2557                .buffer_chars_at(line_offset)
2558                .skip_while(|(c, _)| *c == ' ' || *c == '\t')
2559                .map(|(c, _)| c)
2560                .take(6)
2561                .collect::<String>();
2562
2563            if line_start.starts_with("\n\n") {
2564                // empty line
2565                continue;
2566            } else if line_start.starts_with("#endif") {
2567                depth += 1;
2568            } else if line_start.starts_with("#if") {
2569                if depth > 0 {
2570                    depth -= 1;
2571                } else {
2572                    return Some(line_offset);
2573                }
2574            }
2575        }
2576    }
2577    None
2578}
2579
2580fn comment_delimiter_pair(
2581    map: &DisplaySnapshot,
2582    offset: MultiBufferOffset,
2583) -> Option<(Range<MultiBufferOffset>, Range<MultiBufferOffset>)> {
2584    let snapshot = map.buffer_snapshot();
2585    snapshot
2586        .text_object_ranges(offset..offset, TreeSitterOptions::default())
2587        .find_map(|(range, obj)| {
2588            if !matches!(obj, TextObject::InsideComment | TextObject::AroundComment)
2589                || !range.contains(&offset)
2590            {
2591                return None;
2592            }
2593
2594            let mut chars = snapshot.chars_at(range.start);
2595            if (Some('/'), Some('*')) != (chars.next(), chars.next()) {
2596                return None;
2597            }
2598
2599            let open_range = range.start..range.start + 2usize;
2600            let close_range = range.end - 2..range.end;
2601            Some((open_range, close_range))
2602        })
2603}
2604
2605fn matching(
2606    map: &DisplaySnapshot,
2607    display_point: DisplayPoint,
2608    match_quotes: bool,
2609) -> DisplayPoint {
2610    if !map.is_singleton() {
2611        return display_point;
2612    }
2613    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2614    let display_point = map.clip_at_line_end(display_point);
2615    let point = display_point.to_point(map);
2616    let offset = point.to_offset(&map.buffer_snapshot());
2617    let snapshot = map.buffer_snapshot();
2618
2619    // Ensure the range is contained by the current line.
2620    let mut line_end = map.next_line_boundary(point).0;
2621    if line_end == point {
2622        line_end = map.max_point().to_point(map);
2623    }
2624
2625    let is_quote_char = |ch: char| matches!(ch, '\'' | '"' | '`');
2626
2627    let make_range_filter = |require_on_bracket: bool| {
2628        move |buffer: &language::BufferSnapshot,
2629              opening_range: Range<BufferOffset>,
2630              closing_range: Range<BufferOffset>| {
2631            if !match_quotes
2632                && buffer
2633                    .chars_at(opening_range.start)
2634                    .next()
2635                    .is_some_and(is_quote_char)
2636            {
2637                return false;
2638            }
2639
2640            if require_on_bracket {
2641                // Attempt to find the smallest enclosing bracket range that also contains
2642                // the offset, which only happens if the cursor is currently in a bracket.
2643                opening_range.contains(&BufferOffset(offset.0))
2644                    || closing_range.contains(&BufferOffset(offset.0))
2645            } else {
2646                true
2647            }
2648        }
2649    };
2650
2651    let bracket_ranges = snapshot
2652        .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(true)))
2653        .or_else(|| {
2654            snapshot
2655                .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(false)))
2656        });
2657
2658    if let Some((opening_range, closing_range)) = bracket_ranges {
2659        let mut chars = map.buffer_snapshot().chars_at(offset);
2660        match chars.next() {
2661            Some('/') => {}
2662            _ => {
2663                if opening_range.contains(&offset) {
2664                    return closing_range.start.to_display_point(map);
2665                } else if closing_range.contains(&offset) {
2666                    return opening_range.start.to_display_point(map);
2667                }
2668            }
2669        }
2670    }
2671
2672    let line_range = map.prev_line_boundary(point).0..line_end;
2673    let visible_line_range =
2674        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2675    let line_range = line_range.start.to_offset(&map.buffer_snapshot())
2676        ..line_range.end.to_offset(&map.buffer_snapshot());
2677    let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range);
2678    if let Some(ranges) = ranges {
2679        let mut closest_pair_destination = None;
2680        let mut closest_distance = usize::MAX;
2681
2682        for (open_range, close_range) in ranges {
2683            if !match_quotes
2684                && map
2685                    .buffer_snapshot()
2686                    .chars_at(open_range.start)
2687                    .next()
2688                    .is_some_and(is_quote_char)
2689            {
2690                continue;
2691            }
2692
2693            if map.buffer_snapshot().chars_at(open_range.start).next() == Some('<') {
2694                if offset > open_range.start && offset < close_range.start {
2695                    let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2696                    if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2697                        return display_point;
2698                    }
2699                    if let Some(tag) = matching_tag(map, display_point) {
2700                        return tag;
2701                    }
2702                } else if close_range.contains(&offset) {
2703                    return open_range.start.to_display_point(map);
2704                } else if open_range.contains(&offset) {
2705                    return (close_range.end - 1).to_display_point(map);
2706                }
2707            }
2708
2709            if (open_range.contains(&offset) || open_range.start >= offset)
2710                && line_range.contains(&open_range.start)
2711            {
2712                let distance = open_range.start.saturating_sub(offset);
2713                if distance < closest_distance {
2714                    closest_pair_destination = Some(close_range.start);
2715                    closest_distance = distance;
2716                }
2717            }
2718
2719            if (close_range.contains(&offset) || close_range.start >= offset)
2720                && line_range.contains(&close_range.start)
2721            {
2722                let distance = close_range.start.saturating_sub(offset);
2723                if distance < closest_distance {
2724                    closest_pair_destination = Some(open_range.start);
2725                    closest_distance = distance;
2726                }
2727            }
2728
2729            continue;
2730        }
2731
2732        if let Some((open_range, close_range)) = comment_delimiter_pair(map, offset) {
2733            if open_range.contains(&offset) {
2734                return close_range.start.to_display_point(map);
2735            }
2736
2737            if close_range.contains(&offset) {
2738                return open_range.start.to_display_point(map);
2739            }
2740
2741            let open_candidate = (open_range.start >= offset
2742                && line_range.contains(&open_range.start))
2743            .then_some((open_range.start.saturating_sub(offset), close_range.start));
2744
2745            let close_candidate = (close_range.start >= offset
2746                && line_range.contains(&close_range.start))
2747            .then_some((close_range.start.saturating_sub(offset), open_range.start));
2748
2749            if let Some((_, destination)) = [open_candidate, close_candidate]
2750                .into_iter()
2751                .flatten()
2752                .min_by_key(|(distance, _)| *distance)
2753            {
2754                return destination.to_display_point(map);
2755            }
2756        }
2757
2758        closest_pair_destination
2759            .map(|destination| destination.to_display_point(map))
2760            .unwrap_or_else(|| {
2761                find_matching_bracket_text_based(map, offset, line_range.clone())
2762                    .map(|o| o.to_display_point(map))
2763                    .unwrap_or(display_point)
2764            })
2765    } else {
2766        find_matching_bracket_text_based(map, offset, line_range)
2767            .map(|o| o.to_display_point(map))
2768            .unwrap_or(display_point)
2769    }
2770}
2771
2772// Go to {count} percentage in the file, on the first
2773// non-blank in the line linewise.  To compute the new
2774// line number this formula is used:
2775// ({count} * number-of-lines + 99) / 100
2776//
2777// https://neovim.io/doc/user/motion.html#N%25
2778fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2779    let total_lines = map.buffer_snapshot().max_point().row + 1;
2780    let target_line = (count * total_lines as usize).div_ceil(100);
2781    let target_point = DisplayPoint::new(
2782        DisplayRow(target_line.saturating_sub(1) as u32),
2783        point.column(),
2784    );
2785    map.clip_point(target_point, Bias::Left)
2786}
2787
2788fn unmatched_forward(
2789    map: &DisplaySnapshot,
2790    mut display_point: DisplayPoint,
2791    char: char,
2792    times: usize,
2793) -> DisplayPoint {
2794    for _ in 0..times {
2795        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2796        let point = display_point.to_point(map);
2797        let offset = point.to_offset(&map.buffer_snapshot());
2798
2799        let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2800        let Some(ranges) = ranges else { break };
2801        let mut closest_closing_destination = None;
2802        let mut closest_distance = usize::MAX;
2803
2804        for (_, close_range) in ranges {
2805            if close_range.start > offset {
2806                let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2807                if Some(char) == chars.next() {
2808                    let distance = close_range.start - offset;
2809                    if distance < closest_distance {
2810                        closest_closing_destination = Some(close_range.start);
2811                        closest_distance = distance;
2812                        continue;
2813                    }
2814                }
2815            }
2816        }
2817
2818        let new_point = closest_closing_destination
2819            .map(|destination| destination.to_display_point(map))
2820            .unwrap_or(display_point);
2821        if new_point == display_point {
2822            break;
2823        }
2824        display_point = new_point;
2825    }
2826    display_point
2827}
2828
2829fn unmatched_backward(
2830    map: &DisplaySnapshot,
2831    mut display_point: DisplayPoint,
2832    char: char,
2833    times: usize,
2834) -> DisplayPoint {
2835    for _ in 0..times {
2836        // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2837        let point = display_point.to_point(map);
2838        let offset = point.to_offset(&map.buffer_snapshot());
2839
2840        let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2841        let Some(ranges) = ranges else {
2842            break;
2843        };
2844
2845        let mut closest_starting_destination = None;
2846        let mut closest_distance = usize::MAX;
2847
2848        for (start_range, _) in ranges {
2849            if start_range.start < offset {
2850                let mut chars = map.buffer_snapshot().chars_at(start_range.start);
2851                if Some(char) == chars.next() {
2852                    let distance = offset - start_range.start;
2853                    if distance < closest_distance {
2854                        closest_starting_destination = Some(start_range.start);
2855                        closest_distance = distance;
2856                        continue;
2857                    }
2858                }
2859            }
2860        }
2861
2862        let new_point = closest_starting_destination
2863            .map(|destination| destination.to_display_point(map))
2864            .unwrap_or(display_point);
2865        if new_point == display_point {
2866            break;
2867        } else {
2868            display_point = new_point;
2869        }
2870    }
2871    display_point
2872}
2873
2874fn find_forward(
2875    map: &DisplaySnapshot,
2876    from: DisplayPoint,
2877    before: bool,
2878    target: char,
2879    times: usize,
2880    mode: FindRange,
2881    smartcase: bool,
2882) -> Option<DisplayPoint> {
2883    let mut to = from;
2884    let mut found = false;
2885
2886    for _ in 0..times {
2887        found = false;
2888        let new_to = find_boundary(map, to, mode, &mut |_, right| {
2889            found = is_character_match(target, right, smartcase);
2890            found
2891        });
2892        if to == new_to {
2893            break;
2894        }
2895        to = new_to;
2896    }
2897
2898    if found {
2899        if before && to.column() > 0 {
2900            *to.column_mut() -= 1;
2901            Some(map.clip_point(to, Bias::Left))
2902        } else if before && to.row().0 > 0 {
2903            *to.row_mut() -= 1;
2904            *to.column_mut() = map.line(to.row()).len() as u32;
2905            Some(map.clip_point(to, Bias::Left))
2906        } else {
2907            Some(to)
2908        }
2909    } else {
2910        None
2911    }
2912}
2913
2914fn find_backward(
2915    map: &DisplaySnapshot,
2916    from: DisplayPoint,
2917    after: bool,
2918    target: char,
2919    times: usize,
2920    mode: FindRange,
2921    smartcase: bool,
2922) -> DisplayPoint {
2923    let mut to = from;
2924
2925    for _ in 0..times {
2926        let new_to = find_preceding_boundary_display_point(map, to, mode, &mut |_, right| {
2927            is_character_match(target, right, smartcase)
2928        });
2929        if to == new_to {
2930            break;
2931        }
2932        to = new_to;
2933    }
2934
2935    let next = map.buffer_snapshot().chars_at(to.to_point(map)).next();
2936    if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2937        if after {
2938            *to.column_mut() += 1;
2939            map.clip_point(to, Bias::Right)
2940        } else {
2941            to
2942        }
2943    } else {
2944        from
2945    }
2946}
2947
2948/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2949pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2950    if smartcase {
2951        if target.is_uppercase() {
2952            target == other
2953        } else {
2954            target == other.to_ascii_lowercase()
2955        }
2956    } else {
2957        target == other
2958    }
2959}
2960
2961fn sneak(
2962    map: &DisplaySnapshot,
2963    from: DisplayPoint,
2964    first_target: char,
2965    second_target: char,
2966    times: usize,
2967    smartcase: bool,
2968) -> Option<DisplayPoint> {
2969    let mut to = from;
2970    let mut found = false;
2971
2972    for _ in 0..times {
2973        found = false;
2974        let new_to = find_boundary(
2975            map,
2976            movement::right(map, to),
2977            FindRange::MultiLine,
2978            &mut |left, right| {
2979                found = is_character_match(first_target, left, smartcase)
2980                    && is_character_match(second_target, right, smartcase);
2981                found
2982            },
2983        );
2984        if to == new_to {
2985            break;
2986        }
2987        to = new_to;
2988    }
2989
2990    if found {
2991        Some(movement::left(map, to))
2992    } else {
2993        None
2994    }
2995}
2996
2997fn sneak_backward(
2998    map: &DisplaySnapshot,
2999    from: DisplayPoint,
3000    first_target: char,
3001    second_target: char,
3002    times: usize,
3003    smartcase: bool,
3004) -> Option<DisplayPoint> {
3005    let mut to = from;
3006    let mut found = false;
3007
3008    for _ in 0..times {
3009        found = false;
3010        let new_to = find_preceding_boundary_display_point(
3011            map,
3012            to,
3013            FindRange::MultiLine,
3014            &mut |left, right| {
3015                found = is_character_match(first_target, left, smartcase)
3016                    && is_character_match(second_target, right, smartcase);
3017                found
3018            },
3019        );
3020        if to == new_to {
3021            break;
3022        }
3023        to = new_to;
3024    }
3025
3026    if found {
3027        Some(movement::left(map, to))
3028    } else {
3029        None
3030    }
3031}
3032
3033fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
3034    let correct_line = map.start_of_relative_buffer_row(point, times as isize);
3035    first_non_whitespace(map, false, correct_line)
3036}
3037
3038fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
3039    let correct_line = map.start_of_relative_buffer_row(point, -(times as isize));
3040    first_non_whitespace(map, false, correct_line)
3041}
3042
3043fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
3044    let correct_line = map.start_of_relative_buffer_row(point, 0);
3045    right(map, correct_line, times.saturating_sub(1))
3046}
3047
3048pub(crate) fn next_line_end(
3049    map: &DisplaySnapshot,
3050    mut point: DisplayPoint,
3051    times: usize,
3052) -> DisplayPoint {
3053    if times > 1 {
3054        point = map.start_of_relative_buffer_row(point, times as isize - 1);
3055    }
3056    end_of_line(map, false, point, 1)
3057}
3058
3059fn window_top(
3060    map: &DisplaySnapshot,
3061    point: DisplayPoint,
3062    text_layout_details: &TextLayoutDetails,
3063    mut times: usize,
3064) -> (DisplayPoint, SelectionGoal) {
3065    let first_visible_line = text_layout_details
3066        .scroll_anchor
3067        .scroll_top_display_point(map);
3068
3069    if first_visible_line.row() != DisplayRow(0)
3070        && text_layout_details.vertical_scroll_margin as usize > times
3071    {
3072        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
3073    }
3074
3075    if let Some(visible_rows) = text_layout_details.visible_rows {
3076        let bottom_row = first_visible_line.row().0 + visible_rows as u32;
3077        let new_row = (first_visible_line.row().0 + (times as u32))
3078            .min(bottom_row)
3079            .min(map.max_point().row().0);
3080        let new_col = point.column().min(map.line_len(first_visible_line.row()));
3081
3082        let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
3083        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3084    } else {
3085        let new_row =
3086            DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
3087        let new_col = point.column().min(map.line_len(first_visible_line.row()));
3088
3089        let new_point = DisplayPoint::new(new_row, new_col);
3090        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3091    }
3092}
3093
3094fn window_middle(
3095    map: &DisplaySnapshot,
3096    point: DisplayPoint,
3097    text_layout_details: &TextLayoutDetails,
3098) -> (DisplayPoint, SelectionGoal) {
3099    if let Some(visible_rows) = text_layout_details.visible_rows {
3100        let first_visible_line = text_layout_details
3101            .scroll_anchor
3102            .scroll_top_display_point(map);
3103
3104        let max_visible_rows =
3105            (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
3106
3107        let new_row =
3108            (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
3109        let new_row = DisplayRow(new_row);
3110        let new_col = point.column().min(map.line_len(new_row));
3111        let new_point = DisplayPoint::new(new_row, new_col);
3112        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3113    } else {
3114        (point, SelectionGoal::None)
3115    }
3116}
3117
3118fn window_bottom(
3119    map: &DisplaySnapshot,
3120    point: DisplayPoint,
3121    text_layout_details: &TextLayoutDetails,
3122    mut times: usize,
3123) -> (DisplayPoint, SelectionGoal) {
3124    if let Some(visible_rows) = text_layout_details.visible_rows {
3125        let first_visible_line = text_layout_details
3126            .scroll_anchor
3127            .scroll_top_display_point(map);
3128        let bottom_row = first_visible_line.row().0
3129            + (visible_rows + text_layout_details.scroll_anchor.scroll_anchor.offset.y - 1.).floor()
3130                as u32;
3131        if bottom_row < map.max_point().row().0
3132            && text_layout_details.vertical_scroll_margin as usize > times
3133        {
3134            times = text_layout_details.vertical_scroll_margin.ceil() as usize;
3135        }
3136        let bottom_row_capped = bottom_row.min(map.max_point().row().0);
3137        let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
3138        {
3139            first_visible_line.row()
3140        } else {
3141            DisplayRow(bottom_row_capped.saturating_sub(times as u32))
3142        };
3143        let new_col = point.column().min(map.line_len(new_row));
3144        let new_point = DisplayPoint::new(new_row, new_col);
3145        (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3146    } else {
3147        (point, SelectionGoal::None)
3148    }
3149}
3150
3151fn method_motion(
3152    map: &DisplaySnapshot,
3153    mut display_point: DisplayPoint,
3154    times: usize,
3155    direction: Direction,
3156    is_start: bool,
3157) -> DisplayPoint {
3158    let snapshot = map.buffer_snapshot();
3159    if snapshot.as_singleton().is_none() {
3160        return display_point;
3161    }
3162
3163    for _ in 0..times {
3164        let offset = map
3165            .display_point_to_point(display_point, Bias::Left)
3166            .to_offset(&snapshot);
3167        let range = if direction == Direction::Prev {
3168            MultiBufferOffset(0)..offset
3169        } else {
3170            offset..snapshot.len()
3171        };
3172
3173        let possibilities = snapshot
3174            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
3175            .filter_map(|(range, object)| {
3176                if !matches!(object, language::TextObject::AroundFunction) {
3177                    return None;
3178                }
3179
3180                let relevant = if is_start { range.start } else { range.end };
3181                if direction == Direction::Prev && relevant < offset {
3182                    Some(relevant)
3183                } else if direction == Direction::Next && relevant > offset + 1usize {
3184                    Some(relevant)
3185                } else {
3186                    None
3187                }
3188            });
3189
3190        let dest = if direction == Direction::Prev {
3191            possibilities.max().unwrap_or(offset)
3192        } else {
3193            possibilities.min().unwrap_or(offset)
3194        };
3195        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
3196        if new_point == display_point {
3197            break;
3198        }
3199        display_point = new_point;
3200    }
3201    display_point
3202}
3203
3204fn comment_motion(
3205    map: &DisplaySnapshot,
3206    mut display_point: DisplayPoint,
3207    times: usize,
3208    direction: Direction,
3209) -> DisplayPoint {
3210    let snapshot = map.buffer_snapshot();
3211    if snapshot.as_singleton().is_none() {
3212        return display_point;
3213    }
3214
3215    for _ in 0..times {
3216        let offset = map
3217            .display_point_to_point(display_point, Bias::Left)
3218            .to_offset(&snapshot);
3219        let range = if direction == Direction::Prev {
3220            MultiBufferOffset(0)..offset
3221        } else {
3222            offset..snapshot.len()
3223        };
3224
3225        let possibilities = snapshot
3226            .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
3227            .filter_map(|(range, object)| {
3228                if !matches!(object, language::TextObject::AroundComment) {
3229                    return None;
3230                }
3231
3232                let relevant = if direction == Direction::Prev {
3233                    range.start
3234                } else {
3235                    range.end
3236                };
3237                if direction == Direction::Prev && relevant < offset {
3238                    Some(relevant)
3239                } else if direction == Direction::Next && relevant > offset + 1usize {
3240                    Some(relevant)
3241                } else {
3242                    None
3243                }
3244            });
3245
3246        let dest = if direction == Direction::Prev {
3247            possibilities.max().unwrap_or(offset)
3248        } else {
3249            possibilities.min().unwrap_or(offset)
3250        };
3251        let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
3252        if new_point == display_point {
3253            break;
3254        }
3255        display_point = new_point;
3256    }
3257
3258    display_point
3259}
3260
3261fn section_motion(
3262    map: &DisplaySnapshot,
3263    mut display_point: DisplayPoint,
3264    times: usize,
3265    direction: Direction,
3266    is_start: bool,
3267) -> DisplayPoint {
3268    if map.buffer_snapshot().as_singleton().is_some() {
3269        for _ in 0..times {
3270            let offset = map
3271                .display_point_to_point(display_point, Bias::Left)
3272                .to_offset(&map.buffer_snapshot());
3273            let range = if direction == Direction::Prev {
3274                MultiBufferOffset(0)..offset
3275            } else {
3276                offset..map.buffer_snapshot().len()
3277            };
3278
3279            // we set a max start depth here because we want a section to only be "top level"
3280            // similar to vim's default of '{' in the first column.
3281            // (and without it, ]] at the start of editor.rs is -very- slow)
3282            let mut possibilities = map
3283                .buffer_snapshot()
3284                .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
3285                .filter(|(_, object)| {
3286                    matches!(
3287                        object,
3288                        language::TextObject::AroundClass | language::TextObject::AroundFunction
3289                    )
3290                })
3291                .collect::<Vec<_>>();
3292            possibilities.sort_by_key(|(range_a, _)| range_a.start);
3293            let mut prev_end = None;
3294            let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
3295                if t == language::TextObject::AroundFunction
3296                    && prev_end.is_some_and(|prev_end| prev_end > range.start)
3297                {
3298                    return None;
3299                }
3300                prev_end = Some(range.end);
3301
3302                let relevant = if is_start { range.start } else { range.end };
3303                if direction == Direction::Prev && relevant < offset {
3304                    Some(relevant)
3305                } else if direction == Direction::Next && relevant > offset + 1usize {
3306                    Some(relevant)
3307                } else {
3308                    None
3309                }
3310            });
3311
3312            let offset = if direction == Direction::Prev {
3313                possibilities.max().unwrap_or(MultiBufferOffset(0))
3314            } else {
3315                possibilities.min().unwrap_or(map.buffer_snapshot().len())
3316            };
3317
3318            let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3319            if new_point == display_point {
3320                break;
3321            }
3322            display_point = new_point;
3323        }
3324        return display_point;
3325    };
3326
3327    for _ in 0..times {
3328        let next_point = if is_start {
3329            movement::start_of_excerpt(map, display_point, direction)
3330        } else {
3331            movement::end_of_excerpt(map, display_point, direction)
3332        };
3333        if next_point == display_point {
3334            break;
3335        }
3336        display_point = next_point;
3337    }
3338
3339    display_point
3340}
3341
3342fn matches_indent_type(
3343    target_indent: &text::LineIndent,
3344    current_indent: &text::LineIndent,
3345    indent_type: IndentType,
3346) -> bool {
3347    match indent_type {
3348        IndentType::Lesser => {
3349            target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3350        }
3351        IndentType::Greater => {
3352            target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3353        }
3354        IndentType::Same => {
3355            target_indent.spaces == current_indent.spaces
3356                && target_indent.tabs == current_indent.tabs
3357        }
3358    }
3359}
3360
3361fn indent_motion(
3362    map: &DisplaySnapshot,
3363    mut display_point: DisplayPoint,
3364    times: usize,
3365    direction: Direction,
3366    indent_type: IndentType,
3367) -> DisplayPoint {
3368    let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3369    let current_row = MultiBufferRow(buffer_point.row);
3370    let current_indent = map.line_indent_for_buffer_row(current_row);
3371    if current_indent.is_line_empty() {
3372        return display_point;
3373    }
3374    let max_row = map.max_point().to_point(map).row;
3375
3376    for _ in 0..times {
3377        let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3378
3379        let target_row = match direction {
3380            Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3381                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3382                !indent.is_line_empty()
3383                    && matches_indent_type(&indent, &current_indent, indent_type)
3384            }),
3385            Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3386                let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3387                !indent.is_line_empty()
3388                    && matches_indent_type(&indent, &current_indent, indent_type)
3389            }),
3390        }
3391        .unwrap_or(current_buffer_row);
3392
3393        let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3394        let new_point = first_non_whitespace(map, false, new_point);
3395        if new_point == display_point {
3396            break;
3397        }
3398        display_point = new_point;
3399    }
3400    display_point
3401}
3402
3403#[cfg(test)]
3404mod test {
3405
3406    use crate::{
3407        motion::Matching,
3408        state::Mode,
3409        test::{NeovimBackedTestContext, VimTestContext},
3410    };
3411    use editor::Inlay;
3412    use gpui::KeyBinding;
3413    use indoc::indoc;
3414    use language::Point;
3415    use multi_buffer::MultiBufferRow;
3416
3417    #[gpui::test]
3418    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3419        let mut cx = NeovimBackedTestContext::new(cx).await;
3420
3421        let initial_state = indoc! {r"ˇabc
3422            def
3423
3424            paragraph
3425            the second
3426
3427
3428
3429            third and
3430            final"};
3431
3432        // goes down once
3433        cx.set_shared_state(initial_state).await;
3434        cx.simulate_shared_keystrokes("}").await;
3435        cx.shared_state().await.assert_eq(indoc! {r"abc
3436            def
3437            ˇ
3438            paragraph
3439            the second
3440
3441
3442
3443            third and
3444            final"});
3445
3446        // goes up once
3447        cx.simulate_shared_keystrokes("{").await;
3448        cx.shared_state().await.assert_eq(initial_state);
3449
3450        // goes down twice
3451        cx.simulate_shared_keystrokes("2 }").await;
3452        cx.shared_state().await.assert_eq(indoc! {r"abc
3453            def
3454
3455            paragraph
3456            the second
3457            ˇ
3458
3459
3460            third and
3461            final"});
3462
3463        // goes down over multiple blanks
3464        cx.simulate_shared_keystrokes("}").await;
3465        cx.shared_state().await.assert_eq(indoc! {r"abc
3466                def
3467
3468                paragraph
3469                the second
3470
3471
3472
3473                third and
3474                finaˇl"});
3475
3476        // goes up twice
3477        cx.simulate_shared_keystrokes("2 {").await;
3478        cx.shared_state().await.assert_eq(indoc! {r"abc
3479                def
3480                ˇ
3481                paragraph
3482                the second
3483
3484
3485
3486                third and
3487                final"});
3488    }
3489
3490    #[gpui::test]
3491    async fn test_paragraph_motion_with_whitespace_lines(cx: &mut gpui::TestAppContext) {
3492        let mut cx = NeovimBackedTestContext::new(cx).await;
3493
3494        // Test that whitespace-only lines are NOT treated as paragraph boundaries
3495        // Per vim's :help paragraph - only truly empty lines are boundaries
3496        // Line 2 has 4 spaces (whitespace-only), line 4 is truly empty
3497        cx.set_shared_state("ˇfirst\n    \nstill first\n\nsecond")
3498            .await;
3499        cx.simulate_shared_keystrokes("}").await;
3500
3501        // Should skip whitespace-only line and stop at truly empty line
3502        let mut shared_state = cx.shared_state().await;
3503        shared_state.assert_eq("first\n    \nstill first\nˇ\nsecond");
3504        shared_state.assert_matches();
3505
3506        // Should go back to original position
3507        cx.simulate_shared_keystrokes("{").await;
3508        let mut shared_state = cx.shared_state().await;
3509        shared_state.assert_eq("ˇfirst\n    \nstill first\n\nsecond");
3510        shared_state.assert_matches();
3511    }
3512
3513    #[gpui::test]
3514    async fn test_matching(cx: &mut gpui::TestAppContext) {
3515        let mut cx = NeovimBackedTestContext::new(cx).await;
3516
3517        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3518                do(something(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        // test it works on the last character of the line
3529        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3530            do(something(with<Types>.and_arrays[0, 2]))
3531            }"})
3532            .await;
3533        cx.simulate_shared_keystrokes("%").await;
3534        cx.shared_state()
3535            .await
3536            .assert_eq(indoc! {r"func (a string) {
3537            do(something(with<Types>.and_arrays[0, 2]))
3538            ˇ}"});
3539
3540        // test it works on immediate nesting
3541        cx.set_shared_state("ˇ{()}").await;
3542        cx.simulate_shared_keystrokes("%").await;
3543        cx.shared_state().await.assert_eq("{()ˇ}");
3544        cx.simulate_shared_keystrokes("%").await;
3545        cx.shared_state().await.assert_eq("ˇ{()}");
3546
3547        // test it works on immediate nesting inside braces
3548        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3549        cx.simulate_shared_keystrokes("%").await;
3550        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3551
3552        // test it jumps to the next paren on a line
3553        cx.set_shared_state("func ˇboop() {\n}").await;
3554        cx.simulate_shared_keystrokes("%").await;
3555        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3556    }
3557
3558    #[gpui::test]
3559    async fn test_matching_quotes_disabled(cx: &mut gpui::TestAppContext) {
3560        let mut cx = NeovimBackedTestContext::new(cx).await;
3561
3562        // Bind % to Matching with match_quotes: false to match Neovim's behavior
3563        // (Neovim's % doesn't match quotes by default)
3564        cx.update(|_, cx| {
3565            cx.bind_keys([KeyBinding::new(
3566                "%",
3567                Matching {
3568                    match_quotes: false,
3569                },
3570                None,
3571            )]);
3572        });
3573
3574        cx.set_shared_state("one {two 'thˇree' four}").await;
3575        cx.simulate_shared_keystrokes("%").await;
3576        cx.shared_state().await.assert_eq("one ˇ{two 'three' four}");
3577
3578        cx.set_shared_state("'hello wˇorld'").await;
3579        cx.simulate_shared_keystrokes("%").await;
3580        cx.shared_state().await.assert_eq("'hello wˇorld'");
3581
3582        cx.set_shared_state(r#"func ("teˇst") {}"#).await;
3583        cx.simulate_shared_keystrokes("%").await;
3584        cx.shared_state().await.assert_eq(r#"func ˇ("test") {}"#);
3585
3586        cx.set_shared_state("ˇ'hello'").await;
3587        cx.simulate_shared_keystrokes("%").await;
3588        cx.shared_state().await.assert_eq("ˇ'hello'");
3589
3590        cx.set_shared_state("'helloˇ'").await;
3591        cx.simulate_shared_keystrokes("%").await;
3592        cx.shared_state().await.assert_eq("'helloˇ'");
3593
3594        cx.set_shared_state(indoc! {r"func (a string) {
3595                do('somethiˇng'))
3596            }"})
3597            .await;
3598        cx.simulate_shared_keystrokes("%").await;
3599        cx.shared_state()
3600            .await
3601            .assert_eq(indoc! {r"func (a string) {
3602                doˇ('something'))
3603            }"});
3604    }
3605
3606    #[gpui::test]
3607    async fn test_matching_quotes_enabled(cx: &mut gpui::TestAppContext) {
3608        let mut cx = VimTestContext::new_markdown_with_rust(cx).await;
3609
3610        // Test default behavior (match_quotes: true as configured in keymap/vim.json)
3611        cx.set_state("one {two 'thˇree' four}", Mode::Normal);
3612        cx.simulate_keystrokes("%");
3613        cx.assert_state("one {two ˇ'three' four}", Mode::Normal);
3614
3615        cx.set_state("'hello wˇorld'", Mode::Normal);
3616        cx.simulate_keystrokes("%");
3617        cx.assert_state("ˇ'hello world'", Mode::Normal);
3618
3619        cx.set_state(r#"func ('teˇst') {}"#, Mode::Normal);
3620        cx.simulate_keystrokes("%");
3621        cx.assert_state(r#"func (ˇ'test') {}"#, Mode::Normal);
3622
3623        cx.set_state("ˇ'hello'", Mode::Normal);
3624        cx.simulate_keystrokes("%");
3625        cx.assert_state("'helloˇ'", Mode::Normal);
3626
3627        cx.set_state("'helloˇ'", Mode::Normal);
3628        cx.simulate_keystrokes("%");
3629        cx.assert_state("ˇ'hello'", Mode::Normal);
3630
3631        cx.set_state(
3632            indoc! {r"func (a string) {
3633                do('somethiˇng'))
3634            }"},
3635            Mode::Normal,
3636        );
3637        cx.simulate_keystrokes("%");
3638        cx.assert_state(
3639            indoc! {r"func (a string) {
3640                do(ˇ'something'))
3641            }"},
3642            Mode::Normal,
3643        );
3644    }
3645
3646    #[gpui::test]
3647    async fn test_matching_comments(cx: &mut gpui::TestAppContext) {
3648        let mut cx = NeovimBackedTestContext::new(cx).await;
3649
3650        cx.set_shared_state(indoc! {r"ˇ/*
3651          this is a comment
3652        */"})
3653            .await;
3654        cx.simulate_shared_keystrokes("%").await;
3655        cx.shared_state().await.assert_eq(indoc! {r"/*
3656          this is a comment
3657        ˇ*/"});
3658        cx.simulate_shared_keystrokes("%").await;
3659        cx.shared_state().await.assert_eq(indoc! {r"ˇ/*
3660          this is a comment
3661        */"});
3662        cx.simulate_shared_keystrokes("%").await;
3663        cx.shared_state().await.assert_eq(indoc! {r"/*
3664          this is a comment
3665        ˇ*/"});
3666
3667        cx.set_shared_state("ˇ// comment").await;
3668        cx.simulate_shared_keystrokes("%").await;
3669        cx.shared_state().await.assert_eq("ˇ// comment");
3670    }
3671
3672    #[gpui::test]
3673    async fn test_matching_preprocessor_directives(cx: &mut gpui::TestAppContext) {
3674        let mut cx = NeovimBackedTestContext::new(cx).await;
3675
3676        cx.set_shared_state(indoc! {r"#ˇif
3677
3678            #else
3679
3680            #endif
3681            "})
3682            .await;
3683        cx.simulate_shared_keystrokes("%").await;
3684        cx.shared_state().await.assert_eq(indoc! {r"#if
3685
3686          ˇ#else
3687
3688          #endif
3689          "});
3690
3691        cx.simulate_shared_keystrokes("%").await;
3692        cx.shared_state().await.assert_eq(indoc! {r"#if
3693
3694          #else
3695
3696          ˇ#endif
3697          "});
3698
3699        cx.simulate_shared_keystrokes("%").await;
3700        cx.shared_state().await.assert_eq(indoc! {r"ˇ#if
3701
3702          #else
3703
3704          #endif
3705          "});
3706
3707        cx.set_shared_state(indoc! {r"
3708            #ˇif
3709              #if
3710
3711              #else
3712
3713              #endif
3714
3715            #else
3716            #endif
3717            "})
3718            .await;
3719
3720        cx.simulate_shared_keystrokes("%").await;
3721        cx.shared_state().await.assert_eq(indoc! {r"
3722            #if
3723              #if
3724
3725              #else
3726
3727              #endif
3728
3729            ˇ#else
3730            #endif
3731            "});
3732
3733        cx.simulate_shared_keystrokes("% %").await;
3734        cx.shared_state().await.assert_eq(indoc! {r"
3735            ˇ#if
3736              #if
3737
3738              #else
3739
3740              #endif
3741
3742            #else
3743            #endif
3744            "});
3745        cx.simulate_shared_keystrokes("j % % %").await;
3746        cx.shared_state().await.assert_eq(indoc! {r"
3747            #if
3748              ˇ#if
3749
3750              #else
3751
3752              #endif
3753
3754            #else
3755            #endif
3756            "});
3757    }
3758
3759    #[gpui::test]
3760    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3761        let mut cx = NeovimBackedTestContext::new(cx).await;
3762
3763        // test it works with curly braces
3764        cx.set_shared_state(indoc! {r"func (a string) {
3765                do(something(with<Types>.anˇd_arrays[0, 2]))
3766            }"})
3767            .await;
3768        cx.simulate_shared_keystrokes("] }").await;
3769        cx.shared_state()
3770            .await
3771            .assert_eq(indoc! {r"func (a string) {
3772                do(something(with<Types>.and_arrays[0, 2]))
3773            ˇ}"});
3774
3775        // test it works with brackets
3776        cx.set_shared_state(indoc! {r"func (a string) {
3777                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3778            }"})
3779            .await;
3780        cx.simulate_shared_keystrokes("] )").await;
3781        cx.shared_state()
3782            .await
3783            .assert_eq(indoc! {r"func (a string) {
3784                do(something(with<Types>.and_arrays[0, 2])ˇ)
3785            }"});
3786
3787        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3788            .await;
3789        cx.simulate_shared_keystrokes("] )").await;
3790        cx.shared_state()
3791            .await
3792            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3793
3794        // test it works on immediate nesting
3795        cx.set_shared_state("{ˇ {}{}}").await;
3796        cx.simulate_shared_keystrokes("] }").await;
3797        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3798        cx.set_shared_state("(ˇ ()())").await;
3799        cx.simulate_shared_keystrokes("] )").await;
3800        cx.shared_state().await.assert_eq("( ()()ˇ)");
3801
3802        // test it works on immediate nesting inside braces
3803        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3804        cx.simulate_shared_keystrokes("] }").await;
3805        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3806        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3807        cx.simulate_shared_keystrokes("] )").await;
3808        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3809    }
3810
3811    #[gpui::test]
3812    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3813        let mut cx = NeovimBackedTestContext::new(cx).await;
3814
3815        // test it works with curly braces
3816        cx.set_shared_state(indoc! {r"func (a string) {
3817                do(something(with<Types>.anˇd_arrays[0, 2]))
3818            }"})
3819            .await;
3820        cx.simulate_shared_keystrokes("[ {").await;
3821        cx.shared_state()
3822            .await
3823            .assert_eq(indoc! {r"func (a string) ˇ{
3824                do(something(with<Types>.and_arrays[0, 2]))
3825            }"});
3826
3827        // test it works with brackets
3828        cx.set_shared_state(indoc! {r"func (a string) {
3829                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3830            }"})
3831            .await;
3832        cx.simulate_shared_keystrokes("[ (").await;
3833        cx.shared_state()
3834            .await
3835            .assert_eq(indoc! {r"func (a string) {
3836                doˇ(something(with<Types>.and_arrays[0, 2]))
3837            }"});
3838
3839        // test it works on immediate nesting
3840        cx.set_shared_state("{{}{} ˇ }").await;
3841        cx.simulate_shared_keystrokes("[ {").await;
3842        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3843        cx.set_shared_state("(()() ˇ )").await;
3844        cx.simulate_shared_keystrokes("[ (").await;
3845        cx.shared_state().await.assert_eq("ˇ(()()  )");
3846
3847        // test it works on immediate nesting inside braces
3848        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3849        cx.simulate_shared_keystrokes("[ {").await;
3850        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3851        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3852        cx.simulate_shared_keystrokes("[ (").await;
3853        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3854    }
3855
3856    #[gpui::test]
3857    async fn test_unmatched_forward_markdown(cx: &mut gpui::TestAppContext) {
3858        let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3859
3860        cx.neovim.exec("set filetype=markdown").await;
3861
3862        cx.set_shared_state(indoc! {r"
3863            ```rs
3864            impl Worktree {
3865                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3866            ˇ    }
3867            }
3868            ```
3869        "})
3870            .await;
3871        cx.simulate_shared_keystrokes("] }").await;
3872        cx.shared_state().await.assert_eq(indoc! {r"
3873            ```rs
3874            impl Worktree {
3875                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3876                ˇ}
3877            }
3878            ```
3879        "});
3880
3881        cx.set_shared_state(indoc! {r"
3882            ```rs
3883            impl Worktree {
3884                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3885                }   ˇ
3886            }
3887            ```
3888        "})
3889            .await;
3890        cx.simulate_shared_keystrokes("] }").await;
3891        cx.shared_state().await.assert_eq(indoc! {r"
3892            ```rs
3893            impl Worktree {
3894                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3895                }  •
3896            ˇ}
3897            ```
3898        "});
3899    }
3900
3901    #[gpui::test]
3902    async fn test_unmatched_backward_markdown(cx: &mut gpui::TestAppContext) {
3903        let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3904
3905        cx.neovim.exec("set filetype=markdown").await;
3906
3907        cx.set_shared_state(indoc! {r"
3908            ```rs
3909            impl Worktree {
3910                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3911            ˇ    }
3912            }
3913            ```
3914        "})
3915            .await;
3916        cx.simulate_shared_keystrokes("[ {").await;
3917        cx.shared_state().await.assert_eq(indoc! {r"
3918            ```rs
3919            impl Worktree {
3920                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
3921                }
3922            }
3923            ```
3924        "});
3925
3926        cx.set_shared_state(indoc! {r"
3927            ```rs
3928            impl Worktree {
3929                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3930                }   ˇ
3931            }
3932            ```
3933        "})
3934            .await;
3935        cx.simulate_shared_keystrokes("[ {").await;
3936        cx.shared_state().await.assert_eq(indoc! {r"
3937            ```rs
3938            impl Worktree ˇ{
3939                pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3940                }  •
3941            }
3942            ```
3943        "});
3944    }
3945
3946    #[gpui::test]
3947    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3948        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3949
3950        cx.neovim.exec("set filetype=html").await;
3951
3952        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3953        cx.simulate_shared_keystrokes("%").await;
3954        cx.shared_state()
3955            .await
3956            .assert_eq(indoc! {r"<body><ˇ/body>"});
3957        cx.simulate_shared_keystrokes("%").await;
3958
3959        // test jumping backwards
3960        cx.shared_state()
3961            .await
3962            .assert_eq(indoc! {r"<ˇbody></body>"});
3963
3964        // test self-closing tags
3965        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3966        cx.simulate_shared_keystrokes("%").await;
3967        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3968
3969        // test tag with attributes
3970        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3971            </div>
3972            "})
3973            .await;
3974        cx.simulate_shared_keystrokes("%").await;
3975        cx.shared_state()
3976            .await
3977            .assert_eq(indoc! {r"<div class='test' id='main'>
3978            <ˇ/div>
3979            "});
3980
3981        // test multi-line self-closing tag
3982        cx.set_shared_state(indoc! {r#"<a>
3983            <br
3984                test = "test"
3985            /ˇ>
3986        </a>"#})
3987            .await;
3988        cx.simulate_shared_keystrokes("%").await;
3989        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3990            ˇ<br
3991                test = "test"
3992            />
3993        </a>"#});
3994
3995        // test nested closing tag
3996        cx.set_shared_state(indoc! {r#"<html>
3997            <bˇody>
3998            </body>
3999        </html>"#})
4000            .await;
4001        cx.simulate_shared_keystrokes("%").await;
4002        cx.shared_state().await.assert_eq(indoc! {r#"<html>
4003            <body>
4004            <ˇ/body>
4005        </html>"#});
4006        cx.simulate_shared_keystrokes("%").await;
4007        cx.shared_state().await.assert_eq(indoc! {r#"<html>
4008            <ˇbody>
4009            </body>
4010        </html>"#});
4011    }
4012
4013    #[gpui::test]
4014    async fn test_matching_tag_with_quotes(cx: &mut gpui::TestAppContext) {
4015        let mut cx = NeovimBackedTestContext::new_html(cx).await;
4016        cx.update(|_, cx| {
4017            cx.bind_keys([KeyBinding::new(
4018                "%",
4019                Matching {
4020                    match_quotes: false,
4021                },
4022                None,
4023            )]);
4024        });
4025
4026        cx.neovim.exec("set filetype=html").await;
4027        cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
4028            </div>
4029            "})
4030            .await;
4031        cx.simulate_shared_keystrokes("%").await;
4032        cx.shared_state()
4033            .await
4034            .assert_eq(indoc! {r"<div class='test' id='main'>
4035            <ˇ/div>
4036            "});
4037
4038        cx.update(|_, cx| {
4039            cx.bind_keys([KeyBinding::new("%", Matching { match_quotes: true }, None)]);
4040        });
4041
4042        cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
4043            </div>
4044            "})
4045            .await;
4046        cx.simulate_shared_keystrokes("%").await;
4047        cx.shared_state()
4048            .await
4049            .assert_eq(indoc! {r"<div class='test' id='main'>
4050            <ˇ/div>
4051            "});
4052    }
4053    #[gpui::test]
4054    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
4055        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
4056
4057        // test brackets within tags
4058        cx.set_shared_state(indoc! {r"function f() {
4059            return (
4060                <div rules={ˇ[{ a: 1 }]}>
4061                    <h1>test</h1>
4062                </div>
4063            );
4064        }"})
4065            .await;
4066        cx.simulate_shared_keystrokes("%").await;
4067        cx.shared_state().await.assert_eq(indoc! {r"function f() {
4068            return (
4069                <div rules={[{ a: 1 }ˇ]}>
4070                    <h1>test</h1>
4071                </div>
4072            );
4073        }"});
4074    }
4075
4076    #[gpui::test]
4077    async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
4078        let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
4079
4080        cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
4081            .await;
4082        cx.simulate_shared_keystrokes("%").await;
4083        cx.shared_state()
4084            .await
4085            .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
4086        cx.simulate_shared_keystrokes("%").await;
4087        cx.shared_state()
4088            .await
4089            .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
4090    }
4091
4092    #[gpui::test]
4093    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
4094        let mut cx = NeovimBackedTestContext::new(cx).await;
4095
4096        // f and F
4097        cx.set_shared_state("ˇone two three four").await;
4098        cx.simulate_shared_keystrokes("f o").await;
4099        cx.shared_state().await.assert_eq("one twˇo three four");
4100        cx.simulate_shared_keystrokes(",").await;
4101        cx.shared_state().await.assert_eq("ˇone two three four");
4102        cx.simulate_shared_keystrokes("2 ;").await;
4103        cx.shared_state().await.assert_eq("one two three fˇour");
4104        cx.simulate_shared_keystrokes("shift-f e").await;
4105        cx.shared_state().await.assert_eq("one two threˇe four");
4106        cx.simulate_shared_keystrokes("2 ;").await;
4107        cx.shared_state().await.assert_eq("onˇe two three four");
4108        cx.simulate_shared_keystrokes(",").await;
4109        cx.shared_state().await.assert_eq("one two thrˇee four");
4110
4111        // t and T
4112        cx.set_shared_state("ˇone two three four").await;
4113        cx.simulate_shared_keystrokes("t o").await;
4114        cx.shared_state().await.assert_eq("one tˇwo three four");
4115        cx.simulate_shared_keystrokes(",").await;
4116        cx.shared_state().await.assert_eq("oˇne two three four");
4117        cx.simulate_shared_keystrokes("2 ;").await;
4118        cx.shared_state().await.assert_eq("one two three ˇfour");
4119        cx.simulate_shared_keystrokes("shift-t e").await;
4120        cx.shared_state().await.assert_eq("one two threeˇ four");
4121        cx.simulate_shared_keystrokes("3 ;").await;
4122        cx.shared_state().await.assert_eq("oneˇ two three four");
4123        cx.simulate_shared_keystrokes(",").await;
4124        cx.shared_state().await.assert_eq("one two thˇree four");
4125    }
4126
4127    #[gpui::test]
4128    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
4129        let mut cx = NeovimBackedTestContext::new(cx).await;
4130        let initial_state = indoc! {r"something(ˇfoo)"};
4131        cx.set_shared_state(initial_state).await;
4132        cx.simulate_shared_keystrokes("}").await;
4133        cx.shared_state().await.assert_eq("something(fooˇ)");
4134    }
4135
4136    #[gpui::test]
4137    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
4138        let mut cx = NeovimBackedTestContext::new(cx).await;
4139        cx.set_shared_state("ˇone\n  two\nthree").await;
4140        cx.simulate_shared_keystrokes("enter").await;
4141        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
4142    }
4143
4144    #[gpui::test]
4145    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
4146        let mut cx = NeovimBackedTestContext::new(cx).await;
4147        cx.set_shared_state("ˇ one\n two \nthree").await;
4148        cx.simulate_shared_keystrokes("g _").await;
4149        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
4150
4151        cx.set_shared_state("ˇ one \n two \nthree").await;
4152        cx.simulate_shared_keystrokes("g _").await;
4153        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
4154        cx.simulate_shared_keystrokes("2 g _").await;
4155        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
4156    }
4157
4158    #[gpui::test]
4159    async fn test_end_of_line_with_vertical_motion(cx: &mut gpui::TestAppContext) {
4160        let mut cx = NeovimBackedTestContext::new(cx).await;
4161
4162        // test $ followed by k maintains end-of-line position
4163        cx.set_shared_state(indoc! {"
4164            The quick brown
4165            fˇox
4166            jumps over the
4167            lazy dog
4168            "})
4169            .await;
4170        cx.simulate_shared_keystrokes("$ k").await;
4171        cx.shared_state().await.assert_eq(indoc! {"
4172            The quick browˇn
4173            fox
4174            jumps over the
4175            lazy dog
4176            "});
4177        cx.simulate_shared_keystrokes("j j").await;
4178        cx.shared_state().await.assert_eq(indoc! {"
4179            The quick brown
4180            fox
4181            jumps over thˇe
4182            lazy dog
4183            "});
4184
4185        // test horizontal movement resets the end-of-line behavior
4186        cx.set_shared_state(indoc! {"
4187            The quick brown fox
4188            jumps over the
4189            lazy ˇdog
4190            "})
4191            .await;
4192        cx.simulate_shared_keystrokes("$ k").await;
4193        cx.shared_state().await.assert_eq(indoc! {"
4194            The quick brown fox
4195            jumps over thˇe
4196            lazy dog
4197            "});
4198        cx.simulate_shared_keystrokes("b b").await;
4199        cx.shared_state().await.assert_eq(indoc! {"
4200            The quick brown fox
4201            jumps ˇover the
4202            lazy dog
4203            "});
4204        cx.simulate_shared_keystrokes("k").await;
4205        cx.shared_state().await.assert_eq(indoc! {"
4206            The quˇick brown fox
4207            jumps over the
4208            lazy dog
4209            "});
4210
4211        // Test that, when the cursor is moved to the end of the line using `l`,
4212        // if `$` is used, the cursor stays at the end of the line when moving
4213        // to a longer line, ensuring that the selection goal was correctly
4214        // updated.
4215        cx.set_shared_state(indoc! {"
4216            The quick brown fox
4217            jumps over the
4218            lazy dˇog
4219            "})
4220            .await;
4221        cx.simulate_shared_keystrokes("l").await;
4222        cx.shared_state().await.assert_eq(indoc! {"
4223            The quick brown fox
4224            jumps over the
4225            lazy doˇg
4226            "});
4227        cx.simulate_shared_keystrokes("$ k").await;
4228        cx.shared_state().await.assert_eq(indoc! {"
4229            The quick brown fox
4230            jumps over thˇe
4231            lazy dog
4232            "});
4233    }
4234
4235    #[gpui::test]
4236    async fn test_window_top(cx: &mut gpui::TestAppContext) {
4237        let mut cx = NeovimBackedTestContext::new(cx).await;
4238        let initial_state = indoc! {r"abc
4239          def
4240          paragraph
4241          the second
4242          third ˇand
4243          final"};
4244
4245        cx.set_shared_state(initial_state).await;
4246        cx.simulate_shared_keystrokes("shift-h").await;
4247        cx.shared_state().await.assert_eq(indoc! {r"abˇc
4248          def
4249          paragraph
4250          the second
4251          third and
4252          final"});
4253
4254        // clip point
4255        cx.set_shared_state(indoc! {r"
4256          1 2 3
4257          4 5 6
4258          7 8 ˇ9
4259          "})
4260            .await;
4261        cx.simulate_shared_keystrokes("shift-h").await;
4262        cx.shared_state().await.assert_eq(indoc! {"
4263          1 2 ˇ3
4264          4 5 6
4265          7 8 9
4266          "});
4267
4268        cx.set_shared_state(indoc! {r"
4269          1 2 3
4270          4 5 6
4271          ˇ7 8 9
4272          "})
4273            .await;
4274        cx.simulate_shared_keystrokes("shift-h").await;
4275        cx.shared_state().await.assert_eq(indoc! {"
4276          ˇ1 2 3
4277          4 5 6
4278          7 8 9
4279          "});
4280
4281        cx.set_shared_state(indoc! {r"
4282          1 2 3
4283          4 5 ˇ6
4284          7 8 9"})
4285            .await;
4286        cx.simulate_shared_keystrokes("9 shift-h").await;
4287        cx.shared_state().await.assert_eq(indoc! {"
4288          1 2 3
4289          4 5 6
4290          7 8 ˇ9"});
4291    }
4292
4293    #[gpui::test]
4294    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
4295        let mut cx = NeovimBackedTestContext::new(cx).await;
4296        let initial_state = indoc! {r"abˇc
4297          def
4298          paragraph
4299          the second
4300          third and
4301          final"};
4302
4303        cx.set_shared_state(initial_state).await;
4304        cx.simulate_shared_keystrokes("shift-m").await;
4305        cx.shared_state().await.assert_eq(indoc! {r"abc
4306          def
4307          paˇragraph
4308          the second
4309          third and
4310          final"});
4311
4312        cx.set_shared_state(indoc! {r"
4313          1 2 3
4314          4 5 6
4315          7 8 ˇ9
4316          "})
4317            .await;
4318        cx.simulate_shared_keystrokes("shift-m").await;
4319        cx.shared_state().await.assert_eq(indoc! {"
4320          1 2 3
4321          4 5 ˇ6
4322          7 8 9
4323          "});
4324        cx.set_shared_state(indoc! {r"
4325          1 2 3
4326          4 5 6
4327          ˇ7 8 9
4328          "})
4329            .await;
4330        cx.simulate_shared_keystrokes("shift-m").await;
4331        cx.shared_state().await.assert_eq(indoc! {"
4332          1 2 3
4333          ˇ4 5 6
4334          7 8 9
4335          "});
4336        cx.set_shared_state(indoc! {r"
4337          ˇ1 2 3
4338          4 5 6
4339          7 8 9
4340          "})
4341            .await;
4342        cx.simulate_shared_keystrokes("shift-m").await;
4343        cx.shared_state().await.assert_eq(indoc! {"
4344          1 2 3
4345          ˇ4 5 6
4346          7 8 9
4347          "});
4348        cx.set_shared_state(indoc! {r"
4349          1 2 3
4350          ˇ4 5 6
4351          7 8 9
4352          "})
4353            .await;
4354        cx.simulate_shared_keystrokes("shift-m").await;
4355        cx.shared_state().await.assert_eq(indoc! {"
4356          1 2 3
4357          ˇ4 5 6
4358          7 8 9
4359          "});
4360        cx.set_shared_state(indoc! {r"
4361          1 2 3
4362          4 5 ˇ6
4363          7 8 9
4364          "})
4365            .await;
4366        cx.simulate_shared_keystrokes("shift-m").await;
4367        cx.shared_state().await.assert_eq(indoc! {"
4368          1 2 3
4369          4 5 ˇ6
4370          7 8 9
4371          "});
4372    }
4373
4374    #[gpui::test]
4375    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
4376        let mut cx = NeovimBackedTestContext::new(cx).await;
4377        let initial_state = indoc! {r"abc
4378          deˇf
4379          paragraph
4380          the second
4381          third and
4382          final"};
4383
4384        cx.set_shared_state(initial_state).await;
4385        cx.simulate_shared_keystrokes("shift-l").await;
4386        cx.shared_state().await.assert_eq(indoc! {r"abc
4387          def
4388          paragraph
4389          the second
4390          third and
4391          fiˇnal"});
4392
4393        cx.set_shared_state(indoc! {r"
4394          1 2 3
4395          4 5 ˇ6
4396          7 8 9
4397          "})
4398            .await;
4399        cx.simulate_shared_keystrokes("shift-l").await;
4400        cx.shared_state().await.assert_eq(indoc! {"
4401          1 2 3
4402          4 5 6
4403          7 8 9
4404          ˇ"});
4405
4406        cx.set_shared_state(indoc! {r"
4407          1 2 3
4408          ˇ4 5 6
4409          7 8 9
4410          "})
4411            .await;
4412        cx.simulate_shared_keystrokes("shift-l").await;
4413        cx.shared_state().await.assert_eq(indoc! {"
4414          1 2 3
4415          4 5 6
4416          7 8 9
4417          ˇ"});
4418
4419        cx.set_shared_state(indoc! {r"
4420          1 2 ˇ3
4421          4 5 6
4422          7 8 9
4423          "})
4424            .await;
4425        cx.simulate_shared_keystrokes("shift-l").await;
4426        cx.shared_state().await.assert_eq(indoc! {"
4427          1 2 3
4428          4 5 6
4429          7 8 9
4430          ˇ"});
4431
4432        cx.set_shared_state(indoc! {r"
4433          ˇ1 2 3
4434          4 5 6
4435          7 8 9
4436          "})
4437            .await;
4438        cx.simulate_shared_keystrokes("shift-l").await;
4439        cx.shared_state().await.assert_eq(indoc! {"
4440          1 2 3
4441          4 5 6
4442          7 8 9
4443          ˇ"});
4444
4445        cx.set_shared_state(indoc! {r"
4446          1 2 3
4447          4 5 ˇ6
4448          7 8 9
4449          "})
4450            .await;
4451        cx.simulate_shared_keystrokes("9 shift-l").await;
4452        cx.shared_state().await.assert_eq(indoc! {"
4453          1 2 ˇ3
4454          4 5 6
4455          7 8 9
4456          "});
4457    }
4458
4459    #[gpui::test]
4460    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
4461        let mut cx = NeovimBackedTestContext::new(cx).await;
4462        cx.set_shared_state(indoc! {r"
4463        456 5ˇ67 678
4464        "})
4465            .await;
4466        cx.simulate_shared_keystrokes("g e").await;
4467        cx.shared_state().await.assert_eq(indoc! {"
4468        45ˇ6 567 678
4469        "});
4470
4471        // Test times
4472        cx.set_shared_state(indoc! {r"
4473        123 234 345
4474        456 5ˇ67 678
4475        "})
4476            .await;
4477        cx.simulate_shared_keystrokes("4 g e").await;
4478        cx.shared_state().await.assert_eq(indoc! {"
4479        12ˇ3 234 345
4480        456 567 678
4481        "});
4482
4483        // With punctuation
4484        cx.set_shared_state(indoc! {r"
4485        123 234 345
4486        4;5.6 5ˇ67 678
4487        789 890 901
4488        "})
4489            .await;
4490        cx.simulate_shared_keystrokes("g e").await;
4491        cx.shared_state().await.assert_eq(indoc! {"
4492          123 234 345
4493          4;5.ˇ6 567 678
4494          789 890 901
4495        "});
4496
4497        // With punctuation and count
4498        cx.set_shared_state(indoc! {r"
4499        123 234 345
4500        4;5.6 5ˇ67 678
4501        789 890 901
4502        "})
4503            .await;
4504        cx.simulate_shared_keystrokes("5 g e").await;
4505        cx.shared_state().await.assert_eq(indoc! {"
4506          123 234 345
4507          ˇ4;5.6 567 678
4508          789 890 901
4509        "});
4510
4511        // newlines
4512        cx.set_shared_state(indoc! {r"
4513        123 234 345
4514
4515        78ˇ9 890 901
4516        "})
4517            .await;
4518        cx.simulate_shared_keystrokes("g e").await;
4519        cx.shared_state().await.assert_eq(indoc! {"
4520          123 234 345
4521          ˇ
4522          789 890 901
4523        "});
4524        cx.simulate_shared_keystrokes("g e").await;
4525        cx.shared_state().await.assert_eq(indoc! {"
4526          123 234 34ˇ5
4527
4528          789 890 901
4529        "});
4530
4531        // With punctuation
4532        cx.set_shared_state(indoc! {r"
4533        123 234 345
4534        4;5.ˇ6 567 678
4535        789 890 901
4536        "})
4537            .await;
4538        cx.simulate_shared_keystrokes("g shift-e").await;
4539        cx.shared_state().await.assert_eq(indoc! {"
4540          123 234 34ˇ5
4541          4;5.6 567 678
4542          789 890 901
4543        "});
4544
4545        // With multi byte char
4546        cx.set_shared_state(indoc! {r"
4547        bar ˇó
4548        "})
4549            .await;
4550        cx.simulate_shared_keystrokes("g e").await;
4551        cx.shared_state().await.assert_eq(indoc! {"
4552        baˇr ó
4553        "});
4554    }
4555
4556    #[gpui::test]
4557    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
4558        let mut cx = NeovimBackedTestContext::new(cx).await;
4559
4560        cx.set_shared_state(indoc! {"
4561            fn aˇ() {
4562              return
4563            }
4564        "})
4565            .await;
4566        cx.simulate_shared_keystrokes("v $ %").await;
4567        cx.shared_state().await.assert_eq(indoc! {"
4568            fn a«() {
4569              return
4570            }ˇ»
4571        "});
4572    }
4573
4574    #[gpui::test]
4575    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
4576        let mut cx = VimTestContext::new(cx, true).await;
4577
4578        cx.set_state(
4579            indoc! {"
4580                struct Foo {
4581                ˇ
4582                }
4583            "},
4584            Mode::Normal,
4585        );
4586
4587        cx.update_editor(|editor, _window, cx| {
4588            let range = editor.selections.newest_anchor().range();
4589            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
4590            let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
4591            editor.splice_inlays(&[], vec![inlay], cx);
4592        });
4593
4594        cx.simulate_keystrokes("j");
4595        cx.assert_state(
4596            indoc! {"
4597                struct Foo {
4598
4599                ˇ}
4600            "},
4601            Mode::Normal,
4602        );
4603    }
4604
4605    #[gpui::test]
4606    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
4607        let mut cx = VimTestContext::new(cx, true).await;
4608
4609        cx.set_state(
4610            indoc! {"
4611            ˇstruct Foo {
4612
4613            }
4614        "},
4615            Mode::Normal,
4616        );
4617        cx.update_editor(|editor, _window, cx| {
4618            let snapshot = editor.buffer().read(cx).snapshot(cx);
4619            let end_of_line =
4620                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
4621            let inlay_text = " hint";
4622            let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
4623            editor.splice_inlays(&[], vec![inlay], cx);
4624        });
4625        cx.simulate_keystrokes("$");
4626        cx.assert_state(
4627            indoc! {"
4628            struct Foo ˇ{
4629
4630            }
4631        "},
4632            Mode::Normal,
4633        );
4634    }
4635
4636    #[gpui::test]
4637    async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
4638        let mut cx = VimTestContext::new(cx, true).await;
4639
4640        // Test the exact scenario from issue #29134
4641        cx.set_state(
4642            indoc! {"
4643                fn main() {
4644                    let this_is_a_long_name = Vec::<u32>::new();
4645                    let new_oneˇ = this_is_a_long_name
4646                        .iter()
4647                        .map(|i| i + 1)
4648                        .map(|i| i * 2)
4649                        .collect::<Vec<_>>();
4650                }
4651            "},
4652            Mode::Normal,
4653        );
4654
4655        // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
4656        cx.update_editor(|editor, _window, cx| {
4657            let snapshot = editor.buffer().read(cx).snapshot(cx);
4658            // The empty line is at line 3 (0-indexed)
4659            let line_start = snapshot.anchor_after(Point::new(3, 0));
4660            let inlay_text = ": Vec<u32>";
4661            let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
4662            editor.splice_inlays(&[], vec![inlay], cx);
4663        });
4664
4665        // Enter visual mode
4666        cx.simulate_keystrokes("v");
4667        cx.assert_state(
4668            indoc! {"
4669                fn main() {
4670                    let this_is_a_long_name = Vec::<u32>::new();
4671                    let new_one« ˇ»= this_is_a_long_name
4672                        .iter()
4673                        .map(|i| i + 1)
4674                        .map(|i| i * 2)
4675                        .collect::<Vec<_>>();
4676                }
4677            "},
4678            Mode::Visual,
4679        );
4680
4681        // Move down - should go to the beginning of line 4, not skip to line 5
4682        cx.simulate_keystrokes("j");
4683        cx.assert_state(
4684            indoc! {"
4685                fn main() {
4686                    let this_is_a_long_name = Vec::<u32>::new();
4687                    let new_one« = this_is_a_long_name
4688                      ˇ»  .iter()
4689                        .map(|i| i + 1)
4690                        .map(|i| i * 2)
4691                        .collect::<Vec<_>>();
4692                }
4693            "},
4694            Mode::Visual,
4695        );
4696
4697        // Test with multiple movements
4698        cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
4699
4700        // Add type hint on the empty line
4701        cx.update_editor(|editor, _window, cx| {
4702            let snapshot = editor.buffer().read(cx).snapshot(cx);
4703            let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
4704            let inlay_text = ": i32";
4705            let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
4706            editor.splice_inlays(&[], vec![inlay], cx);
4707        });
4708
4709        // Enter visual mode and move down twice
4710        cx.simulate_keystrokes("v j j");
4711        cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
4712    }
4713
4714    #[gpui::test]
4715    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
4716        let mut cx = NeovimBackedTestContext::new(cx).await;
4717        // Normal mode
4718        cx.set_shared_state(indoc! {"
4719            The ˇquick brown
4720            fox jumps over
4721            the lazy dog
4722            The quick brown
4723            fox jumps over
4724            the lazy dog
4725            The quick brown
4726            fox jumps over
4727            the lazy dog"})
4728            .await;
4729        cx.simulate_shared_keystrokes("2 0 %").await;
4730        cx.shared_state().await.assert_eq(indoc! {"
4731            The quick brown
4732            fox ˇjumps over
4733            the lazy dog
4734            The quick brown
4735            fox jumps over
4736            the lazy dog
4737            The quick brown
4738            fox jumps over
4739            the lazy dog"});
4740
4741        cx.simulate_shared_keystrokes("2 5 %").await;
4742        cx.shared_state().await.assert_eq(indoc! {"
4743            The quick brown
4744            fox jumps over
4745            the ˇlazy dog
4746            The quick brown
4747            fox jumps over
4748            the lazy dog
4749            The quick brown
4750            fox jumps over
4751            the lazy dog"});
4752
4753        cx.simulate_shared_keystrokes("7 5 %").await;
4754        cx.shared_state().await.assert_eq(indoc! {"
4755            The quick brown
4756            fox jumps over
4757            the lazy dog
4758            The quick brown
4759            fox jumps over
4760            the lazy dog
4761            The ˇquick brown
4762            fox jumps over
4763            the lazy dog"});
4764
4765        // Visual mode
4766        cx.set_shared_state(indoc! {"
4767            The ˇquick brown
4768            fox jumps over
4769            the lazy dog
4770            The quick brown
4771            fox jumps over
4772            the lazy dog
4773            The quick brown
4774            fox jumps over
4775            the lazy dog"})
4776            .await;
4777        cx.simulate_shared_keystrokes("v 5 0 %").await;
4778        cx.shared_state().await.assert_eq(indoc! {"
4779            The «quick brown
4780            fox jumps over
4781            the lazy dog
4782            The quick brown
4783            fox jˇ»umps over
4784            the lazy dog
4785            The quick brown
4786            fox jumps over
4787            the lazy dog"});
4788
4789        cx.set_shared_state(indoc! {"
4790            The ˇquick brown
4791            fox jumps over
4792            the lazy dog
4793            The quick brown
4794            fox jumps over
4795            the lazy dog
4796            The quick brown
4797            fox jumps over
4798            the lazy dog"})
4799            .await;
4800        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4801        cx.shared_state().await.assert_eq(indoc! {"
4802            The «quick brown
4803            fox jumps over
4804            the lazy dog
4805            The quick brown
4806            fox jumps over
4807            the lazy dog
4808            The quick brown
4809            fox jumps over
4810            the lˇ»azy dog"});
4811    }
4812
4813    #[gpui::test]
4814    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4815        let mut cx = NeovimBackedTestContext::new(cx).await;
4816
4817        cx.set_shared_state("ˇπππππ").await;
4818        cx.simulate_shared_keystrokes("3 space").await;
4819        cx.shared_state().await.assert_eq("πππˇππ");
4820    }
4821
4822    #[gpui::test]
4823    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4824        let mut cx = NeovimBackedTestContext::new(cx).await;
4825
4826        cx.set_shared_state(indoc! {"
4827            ππππˇπ
4828            πanotherline"})
4829            .await;
4830        cx.simulate_shared_keystrokes("4 space").await;
4831        cx.shared_state().await.assert_eq(indoc! {"
4832            πππππ
4833            πanˇotherline"});
4834    }
4835
4836    #[gpui::test]
4837    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4838        let mut cx = NeovimBackedTestContext::new(cx).await;
4839
4840        cx.set_shared_state(indoc! {"
4841                        ππππ
4842                        πanˇotherline"})
4843            .await;
4844        cx.simulate_shared_keystrokes("4 backspace").await;
4845        cx.shared_state().await.assert_eq(indoc! {"
4846                        πππˇπ
4847                        πanotherline"});
4848    }
4849
4850    #[gpui::test]
4851    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4852        let mut cx = VimTestContext::new(cx, true).await;
4853        cx.set_state(
4854            indoc! {
4855                "func empty(a string) bool {
4856                     ˇif a == \"\" {
4857                         return true
4858                     }
4859                     return false
4860                }"
4861            },
4862            Mode::Normal,
4863        );
4864        cx.simulate_keystrokes("[ -");
4865        cx.assert_state(
4866            indoc! {
4867                "ˇfunc empty(a string) bool {
4868                     if a == \"\" {
4869                         return true
4870                     }
4871                     return false
4872                }"
4873            },
4874            Mode::Normal,
4875        );
4876        cx.simulate_keystrokes("] =");
4877        cx.assert_state(
4878            indoc! {
4879                "func empty(a string) bool {
4880                     if a == \"\" {
4881                         return true
4882                     }
4883                     return false
4884                ˇ}"
4885            },
4886            Mode::Normal,
4887        );
4888        cx.simulate_keystrokes("[ +");
4889        cx.assert_state(
4890            indoc! {
4891                "func empty(a string) bool {
4892                     if a == \"\" {
4893                         return true
4894                     }
4895                     ˇreturn false
4896                }"
4897            },
4898            Mode::Normal,
4899        );
4900        cx.simulate_keystrokes("2 [ =");
4901        cx.assert_state(
4902            indoc! {
4903                "func empty(a string) bool {
4904                     ˇif a == \"\" {
4905                         return true
4906                     }
4907                     return false
4908                }"
4909            },
4910            Mode::Normal,
4911        );
4912        cx.simulate_keystrokes("] +");
4913        cx.assert_state(
4914            indoc! {
4915                "func empty(a string) bool {
4916                     if a == \"\" {
4917                         ˇreturn true
4918                     }
4919                     return false
4920                }"
4921            },
4922            Mode::Normal,
4923        );
4924        cx.simulate_keystrokes("] -");
4925        cx.assert_state(
4926            indoc! {
4927                "func empty(a string) bool {
4928                     if a == \"\" {
4929                         return true
4930                     ˇ}
4931                     return false
4932                }"
4933            },
4934            Mode::Normal,
4935        );
4936    }
4937
4938    #[gpui::test]
4939    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4940        let mut cx = NeovimBackedTestContext::new(cx).await;
4941        cx.set_shared_state("abˇc").await;
4942        cx.simulate_shared_keystrokes("delete").await;
4943        cx.shared_state().await.assert_eq("aˇb");
4944    }
4945
4946    #[gpui::test]
4947    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4948        let mut cx = NeovimBackedTestContext::new(cx).await;
4949
4950        cx.set_shared_state(indoc! {"
4951             ˇthe quick brown fox
4952             jumped over the lazy dog"})
4953            .await;
4954        cx.simulate_shared_keystrokes("d v 0").await;
4955        cx.shared_state().await.assert_eq(indoc! {"
4956             ˇhe quick brown fox
4957             jumped over the lazy dog"});
4958        assert!(!cx.cx.forced_motion());
4959
4960        cx.set_shared_state(indoc! {"
4961            the quick bˇrown fox
4962            jumped over the lazy dog"})
4963            .await;
4964        cx.simulate_shared_keystrokes("d v 0").await;
4965        cx.shared_state().await.assert_eq(indoc! {"
4966            ˇown fox
4967            jumped over the lazy dog"});
4968        assert!(!cx.cx.forced_motion());
4969
4970        cx.set_shared_state(indoc! {"
4971            the quick brown foˇx
4972            jumped over the lazy dog"})
4973            .await;
4974        cx.simulate_shared_keystrokes("d v 0").await;
4975        cx.shared_state().await.assert_eq(indoc! {"
4976            ˇ
4977            jumped over the lazy dog"});
4978        assert!(!cx.cx.forced_motion());
4979    }
4980
4981    #[gpui::test]
4982    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4983        let mut cx = NeovimBackedTestContext::new(cx).await;
4984
4985        cx.set_shared_state(indoc! {"
4986             ˇthe quick brown fox
4987             jumped over the lazy dog"})
4988            .await;
4989        cx.simulate_shared_keystrokes("d v g shift-m").await;
4990        cx.shared_state().await.assert_eq(indoc! {"
4991             ˇbrown fox
4992             jumped over the lazy dog"});
4993        assert!(!cx.cx.forced_motion());
4994
4995        cx.set_shared_state(indoc! {"
4996            the quick bˇrown fox
4997            jumped over the lazy dog"})
4998            .await;
4999        cx.simulate_shared_keystrokes("d v g shift-m").await;
5000        cx.shared_state().await.assert_eq(indoc! {"
5001            the quickˇown fox
5002            jumped over the lazy dog"});
5003        assert!(!cx.cx.forced_motion());
5004
5005        cx.set_shared_state(indoc! {"
5006            the quick brown foˇx
5007            jumped over the lazy dog"})
5008            .await;
5009        cx.simulate_shared_keystrokes("d v g shift-m").await;
5010        cx.shared_state().await.assert_eq(indoc! {"
5011            the quicˇk
5012            jumped over the lazy dog"});
5013        assert!(!cx.cx.forced_motion());
5014
5015        cx.set_shared_state(indoc! {"
5016            ˇthe quick brown fox
5017            jumped over the lazy dog"})
5018            .await;
5019        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
5020        cx.shared_state().await.assert_eq(indoc! {"
5021            ˇ fox
5022            jumped over the lazy dog"});
5023        assert!(!cx.cx.forced_motion());
5024
5025        cx.set_shared_state(indoc! {"
5026            ˇthe quick brown fox
5027            jumped over the lazy dog"})
5028            .await;
5029        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
5030        cx.shared_state().await.assert_eq(indoc! {"
5031            ˇuick brown fox
5032            jumped over the lazy dog"});
5033        assert!(!cx.cx.forced_motion());
5034    }
5035
5036    #[gpui::test]
5037    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
5038        let mut cx = NeovimBackedTestContext::new(cx).await;
5039
5040        cx.set_shared_state(indoc! {"
5041             the quick brown foˇx
5042             jumped over the lazy dog"})
5043            .await;
5044        cx.simulate_shared_keystrokes("d v $").await;
5045        cx.shared_state().await.assert_eq(indoc! {"
5046             the quick brown foˇx
5047             jumped over the lazy dog"});
5048        assert!(!cx.cx.forced_motion());
5049
5050        cx.set_shared_state(indoc! {"
5051             ˇthe quick brown fox
5052             jumped over the lazy dog"})
5053            .await;
5054        cx.simulate_shared_keystrokes("d v $").await;
5055        cx.shared_state().await.assert_eq(indoc! {"
5056             ˇx
5057             jumped over the lazy dog"});
5058        assert!(!cx.cx.forced_motion());
5059    }
5060
5061    #[gpui::test]
5062    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
5063        let mut cx = NeovimBackedTestContext::new(cx).await;
5064
5065        cx.set_shared_state(indoc! {"
5066               ˇthe quick brown fox
5067               jumped over the lazy dog"})
5068            .await;
5069        cx.simulate_shared_keystrokes("y v j p").await;
5070        cx.shared_state().await.assert_eq(indoc! {"
5071               the quick brown fox
5072               ˇthe quick brown fox
5073               jumped over the lazy dog"});
5074        assert!(!cx.cx.forced_motion());
5075
5076        cx.set_shared_state(indoc! {"
5077              the quick bˇrown fox
5078              jumped over the lazy dog"})
5079            .await;
5080        cx.simulate_shared_keystrokes("y v j p").await;
5081        cx.shared_state().await.assert_eq(indoc! {"
5082              the quick brˇrown fox
5083              jumped overown fox
5084              jumped over the lazy dog"});
5085        assert!(!cx.cx.forced_motion());
5086
5087        cx.set_shared_state(indoc! {"
5088             the quick brown foˇx
5089             jumped over the lazy dog"})
5090            .await;
5091        cx.simulate_shared_keystrokes("y v j p").await;
5092        cx.shared_state().await.assert_eq(indoc! {"
5093             the quick brown foxˇx
5094             jumped over the la
5095             jumped over the lazy dog"});
5096        assert!(!cx.cx.forced_motion());
5097
5098        cx.set_shared_state(indoc! {"
5099             the quick brown fox
5100             jˇumped over the lazy dog"})
5101            .await;
5102        cx.simulate_shared_keystrokes("y v k p").await;
5103        cx.shared_state().await.assert_eq(indoc! {"
5104            thˇhe quick brown fox
5105            je quick brown fox
5106            jumped over the lazy dog"});
5107        assert!(!cx.cx.forced_motion());
5108    }
5109
5110    #[gpui::test]
5111    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
5112        let mut cx = NeovimBackedTestContext::new(cx).await;
5113
5114        cx.set_shared_state(indoc! {"
5115              ˇthe quick brown fox
5116              jumped over the lazy dog"})
5117            .await;
5118        cx.simulate_shared_keystrokes("d v e").await;
5119        cx.shared_state().await.assert_eq(indoc! {"
5120              ˇe quick brown fox
5121              jumped over the lazy dog"});
5122        assert!(!cx.cx.forced_motion());
5123
5124        cx.set_shared_state(indoc! {"
5125              the quick bˇrown fox
5126              jumped over the lazy dog"})
5127            .await;
5128        cx.simulate_shared_keystrokes("d v e").await;
5129        cx.shared_state().await.assert_eq(indoc! {"
5130              the quick bˇn fox
5131              jumped over the lazy dog"});
5132        assert!(!cx.cx.forced_motion());
5133
5134        cx.set_shared_state(indoc! {"
5135             the quick brown foˇx
5136             jumped over the lazy dog"})
5137            .await;
5138        cx.simulate_shared_keystrokes("d v e").await;
5139        cx.shared_state().await.assert_eq(indoc! {"
5140        the quick brown foˇd over the lazy dog"});
5141        assert!(!cx.cx.forced_motion());
5142    }
5143
5144    #[gpui::test]
5145    async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
5146        let mut cx = VimTestContext::new(cx, true).await;
5147
5148        // Setup custom keybindings for subword motions so we can use the bindings
5149        // in `simulate_keystrokes`.
5150        cx.update(|_window, cx| {
5151            cx.bind_keys([KeyBinding::new(
5152                "w",
5153                super::NextSubwordStart {
5154                    ignore_punctuation: false,
5155                },
5156                None,
5157            )]);
5158        });
5159
5160        cx.set_state("ˇfoo.bar", Mode::Normal);
5161        cx.simulate_keystrokes("w");
5162        cx.assert_state("foo.ˇbar", Mode::Normal);
5163
5164        cx.set_state("ˇfoo(bar)", Mode::Normal);
5165        cx.simulate_keystrokes("w");
5166        cx.assert_state("fooˇ(bar)", Mode::Normal);
5167        cx.simulate_keystrokes("w");
5168        cx.assert_state("foo(ˇbar)", Mode::Normal);
5169        cx.simulate_keystrokes("w");
5170        cx.assert_state("foo(barˇ)", Mode::Normal);
5171
5172        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
5173        cx.simulate_keystrokes("w");
5174        cx.assert_state("foo_ˇbar_baz", Mode::Normal);
5175        cx.simulate_keystrokes("w");
5176        cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
5177
5178        cx.set_state("ˇfooBarBaz", Mode::Normal);
5179        cx.simulate_keystrokes("w");
5180        cx.assert_state("fooˇBarBaz", Mode::Normal);
5181        cx.simulate_keystrokes("w");
5182        cx.assert_state("fooBarˇBaz", Mode::Normal);
5183
5184        cx.set_state("ˇfoo;bar", Mode::Normal);
5185        cx.simulate_keystrokes("w");
5186        cx.assert_state("foo;ˇbar", Mode::Normal);
5187
5188        cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::Normal);
5189        cx.simulate_keystrokes("w");
5190        cx.assert_state("<?ˇphp\n\n$someVariable = 2;", Mode::Normal);
5191        cx.simulate_keystrokes("w");
5192        cx.assert_state("<?php\nˇ\n$someVariable = 2;", Mode::Normal);
5193        cx.simulate_keystrokes("w");
5194        cx.assert_state("<?php\n\nˇ$someVariable = 2;", Mode::Normal);
5195        cx.simulate_keystrokes("w");
5196        cx.assert_state("<?php\n\n$ˇsomeVariable = 2;", Mode::Normal);
5197        cx.simulate_keystrokes("w");
5198        cx.assert_state("<?php\n\n$someˇVariable = 2;", Mode::Normal);
5199        cx.simulate_keystrokes("w");
5200        cx.assert_state("<?php\n\n$someVariable ˇ= 2;", Mode::Normal);
5201        cx.simulate_keystrokes("w");
5202        cx.assert_state("<?php\n\n$someVariable = ˇ2;", Mode::Normal);
5203    }
5204
5205    #[gpui::test]
5206    async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
5207        let mut cx = VimTestContext::new(cx, true).await;
5208
5209        // Setup custom keybindings for subword motions so we can use the bindings
5210        // in `simulate_keystrokes`.
5211        cx.update(|_window, cx| {
5212            cx.bind_keys([KeyBinding::new(
5213                "e",
5214                super::NextSubwordEnd {
5215                    ignore_punctuation: false,
5216                },
5217                None,
5218            )]);
5219        });
5220
5221        cx.set_state("ˇfoo.bar", Mode::Normal);
5222        cx.simulate_keystrokes("e");
5223        cx.assert_state("foˇo.bar", Mode::Normal);
5224        cx.simulate_keystrokes("e");
5225        cx.assert_state("fooˇ.bar", Mode::Normal);
5226        cx.simulate_keystrokes("e");
5227        cx.assert_state("foo.baˇr", Mode::Normal);
5228
5229        cx.set_state("ˇfoo(bar)", Mode::Normal);
5230        cx.simulate_keystrokes("e");
5231        cx.assert_state("foˇo(bar)", Mode::Normal);
5232        cx.simulate_keystrokes("e");
5233        cx.assert_state("fooˇ(bar)", Mode::Normal);
5234        cx.simulate_keystrokes("e");
5235        cx.assert_state("foo(baˇr)", Mode::Normal);
5236        cx.simulate_keystrokes("e");
5237        cx.assert_state("foo(barˇ)", Mode::Normal);
5238
5239        cx.set_state("ˇfoo_bar_baz", Mode::Normal);
5240        cx.simulate_keystrokes("e");
5241        cx.assert_state("foˇo_bar_baz", Mode::Normal);
5242        cx.simulate_keystrokes("e");
5243        cx.assert_state("foo_baˇr_baz", Mode::Normal);
5244        cx.simulate_keystrokes("e");
5245        cx.assert_state("foo_bar_baˇz", Mode::Normal);
5246
5247        cx.set_state("ˇfooBarBaz", Mode::Normal);
5248        cx.simulate_keystrokes("e");
5249        cx.set_state("foˇoBarBaz", Mode::Normal);
5250        cx.simulate_keystrokes("e");
5251        cx.set_state("fooBaˇrBaz", Mode::Normal);
5252        cx.simulate_keystrokes("e");
5253        cx.set_state("fooBarBaˇz", Mode::Normal);
5254
5255        cx.set_state("ˇfoo;bar", Mode::Normal);
5256        cx.simulate_keystrokes("e");
5257        cx.set_state("foˇo;bar", Mode::Normal);
5258        cx.simulate_keystrokes("e");
5259        cx.set_state("fooˇ;bar", Mode::Normal);
5260        cx.simulate_keystrokes("e");
5261        cx.set_state("foo;baˇr", Mode::Normal);
5262    }
5263
5264    #[gpui::test]
5265    async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
5266        let mut cx = VimTestContext::new(cx, true).await;
5267
5268        // Setup custom keybindings for subword motions so we can use the bindings
5269        // in `simulate_keystrokes`.
5270        cx.update(|_window, cx| {
5271            cx.bind_keys([KeyBinding::new(
5272                "b",
5273                super::PreviousSubwordStart {
5274                    ignore_punctuation: false,
5275                },
5276                None,
5277            )]);
5278        });
5279
5280        cx.set_state("foo.barˇ", Mode::Normal);
5281        cx.simulate_keystrokes("b");
5282        cx.assert_state("foo.ˇbar", Mode::Normal);
5283        cx.simulate_keystrokes("b");
5284        cx.assert_state("fooˇ.bar", Mode::Normal);
5285        cx.simulate_keystrokes("b");
5286        cx.assert_state("ˇfoo.bar", Mode::Normal);
5287
5288        cx.set_state("foo(barˇ)", Mode::Normal);
5289        cx.simulate_keystrokes("b");
5290        cx.assert_state("foo(ˇbar)", Mode::Normal);
5291        cx.simulate_keystrokes("b");
5292        cx.assert_state("fooˇ(bar)", Mode::Normal);
5293        cx.simulate_keystrokes("b");
5294        cx.assert_state("ˇfoo(bar)", Mode::Normal);
5295
5296        cx.set_state("foo_bar_bazˇ", Mode::Normal);
5297        cx.simulate_keystrokes("b");
5298        cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
5299        cx.simulate_keystrokes("b");
5300        cx.assert_state("foo_ˇbar_baz", Mode::Normal);
5301        cx.simulate_keystrokes("b");
5302        cx.assert_state("ˇfoo_bar_baz", Mode::Normal);
5303
5304        cx.set_state("fooBarBazˇ", Mode::Normal);
5305        cx.simulate_keystrokes("b");
5306        cx.assert_state("fooBarˇBaz", Mode::Normal);
5307        cx.simulate_keystrokes("b");
5308        cx.assert_state("fooˇBarBaz", Mode::Normal);
5309        cx.simulate_keystrokes("b");
5310        cx.assert_state("ˇfooBarBaz", Mode::Normal);
5311
5312        cx.set_state("foo;barˇ", Mode::Normal);
5313        cx.simulate_keystrokes("b");
5314        cx.assert_state("foo;ˇbar", Mode::Normal);
5315        cx.simulate_keystrokes("b");
5316        cx.assert_state("ˇfoo;bar", Mode::Normal);
5317
5318        cx.set_state("<?php\n\n$someVariable = 2ˇ;", Mode::Normal);
5319        cx.simulate_keystrokes("b");
5320        cx.assert_state("<?php\n\n$someVariable = ˇ2;", Mode::Normal);
5321        cx.simulate_keystrokes("b");
5322        cx.assert_state("<?php\n\n$someVariable ˇ= 2;", Mode::Normal);
5323        cx.simulate_keystrokes("b");
5324        cx.assert_state("<?php\n\n$someˇVariable = 2;", Mode::Normal);
5325        cx.simulate_keystrokes("b");
5326        cx.assert_state("<?php\n\n$ˇsomeVariable = 2;", Mode::Normal);
5327        cx.simulate_keystrokes("b");
5328        cx.assert_state("<?php\n\nˇ$someVariable = 2;", Mode::Normal);
5329        cx.simulate_keystrokes("b");
5330        cx.assert_state("<?php\nˇ\n$someVariable = 2;", Mode::Normal);
5331        cx.simulate_keystrokes("b");
5332        cx.assert_state("<?ˇphp\n\n$someVariable = 2;", Mode::Normal);
5333        cx.simulate_keystrokes("b");
5334        cx.assert_state("ˇ<?php\n\n$someVariable = 2;", Mode::Normal);
5335    }
5336
5337    #[gpui::test]
5338    async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
5339        let mut cx = VimTestContext::new(cx, true).await;
5340
5341        // Setup custom keybindings for subword motions so we can use the bindings
5342        // in `simulate_keystrokes`.
5343        cx.update(|_window, cx| {
5344            cx.bind_keys([KeyBinding::new(
5345                "g e",
5346                super::PreviousSubwordEnd {
5347                    ignore_punctuation: false,
5348                },
5349                None,
5350            )]);
5351        });
5352
5353        cx.set_state("foo.baˇr", Mode::Normal);
5354        cx.simulate_keystrokes("g e");
5355        cx.assert_state("fooˇ.bar", Mode::Normal);
5356        cx.simulate_keystrokes("g e");
5357        cx.assert_state("foˇo.bar", Mode::Normal);
5358
5359        cx.set_state("foo(barˇ)", Mode::Normal);
5360        cx.simulate_keystrokes("g e");
5361        cx.assert_state("foo(baˇr)", Mode::Normal);
5362        cx.simulate_keystrokes("g e");
5363        cx.assert_state("fooˇ(bar)", Mode::Normal);
5364        cx.simulate_keystrokes("g e");
5365        cx.assert_state("foˇo(bar)", Mode::Normal);
5366
5367        cx.set_state("foo_bar_baˇz", Mode::Normal);
5368        cx.simulate_keystrokes("g e");
5369        cx.assert_state("foo_baˇr_baz", Mode::Normal);
5370        cx.simulate_keystrokes("g e");
5371        cx.assert_state("foˇo_bar_baz", Mode::Normal);
5372
5373        cx.set_state("fooBarBaˇz", Mode::Normal);
5374        cx.simulate_keystrokes("g e");
5375        cx.assert_state("fooBaˇrBaz", Mode::Normal);
5376        cx.simulate_keystrokes("g e");
5377        cx.assert_state("foˇoBarBaz", Mode::Normal);
5378
5379        cx.set_state("foo;baˇr", Mode::Normal);
5380        cx.simulate_keystrokes("g e");
5381        cx.assert_state("fooˇ;bar", Mode::Normal);
5382        cx.simulate_keystrokes("g e");
5383        cx.assert_state("foˇo;bar", Mode::Normal);
5384    }
5385
5386    #[gpui::test]
5387    async fn test_method_motion_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
5388        let mut cx = VimTestContext::new(cx, true).await;
5389
5390        let diff_base = indoc! {r#"
5391            fn first() {
5392                println!("first");
5393                println!("removed line");
5394            }
5395
5396            fn second() {
5397                println!("second");
5398            }
5399
5400            fn third() {
5401                println!("third");
5402            }
5403        "#};
5404
5405        let current_text = indoc! {r#"
5406            fn first() {
5407                println!("first");
5408            }
5409
5410            fn second() {
5411                println!("second");
5412            }
5413
5414            fn third() {
5415                println!("third");
5416            }
5417        "#};
5418
5419        cx.set_state(&format!("ˇ{}", current_text), Mode::Normal);
5420        cx.set_head_text(diff_base);
5421        cx.update_editor(|editor, window, cx| {
5422            editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
5423        });
5424
5425        // When diff hunks are expanded, the deleted line from the diff base
5426        // appears in the MultiBuffer. The method motion should correctly
5427        // navigate to the second function even with this extra content.
5428        cx.simulate_keystrokes("] m");
5429        cx.assert_editor_state(indoc! {r#"
5430            fn first() {
5431                println!("first");
5432                println!("removed line");
5433            }
5434
5435            ˇfn second() {
5436                println!("second");
5437            }
5438
5439            fn third() {
5440                println!("third");
5441            }
5442        "#});
5443
5444        cx.simulate_keystrokes("] m");
5445        cx.assert_editor_state(indoc! {r#"
5446            fn first() {
5447                println!("first");
5448                println!("removed line");
5449            }
5450
5451            fn second() {
5452                println!("second");
5453            }
5454
5455            ˇfn third() {
5456                println!("third");
5457            }
5458        "#});
5459
5460        cx.simulate_keystrokes("[ m");
5461        cx.assert_editor_state(indoc! {r#"
5462            fn first() {
5463                println!("first");
5464                println!("removed line");
5465            }
5466
5467            ˇfn second() {
5468                println!("second");
5469            }
5470
5471            fn third() {
5472                println!("third");
5473            }
5474        "#});
5475
5476        cx.simulate_keystrokes("[ m");
5477        cx.assert_editor_state(indoc! {r#"
5478            ˇfn first() {
5479                println!("first");
5480                println!("removed line");
5481            }
5482
5483            fn second() {
5484                println!("second");
5485            }
5486
5487            fn third() {
5488                println!("third");
5489            }
5490        "#});
5491    }
5492
5493    #[gpui::test]
5494    async fn test_comment_motion_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
5495        let mut cx = VimTestContext::new(cx, true).await;
5496
5497        let diff_base = indoc! {r#"
5498            // first comment
5499            fn first() {
5500                // removed comment
5501                println!("first");
5502            }
5503
5504            // second comment
5505            fn second() { println!("second"); }
5506        "#};
5507
5508        let current_text = indoc! {r#"
5509            // first comment
5510            fn first() {
5511                println!("first");
5512            }
5513
5514            // second comment
5515            fn second() { println!("second"); }
5516        "#};
5517
5518        cx.set_state(&format!("ˇ{}", current_text), Mode::Normal);
5519        cx.set_head_text(diff_base);
5520        cx.update_editor(|editor, window, cx| {
5521            editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
5522        });
5523
5524        // The first `] /` (vim::NextComment) should go to the end of the first
5525        // comment.
5526        cx.simulate_keystrokes("] /");
5527        cx.assert_editor_state(indoc! {r#"
5528            // first commenˇt
5529            fn first() {
5530                // removed comment
5531                println!("first");
5532            }
5533
5534            // second comment
5535            fn second() { println!("second"); }
5536        "#});
5537
5538        // The next `] /` (vim::NextComment) should go to the end of the second
5539        // comment, skipping over the removed comment, since it's not in the
5540        // actual buffer.
5541        cx.simulate_keystrokes("] /");
5542        cx.assert_editor_state(indoc! {r#"
5543            // first comment
5544            fn first() {
5545                // removed comment
5546                println!("first");
5547            }
5548
5549            // second commenˇt
5550            fn second() { println!("second"); }
5551        "#});
5552
5553        // Going back to previous comment with `[ /` (vim::PreviousComment)
5554        // should go back to the start of the second comment.
5555        cx.simulate_keystrokes("[ /");
5556        cx.assert_editor_state(indoc! {r#"
5557            // first comment
5558            fn first() {
5559                // removed comment
5560                println!("first");
5561            }
5562
5563            ˇ// second comment
5564            fn second() { println!("second"); }
5565        "#});
5566
5567        // Going back again with `[ /` (vim::PreviousComment) should finally put
5568        // the cursor at the start of the first comment.
5569        cx.simulate_keystrokes("[ /");
5570        cx.assert_editor_state(indoc! {r#"
5571            ˇ// first comment
5572            fn first() {
5573                // removed comment
5574                println!("first");
5575            }
5576
5577            // second comment
5578            fn second() { println!("second"); }
5579        "#});
5580    }
5581}