motion.rs

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