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