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