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