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