motion.rs

   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) fn for_mode(mode: Mode) -> Self {
  32        match mode {
  33            Mode::VisualLine => MotionKind::Linewise,
  34            _ => MotionKind::Exclusive,
  35        }
  36    }
  37
  38    pub(crate) fn linewise(&self) -> bool {
  39        matches!(self, MotionKind::Linewise)
  40    }
  41}
  42
  43#[derive(Clone, Debug, PartialEq, Eq)]
  44pub enum Motion {
  45    Left,
  46    WrappingLeft,
  47    Down {
  48        display_lines: bool,
  49    },
  50    Up {
  51        display_lines: bool,
  52    },
  53    Right,
  54    WrappingRight,
  55    NextWordStart {
  56        ignore_punctuation: bool,
  57    },
  58    NextWordEnd {
  59        ignore_punctuation: bool,
  60    },
  61    PreviousWordStart {
  62        ignore_punctuation: bool,
  63    },
  64    PreviousWordEnd {
  65        ignore_punctuation: bool,
  66    },
  67    NextSubwordStart {
  68        ignore_punctuation: bool,
  69    },
  70    NextSubwordEnd {
  71        ignore_punctuation: bool,
  72    },
  73    PreviousSubwordStart {
  74        ignore_punctuation: bool,
  75    },
  76    PreviousSubwordEnd {
  77        ignore_punctuation: bool,
  78    },
  79    FirstNonWhitespace {
  80        display_lines: bool,
  81    },
  82    CurrentLine,
  83    StartOfLine {
  84        display_lines: bool,
  85    },
  86    MiddleOfLine {
  87        display_lines: bool,
  88    },
  89    EndOfLine {
  90        display_lines: bool,
  91    },
  92    SentenceBackward,
  93    SentenceForward,
  94    StartOfParagraph,
  95    EndOfParagraph,
  96    StartOfDocument,
  97    EndOfDocument,
  98    Matching,
  99    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    fn skip_exclusive_special_case(&self) -> bool {
 813        matches!(self, Motion::WrappingLeft | Motion::WrappingRight)
 814    }
 815
 816    pub(crate) 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 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 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
3018fn 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, &current_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, &current_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::Inlay;
3087    use indoc::indoc;
3088    use language::Point;
3089    use multi_buffer::MultiBufferRow;
3090    use text::Rope;
3091
3092    #[gpui::test]
3093    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3094        let mut cx = NeovimBackedTestContext::new(cx).await;
3095
3096        let initial_state = indoc! {r"ˇabc
3097            def
3098
3099            paragraph
3100            the second
3101
3102
3103
3104            third and
3105            final"};
3106
3107        // goes down once
3108        cx.set_shared_state(initial_state).await;
3109        cx.simulate_shared_keystrokes("}").await;
3110        cx.shared_state().await.assert_eq(indoc! {r"abc
3111            def
3112            ˇ
3113            paragraph
3114            the second
3115
3116
3117
3118            third and
3119            final"});
3120
3121        // goes up once
3122        cx.simulate_shared_keystrokes("{").await;
3123        cx.shared_state().await.assert_eq(initial_state);
3124
3125        // goes down twice
3126        cx.simulate_shared_keystrokes("2 }").await;
3127        cx.shared_state().await.assert_eq(indoc! {r"abc
3128            def
3129
3130            paragraph
3131            the second
3132            ˇ
3133
3134
3135            third and
3136            final"});
3137
3138        // goes down over multiple blanks
3139        cx.simulate_shared_keystrokes("}").await;
3140        cx.shared_state().await.assert_eq(indoc! {r"abc
3141                def
3142
3143                paragraph
3144                the second
3145
3146
3147
3148                third and
3149                finaˇl"});
3150
3151        // goes up twice
3152        cx.simulate_shared_keystrokes("2 {").await;
3153        cx.shared_state().await.assert_eq(indoc! {r"abc
3154                def
3155                ˇ
3156                paragraph
3157                the second
3158
3159
3160
3161                third and
3162                final"});
3163    }
3164
3165    #[gpui::test]
3166    async fn test_matching(cx: &mut gpui::TestAppContext) {
3167        let mut cx = NeovimBackedTestContext::new(cx).await;
3168
3169        cx.set_shared_state(indoc! {r"func ˇ(a string) {
3170                do(something(with<Types>.and_arrays[0, 2]))
3171            }"})
3172            .await;
3173        cx.simulate_shared_keystrokes("%").await;
3174        cx.shared_state()
3175            .await
3176            .assert_eq(indoc! {r"func (a stringˇ) {
3177                do(something(with<Types>.and_arrays[0, 2]))
3178            }"});
3179
3180        // test it works on the last character of the line
3181        cx.set_shared_state(indoc! {r"func (a string) ˇ{
3182            do(something(with<Types>.and_arrays[0, 2]))
3183            }"})
3184            .await;
3185        cx.simulate_shared_keystrokes("%").await;
3186        cx.shared_state()
3187            .await
3188            .assert_eq(indoc! {r"func (a string) {
3189            do(something(with<Types>.and_arrays[0, 2]))
3190            ˇ}"});
3191
3192        // test it works on immediate nesting
3193        cx.set_shared_state("ˇ{()}").await;
3194        cx.simulate_shared_keystrokes("%").await;
3195        cx.shared_state().await.assert_eq("{()ˇ}");
3196        cx.simulate_shared_keystrokes("%").await;
3197        cx.shared_state().await.assert_eq("ˇ{()}");
3198
3199        // test it works on immediate nesting inside braces
3200        cx.set_shared_state("{\n    ˇ{()}\n}").await;
3201        cx.simulate_shared_keystrokes("%").await;
3202        cx.shared_state().await.assert_eq("{\n    {()ˇ}\n}");
3203
3204        // test it jumps to the next paren on a line
3205        cx.set_shared_state("func ˇboop() {\n}").await;
3206        cx.simulate_shared_keystrokes("%").await;
3207        cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3208    }
3209
3210    #[gpui::test]
3211    async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3212        let mut cx = NeovimBackedTestContext::new(cx).await;
3213
3214        // test it works with curly braces
3215        cx.set_shared_state(indoc! {r"func (a string) {
3216                do(something(with<Types>.anˇd_arrays[0, 2]))
3217            }"})
3218            .await;
3219        cx.simulate_shared_keystrokes("] }").await;
3220        cx.shared_state()
3221            .await
3222            .assert_eq(indoc! {r"func (a string) {
3223                do(something(with<Types>.and_arrays[0, 2]))
3224            ˇ}"});
3225
3226        // test it works with brackets
3227        cx.set_shared_state(indoc! {r"func (a string) {
3228                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3229            }"})
3230            .await;
3231        cx.simulate_shared_keystrokes("] )").await;
3232        cx.shared_state()
3233            .await
3234            .assert_eq(indoc! {r"func (a string) {
3235                do(something(with<Types>.and_arrays[0, 2])ˇ)
3236            }"});
3237
3238        cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3239            .await;
3240        cx.simulate_shared_keystrokes("] )").await;
3241        cx.shared_state()
3242            .await
3243            .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3244
3245        // test it works on immediate nesting
3246        cx.set_shared_state("{ˇ {}{}}").await;
3247        cx.simulate_shared_keystrokes("] }").await;
3248        cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3249        cx.set_shared_state("(ˇ ()())").await;
3250        cx.simulate_shared_keystrokes("] )").await;
3251        cx.shared_state().await.assert_eq("( ()()ˇ)");
3252
3253        // test it works on immediate nesting inside braces
3254        cx.set_shared_state("{\n    ˇ {()}\n}").await;
3255        cx.simulate_shared_keystrokes("] }").await;
3256        cx.shared_state().await.assert_eq("{\n     {()}\nˇ}");
3257        cx.set_shared_state("(\n    ˇ {()}\n)").await;
3258        cx.simulate_shared_keystrokes("] )").await;
3259        cx.shared_state().await.assert_eq("(\n     {()}\nˇ)");
3260    }
3261
3262    #[gpui::test]
3263    async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3264        let mut cx = NeovimBackedTestContext::new(cx).await;
3265
3266        // test it works with curly braces
3267        cx.set_shared_state(indoc! {r"func (a string) {
3268                do(something(with<Types>.anˇd_arrays[0, 2]))
3269            }"})
3270            .await;
3271        cx.simulate_shared_keystrokes("[ {").await;
3272        cx.shared_state()
3273            .await
3274            .assert_eq(indoc! {r"func (a string) ˇ{
3275                do(something(with<Types>.and_arrays[0, 2]))
3276            }"});
3277
3278        // test it works with brackets
3279        cx.set_shared_state(indoc! {r"func (a string) {
3280                do(somethiˇng(with<Types>.and_arrays[0, 2]))
3281            }"})
3282            .await;
3283        cx.simulate_shared_keystrokes("[ (").await;
3284        cx.shared_state()
3285            .await
3286            .assert_eq(indoc! {r"func (a string) {
3287                doˇ(something(with<Types>.and_arrays[0, 2]))
3288            }"});
3289
3290        // test it works on immediate nesting
3291        cx.set_shared_state("{{}{} ˇ }").await;
3292        cx.simulate_shared_keystrokes("[ {").await;
3293        cx.shared_state().await.assert_eq("ˇ{{}{}  }");
3294        cx.set_shared_state("(()() ˇ )").await;
3295        cx.simulate_shared_keystrokes("[ (").await;
3296        cx.shared_state().await.assert_eq("ˇ(()()  )");
3297
3298        // test it works on immediate nesting inside braces
3299        cx.set_shared_state("{\n    {()} ˇ\n}").await;
3300        cx.simulate_shared_keystrokes("[ {").await;
3301        cx.shared_state().await.assert_eq("ˇ{\n    {()} \n}");
3302        cx.set_shared_state("(\n    {()} ˇ\n)").await;
3303        cx.simulate_shared_keystrokes("[ (").await;
3304        cx.shared_state().await.assert_eq("ˇ(\n    {()} \n)");
3305    }
3306
3307    #[gpui::test]
3308    async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3309        let mut cx = NeovimBackedTestContext::new_html(cx).await;
3310
3311        cx.neovim.exec("set filetype=html").await;
3312
3313        cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3314        cx.simulate_shared_keystrokes("%").await;
3315        cx.shared_state()
3316            .await
3317            .assert_eq(indoc! {r"<body><ˇ/body>"});
3318        cx.simulate_shared_keystrokes("%").await;
3319
3320        // test jumping backwards
3321        cx.shared_state()
3322            .await
3323            .assert_eq(indoc! {r"<ˇbody></body>"});
3324
3325        // test self-closing tags
3326        cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3327        cx.simulate_shared_keystrokes("%").await;
3328        cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3329
3330        // test tag with attributes
3331        cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3332            </div>
3333            "})
3334            .await;
3335        cx.simulate_shared_keystrokes("%").await;
3336        cx.shared_state()
3337            .await
3338            .assert_eq(indoc! {r"<div class='test' id='main'>
3339            <ˇ/div>
3340            "});
3341
3342        // test multi-line self-closing tag
3343        cx.set_shared_state(indoc! {r#"<a>
3344            <br
3345                test = "test"
3346            /ˇ>
3347        </a>"#})
3348            .await;
3349        cx.simulate_shared_keystrokes("%").await;
3350        cx.shared_state().await.assert_eq(indoc! {r#"<a>
3351            ˇ<br
3352                test = "test"
3353            />
3354        </a>"#});
3355    }
3356
3357    #[gpui::test]
3358    async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3359        let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3360
3361        // test brackets within tags
3362        cx.set_shared_state(indoc! {r"function f() {
3363            return (
3364                <div rules={ˇ[{ a: 1 }]}>
3365                    <h1>test</h1>
3366                </div>
3367            );
3368        }"})
3369            .await;
3370        cx.simulate_shared_keystrokes("%").await;
3371        cx.shared_state().await.assert_eq(indoc! {r"function f() {
3372            return (
3373                <div rules={[{ a: 1 }ˇ]}>
3374                    <h1>test</h1>
3375                </div>
3376            );
3377        }"});
3378    }
3379
3380    #[gpui::test]
3381    async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
3382        let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
3383
3384        cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
3385            .await;
3386        cx.simulate_shared_keystrokes("%").await;
3387        cx.shared_state()
3388            .await
3389            .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
3390        cx.simulate_shared_keystrokes("%").await;
3391        cx.shared_state()
3392            .await
3393            .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
3394    }
3395
3396    #[gpui::test]
3397    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3398        let mut cx = NeovimBackedTestContext::new(cx).await;
3399
3400        // f and F
3401        cx.set_shared_state("ˇone two three four").await;
3402        cx.simulate_shared_keystrokes("f o").await;
3403        cx.shared_state().await.assert_eq("one twˇo three four");
3404        cx.simulate_shared_keystrokes(",").await;
3405        cx.shared_state().await.assert_eq("ˇone two three four");
3406        cx.simulate_shared_keystrokes("2 ;").await;
3407        cx.shared_state().await.assert_eq("one two three fˇour");
3408        cx.simulate_shared_keystrokes("shift-f e").await;
3409        cx.shared_state().await.assert_eq("one two threˇe four");
3410        cx.simulate_shared_keystrokes("2 ;").await;
3411        cx.shared_state().await.assert_eq("onˇe two three four");
3412        cx.simulate_shared_keystrokes(",").await;
3413        cx.shared_state().await.assert_eq("one two thrˇee four");
3414
3415        // t and T
3416        cx.set_shared_state("ˇone two three four").await;
3417        cx.simulate_shared_keystrokes("t o").await;
3418        cx.shared_state().await.assert_eq("one tˇwo three four");
3419        cx.simulate_shared_keystrokes(",").await;
3420        cx.shared_state().await.assert_eq("oˇne two three four");
3421        cx.simulate_shared_keystrokes("2 ;").await;
3422        cx.shared_state().await.assert_eq("one two three ˇfour");
3423        cx.simulate_shared_keystrokes("shift-t e").await;
3424        cx.shared_state().await.assert_eq("one two threeˇ four");
3425        cx.simulate_shared_keystrokes("3 ;").await;
3426        cx.shared_state().await.assert_eq("oneˇ two three four");
3427        cx.simulate_shared_keystrokes(",").await;
3428        cx.shared_state().await.assert_eq("one two thˇree four");
3429    }
3430
3431    #[gpui::test]
3432    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3433        let mut cx = NeovimBackedTestContext::new(cx).await;
3434        let initial_state = indoc! {r"something(ˇfoo)"};
3435        cx.set_shared_state(initial_state).await;
3436        cx.simulate_shared_keystrokes("}").await;
3437        cx.shared_state().await.assert_eq("something(fooˇ)");
3438    }
3439
3440    #[gpui::test]
3441    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3442        let mut cx = NeovimBackedTestContext::new(cx).await;
3443        cx.set_shared_state("ˇone\n  two\nthree").await;
3444        cx.simulate_shared_keystrokes("enter").await;
3445        cx.shared_state().await.assert_eq("one\n  ˇtwo\nthree");
3446    }
3447
3448    #[gpui::test]
3449    async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3450        let mut cx = NeovimBackedTestContext::new(cx).await;
3451        cx.set_shared_state("ˇ one\n two \nthree").await;
3452        cx.simulate_shared_keystrokes("g _").await;
3453        cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3454
3455        cx.set_shared_state("ˇ one \n two \nthree").await;
3456        cx.simulate_shared_keystrokes("g _").await;
3457        cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3458        cx.simulate_shared_keystrokes("2 g _").await;
3459        cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3460    }
3461
3462    #[gpui::test]
3463    async fn test_window_top(cx: &mut gpui::TestAppContext) {
3464        let mut cx = NeovimBackedTestContext::new(cx).await;
3465        let initial_state = indoc! {r"abc
3466          def
3467          paragraph
3468          the second
3469          third ˇand
3470          final"};
3471
3472        cx.set_shared_state(initial_state).await;
3473        cx.simulate_shared_keystrokes("shift-h").await;
3474        cx.shared_state().await.assert_eq(indoc! {r"abˇc
3475          def
3476          paragraph
3477          the second
3478          third and
3479          final"});
3480
3481        // clip point
3482        cx.set_shared_state(indoc! {r"
3483          1 2 3
3484          4 5 6
3485          7 8 ˇ9
3486          "})
3487            .await;
3488        cx.simulate_shared_keystrokes("shift-h").await;
3489        cx.shared_state().await.assert_eq(indoc! {"
3490          1 2 ˇ3
3491          4 5 6
3492          7 8 9
3493          "});
3494
3495        cx.set_shared_state(indoc! {r"
3496          1 2 3
3497          4 5 6
3498          ˇ7 8 9
3499          "})
3500            .await;
3501        cx.simulate_shared_keystrokes("shift-h").await;
3502        cx.shared_state().await.assert_eq(indoc! {"
3503          ˇ1 2 3
3504          4 5 6
3505          7 8 9
3506          "});
3507
3508        cx.set_shared_state(indoc! {r"
3509          1 2 3
3510          4 5 ˇ6
3511          7 8 9"})
3512            .await;
3513        cx.simulate_shared_keystrokes("9 shift-h").await;
3514        cx.shared_state().await.assert_eq(indoc! {"
3515          1 2 3
3516          4 5 6
3517          7 8 ˇ9"});
3518    }
3519
3520    #[gpui::test]
3521    async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3522        let mut cx = NeovimBackedTestContext::new(cx).await;
3523        let initial_state = indoc! {r"abˇc
3524          def
3525          paragraph
3526          the second
3527          third and
3528          final"};
3529
3530        cx.set_shared_state(initial_state).await;
3531        cx.simulate_shared_keystrokes("shift-m").await;
3532        cx.shared_state().await.assert_eq(indoc! {r"abc
3533          def
3534          paˇragraph
3535          the second
3536          third and
3537          final"});
3538
3539        cx.set_shared_state(indoc! {r"
3540          1 2 3
3541          4 5 6
3542          7 8 ˇ9
3543          "})
3544            .await;
3545        cx.simulate_shared_keystrokes("shift-m").await;
3546        cx.shared_state().await.assert_eq(indoc! {"
3547          1 2 3
3548          4 5 ˇ6
3549          7 8 9
3550          "});
3551        cx.set_shared_state(indoc! {r"
3552          1 2 3
3553          4 5 6
3554          ˇ7 8 9
3555          "})
3556            .await;
3557        cx.simulate_shared_keystrokes("shift-m").await;
3558        cx.shared_state().await.assert_eq(indoc! {"
3559          1 2 3
3560          ˇ4 5 6
3561          7 8 9
3562          "});
3563        cx.set_shared_state(indoc! {r"
3564          ˇ1 2 3
3565          4 5 6
3566          7 8 9
3567          "})
3568            .await;
3569        cx.simulate_shared_keystrokes("shift-m").await;
3570        cx.shared_state().await.assert_eq(indoc! {"
3571          1 2 3
3572          ˇ4 5 6
3573          7 8 9
3574          "});
3575        cx.set_shared_state(indoc! {r"
3576          1 2 3
3577          ˇ4 5 6
3578          7 8 9
3579          "})
3580            .await;
3581        cx.simulate_shared_keystrokes("shift-m").await;
3582        cx.shared_state().await.assert_eq(indoc! {"
3583          1 2 3
3584          ˇ4 5 6
3585          7 8 9
3586          "});
3587        cx.set_shared_state(indoc! {r"
3588          1 2 3
3589          4 5 ˇ6
3590          7 8 9
3591          "})
3592            .await;
3593        cx.simulate_shared_keystrokes("shift-m").await;
3594        cx.shared_state().await.assert_eq(indoc! {"
3595          1 2 3
3596          4 5 ˇ6
3597          7 8 9
3598          "});
3599    }
3600
3601    #[gpui::test]
3602    async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3603        let mut cx = NeovimBackedTestContext::new(cx).await;
3604        let initial_state = indoc! {r"abc
3605          deˇf
3606          paragraph
3607          the second
3608          third and
3609          final"};
3610
3611        cx.set_shared_state(initial_state).await;
3612        cx.simulate_shared_keystrokes("shift-l").await;
3613        cx.shared_state().await.assert_eq(indoc! {r"abc
3614          def
3615          paragraph
3616          the second
3617          third and
3618          fiˇnal"});
3619
3620        cx.set_shared_state(indoc! {r"
3621          1 2 3
3622          4 5 ˇ6
3623          7 8 9
3624          "})
3625            .await;
3626        cx.simulate_shared_keystrokes("shift-l").await;
3627        cx.shared_state().await.assert_eq(indoc! {"
3628          1 2 3
3629          4 5 6
3630          7 8 9
3631          ˇ"});
3632
3633        cx.set_shared_state(indoc! {r"
3634          1 2 3
3635          ˇ4 5 6
3636          7 8 9
3637          "})
3638            .await;
3639        cx.simulate_shared_keystrokes("shift-l").await;
3640        cx.shared_state().await.assert_eq(indoc! {"
3641          1 2 3
3642          4 5 6
3643          7 8 9
3644          ˇ"});
3645
3646        cx.set_shared_state(indoc! {r"
3647          1 2 ˇ3
3648          4 5 6
3649          7 8 9
3650          "})
3651            .await;
3652        cx.simulate_shared_keystrokes("shift-l").await;
3653        cx.shared_state().await.assert_eq(indoc! {"
3654          1 2 3
3655          4 5 6
3656          7 8 9
3657          ˇ"});
3658
3659        cx.set_shared_state(indoc! {r"
3660          ˇ1 2 3
3661          4 5 6
3662          7 8 9
3663          "})
3664            .await;
3665        cx.simulate_shared_keystrokes("shift-l").await;
3666        cx.shared_state().await.assert_eq(indoc! {"
3667          1 2 3
3668          4 5 6
3669          7 8 9
3670          ˇ"});
3671
3672        cx.set_shared_state(indoc! {r"
3673          1 2 3
3674          4 5 ˇ6
3675          7 8 9
3676          "})
3677            .await;
3678        cx.simulate_shared_keystrokes("9 shift-l").await;
3679        cx.shared_state().await.assert_eq(indoc! {"
3680          1 2 ˇ3
3681          4 5 6
3682          7 8 9
3683          "});
3684    }
3685
3686    #[gpui::test]
3687    async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3688        let mut cx = NeovimBackedTestContext::new(cx).await;
3689        cx.set_shared_state(indoc! {r"
3690        456 5ˇ67 678
3691        "})
3692            .await;
3693        cx.simulate_shared_keystrokes("g e").await;
3694        cx.shared_state().await.assert_eq(indoc! {"
3695        45ˇ6 567 678
3696        "});
3697
3698        // Test times
3699        cx.set_shared_state(indoc! {r"
3700        123 234 345
3701        456 5ˇ67 678
3702        "})
3703            .await;
3704        cx.simulate_shared_keystrokes("4 g e").await;
3705        cx.shared_state().await.assert_eq(indoc! {"
3706        12ˇ3 234 345
3707        456 567 678
3708        "});
3709
3710        // With punctuation
3711        cx.set_shared_state(indoc! {r"
3712        123 234 345
3713        4;5.6 5ˇ67 678
3714        789 890 901
3715        "})
3716            .await;
3717        cx.simulate_shared_keystrokes("g e").await;
3718        cx.shared_state().await.assert_eq(indoc! {"
3719          123 234 345
3720          4;5.ˇ6 567 678
3721          789 890 901
3722        "});
3723
3724        // With punctuation and count
3725        cx.set_shared_state(indoc! {r"
3726        123 234 345
3727        4;5.6 5ˇ67 678
3728        789 890 901
3729        "})
3730            .await;
3731        cx.simulate_shared_keystrokes("5 g e").await;
3732        cx.shared_state().await.assert_eq(indoc! {"
3733          123 234 345
3734          ˇ4;5.6 567 678
3735          789 890 901
3736        "});
3737
3738        // newlines
3739        cx.set_shared_state(indoc! {r"
3740        123 234 345
3741
3742        78ˇ9 890 901
3743        "})
3744            .await;
3745        cx.simulate_shared_keystrokes("g e").await;
3746        cx.shared_state().await.assert_eq(indoc! {"
3747          123 234 345
3748          ˇ
3749          789 890 901
3750        "});
3751        cx.simulate_shared_keystrokes("g e").await;
3752        cx.shared_state().await.assert_eq(indoc! {"
3753          123 234 34ˇ5
3754
3755          789 890 901
3756        "});
3757
3758        // With punctuation
3759        cx.set_shared_state(indoc! {r"
3760        123 234 345
3761        4;5.ˇ6 567 678
3762        789 890 901
3763        "})
3764            .await;
3765        cx.simulate_shared_keystrokes("g shift-e").await;
3766        cx.shared_state().await.assert_eq(indoc! {"
3767          123 234 34ˇ5
3768          4;5.6 567 678
3769          789 890 901
3770        "});
3771
3772        // With multi byte char
3773        cx.set_shared_state(indoc! {r"
3774        bar ˇó
3775        "})
3776            .await;
3777        cx.simulate_shared_keystrokes("g e").await;
3778        cx.shared_state().await.assert_eq(indoc! {"
3779        baˇr ó
3780        "});
3781    }
3782
3783    #[gpui::test]
3784    async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3785        let mut cx = NeovimBackedTestContext::new(cx).await;
3786
3787        cx.set_shared_state(indoc! {"
3788            fn aˇ() {
3789              return
3790            }
3791        "})
3792            .await;
3793        cx.simulate_shared_keystrokes("v $ %").await;
3794        cx.shared_state().await.assert_eq(indoc! {"
3795            fn a«() {
3796              return
3797            }ˇ»
3798        "});
3799    }
3800
3801    #[gpui::test]
3802    async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3803        let mut cx = VimTestContext::new(cx, true).await;
3804
3805        cx.set_state(
3806            indoc! {"
3807                struct Foo {
3808                ˇ
3809                }
3810            "},
3811            Mode::Normal,
3812        );
3813
3814        cx.update_editor(|editor, _window, cx| {
3815            let range = editor.selections.newest_anchor().range();
3816            let inlay_text = "  field: int,\n  field2: string\n  field3: float";
3817            let inlay = Inlay::edit_prediction(1, range.start, Rope::from_str_small(inlay_text));
3818            editor.splice_inlays(&[], vec![inlay], cx);
3819        });
3820
3821        cx.simulate_keystrokes("j");
3822        cx.assert_state(
3823            indoc! {"
3824                struct Foo {
3825
3826                ˇ}
3827            "},
3828            Mode::Normal,
3829        );
3830    }
3831
3832    #[gpui::test]
3833    async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3834        let mut cx = VimTestContext::new(cx, true).await;
3835
3836        cx.set_state(
3837            indoc! {"
3838            ˇstruct Foo {
3839
3840            }
3841        "},
3842            Mode::Normal,
3843        );
3844        cx.update_editor(|editor, _window, cx| {
3845            let snapshot = editor.buffer().read(cx).snapshot(cx);
3846            let end_of_line =
3847                snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3848            let inlay_text = " hint";
3849            let inlay = Inlay::edit_prediction(1, end_of_line, Rope::from_str_small(inlay_text));
3850            editor.splice_inlays(&[], vec![inlay], cx);
3851        });
3852        cx.simulate_keystrokes("$");
3853        cx.assert_state(
3854            indoc! {"
3855            struct Foo ˇ{
3856
3857            }
3858        "},
3859            Mode::Normal,
3860        );
3861    }
3862
3863    #[gpui::test]
3864    async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
3865        let mut cx = VimTestContext::new(cx, true).await;
3866
3867        // Test the exact scenario from issue #29134
3868        cx.set_state(
3869            indoc! {"
3870                fn main() {
3871                    let this_is_a_long_name = Vec::<u32>::new();
3872                    let new_oneˇ = this_is_a_long_name
3873                        .iter()
3874                        .map(|i| i + 1)
3875                        .map(|i| i * 2)
3876                        .collect::<Vec<_>>();
3877                }
3878            "},
3879            Mode::Normal,
3880        );
3881
3882        // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
3883        cx.update_editor(|editor, _window, cx| {
3884            let snapshot = editor.buffer().read(cx).snapshot(cx);
3885            // The empty line is at line 3 (0-indexed)
3886            let line_start = snapshot.anchor_after(Point::new(3, 0));
3887            let inlay_text = ": Vec<u32>";
3888            let inlay = Inlay::edit_prediction(1, line_start, Rope::from_str_small(inlay_text));
3889            editor.splice_inlays(&[], vec![inlay], cx);
3890        });
3891
3892        // Enter visual mode
3893        cx.simulate_keystrokes("v");
3894        cx.assert_state(
3895            indoc! {"
3896                fn main() {
3897                    let this_is_a_long_name = Vec::<u32>::new();
3898                    let new_one« ˇ»= this_is_a_long_name
3899                        .iter()
3900                        .map(|i| i + 1)
3901                        .map(|i| i * 2)
3902                        .collect::<Vec<_>>();
3903                }
3904            "},
3905            Mode::Visual,
3906        );
3907
3908        // Move down - should go to the beginning of line 4, not skip to line 5
3909        cx.simulate_keystrokes("j");
3910        cx.assert_state(
3911            indoc! {"
3912                fn main() {
3913                    let this_is_a_long_name = Vec::<u32>::new();
3914                    let new_one« = this_is_a_long_name
3915                      ˇ»  .iter()
3916                        .map(|i| i + 1)
3917                        .map(|i| i * 2)
3918                        .collect::<Vec<_>>();
3919                }
3920            "},
3921            Mode::Visual,
3922        );
3923
3924        // Test with multiple movements
3925        cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
3926
3927        // Add type hint on the empty line
3928        cx.update_editor(|editor, _window, cx| {
3929            let snapshot = editor.buffer().read(cx).snapshot(cx);
3930            let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
3931            let inlay_text = ": i32";
3932            let inlay =
3933                Inlay::edit_prediction(2, empty_line_start, Rope::from_str_small(inlay_text));
3934            editor.splice_inlays(&[], vec![inlay], cx);
3935        });
3936
3937        // Enter visual mode and move down twice
3938        cx.simulate_keystrokes("v j j");
3939        cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
3940    }
3941
3942    #[gpui::test]
3943    async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3944        let mut cx = NeovimBackedTestContext::new(cx).await;
3945        // Normal mode
3946        cx.set_shared_state(indoc! {"
3947            The ˇquick brown
3948            fox jumps over
3949            the lazy dog
3950            The quick brown
3951            fox jumps over
3952            the lazy dog
3953            The quick brown
3954            fox jumps over
3955            the lazy dog"})
3956            .await;
3957        cx.simulate_shared_keystrokes("2 0 %").await;
3958        cx.shared_state().await.assert_eq(indoc! {"
3959            The quick brown
3960            fox ˇjumps over
3961            the lazy dog
3962            The quick brown
3963            fox jumps over
3964            the lazy dog
3965            The quick brown
3966            fox jumps over
3967            the lazy dog"});
3968
3969        cx.simulate_shared_keystrokes("2 5 %").await;
3970        cx.shared_state().await.assert_eq(indoc! {"
3971            The quick brown
3972            fox jumps over
3973            the ˇlazy dog
3974            The quick brown
3975            fox jumps over
3976            the lazy dog
3977            The quick brown
3978            fox jumps over
3979            the lazy dog"});
3980
3981        cx.simulate_shared_keystrokes("7 5 %").await;
3982        cx.shared_state().await.assert_eq(indoc! {"
3983            The quick brown
3984            fox jumps over
3985            the lazy dog
3986            The quick brown
3987            fox jumps over
3988            the lazy dog
3989            The ˇquick brown
3990            fox jumps over
3991            the lazy dog"});
3992
3993        // Visual mode
3994        cx.set_shared_state(indoc! {"
3995            The ˇquick brown
3996            fox jumps over
3997            the lazy dog
3998            The quick brown
3999            fox jumps over
4000            the lazy dog
4001            The quick brown
4002            fox jumps over
4003            the lazy dog"})
4004            .await;
4005        cx.simulate_shared_keystrokes("v 5 0 %").await;
4006        cx.shared_state().await.assert_eq(indoc! {"
4007            The «quick brown
4008            fox jumps over
4009            the lazy dog
4010            The quick brown
4011            fox jˇ»umps over
4012            the lazy dog
4013            The quick brown
4014            fox jumps over
4015            the lazy dog"});
4016
4017        cx.set_shared_state(indoc! {"
4018            The ˇquick brown
4019            fox jumps over
4020            the lazy dog
4021            The quick brown
4022            fox jumps over
4023            the lazy dog
4024            The quick brown
4025            fox jumps over
4026            the lazy dog"})
4027            .await;
4028        cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4029        cx.shared_state().await.assert_eq(indoc! {"
4030            The «quick brown
4031            fox jumps over
4032            the lazy dog
4033            The quick brown
4034            fox jumps over
4035            the lazy dog
4036            The quick brown
4037            fox jumps over
4038            the lˇ»azy dog"});
4039    }
4040
4041    #[gpui::test]
4042    async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4043        let mut cx = NeovimBackedTestContext::new(cx).await;
4044
4045        cx.set_shared_state("ˇπππππ").await;
4046        cx.simulate_shared_keystrokes("3 space").await;
4047        cx.shared_state().await.assert_eq("πππˇππ");
4048    }
4049
4050    #[gpui::test]
4051    async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4052        let mut cx = NeovimBackedTestContext::new(cx).await;
4053
4054        cx.set_shared_state(indoc! {"
4055            ππππˇπ
4056            πanotherline"})
4057            .await;
4058        cx.simulate_shared_keystrokes("4 space").await;
4059        cx.shared_state().await.assert_eq(indoc! {"
4060            πππππ
4061            πanˇotherline"});
4062    }
4063
4064    #[gpui::test]
4065    async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4066        let mut cx = NeovimBackedTestContext::new(cx).await;
4067
4068        cx.set_shared_state(indoc! {"
4069                        ππππ
4070                        πanˇotherline"})
4071            .await;
4072        cx.simulate_shared_keystrokes("4 backspace").await;
4073        cx.shared_state().await.assert_eq(indoc! {"
4074                        πππˇπ
4075                        πanotherline"});
4076    }
4077
4078    #[gpui::test]
4079    async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4080        let mut cx = VimTestContext::new(cx, true).await;
4081        cx.set_state(
4082            indoc! {
4083                "func empty(a string) bool {
4084                     ˇif a == \"\" {
4085                         return true
4086                     }
4087                     return false
4088                }"
4089            },
4090            Mode::Normal,
4091        );
4092        cx.simulate_keystrokes("[ -");
4093        cx.assert_state(
4094            indoc! {
4095                "ˇfunc empty(a string) bool {
4096                     if a == \"\" {
4097                         return true
4098                     }
4099                     return false
4100                }"
4101            },
4102            Mode::Normal,
4103        );
4104        cx.simulate_keystrokes("] =");
4105        cx.assert_state(
4106            indoc! {
4107                "func empty(a string) bool {
4108                     if a == \"\" {
4109                         return true
4110                     }
4111                     return false
4112                ˇ}"
4113            },
4114            Mode::Normal,
4115        );
4116        cx.simulate_keystrokes("[ +");
4117        cx.assert_state(
4118            indoc! {
4119                "func empty(a string) bool {
4120                     if a == \"\" {
4121                         return true
4122                     }
4123                     ˇreturn false
4124                }"
4125            },
4126            Mode::Normal,
4127        );
4128        cx.simulate_keystrokes("2 [ =");
4129        cx.assert_state(
4130            indoc! {
4131                "func empty(a string) bool {
4132                     ˇif a == \"\" {
4133                         return true
4134                     }
4135                     return false
4136                }"
4137            },
4138            Mode::Normal,
4139        );
4140        cx.simulate_keystrokes("] +");
4141        cx.assert_state(
4142            indoc! {
4143                "func empty(a string) bool {
4144                     if a == \"\" {
4145                         ˇreturn true
4146                     }
4147                     return false
4148                }"
4149            },
4150            Mode::Normal,
4151        );
4152        cx.simulate_keystrokes("] -");
4153        cx.assert_state(
4154            indoc! {
4155                "func empty(a string) bool {
4156                     if a == \"\" {
4157                         return true
4158                     ˇ}
4159                     return false
4160                }"
4161            },
4162            Mode::Normal,
4163        );
4164    }
4165
4166    #[gpui::test]
4167    async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4168        let mut cx = NeovimBackedTestContext::new(cx).await;
4169        cx.set_shared_state("abˇc").await;
4170        cx.simulate_shared_keystrokes("delete").await;
4171        cx.shared_state().await.assert_eq("aˇb");
4172    }
4173
4174    #[gpui::test]
4175    async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4176        let mut cx = NeovimBackedTestContext::new(cx).await;
4177
4178        cx.set_shared_state(indoc! {"
4179             ˇthe quick brown fox
4180             jumped over the lazy dog"})
4181            .await;
4182        cx.simulate_shared_keystrokes("d v 0").await;
4183        cx.shared_state().await.assert_eq(indoc! {"
4184             ˇhe quick brown fox
4185             jumped over the lazy dog"});
4186        assert!(!cx.cx.forced_motion());
4187
4188        cx.set_shared_state(indoc! {"
4189            the quick bˇrown fox
4190            jumped over the lazy dog"})
4191            .await;
4192        cx.simulate_shared_keystrokes("d v 0").await;
4193        cx.shared_state().await.assert_eq(indoc! {"
4194            ˇown fox
4195            jumped over the lazy dog"});
4196        assert!(!cx.cx.forced_motion());
4197
4198        cx.set_shared_state(indoc! {"
4199            the quick brown foˇx
4200            jumped over the lazy dog"})
4201            .await;
4202        cx.simulate_shared_keystrokes("d v 0").await;
4203        cx.shared_state().await.assert_eq(indoc! {"
4204            ˇ
4205            jumped over the lazy dog"});
4206        assert!(!cx.cx.forced_motion());
4207    }
4208
4209    #[gpui::test]
4210    async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4211        let mut cx = NeovimBackedTestContext::new(cx).await;
4212
4213        cx.set_shared_state(indoc! {"
4214             ˇthe quick brown fox
4215             jumped over the lazy dog"})
4216            .await;
4217        cx.simulate_shared_keystrokes("d v g shift-m").await;
4218        cx.shared_state().await.assert_eq(indoc! {"
4219             ˇbrown fox
4220             jumped over the lazy dog"});
4221        assert!(!cx.cx.forced_motion());
4222
4223        cx.set_shared_state(indoc! {"
4224            the quick bˇrown fox
4225            jumped over the lazy dog"})
4226            .await;
4227        cx.simulate_shared_keystrokes("d v g shift-m").await;
4228        cx.shared_state().await.assert_eq(indoc! {"
4229            the quickˇown fox
4230            jumped over the lazy dog"});
4231        assert!(!cx.cx.forced_motion());
4232
4233        cx.set_shared_state(indoc! {"
4234            the quick brown foˇx
4235            jumped over the lazy dog"})
4236            .await;
4237        cx.simulate_shared_keystrokes("d v g shift-m").await;
4238        cx.shared_state().await.assert_eq(indoc! {"
4239            the quicˇk
4240            jumped over the lazy dog"});
4241        assert!(!cx.cx.forced_motion());
4242
4243        cx.set_shared_state(indoc! {"
4244            ˇthe quick brown fox
4245            jumped over the lazy dog"})
4246            .await;
4247        cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4248        cx.shared_state().await.assert_eq(indoc! {"
4249            ˇ fox
4250            jumped over the lazy dog"});
4251        assert!(!cx.cx.forced_motion());
4252
4253        cx.set_shared_state(indoc! {"
4254            ˇthe quick brown fox
4255            jumped over the lazy dog"})
4256            .await;
4257        cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4258        cx.shared_state().await.assert_eq(indoc! {"
4259            ˇuick brown fox
4260            jumped over the lazy dog"});
4261        assert!(!cx.cx.forced_motion());
4262    }
4263
4264    #[gpui::test]
4265    async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4266        let mut cx = NeovimBackedTestContext::new(cx).await;
4267
4268        cx.set_shared_state(indoc! {"
4269             the quick brown foˇx
4270             jumped over the lazy dog"})
4271            .await;
4272        cx.simulate_shared_keystrokes("d v $").await;
4273        cx.shared_state().await.assert_eq(indoc! {"
4274             the quick brown foˇx
4275             jumped over the lazy dog"});
4276        assert!(!cx.cx.forced_motion());
4277
4278        cx.set_shared_state(indoc! {"
4279             ˇthe quick brown fox
4280             jumped over the lazy dog"})
4281            .await;
4282        cx.simulate_shared_keystrokes("d v $").await;
4283        cx.shared_state().await.assert_eq(indoc! {"
4284             ˇx
4285             jumped over the lazy dog"});
4286        assert!(!cx.cx.forced_motion());
4287    }
4288
4289    #[gpui::test]
4290    async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4291        let mut cx = NeovimBackedTestContext::new(cx).await;
4292
4293        cx.set_shared_state(indoc! {"
4294               ˇthe quick brown fox
4295               jumped over the lazy dog"})
4296            .await;
4297        cx.simulate_shared_keystrokes("y v j p").await;
4298        cx.shared_state().await.assert_eq(indoc! {"
4299               the quick brown fox
4300               ˇthe quick brown fox
4301               jumped over the lazy dog"});
4302        assert!(!cx.cx.forced_motion());
4303
4304        cx.set_shared_state(indoc! {"
4305              the quick bˇrown fox
4306              jumped over the lazy dog"})
4307            .await;
4308        cx.simulate_shared_keystrokes("y v j p").await;
4309        cx.shared_state().await.assert_eq(indoc! {"
4310              the quick brˇrown fox
4311              jumped overown fox
4312              jumped over the lazy dog"});
4313        assert!(!cx.cx.forced_motion());
4314
4315        cx.set_shared_state(indoc! {"
4316             the quick brown foˇx
4317             jumped over the lazy dog"})
4318            .await;
4319        cx.simulate_shared_keystrokes("y v j p").await;
4320        cx.shared_state().await.assert_eq(indoc! {"
4321             the quick brown foxˇx
4322             jumped over the la
4323             jumped over the lazy dog"});
4324        assert!(!cx.cx.forced_motion());
4325
4326        cx.set_shared_state(indoc! {"
4327             the quick brown fox
4328             jˇumped over the lazy dog"})
4329            .await;
4330        cx.simulate_shared_keystrokes("y v k p").await;
4331        cx.shared_state().await.assert_eq(indoc! {"
4332            thˇhe quick brown fox
4333            je quick brown fox
4334            jumped over the lazy dog"});
4335        assert!(!cx.cx.forced_motion());
4336    }
4337
4338    #[gpui::test]
4339    async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4340        let mut cx = NeovimBackedTestContext::new(cx).await;
4341
4342        cx.set_shared_state(indoc! {"
4343              ˇthe quick brown fox
4344              jumped over the lazy dog"})
4345            .await;
4346        cx.simulate_shared_keystrokes("d v e").await;
4347        cx.shared_state().await.assert_eq(indoc! {"
4348              ˇe quick brown fox
4349              jumped over the lazy dog"});
4350        assert!(!cx.cx.forced_motion());
4351
4352        cx.set_shared_state(indoc! {"
4353              the quick bˇrown fox
4354              jumped over the lazy dog"})
4355            .await;
4356        cx.simulate_shared_keystrokes("d v e").await;
4357        cx.shared_state().await.assert_eq(indoc! {"
4358              the quick bˇn fox
4359              jumped over the lazy dog"});
4360        assert!(!cx.cx.forced_motion());
4361
4362        cx.set_shared_state(indoc! {"
4363             the quick brown foˇx
4364             jumped over the lazy dog"})
4365            .await;
4366        cx.simulate_shared_keystrokes("d v e").await;
4367        cx.shared_state().await.assert_eq(indoc! {"
4368        the quick brown foˇd over the lazy dog"});
4369        assert!(!cx.cx.forced_motion());
4370    }
4371}